From bf9b34398d142db5bbd61b3f106ee41c27e5972b Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Thu, 8 Oct 2026 15:56:00 +0200 Subject: [PATCH] Add cable management: drawing, cores, numbering, cross-references and cable list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cables are drawn line objects (toolbar button next to the auto break/reconnect button, click-move-click, right click cancels), not components. The type comes from a CSV catalog following the material list pattern (designation, cores, core colours, editable through the entry dialog with "Nouvelle entrée" / "Modifier l'entrée"). Each core is bound to the wire its colour label stands on, and the cable reference is generated per core and written into the Conductor entry via Conductor::setCableReference(label, cable uuid, slot). The conductor entry is where it has to live because the terminal strip plan (Klemmenplan) consumes it in the next step. Numbering: "Cables" rule under Programmeinstellungen/Neues Projekt/Nummerierung auto (mirrored in project properties), literal "W" fallback without a rule, French gate dialog when no rule exists, Nummerierung menu with the renumber dialog (preferred axis, per-folio counter, rule and counter state written back, hand-typed names ask, whole-project scope). Also included: label block and under-line texts with per-text font and alignment plus the material-list cell margins, free-core placement and right-click removal, claim questions for wires of other cables, multi-selection drag as one undo step, type change with confirmation, cross-references for type "Cable" (%f-%l%c, own font, clickable links in exported PDFs), report propagation of the cable definition, Listes menu (table of contents, material list, terminal strip manager, terminal generator plugin, cable list), cable list with freely selectable columns (installation/localisation of cable, start and end, Blatt start/end, length, used cores...), CSV export dialog with column pick and preview in the Projekt menu, script API (addTable kind "cable_list", exportCableList) and a unit test for the type catalog. --- CMakeLists.txt | 1 + cmake/qet_compilation_vars.cmake | 42 + ico/generated/traced-24/cable.svg | 8 + ico/generated/traced-large-24/cable.svg | 8 + ico/icon-themes.qrc | 36 + ico/themes/qet-dark/traced-24/cable.svg | 8 + ico/themes/qet-dark/traced-large-24/cable.svg | 8 + ico/themes/qet-dark/traced/16/cable.svg | 6 + ico/themes/qet-dark/traced/22/cable.svg | 6 + ico/themes/qet-dark/traced/large16/cable.svg | 6 + ico/themes/qet-dark/traced/large22/cable.svg | 6 + ico/traced/16/cable.svg | 6 + ico/traced/22/cable.svg | 6 + ico/traced/large16/cable.svg | 6 + ico/traced/large22/cable.svg | 6 + sources/autoNum/assignvariables.cpp | 68 + sources/autoNum/assignvariables.h | 25 + sources/autoNum/ui/numparteditorw.cpp | 23 +- sources/autoNum/ui/numparteditorw.h | 8 + sources/autoNum/ui/selectautonumw.cpp | 121 +- sources/autoNum/ui/selectautonumw.h | 59 +- sources/bomexport.cpp | 5 +- sources/bomexport.h | 5 + sources/cable/addcablecommand.cpp | 213 ++ sources/cable/addcablecommand.h | 86 + sources/cable/cable.cpp | 921 +++++ sources/cable/cable.h | 376 ++ sources/cable/cablecopy.cpp | 454 +++ sources/cable/cablecopy.h | 126 + sources/cable/cablecreatedialog.cpp | 1407 +++++++ sources/cable/cablecreatedialog.h | 236 ++ sources/cable/cablemanager.cpp | 635 ++++ sources/cable/cablemanager.h | 113 + sources/cable/cablepart.cpp | 3377 +++++++++++++++++ sources/cable/cablepart.h | 513 +++ sources/cable/cablepropertiesdialog.cpp | 279 ++ sources/cable/cablepropertiesdialog.h | 99 + sources/cable/cablepropertieseditorwidget.cpp | 389 ++ sources/cable/cablepropertieseditorwidget.h | 115 + sources/cable/cablepropertieswidget.cpp | 409 ++ sources/cable/cablepropertieswidget.h | 158 + sources/cable/cablerenumber.cpp | 282 ++ sources/cable/cablerenumber.h | 102 + sources/cable/cabletextformatdialog.cpp | 278 ++ sources/cable/cabletextformatdialog.h | 83 + sources/cable/editcablecommand.cpp | 494 +++ sources/cable/editcablecommand.h | 285 ++ sources/cablelist/cableexportdialog.cpp | 155 + sources/cablelist/cableexportdialog.h | 58 + sources/cablelist/cablelistmodel.cpp | 421 ++ sources/cablelist/cablelistmodel.h | 107 + .../cablelistmodelpropertieswidget.cpp | 100 + .../cablelistmodelpropertieswidget.h | 55 + sources/cablelist/cablelistrows.cpp | 468 +++ sources/cablelist/cablelistrows.h | 101 + sources/cablelist/cablequerywidget.cpp | 319 ++ sources/cablelist/cablequerywidget.h | 71 + sources/cablelist/cabletypelist.cpp | 790 ++++ sources/cablelist/cabletypelist.h | 134 + sources/conductorproperties.cpp | 13 + sources/conductorproperties.h | 9 + sources/diagram.cpp | 22 +- sources/diagram.h | 12 +- sources/diagramevent/diagrameventaddcable.cpp | 463 +++ sources/diagramevent/diagrameventaddcable.h | 78 + sources/diagramevent/diagrameventaddpaste.cpp | 102 +- sources/diagramevent/diagrameventaddpaste.h | 21 + sources/diagramview.cpp | 285 +- sources/diagramview.h | 14 + sources/elementsmover.cpp | 200 +- sources/elementsmover.h | 35 + sources/factory/propertieseditorfactory.cpp | 38 +- sources/factory/qetgraphicstablefactory.cpp | 53 +- sources/factory/qetgraphicstablefactory.h | 1 + sources/pdf_links.cpp | 58 +- sources/properties/xrefproperties.cpp | 25 +- sources/properties/xrefproperties.h | 9 + sources/qetapp.cpp | 91 + sources/qetapp.h | 18 + sources/qetdiagrameditor.cpp | 239 +- sources/qetdiagrameditor.h | 17 + .../ViewItem/qetgraphicstableitem.cpp | 51 +- sources/qetgraphicsitem/conductor.cpp | 26 + sources/qetgraphicsitem/conductor.h | 9 + sources/qeticons.cpp | 2 + sources/qeticons.h | 1 + sources/qetproject.cpp | 380 +- sources/qetproject.h | 53 + sources/scripting/qetscriptapi.cpp | 64 +- sources/scripting/qetscriptapi.h | 10 +- sources/ui/cablenumberingdialog.cpp | 386 ++ sources/ui/cablenumberingdialog.h | 86 + sources/ui/configpage/configpages.cpp | 99 +- sources/ui/configpage/configpages.h | 6 + .../configpage/generalconfigurationpage.cpp | 180 + .../ui/configpage/generalconfigurationpage.h | 4 + .../ui/configpage/generalconfigurationpage.ui | 148 + sources/ui/configpage/projectconfigpages.cpp | 133 +- sources/ui/configpage/projectconfigpages.h | 9 + sources/ui/projectpropertiesdialog.cpp | 14 + sources/ui/projectpropertiesdialog.h | 2 + sources/ui/xrefpropertieswidget.cpp | 101 +- sources/ui/xrefpropertieswidget.h | 4 + sources/ui/xrefpropertieswidget.ui | 278 +- .../deleteqgraphicsitemcommand.cpp | 21 + sources/undocommand/linkelementcommand.cpp | 20 + tests/qttest/CMakeLists.txt | 17 + tests/qttest/tst_cabletypelist.cpp | 186 + 108 files changed, 18482 insertions(+), 293 deletions(-) create mode 100644 ico/generated/traced-24/cable.svg create mode 100644 ico/generated/traced-large-24/cable.svg create mode 100644 ico/themes/qet-dark/traced-24/cable.svg create mode 100644 ico/themes/qet-dark/traced-large-24/cable.svg create mode 100644 ico/themes/qet-dark/traced/16/cable.svg create mode 100644 ico/themes/qet-dark/traced/22/cable.svg create mode 100644 ico/themes/qet-dark/traced/large16/cable.svg create mode 100644 ico/themes/qet-dark/traced/large22/cable.svg create mode 100644 ico/traced/16/cable.svg create mode 100644 ico/traced/22/cable.svg create mode 100644 ico/traced/large16/cable.svg create mode 100644 ico/traced/large22/cable.svg create mode 100644 sources/cable/addcablecommand.cpp create mode 100644 sources/cable/addcablecommand.h create mode 100644 sources/cable/cable.cpp create mode 100644 sources/cable/cable.h create mode 100644 sources/cable/cablecopy.cpp create mode 100644 sources/cable/cablecopy.h create mode 100644 sources/cable/cablecreatedialog.cpp create mode 100644 sources/cable/cablecreatedialog.h create mode 100644 sources/cable/cablemanager.cpp create mode 100644 sources/cable/cablemanager.h create mode 100644 sources/cable/cablepart.cpp create mode 100644 sources/cable/cablepart.h create mode 100644 sources/cable/cablepropertiesdialog.cpp create mode 100644 sources/cable/cablepropertiesdialog.h create mode 100644 sources/cable/cablepropertieseditorwidget.cpp create mode 100644 sources/cable/cablepropertieseditorwidget.h create mode 100644 sources/cable/cablepropertieswidget.cpp create mode 100644 sources/cable/cablepropertieswidget.h create mode 100644 sources/cable/cablerenumber.cpp create mode 100644 sources/cable/cablerenumber.h create mode 100644 sources/cable/cabletextformatdialog.cpp create mode 100644 sources/cable/cabletextformatdialog.h create mode 100644 sources/cable/editcablecommand.cpp create mode 100644 sources/cable/editcablecommand.h create mode 100644 sources/cablelist/cableexportdialog.cpp create mode 100644 sources/cablelist/cableexportdialog.h create mode 100644 sources/cablelist/cablelistmodel.cpp create mode 100644 sources/cablelist/cablelistmodel.h create mode 100644 sources/cablelist/cablelistmodelpropertieswidget.cpp create mode 100644 sources/cablelist/cablelistmodelpropertieswidget.h create mode 100644 sources/cablelist/cablelistrows.cpp create mode 100644 sources/cablelist/cablelistrows.h create mode 100644 sources/cablelist/cablequerywidget.cpp create mode 100644 sources/cablelist/cablequerywidget.h create mode 100644 sources/cablelist/cabletypelist.cpp create mode 100644 sources/cablelist/cabletypelist.h create mode 100644 sources/diagramevent/diagrameventaddcable.cpp create mode 100644 sources/diagramevent/diagrameventaddcable.h create mode 100644 sources/ui/cablenumberingdialog.cpp create mode 100644 sources/ui/cablenumberingdialog.h create mode 100644 tests/qttest/tst_cabletypelist.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 120f53a40..243ca3e04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -333,6 +333,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 4593d5d56..6fd509de2 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -354,6 +354,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/diagramevent/diagrameventaddshape.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 @@ -774,6 +776,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 @@ -887,6 +891,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/generated/traced-24/cable.svg b/ico/generated/traced-24/cable.svg new file mode 100644 index 000000000..ff2411462 --- /dev/null +++ b/ico/generated/traced-24/cable.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ico/generated/traced-large-24/cable.svg b/ico/generated/traced-large-24/cable.svg new file mode 100644 index 000000000..778188781 --- /dev/null +++ b/ico/generated/traced-large-24/cable.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ico/icon-themes.qrc b/ico/icon-themes.qrc index 12b084713..2cfd647af 100644 --- a/ico/icon-themes.qrc +++ b/ico/icon-themes.qrc @@ -232,6 +232,21 @@ generated/traced-24/arrow-right-double.svg generated/traced-large-24/arrow-right-double.svg traced/large22/arrow-right-double.svg + traced/16/cable.svg + traced/large16/cable.svg + traced/22/cable.svg + traced/large22/cable.svg + traced/large16/cable.svg + traced/large16/cable.svg + generated/traced-large-24/cable.svg + generated/traced-large-24/cable.svg + traced/large16/cable.svg + traced/large16/cable.svg + traced/large16/cable.svg + traced/large16/cable.svg + generated/traced-24/cable.svg + generated/traced-large-24/cable.svg + traced/large22/cable.svg breeze/16/draw-circle.svg breeze/22/draw-circle.svg generated/breeze-24/draw-circle.svg @@ -2432,6 +2447,12 @@ themes/qet-dark/traced/large22/arrow-right-double.svg themes/qet-dark/traced-large-24/arrow-right-double.svg themes/qet-dark/traced-24/arrow-right-double.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.svg themes/qet-dark/breeze/16/draw-circle.svg themes/qet-dark/breeze/22/draw-circle.svg themes/qet-dark/breeze-24/draw-circle.svg @@ -3133,6 +3154,21 @@ themes/qet-dark/traced-24/arrow-right-double.svg themes/qet-dark/traced-large-24/arrow-right-double.svg themes/qet-dark/traced/large22/arrow-right-double.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.svg themes/qet-dark/breeze/16/draw-circle.svg themes/qet-dark/breeze/22/draw-circle.svg themes/qet-dark/breeze-24/draw-circle.svg 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/ico/traced/16/cable.svg b/ico/traced/16/cable.svg new file mode 100644 index 000000000..7c619123c --- /dev/null +++ b/ico/traced/16/cable.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ico/traced/22/cable.svg b/ico/traced/22/cable.svg new file mode 100644 index 000000000..e0a389dea --- /dev/null +++ b/ico/traced/22/cable.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ico/traced/large16/cable.svg b/ico/traced/large16/cable.svg new file mode 100644 index 000000000..623488647 --- /dev/null +++ b/ico/traced/large16/cable.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ico/traced/large22/cable.svg b/ico/traced/large22/cable.svg new file mode 100644 index 000000000..97f52785c --- /dev/null +++ b/ico/traced/large22/cable.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/sources/autoNum/assignvariables.cpp b/sources/autoNum/assignvariables.cpp index 3aa46b54c..890907c28 100644 --- a/sources/autoNum/assignvariables.cpp +++ b/sources/autoNum/assignvariables.cpp @@ -642,6 +642,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 69db1a119..ae5394aa9 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("Alphabétique") << tr("Texte"); } - 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("Chiffre 1") << tr("Chiffre 1 - Folio") << tr("Chiffre 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/selectautonumw.cpp b/sources/autoNum/ui/selectautonumw.cpp index 1d820fde0..a3930a20c 100644 --- a/sources/autoNum/ui/selectautonumw.cpp +++ b/sources/autoNum/ui/selectautonumw.cpp @@ -25,6 +25,7 @@ #include "ui_selectautonumw.h" #include +#include /** @brief SelectAutonumW::SelectAutonumW @@ -112,7 +113,11 @@ void SelectAutonumW::setContext(const NumerotationContext &context) else { for (int i=0; i editor_layout -> addWidget(part); } @@ -123,6 +128,9 @@ void SelectAutonumW::setContext(const NumerotationContext &context) ui -> remove_button -> setEnabled (true); 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; } /** @@ -144,11 +152,17 @@ NumerotationContext SelectAutonumW::toNumContext() const void SelectAutonumW::on_add_button_clicked() { applyEnable(false); + m_dirty = true; NumPartEditorW *part = new NumPartEditorW(m_edited_type, this); - connect(part, &NumPartEditorW::changed, this, [this]() { applyEnable(); }); + connect(part, &NumPartEditorW::changed, this, [this]() { + m_dirty = true; + applyEnable(); + emit contextEdited(); + }); num_part_list_ << part; ui -> editor_layout -> addWidget(part); ui -> remove_button -> setEnabled(true); + emit contextEdited(); } /** @@ -159,6 +173,7 @@ void SelectAutonumW::on_remove_button_clicked() { //remove if @num_part_list contains more than one item if (num_part_list_.size() > 1) { + m_dirty = true; NumPartEditorW *part = num_part_list_.takeLast(); // deliberately not disconnecting as not possible to resolve with lambda and will happen automatically when the object "part" is destroyed. delete part; @@ -167,6 +182,7 @@ void SelectAutonumW::on_remove_button_clicked() } } applyEnable(); + emit contextEdited(); } /** @@ -188,6 +204,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("Supprimer la règle"), this); + m_rule_remove_pb->setToolTip(tr( + "Supprimer la règle de numérotation des câbles : les câbles " + "sont à nouveau numérotés comme avant, et la question d'en " + "définir une revient au prochain dessin.")); + 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 @@ -252,6 +340,32 @@ void SelectAutonumW::on_buttonBox_clicked(QAbstractButton *button) )); break; } + else if (m_edited_type == 3) + { + QMessageBox::information ( + this, + tr("Câble Autonumérotation", + "title window"), + tr("C'est ici que vous pouvez définir la manière dont seront numérotés les nouveaux câbles.\n" + "-Une numérotation est composée d'une variable minimum.\n" + "-Vous pouvez ajouter ou supprimer une variable de numérotation par le biais des boutons - et +.\n" + "-Une variable de numérotation comprend : un type, une valeur et une incrémentation.\n" + + "\n-les types \"Chiffre 1\", \"Chiffre 01\" et \"Chiffre 001\", représentent un type numérique défini dans le champ \"Valeur\", " + "qui s'incrémente à chaque nouveau câble de la valeur du champ \"Incrémentation\".\n" + "-\"Chiffre 01\" et \"Chiffre 001\", sont respectivement représentés sur le schéma par deux et trois digits minimum.\n" + "Si le chiffre défini dans le champ Valeur possède moins de digits que le type choisi," + "celui-ci sera précédé par un ou deux 0 afin de respecter son type.\n" + + "\n-Le type \"Texte\", représente un texte fixe.\nLe champ \"Incrémentation\" n'est pas utilisé.\n" + + "\n-Le type \"N° folio\" représente le n° du folio en cours.\nLes autres champs ne sont pas utilisés.\n" + + "\n-Le type \"Folio\" représente le nom du folio en cours.\nLes autres champs ne sont pas utilisés.", + "help dialog about the cable autonumerotation" + )); + break; + } else { QMessageBox::information ( @@ -282,6 +396,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 790f81c55..f93502f2e 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; namespace Ui { class SelectAutonumW; @@ -52,10 +53,54 @@ 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); 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(); //SLOT private slots: @@ -76,7 +121,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 a84bb51b2..bcb96ae18 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 0241165a0..a2944960f 100644 --- a/sources/bomexport.h +++ b/sources/bomexport.h @@ -28,6 +28,11 @@ namespace BomExport { QStringList defaultColumns(); QString defaultQuery(); + /// 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..b7ad72c38 --- /dev/null +++ b/sources/cable/addcablecommand.cpp @@ -0,0 +1,213 @@ +/* + 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. +*/ +AddCableCommand::~AddCableCommand() +{ + if (m_cable && m_own_cable && !project()->cables().contains(m_cable)) { + delete m_cable; + } + 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..bb4785225 --- /dev/null +++ b/sources/cable/addcablecommand.h @@ -0,0 +1,86 @@ +/* + 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 + Cable *m_cable = nullptr; + ///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..0fbce44f4 --- /dev/null +++ b/sources/cable/cablecreatedialog.cpp @@ -0,0 +1,1407 @@ +/* + 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; + beginFilterChange(); + endFilterChange(QSortFilterProxyModel::Direction::Rows); +} + +/** + @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; + beginFilterChange(); + endFilterChange(QSortFilterProxyModel::Direction::Rows); +} + +/** + @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("Câble")); + + 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("(aucun type)") : m_change->type(); + m_header = new QLabel(tr("Changer le type : %1, %n âme(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 âme(s) reconnue(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("Rechercher :"), page)); + m_new_search = new QLineEdit(page); + m_new_search->setClearButtonEnabled(true); + m_new_search->setPlaceholderText(tr("Filtrer les types de câbles")); + 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("Masquer les types qui ont moins d'âmes " + "que ce câble") + : tr("Masquer les types qui ont moins d'âmes " + "que de conducteurs reconnus"), 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("Décochez pour voir aussi les types qui ont " + "moins d'âmes : en les prenant, les âmes " + "au-delà de leur nombre quittent le câble.") + : tr("Une ligne barrée est un type qui a moins " + "d'âmes que de conducteurs reconnus : il ne " + "peut pas porter ce câble."), 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("Nouvelle entrée"), page); + add_button->setToolTip(tr("Ajouter un nouveau type de câble à la liste")); + connect(add_button, &QPushButton::clicked, + this, &CableCreateDialog::addTypeRecord); + add_row->addWidget(add_button); + + m_edit_button = new QPushButton(tr("Modifier l'entrée"), page); + m_edit_button->setToolTip(tr("Modifier le type de câble sélectionné dans " + "la liste.")); + 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("Nouveau câble")); + + 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("Rechercher :"), page)); + m_existing_search = new QLineEdit(page); + m_existing_search->setClearButtonEnabled(true); + m_existing_search->setPlaceholderText(tr("Filtrer les câbles existants")); + search_row->addWidget(m_existing_search, 1); + layout->addLayout(search_row); + + m_existing_hide = new QCheckBox(tr("Masquer les câbles qui ont moins " + "d'adresses libres que de conducteurs reconnus"), 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("Une ligne barrée est un câble qui a moins " + "d'adresses libres que de conducteurs " + "reconnus : il ne peut pas les recevoir."), 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("Câbles existants")); + + 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("Désignation"), m_designation); + form->addRow(tr("Installation (=)"), m_installation); + form->addRow(tr("Localisation (+)"), 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("Désignation"), + tr("Type"), + tr("Âmes"), + tr("Installation"), + tr("Localisation") + }); + + 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("Ce type a moins d'âmes que ce câble : %1 au lieu de %2. " + "Les âmes au-delà de %1 quittent le câble et leurs marques " + "disparaissent du tracé.") + .arg(cores) + .arg(needed); + } + + return tr("Trop d'âmes sélectionnées : le type n'en propose que %1, " + "%2 conducteurs sont reconnus") + .arg(cores) + .arg(needed); + } + + const Cable *cable = m_existing.value(index.row()); + return tr("Seulement %1 adresses libres, %2 demandées") + .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("Aucun type de câble disponible : configurez le fichier " + "des types de câbles dans les préférences.")); + 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("Choisissez un type de câble.") + : tr("Choisissez un câble existant.")); + 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 âme(s) au lieu de %2 : les âmes supplémentaires " + "restent sans être placées.") + .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 conducteur(s) traversé(s) déjà rattaché(s) à un autre câble\n\n" + "En validant, l'autre câble perdra ces âmes. Voulez-vous continuer ?", + "ask before taking cores away from another cable", + m_taken.size()); + + if (QMessageBox::warning(this, tr("Câble"), 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("Écriture impossible"), + tr("Impossible d'écrire dans le fichier :\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("Écriture impossible"), + tr("Impossible d'écrire dans le fichier :\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 âme(s) de ce câble est/sont rattachée(s) à un conducteur. " + "Avec le type choisi, elle(s) quitte(nt) ce câble et leur marque " + "disparaît du tracé. Continuer ?", + "ask before cores lose the mark they show on the drawing", + wired); + + if (QMessageBox::warning(this, tr("Câble"), 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", + "Changer le type du câble")); + + //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("Nouvelle entrée")); + + auto *main_layout = new QVBoxLayout(this); + + auto *intro = new QLabel(tr("Renseignez le type de câble à ajouter. Les champs " + "laissés vides restent vides dans le fichier."), 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("Combien d'âmes ce type a. Le tiret veut dire que " + "le fichier ne le dit pas.")); + 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("La couleur de chaque âme, séparées par une " + "virgule et dans l'ordre des âmes.")); + } + 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("Enregistrer")); + buttons->button(QDialogButtonBox::Cancel)->setText(tr("Annuler")); + 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("Modifier l'entrée")); + if (m_intro) + { + m_intro->setText(tr("Modifiez le type de câble sélectionné dans la liste. " + "Les champs laissés vides restent vides dans le fichier.")); + } + + 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("Aucun renseignement"), + tr("Saisissez au moins un renseignement " + "pour créer une entrée.")); + 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..76737d66e --- /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 + //Querverweise -- 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 + //Querverweise for the type "Câble": 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 Querverweise. + + 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", "Déplacer %n câble(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", + "Placer une âme de câble") + : 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", + "Retirer une âme de la ligne"))); + 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("Oui", "Prendre le conducteur à l'autre câble."), + QMessageBox::YesRole); + QPushButton *leave = box.addButton( + tr("Non", "Laisser le conducteur à l'autre câble, on ne change rien."), + QMessageBox::NoRole); + QPushButton *cancel = box.addButton( + tr("Annuler", "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("Ce conducteur est déjà décrit par un autre câble. Voulez-vous l'attribuer au câble %1 ?", + "Un conducteur ne peut appartenir qu'à un câble : on demande avant de le prendre à l'autre.") + : tr("Ce conducteur est déjà décrit par le câble %1. Voulez-vous l'attribuer au câble %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("Conducteur déjà attribué", "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("Conducteur déjà attribué", "Un conducteur ne peut appartenir qu'à un câble.") + : tr("Conducteurs déjà attribués", "Plusieurs conducteurs sont pris en une seule fois.", n); + + const QString text = n <= 1 + ? (unnamed + ? tr("Ce conducteur est déjà décrit par un autre câble. Voulez-vous l'attribuer au câble %1 ?", + "Un conducteur ne peut appartenir qu'à un câble : on demande avant de le prendre à l'autre.") + : tr("Ce conducteur est déjà décrit par le câble %1. Voulez-vous l'attribuer au câble %2 ?", + "Un conducteur ne peut appartenir qu'à un câble : on demande avant de le prendre à l'autre.")) + : (unnamed + ? tr("%n conducteurs sont déjà décrits par d'autres câbles. Voulez-vous les attribuer au câble %1 ?", + "On prend plusieurs conducteurs à la fois : on demande avant de les prendre aux autres câbles.", n) + : tr("%n conducteurs sont déjà décrits par le câble %1. Voulez-vous les attribuer au câble %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..997fd2e39 --- /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 + "Câble" under Querverweise, 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 Querverweise. + */ + 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..47449d4bb --- /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 +#include + +/** + @brief CablePropertiesDialog::CablePropertiesDialog + @param cable the cable being edited + @param diagram the folio the line was double-clicked on, whose undo + stack takes the edit back + @param part the very line which was double-clicked: a core put down + from this dialog lands on that line, never on the first line of a + cable which stands on two folios + @param parent +*/ +CablePropertiesDialog::CablePropertiesDialog(Cable *cable, + Diagram *diagram, + CablePart *part, + QWidget *parent) : + QDialog(parent), + m_cable(cable), + m_diagram(diagram), + m_part(part) +{ + setWindowTitle(tr("Câble")); + + auto *layout = new QVBoxLayout(this); + m_fields = new CablePropertiesWidget(this); + layout->addWidget(m_fields); + + //The button beside the type field: it opens the window which + //opens when a cable line has just been drawn, in the mode where + //it changes the type of this very cable. + connect(m_fields, &CablePropertiesWidget::typeChangeRequested, + this, &CablePropertiesDialog::changeTypeRequested); + + m_cores = new QLabel(this); + m_cores->setWordWrap(true); + layout->addWidget(m_cores); + + //The cores the type offers which are not on the sheet yet. + //Double-clicking one brings it in: the line it was asked from + //takes the mouse and the next click puts the core down there. + m_free_label = new QLabel(tr("Âmes non placées :"), this); + m_free_cores = new QListWidget(this); + m_free_cores->setSelectionMode(QAbstractItemView::SingleSelection); + m_free_cores->setAlternatingRowColors(true); + m_free_cores->setToolTip(tr("Double-cliquer sur une âme pour la poser sur la " + "ligne : le trait suit la souris et le clic la " + "laisse à l'endroit visé.")); + m_free_cores->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_free_cores, &QWidget::customContextMenuRequested, + this, &CablePropertiesDialog::freeCoreMenu); + connect(m_free_cores, &QListWidget::itemDoubleClicked, + this, &CablePropertiesDialog::placeRequested); + connect(m_free_cores, &QListWidget::itemActivated, + this, &CablePropertiesDialog::placeRequested); + layout->addWidget(m_free_label); + layout->addWidget(m_free_cores); + + auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + connect(buttons, &QDialogButtonBox::accepted, this, &CablePropertiesDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &CablePropertiesDialog::reject); + layout->addWidget(buttons); + + if (m_cable) { + m_fields->setProperties(m_cable->properties()); + } + updateCoreCount(); + + fillFreeCores(); + + setMinimumWidth(400); +} + +/** + @brief CablePropertiesDialog::updateCoreCount + Say how many cores this cable has now and how many of them are wired + to a conductor. It is said again after the type was changed, since + the new type may hold more cores -- or fewer. +*/ +void CablePropertiesDialog::updateCoreCount() +{ + if (!m_cores) return; + + if (!m_cable) + { + m_cores->setText(tr("Aucun câble.")); + return; + } + + m_cores->setText(tr("%1 âme(s) sur %2 reliée(s) à un conducteur", + "how many cores of this cable are wired", + m_cable->usedCoreCount()) + .arg(m_cable->usedCoreCount()) + .arg(m_cable->coreCount())); +} + +/** + @brief CablePropertiesDialog::changeTypeRequested + Open the window which opens when a cable line has just been drawn, + in the mode where it changes the type of this very cable: he picks + the new type there out of the cable type file, and the cable takes + it -- cores reworked, marks redrawn -- as one single undo step. + + Whatever he typed into the form is written first, so the change and + the typing cannot overwrite each other; and the form is filled in + again afterwards, since pressing OK right after would otherwise + write the old type back. +*/ +void CablePropertiesDialog::changeTypeRequested() +{ + if (!m_cable) return; + + apply(); + + CableCreateDialog dialog(m_diagram, m_cable, this); + if (dialog.exec() != QDialog::Accepted) return; + + m_fields->setProperties(m_cable->properties()); + updateCoreCount(); + fillFreeCores(); +} + +/** + @brief CablePropertiesDialog::fillFreeCores + List every core the type of this cable offers and which stands + nowhere on the sheet: those are the ones he can still put down. The + list is of no use when there is none, so it takes no room then. +*/ +void CablePropertiesDialog::fillFreeCores() +{ + if (!m_free_cores) return; + + m_free_cores->clear(); + for (int i = 0; m_cable && i < m_cable->coreCount(); ++i) + { + if (m_cable->hasCore(i)) continue; + + const QString color = m_cable->colorOfCore(i); + auto *item = new QListWidgetItem( + color.isEmpty() ? tr("Âme %1").arg(i + 1) + : tr("Âme %1 — %2").arg(i + 1).arg(color), + m_free_cores); + item->setData(Qt::UserRole, i); + } + + const bool any = m_part && m_free_cores->count() > 0; + m_free_label->setVisible(any); + m_free_cores->setVisible(any); + if (!any) return; + + //As high as the entries it really shows, six of them at most: + //a handful of free cores must not turn the dialog into a wall. + const int row = qMax(m_free_cores->sizeHintForRow(0), 18); + m_free_cores->setFixedHeight(row * qMin(6, m_free_cores->count()) + + 2 * m_free_cores->frameWidth() + 4); +} + +/** + @brief CablePropertiesDialog::placeRequested + He picked one of the cores which stand nowhere: whatever is typed in + the form is written first, the dialog closes, and the line he asked + it from takes the mouse -- so the core goes onto that very line, and + the next click leaves it standing there. + @param item the entry he double-clicked +*/ +void CablePropertiesDialog::placeRequested(QListWidgetItem *item) +{ + //A double click reports itself twice on some platforms (once as + //activated, once as double-clicked): the first one takes the + //mouse away, the second must not ask for it again. + if (!item || !m_part || m_placing) return; + + m_placing = true; + const int core = item->data(Qt::UserRole).toInt(); + QPointer part = m_part; + + apply(); + QDialog::accept(); + + part->startPlacingCore(core); +} + +/** + @brief CablePropertiesDialog::freeCoreMenu + The small menu a right click on a core which stands nowhere opens: + one entry, doing what a double click does -- this dialog writes + whatever he typed, closes itself, the line he asked it from takes + the mouse, the mark shows on that line at once and the mouse is put + down there, so that the very next click leaves the core standing on + that line. + + @param pos where he right-clicked, in list coordinates +*/ +void CablePropertiesDialog::freeCoreMenu(const QPoint &pos) +{ + QListWidgetItem *item = m_free_cores ? m_free_cores->itemAt(pos) : nullptr; + if (!item || !m_part || m_placing) return; + + m_free_cores->setCurrentItem(item); + + QMenu menu(m_free_cores); + QAction *place = menu.addAction(tr("Placer cette âme sur la ligne")); + place->setIcon(QET::Icons::Cable); + + if (menu.exec(m_free_cores->viewport()->mapToGlobal(pos)) == place) { + //Gone before he is sent off: this dialog is closing, the + //mouse goes to the line and nothing may be left standing + //over the place it is sent to. + menu.hide(); + placeRequested(item); + } +} + +/** + @brief CablePropertiesDialog::accept + Whatever was typed here is written into the cable as one undo step, + and the cable works out the cable field of every conductor it feeds + again on its own: rename the cable and every drawing which shows it + follows. +*/ +void CablePropertiesDialog::accept() +{ + apply(); + QDialog::accept(); +} + +/** + @brief CablePropertiesDialog::apply + Everything the form holds goes into the cable as one single undo + step -- and only when something really changed, so that closing the + dialog by any means leaves no step of undo to walk back through. +*/ +void CablePropertiesDialog::apply() +{ + if (!m_cable) return; + + const CableProperties before = m_cable->properties(); + const CableProperties after = m_fields->properties(); + if (after != before) + { + if (m_diagram) { + m_diagram->undoStack().push( + new ChangeCablePropertiesCommand(m_cable, before, after)); + } else { + m_cable->setProperties(after); + } + } +} diff --git a/sources/cable/cablepropertiesdialog.h b/sources/cable/cablepropertiesdialog.h new file mode 100644 index 000000000..b0eddcf1e --- /dev/null +++ b/sources/cable/cablepropertiesdialog.h @@ -0,0 +1,99 @@ +/* + 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 CABLEPROPERTIESDIALOG_H +#define CABLEPROPERTIESDIALOG_H + +#include "cable.h" + +#include +#include + +class CablePropertiesWidget; +class CablePart; +class Diagram; +class QLabel; +class QListWidget; +class QListWidgetItem; + +/** + @brief The dialog which edits a cable once it is drawn. + + Everything it holds belongs to the cable itself rather than to the + line which was drawn: whatever is typed here applies to every section + of that cable, on every folio, and the conductors it feeds get their + cable field worked out again as soon as it says it changed. + + Under the count of cores it lists the cores of the type which stand + nowhere on the sheet yet: double-clicking one sends it to the very + line this dialog was asked from, which takes the mouse until he + clicks and leaves the core standing there. +*/ +class CablePropertiesDialog : public QDialog +{ + Q_OBJECT + + public: + /** + @param cable the cable being edited + @param diagram the folio whose undo stack takes the edit + @param part the very line which was double-clicked, where a + core chosen in this dialog is to be put down + @param parent + */ + explicit CablePropertiesDialog(Cable *cable, + Diagram *diagram = nullptr, + CablePart *part = nullptr, + QWidget *parent = nullptr); + + void accept() override; + + private: + ///Everything the form holds goes into the cable, as one step + void apply(); + ///What the count of cores under the form says right now + void updateCoreCount(); + ///He asked for the window choosing another type for this cable + void changeTypeRequested(); + ///Lists the cores of the type which stand nowhere yet + void fillFreeCores(); + ///He double-clicked one of them: put it down on the line + void placeRequested(QListWidgetItem *item); + /** + The small menu a right click on a core which stands + nowhere opens: one entry doing what a double click does + -- the dialog closes, the line takes the mouse, the mark + shows on it at once and the mouse is put down there. + @param pos where he right-clicked, in list coordinates + */ + void freeCoreMenu(const QPoint &pos); + + private: + QPointer m_cable; + Diagram *m_diagram = nullptr; + ///The line whose dialog this is, and the one a core chosen + ///here is put down on + QPointer m_part; + CablePropertiesWidget *m_fields = nullptr; + QLabel *m_cores = nullptr; + QLabel *m_free_label = nullptr; + QListWidget *m_free_cores = nullptr; + ///True from the moment he picks a core to put down + bool m_placing = false; +}; + +#endif // CABLEPROPERTIESDIALOG_H diff --git a/sources/cable/cablepropertieseditorwidget.cpp b/sources/cable/cablepropertieseditorwidget.cpp new file mode 100644 index 000000000..42b82b6d6 --- /dev/null +++ b/sources/cable/cablepropertieseditorwidget.cpp @@ -0,0 +1,389 @@ +/* + 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 "cablepropertieseditorwidget.h" + +#include "cablecreatedialog.h" +#include "cablepart.h" +#include "cablepropertieswidget.h" +#include "editcablecommand.h" +#include "../qeticons.h" +#include "../qetproject.h" + +#include +#include +#include +#include +#include +#include +#include + +/** + @brief CablePropertiesEditorWidget::CablePropertiesEditorWidget + @param cable the cable being edited + @param part the line of that cable this panel was opened by, which + is the line a core picked here is put down on + @param parent parent widget +*/ +CablePropertiesEditorWidget::CablePropertiesEditorWidget(Cable *cable, + CablePart *part, + QWidget *parent) : + PropertiesEditorWidget(parent), + m_fields(new CablePropertiesWidget(this)), + m_info(new QLabel(this)), + m_free_label(new QLabel(this)), + m_free_cores(new QListWidget(this)) +{ + m_info->setWordWrap(true); + + //The cores which stand nowhere yet, listed under the count of + //wired ones: double-clicking one puts it down on this very line + //without any dialog having to be opened for it. The list takes + //no keyboard focus, so clicking it never takes the drawing away + //from the user -- startPlacingCore() hands the focus to the + //sheet itself, which is where Escape has to be heard. + m_free_label->setText(tr("Âmes non placées :")); + m_free_label->setVisible(false); + m_free_cores->setSelectionMode(QAbstractItemView::SingleSelection); + m_free_cores->setAlternatingRowColors(true); + m_free_cores->setFocusPolicy(Qt::NoFocus); + m_free_cores->setToolTip(tr("Double-cliquer sur une âme pour la poser sur la " + "ligne : le trait suit la souris et le clic la " + "laisse à l'endroit visé.")); + m_free_cores->setVisible(false); + m_free_cores->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_free_cores, &QWidget::customContextMenuRequested, + this, &CablePropertiesEditorWidget::freeCoreMenu); + connect(m_free_cores, &QListWidget::itemDoubleClicked, + this, &CablePropertiesEditorWidget::placeRequested); + connect(m_free_cores, &QListWidget::itemActivated, + this, &CablePropertiesEditorWidget::placeRequested); + + auto *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(4); + layout->addWidget(m_fields); + + //The button beside the type field: it opens the window which + //opens when a cable line has just been drawn, in the mode where + //it changes the type of this very cable. + connect(m_fields, &CablePropertiesWidget::typeChangeRequested, + this, &CablePropertiesEditorWidget::changeTypeRequested); + + layout->addWidget(m_info); + layout->addWidget(m_free_label); + layout->addWidget(m_free_cores); + layout->addStretch(1); + + //Wide enough for the labels, the ticks, the fields, the buttons + //setting how a text is written and the two distances nudging + //the label and the row of colours -- they sit side by side. + setMinimumWidth(250); + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + + setDisabled(true); + setCable(cable, part); +} + +/** + @brief CablePropertiesEditorWidget::setCable + Set (or change) the cable whose properties are edited. + @param cable + @param part the line of that cable this panel was opened by: the one + a core picked in the list is put down on, and null while no single + line is selected -- then nothing can be put down and the list stays + out of the way +*/ +void CablePropertiesEditorWidget::setCable(Cable *cable, CablePart *part) +{ + if (!cable) return; + m_part = part; + if (m_cable && m_cable != cable) + disconnect(m_cable, nullptr, this, nullptr); + + m_cable = cable; + //Keep the panel in sync when the cable is edited elsewhere (the + //dialog reached by double-clicking its line, for one): otherwise + //the panel would write back what it held before that edit. + connect(m_cable, &Cable::changed, + this, &CablePropertiesEditorWidget::updateUi, Qt::UniqueConnection); + setEnabled(true); + updateUi(); +} + +/** + @brief CablePropertiesEditorWidget::setLiveEdit + In live-edit mode (how the panel uses every editor) an edit is + applied as soon as a field is finished, rather than waiting for a + call to apply(). On top of that the text itself is written onto the + cable with every character, so what he types shows up on the drawing + while he is still typing it -- the whole editing of one field stays + one single undo step, which is worked out when the field is left. + @param live_edit true to enable live edit + @return always true +*/ +bool CablePropertiesEditorWidget::setLiveEdit(bool live_edit) +{ + if (m_live_edit == live_edit) return true; + m_live_edit = live_edit; + + if (m_live_edit) { + m_live_connection = connect(m_fields, &CablePropertiesWidget::propertiesEdited, + this, &CablePropertiesEditorWidget::apply); + connect(m_fields, &CablePropertiesWidget::propertiesTyped, + this, &CablePropertiesEditorWidget::applyLive); + } else { + disconnect(m_live_connection); + disconnect(m_fields, &CablePropertiesWidget::propertiesTyped, + this, &CablePropertiesEditorWidget::applyLive); + } + return true; +} + +/** + @brief CablePropertiesEditorWidget::applyLive + Put what is being typed straight onto the cable, without pushing an + undo step: the whole editing of one field is one step, which + apply() works out from what the panel showed when the field is left. +*/ +void CablePropertiesEditorWidget::applyLive() +{ + //Ignore the signals sent while the form is being filled in: at + //that moment it holds a half loaded state. + if (m_updating) return; + if (!m_cable) return; + + m_live_cable = m_cable; + m_cable->setProperties(m_fields->properties()); +} + +/** + @brief CablePropertiesEditorWidget::changeTypeRequested + Open the window which opens when a cable line has just been drawn, + in the mode where it changes the type of this very cable: he picks + the new type there out of the cable type file, and the cable takes + it -- cores reworked, marks redrawn -- as one single undo step. + + The form is filled in again afterwards rather than through the usual + path: the type has just changed under it, and it may not go on + offering the one which was left. +*/ +void CablePropertiesEditorWidget::changeTypeRequested() +{ + if (!m_cable) return; + + CableCreateDialog dialog(m_part ? m_part->diagram() : nullptr, + m_cable, + this); + if (dialog.exec() != QDialog::Accepted) return; + + m_updating = true; + m_fields->setProperties(m_cable->properties()); + m_shown = m_cable->properties(); + m_updating = false; + updateInfo(); +} + +/** + @brief CablePropertiesEditorWidget::apply + Put what was typed onto the cable as one undo step. +*/ +void CablePropertiesEditorWidget::apply() +{ + //Ignore the signals sent while the form is being filled in: at + //that moment it holds a half loaded state. + if (m_updating) return; + if (!m_cable) return; + + if (QUndoCommand *undo = associatedUndo()) + { + auto *project = qobject_cast(m_cable->parent()); + if (project && project->undoStack()) { + project->undoStack()->push(undo); + } else { + m_cable->setProperties(m_fields->properties()); + delete undo; + } + } + m_shown = m_fields->properties(); +} + +/** + @brief CablePropertiesEditorWidget::associatedUndo + @return the edit as one undo step, or nullptr when nothing changed +*/ +QUndoCommand *CablePropertiesEditorWidget::associatedUndo() const +{ + if (!m_cable) return nullptr; + + const CableProperties after = m_fields->properties(); + if (after == m_shown) return nullptr; + return new ChangeCablePropertiesCommand(m_cable, m_shown, after); +} + +/** + @brief CablePropertiesEditorWidget::reset + Discard the edit in progress, putting back what the panel showed. +*/ +void CablePropertiesEditorWidget::reset() +{ + if (!m_cable) return; + m_updating = true; + m_fields->setProperties(m_shown); + m_updating = false; +} + +/** + @brief CablePropertiesEditorWidget::updateInfo + Say how many cores of this cable are wired to a conductor. +*/ +void CablePropertiesEditorWidget::updateInfo() +{ + if (!m_cable) return; + + m_info->setText(tr("%1 âme(s) sur %2 reliée(s) à un conducteur", + "how many cores of this cable are wired", + m_cable->usedCoreCount()) + .arg(m_cable->usedCoreCount()) + .arg(m_cable->coreCount())); + + updateFreeCores(); +} + +/** + @brief CablePropertiesEditorWidget::updateFreeCores + List the cores the type of this cable offers which stand nowhere on + the sheet yet: those are the ones he can still put down, and they + come down straight from here, on the line this panel was opened by. + + The list is of no use when there is none -- and it cannot work at + all when no single line of the cable is the selected one, since a + core then has no line to be put down on -- so it takes no room then. +*/ +void CablePropertiesEditorWidget::updateFreeCores() +{ + if (!m_free_cores) return; + + const bool usable = m_cable && m_part && m_part->cable() == m_cable; + + m_free_cores->clear(); + if (usable) + { + for (int i = 0; i < m_cable->coreCount(); ++i) + { + if (m_cable->hasCore(i)) continue; + + const QString color = m_cable->colorOfCore(i); + auto *item = new QListWidgetItem( + color.isEmpty() ? tr("Âme %1").arg(i + 1) + : tr("Âme %1 — %2").arg(i + 1).arg(color), + m_free_cores); + item->setData(Qt::UserRole, i); + } + } + + const bool any = usable && m_free_cores->count() > 0; + m_free_label->setVisible(any); + m_free_cores->setVisible(any); + if (!any) return; + + //As high as the entries it really shows, four of them at most: + //the dock is small, and a handful of free cores must not turn + //it into a wall. + const int row = qMax(m_free_cores->sizeHintForRow(0), 18); + m_free_cores->setFixedHeight(row * qMin(4, m_free_cores->count()) + + 2 * m_free_cores->frameWidth() + 4); +} + +/** + @brief CablePropertiesEditorWidget::placeRequested + He double-clicked one of the cores which stand nowhere: this very + line takes the mouse and waits for the click which puts the core + down. There is no dialog to close here -- the panel stays where it + is, and it reads the cable back as soon as the core lands. + + @param item the entry he double-clicked +*/ +void CablePropertiesEditorWidget::placeRequested(QListWidgetItem *item) +{ + if (!item || !m_part) return; + + m_part->startPlacingCore(item->data(Qt::UserRole).toInt()); +} + +/** + @brief CablePropertiesEditorWidget::freeCoreMenu + The small menu a right click on a core which stands nowhere opens: + one entry, doing what a double click does -- the line takes the + mouse, the mark shows on it at once and the mouse is put down there, + so the very next click leaves the core standing on that line. + + @param pos where he right-clicked, in list coordinates +*/ +void CablePropertiesEditorWidget::freeCoreMenu(const QPoint &pos) +{ + QListWidgetItem *item = m_free_cores ? m_free_cores->itemAt(pos) : nullptr; + if (!item || !m_part) return; + + m_free_cores->setCurrentItem(item); + + QMenu menu(m_free_cores); + QAction *place = menu.addAction(tr("Placer cette âme sur la ligne")); + place->setIcon(QET::Icons::Cable); + + if (menu.exec(m_free_cores->viewport()->mapToGlobal(pos)) == place) { + //Gone before he is sent off: the mouse goes to the line + //and nothing may be left standing over the place it is + //sent to. + menu.hide(); + placeRequested(item); + } +} + +/** + @brief CablePropertiesEditorWidget::updateUi + Read the cable back into the panel. + + Not while a field is being typed in: writing what was just typed + back into that field would take the cursor away from the user with + every key, and it would take back the state the whole editing of + that field is measured against when the undo step is worked out. +*/ +void CablePropertiesEditorWidget::updateUi() +{ + if (!m_cable) return; + + if (m_fields->editing() && m_live_cable == m_cable) { + updateInfo(); + return; + } + + m_updating = true; + m_fields->setProperties(m_cable->properties()); + m_shown = m_cable->properties(); + updateInfo(); + m_updating = false; +} + +/** + @brief CablePropertiesEditorWidget::title + @return the panel title +*/ +QString CablePropertiesEditorWidget::title() const +{ + return tr("Câble"); +} diff --git a/sources/cable/cablepropertieseditorwidget.h b/sources/cable/cablepropertieseditorwidget.h new file mode 100644 index 000000000..52268a0ce --- /dev/null +++ b/sources/cable/cablepropertieseditorwidget.h @@ -0,0 +1,115 @@ +/* + 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 CABLEPROPERTIESEDITORWIDGET_H +#define CABLEPROPERTIESEDITORWIDGET_H + +#include "cable.h" + +#include "../PropertiesEditor/propertieseditorwidget.h" + +#include +#include +#include + +class CablePropertiesWidget; +class CablePart; +class QLabel; +class QListWidget; +class QListWidgetItem; + +/** + @brief The cable shown in the "Propriétés de la sélection" panel. + + Selecting a line of a cable used to bring up nothing at all there: + the panel only knew the item types it already had an editor for. + Now it holds the very fields the cable is filled in with -- each of + them with its own tick saying whether the drawing shows it -- and + what stands there is written back live, one undo step per edit. +*/ +class CablePropertiesEditorWidget : public PropertiesEditorWidget +{ + Q_OBJECT + + public: + explicit CablePropertiesEditorWidget(Cable *cable = nullptr, + CablePart *part = nullptr, + QWidget *parent = nullptr); + + void setCable(Cable *cable, CablePart *part = nullptr); + + void apply() override; + void reset() override; + void updateUi() override; + QUndoCommand *associatedUndo() const override; + QString title() const override; + bool setLiveEdit(bool live_edit) override; + + private: + ///Write what is being typed onto the cable right away + void applyLive(); + ///He asked for the window choosing another type for this cable + void changeTypeRequested(); + ///Say how many cores of this cable are wired + void updateInfo(); + /** + List the cores which stand nowhere on the sheet yet, under + the count of wired ones: those are the ones he can still + put down, and double-clicking one puts it down straight + from here, on the line this panel was opened for. + */ + void updateFreeCores(); + ///He double-clicked one of those cores: give it this line + void placeRequested(QListWidgetItem *item); + /** + He right-clicked one of them: a small menu with the one + thing which can be done to a core which stands nowhere + -- putting it down -- for whoever does not know that a + double click does the same. + @param pos where he clicked, in list coordinates + */ + void freeCoreMenu(const QPoint &pos); + + private: + CablePropertiesWidget *m_fields = nullptr; + QLabel *m_info = nullptr; + ///The cores which stand nowhere yet, listed under the count + QLabel *m_free_label = nullptr; + QListWidget *m_free_cores = nullptr; + QPointer m_cable; + /** + The line this panel was opened by, which is the one a core + picked here is put down on: taking a core always belongs to + the line whose dialog was asked for, never to the first + line of the cable. Null while no single line is selected, + and then there is nothing to put a core down on and the + list takes no room. + */ + QPointer m_part; + ///What the panel showed before the edit: the fields which + ///differ from it are the ones the user changed + CableProperties m_shown; + ///The cable the text is being typed straight into, so that + ///reading the cable back only skips the very field which is + ///under the fingers of the user -- and never for another + ///cable which has just become the selected one + QPointer m_live_cable; + QMetaObject::Connection m_live_connection; + bool m_updating = false; +}; + +#endif // CABLEPROPERTIESEDITORWIDGET_H diff --git a/sources/cable/cablepropertieswidget.cpp b/sources/cable/cablepropertieswidget.cpp new file mode 100644 index 000000000..5d1d7d757 --- /dev/null +++ b/sources/cable/cablepropertieswidget.cpp @@ -0,0 +1,409 @@ +/* + 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 "cablepropertieswidget.h" + +#include "cabletextformatdialog.h" +#include "../qetapp.h" +#include "../qeticons.h" +#include "../utils/qetutils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + @brief CablePropertiesWidget::CablePropertiesWidget + One row per field: its name, the tick saying whether the drawing + shows it, the field itself, then the button setting how that one + text is written -- one last row for the colours of the cores, which + are written all together along the line, and one row per distance + he may nudge the whole label and that row of colours with. + @param parent +*/ +CablePropertiesWidget::CablePropertiesWidget(QWidget *parent) : + QWidget(parent) +{ + auto *grid = new QGridLayout(this); + grid->setContentsMargins(0, 0, 0, 0); + grid->setHorizontalSpacing(6); + grid->setVerticalSpacing(3); + + auto *plan = new QLabel(tr("Plan", + "column header: the drawing shows this field"), this); + plan->setAlignment(Qt::AlignCenter); + plan->setToolTip(tr("À cocher pour écrire ce champ sur le plan.", + "tooltip of the tick before each field")); + grid->addWidget(plan, 0, 1, Qt::AlignCenter); + + int row = 1; + addField(grid, row++, QStringLiteral("designation"), + tr("Désignation"), &m_designation, &m_show_designation); + const int type_row = row; + addField(grid, row++, QStringLiteral("type"), + tr("Type"), &m_type, &m_show_type); + + //A button beside the type field, opening the very window which + //opens when a cable line has just been drawn: the type is + //picked from the cable type file there -- with its search, its + //filter and its own button for a type the file does not hold + //yet -- rather than typed in as plain text. + auto *pick_type = new QPushButton(this); + pick_type->setFocusPolicy(Qt::NoFocus); + pick_type->setIcon(QET::Icons::Cable); + pick_type->setToolTip(tr("Choisir le type de ce câble dans la liste des " + "types de câbles…")); + connect(pick_type, &QPushButton::clicked, + this, &CablePropertiesWidget::typeChangeRequested); + grid->addWidget(pick_type, type_row, 4, Qt::AlignRight | Qt::AlignVCenter); + + addField(grid, row++, QStringLiteral("installation"), + tr("Installation (=)"), &m_installation, &m_show_installation); + addField(grid, row++, QStringLiteral("location"), + tr("Localisation (+)"), &m_location, &m_show_location); + addField(grid, row++, QStringLiteral("length"), + tr("Longueur"), &m_length, &m_show_length); + + //The colours of the cores: one setting for every one of them at + //once, since they are written in one row along the line and are + //read as one row. + auto *cores_label = new QLabel(tr("Couleurs des âmes"), this); + cores_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + cores_label->setToolTip(tr("La police des couleurs de toutes les âmes " + "de ce câble, écrites le long de la ligne.")); + grid->addWidget(cores_label, row, 0, Qt::AlignRight | Qt::AlignVCenter); + addFormatButton(grid, row, QStringLiteral("cores")); + + //Where that whole label stands: one distance along the line and + //one across it, in pixels, moving the label as one thing -- + //installation, place and designation over the line, type and + //length under it -- and the line not at all. + addOffsetRow(grid, row + 1, tr("Position du texte"), + tr("Décale tout le texte du câble : installation, emplacement " + "et désignation d'un côté, type et longueur de l'autre. " + "La ligne, les slashes et le câblage restent en place. " + "10 px font une case de la grille."), + &m_text_offset_x, &m_text_offset_y); + + //And where the row of colours stands from the slashes it names. + addOffsetRow(grid, row + 2, tr("Position des couleurs"), + tr("Décale les couleurs des âmes par rapport à leurs slashes. " + "Les slashes, les entrées et le câblage restent en place. " + "10 px font une case de la grille."), + &m_core_offset_x, &m_core_offset_y); + + grid->setColumnStretch(2, 1); +} + +/** + @brief CablePropertiesWidget::addField + Build one row of the form. + @param grid the form being built + @param row where to put it + @param key the name this text goes by in the file + @param name what the field holds + @param edit receives the line edit + @param tick receives the tick before it +*/ +void CablePropertiesWidget::addField(QGridLayout *grid, + int row, + const QString &key, + const QString &name, + QLineEdit **edit, + QCheckBox **tick) +{ + auto *label = new QLabel(name, this); + auto *check = new QCheckBox(this); + check->setToolTip(tr("À cocher pour écrire ce champ sur le plan.", + "tooltip of the tick before each field")); + auto *field = new QLineEdit(this); + field->setClearButtonEnabled(true); + + connect(field, &QLineEdit::editingFinished, + this, &CablePropertiesWidget::propertiesEdited); + //Every character too: what is typed is written onto the cable + //as it stands, so the drawing follows while he is still typing + //rather than only once he leaves the field. + connect(field, &QLineEdit::textChanged, + this, &CablePropertiesWidget::propertiesTyped); + connect(check, &QCheckBox::toggled, + this, &CablePropertiesWidget::propertiesEdited); + + grid->addWidget(label, row, 0, Qt::AlignRight | Qt::AlignVCenter); + grid->addWidget(check, row, 1, Qt::AlignCenter); + grid->addWidget(field, row, 2); + addFormatButton(grid, row, key); + + *edit = field; + *tick = check; +} + +/** + @brief CablePropertiesWidget::addFormatButton + The small button standing at the right of a text: it says which + size that text is written with, and opens the window where that + size -- and the rest of how the text is written -- is set. + @param grid the form being built + @param row where to put it + @param key the name of the text + @return the button, so the caller may reach it again +*/ +QPushButton *CablePropertiesWidget::addFormatButton(QGridLayout *grid, + int row, + const QString &key) +{ + auto *button = new QPushButton(this); + button->setFocusPolicy(Qt::NoFocus); + connect(button, &QPushButton::clicked, this, + [this, key]() {editFormat(key);}); + + grid->addWidget(button, row, 3, Qt::AlignRight | Qt::AlignVCenter); + m_format_buttons.insert(key, button); + refreshFormatButton(key); + return button; +} + +/** + @brief CablePropertiesWidget::addOffsetRow + Build one of the two rows nudging what is drawn: a name at the left + of the form, then two distances in pixels -- one measured along the + line, one across it -- the way the cross references of a slave offer + theirs. + + Both boxes speak of the drawing and nothing else: what he types here + moves texts, and the line, the slashes and the wires behind them keep + standing exactly where they were. + @param grid the form being built + @param row where to put it + @param name what that distance moves + @param tip what it does and does not touch + @param dx receives the spin box for the distance along the line + @param dy receives the spin box for the distance across it +*/ +void CablePropertiesWidget::addOffsetRow(QGridLayout *grid, + int row, + const QString &name, + const QString &tip, + QSpinBox **dx, + QSpinBox **dy) +{ + auto *label = new QLabel(name, this); + label->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + label->setToolTip(tip); + + const auto spin = [this, tip]() { + auto *box = new QSpinBox(this); + box->setRange(-200, 200); + box->setSingleStep(1); + box->setSuffix(QStringLiteral(" px")); + box->setAlignment(Qt::AlignRight); + box->setToolTip(tip); + connect(box, &QSpinBox::valueChanged, + this, &CablePropertiesWidget::propertiesEdited); + return box; + }; + QSpinBox *x_box = spin(); + QSpinBox *y_box = spin(); + + auto *along = new QLabel(tr("X :"), this); + along->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + auto *across = new QLabel(tr("Y :"), this); + across->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + + auto *values = new QHBoxLayout; + values->setContentsMargins(0, 0, 0, 0); + values->setSpacing(4); + values->addWidget(along); + values->addWidget(x_box); + values->addWidget(across); + values->addWidget(y_box); + values->addStretch(1); + + grid->addWidget(label, row, 0, Qt::AlignRight | Qt::AlignVCenter); + grid->addLayout(values, row, 2); + + *dx = x_box; + *dy = y_box; +} + +/** + @brief CablePropertiesWidget::editFormat + Open the window where one text of the cable is set, and -- when he + came out of it having really changed something -- write the answer + back into the form. One window is one change, so one step of undo. + @param key the name of the text +*/ +void CablePropertiesWidget::editFormat(const QString &key) +{ + CableTextFormatDialog dialog(m_formats.value(key), coresKey(key), this); + if (dialog.exec() != QDialog::Accepted) return; + + const CableTextFormat answer = dialog.format(); + if (m_formats.value(key) == answer) return; + + if (answer == CableTextFormat()) m_formats.remove(key); + else m_formats.insert(key, answer); + + refreshFormatButton(key); + emit propertiesEdited(); +} + +/** + @brief CablePropertiesWidget::refreshFormatButton + Let the button of one text say which size that text is written with, + so that the size is read off the form rather than only in the + drawing. + @param key the name of the text +*/ +void CablePropertiesWidget::refreshFormatButton(const QString &key) +{ + QPushButton *button = m_format_buttons.value(key); + if (!button) return; + + const QFont font = effectiveFont(key); + button->setText(tr("%1 pt").arg(QString::number(font.pointSizeF(), 'g', 3))); + + const QString tip = coresKey(key) + ? tr("Police, taille et style des couleurs des âmes.") + : tr("Police, taille, style et alignement de ce texte."); + const CableTextFormat format = m_formats.value(key); + const bool its_own = !format.font.isEmpty() || format.alignment != 0; + button->setToolTip(tip + QLatin1Char(' ') + (its_own + ? tr("Réglé pour ce câble.") + : tr("Valeur définie dans les préférences."))); +} + +/** + @brief CablePropertiesWidget::effectiveFont + @param key the name of a text of the form + @return the font that text really is written with on the drawing: + the one this cable asks for, or -- when it asks for none -- the one + the preferences offer +*/ +QFont CablePropertiesWidget::effectiveFont(const QString &key) const +{ + const CableTextFormat format = m_formats.value(key); + if (!format.font.isEmpty()) + { + QFont font; + if (QETUtils::fontFromString(font, format.font)) { + return font; + } + } + return coresKey(key) ? QETApp::cableCoreFont() : QETApp::cableTextsFont(); +} + +/** + @brief CablePropertiesWidget::properties + @return everything the form holds right now +*/ +CableProperties CablePropertiesWidget::properties() const +{ + CableProperties properties; + properties.designation = m_designation->text().trimmed(); + //A name which is not the one read from the cable can only have + //come from him typing it in: from that moment on the cable + //carries a name of his own rather than one the numbering rule + //handed out. Putting the very same name back by hand keeps the + //cable as it stood, which is what the field says. + properties.designation_by_hand = properties.designation == m_designation_text + ? m_designation_by_hand : true; + properties.type = m_type->text().trimmed(); + properties.installation = m_installation->text().trimmed(); + properties.location = m_location->text().trimmed(); + properties.length = m_length->text().trimmed(); + properties.show_designation = m_show_designation->isChecked(); + properties.show_type = m_show_type->isChecked(); + properties.show_installation = m_show_installation->isChecked(); + properties.show_location = m_show_location->isChecked(); + properties.show_length = m_show_length->isChecked(); + properties.texts = m_formats; + properties.text_offset = QPointF(m_text_offset_x->value(), + m_text_offset_y->value()); + properties.core_offset = QPointF(m_core_offset_x->value(), + m_core_offset_y->value()); + return properties; +} + +/** + @brief CablePropertiesWidget::setProperties + Fill the form in. The ticks say what is drawn, so a field which is + switched off still keeps the text standing in it -- hiding it on the + drawing never throws away what the user typed. + @param properties +*/ +void CablePropertiesWidget::setProperties(const CableProperties &properties) +{ + m_designation->setText(properties.designation); + //What the field holds now is what the cable said: anything he + //types from here on is a name of his own + m_designation_text = properties.designation; + m_designation_by_hand = properties.designation_by_hand; + m_type->setText(properties.type); + m_installation->setText(properties.installation); + m_location->setText(properties.location); + m_length->setText(properties.length); + m_show_designation->setChecked(properties.show_designation); + m_show_type->setChecked(properties.show_type); + m_show_installation->setChecked(properties.show_installation); + m_show_location->setChecked(properties.show_location); + m_show_length->setChecked(properties.show_length); + + //Filling the form in is not the user nudging anything: those + //two rows are muted while they are set, so that loading a cable + //can never look like an edit which then has to be undone. + const QSignalBlocker block_text_x(m_text_offset_x); + const QSignalBlocker block_text_y(m_text_offset_y); + const QSignalBlocker block_core_x(m_core_offset_x); + const QSignalBlocker block_core_y(m_core_offset_y); + m_text_offset_x->setValue(qRound(properties.text_offset.x())); + m_text_offset_y->setValue(qRound(properties.text_offset.y())); + m_core_offset_x->setValue(qRound(properties.core_offset.x())); + m_core_offset_y->setValue(qRound(properties.core_offset.y())); + + //The texts this cable writes its own way. Only what really has + //an answer of its own is kept, so that a stray empty entry can + //never make the form claim a change it never made. + m_formats.clear(); + for (auto it = properties.texts.cbegin(); it != properties.texts.cend(); ++it) + { + if (it.value() == CableTextFormat()) continue; + m_formats.insert(it.key(), it.value()); + } + for (auto it = m_format_buttons.cbegin(); it != m_format_buttons.cend(); ++it) { + refreshFormatButton(it.key()); + } +} + +/** + @brief CablePropertiesWidget::editing + @return true when the keyboard stands in one of the fields of this + form. The panel uses it to keep its hands off what is being typed: + writing the cable back into the form while he is typing would take + the cursor away from him with every key. +*/ +bool CablePropertiesWidget::editing() const +{ + QWidget *focus = QApplication::focusWidget(); + return focus && (focus == this || isAncestorOf(focus)); +} diff --git a/sources/cable/cablepropertieswidget.h b/sources/cable/cablepropertieswidget.h new file mode 100644 index 000000000..ea4b83fef --- /dev/null +++ b/sources/cable/cablepropertieswidget.h @@ -0,0 +1,158 @@ +/* + 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 CABLEPROPERTIESWIDGET_H +#define CABLEPROPERTIESWIDGET_H + +#include "cable.h" + +#include + +class QCheckBox; +class QGridLayout; +class QLineEdit; +class QPushButton; +class QSpinBox; + +/** + @brief The fields a cable is filled in with, and the tick before each + of them saying whether what stands there is written on the drawing. + + It holds no cable of its own: it is a plain form, filled in with + setProperties() and read back with properties(). That way the dialog + editing one cable and the selection panel of QElectroTech offer + exactly the same fields in the same order, and neither of them can + drift away from the other. + + Every field is followed by a checkbox: what the checkbox says is + written next to the trunk line of the drawing, what it does not say + stays in the cable alone and only shows up in lists. + + Behind every field stands a button opening the window where that one + text is written: its font, its size, its style and -- for the texts + drawn next to the line -- the way it lines up there. The colours of + the cores get a button of their own at the foot of the form, all of + them at once, because they are written in one row along the line and + are never set apart. + + The type field carries one more button, on the right of that row. It + does not say how the word is written: it asks for the window which + opens when a cable line has just been drawn, where a type is picked + out of the cable type file -- with its search, its filter and its own + button for a type the file does not hold yet -- and taken onto the + cable. Whoever holds this form answers typeChangeRequested(). + + At the foot of the form stand the two distances he may nudge: one for + the whole label, one for the row of colours. Both are on the drawing + only -- the line, the slashes and the wiring stay where they are. +*/ +class CablePropertiesWidget : public QWidget +{ + Q_OBJECT + + public: + explicit CablePropertiesWidget(QWidget *parent = nullptr); + + ///What the form holds right now + CableProperties properties() const; + ///Fill the form in + void setProperties(const CableProperties &properties); + ///True while one of the fields is being typed in + bool editing() const; + + signals: + ///Emitted when a field or a tick was really changed by hand + void propertiesEdited(); + ///Emitted with every character typed, so the drawing can + ///follow the text as it is written rather than only once the + ///field is left + void propertiesTyped(); + ///Emitted when he asks for the window choosing the type of + ///this cable out of the cable type file + void typeChangeRequested(); + + private: + void addField(QGridLayout *grid, + int row, + const QString &key, + const QString &name, + QLineEdit **edit, + QCheckBox **tick); + ///The button opening the window which sets one text + QPushButton *addFormatButton(QGridLayout *grid, int row, const QString &key); + /** + Build one row of the two spin boxes nudging what is + drawn: one for the label of the cable, one for the row + of its colours. + @param grid the form being built + @param row where to put it + @param name what the row moves + @param tip what that distance does and does not touch + @param dx receives the spin box for the distance along + the line + @param dy receives the spin box for the distance across + it + */ + void addOffsetRow(QGridLayout *grid, + int row, + const QString &name, + const QString &tip, + QSpinBox **dx, + QSpinBox **dy); + ///Open the window setting one text, then say so if it changed + void editFormat(const QString &key); + ///Let the button say which size that text is written with + void refreshFormatButton(const QString &key); + ///The font this text is really written with right now + QFont effectiveFont(const QString &key) const; + ///True when this key names the colours of the cores + static bool coresKey(const QString &key) {return key == QLatin1String("cores");} + + private: + QLineEdit *m_designation = nullptr; + /** + The name exactly as it was read from the cable, and + whether that name was typed in: anything else he puts + into the field from then on is his own work, and the + cable is marked as carrying a name he chose (see + properties()) + */ + QString m_designation_text; + bool m_designation_by_hand = false; + QLineEdit *m_type = nullptr; + QLineEdit *m_installation = nullptr; + QLineEdit *m_location = nullptr; + QLineEdit *m_length = nullptr; + QCheckBox *m_show_designation = nullptr; + QCheckBox *m_show_type = nullptr; + QCheckBox *m_show_installation = nullptr; + QCheckBox *m_show_location = nullptr; + QCheckBox *m_show_length = nullptr; + ///What each text of this form is written with; a text which + ///is not in here follows what the preferences say + QHash m_formats; + ///The button opening the window of each text, by name + QHash m_format_buttons; + ///How far the label of the cable is nudged on the drawing + QSpinBox *m_text_offset_x = nullptr; + QSpinBox *m_text_offset_y = nullptr; + ///How far the row of colours is nudged on the drawing + QSpinBox *m_core_offset_x = nullptr; + QSpinBox *m_core_offset_y = nullptr; +}; + +#endif // CABLEPROPERTIESWIDGET_H diff --git a/sources/cable/cablerenumber.cpp b/sources/cable/cablerenumber.cpp new file mode 100644 index 000000000..9e36998e6 --- /dev/null +++ b/sources/cable/cablerenumber.cpp @@ -0,0 +1,282 @@ +/* + 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 "cablerenumber.h" + +#include "cable.h" +#include "../autoNum/assignvariables.h" +#include "../autoNum/numerotationcontextcommands.h" +#include "../diagram.h" +#include "../qetproject.h" + +#include +#include + +#include +#include + +namespace { + +/** + @brief One cable of the project, and where it stands so that the + cables can be laid out in an order everybody can see again. +*/ +struct OrderedCable +{ + Cable *cable = nullptr; + ///The lowest folio the cable is drawn on + int folio = 0; + ///How far it reaches to the left and how far to the top on + ///that folio + qreal left = 0.0; + qreal top = 0.0; + ///The folio the rule is read on for this cable + Diagram *diagram = nullptr; +}; + +/** + @brief Read a rule once, the way the numbering of a cable reads it. + + The rule is read on a copy, since reading it fills in what its + folio-bound parts are worth on that one folio, and that must not + creep into the rule itself. + @param context the rule as it stands at this point of the numbering + @param diagram the folio the number is worked out on + @param key the name the rule is kept under in the project + @return the number, empty when the rule says nothing here +*/ +QString readRule(NumerotationContext context, Diagram *diagram, const QString &key) +{ + const QString formula = autonum::numerotationContextToFormula(context); + autonum::sequentialNumbers seq; + autonum::setSequential(formula, seq, context, diagram, key); + return autonum::AssignVariables::formulaToLabel(formula, seq, diagram); +} + +} // namespace + +/** + @brief CableRenumber::byHandCount + @param project + @return how many cables of that project carry a name typed in by hand +*/ +int CableRenumber::byHandCount(QETProject *project) +{ + if (!project) return 0; + + int count = 0; + for (Cable *cable : project->cables()) + { + if (cable && cable->designationByHand()) { + ++count; + } + } + return count; +} + +/** + @brief CableRenumber::plan + Work the number of every cable of the project out, without writing + anywhere. The cables are put in the order they are numbered in -- + folio by folio, and on each folio along whichever axis leads -- and + the rule is then read from its beginning once for each of them, so + that the numbering runs on without a hole. + @param project the project whose cables are numbered + @param rule the numbering rule to read + @param x_axis_first true when the leftmost cable of a folio comes first + @param include_by_hand false to leave the names typed in by hand alone + @return the numbers, or what stopped the numbering +*/ +CableRenumberPlan CableRenumber::plan(QETProject *project, + const NumerotationContext &rule, + bool x_axis_first, + bool include_by_hand) +{ + CableRenumberPlan result; + if (!project) + { + result.error = QCoreApplication::translate( + "CableRenumber", "Aucun projet n'est ouvert."); + return result; + } + if (project->isReadOnly()) + { + result.error = QCoreApplication::translate( + "CableRenumber", + "Ce projet est en lecture seule : ses câbles ne peuvent pas " + "être renumérotés."); + return result; + } + const QString key = project->cableCurrentAutoNum(); + if (key.isEmpty() || rule.isEmpty()) + { + result.error = QCoreApplication::translate( + "CableRenumber", + "Aucune règle de numérotation des câbles n'est définie pour ce " + "projet. Définissez-en une dans cette fenêtre avant de " + "renuméroter ses câbles."); + return result; + } + + //Where every cable stands, and which ones are left alone: their + //names stay as they are, so the numbering must not hand the + //same number out again + QList ordered; + QSet taken; + for (Cable *cable : project->cables()) + { + if (!cable) continue; + if (!include_by_hand && cable->designationByHand()) + { + if (!cable->designation().isEmpty()) { + taken.insert(cable->designation()); + } + continue; + } + + OrderedCable entry; + entry.cable = cable; + + for (const CablePartData &part : cable->parts()) + { + Diagram *diagram = project->diagramByUuid(part.diagram); + if (!diagram) continue; + + const int index = project->folioIndex(diagram); + const qreal left = qMin(part.p1.x(), part.p2.x()); + const qreal top = qMin(part.p1.y(), part.p2.y()); + + if (!entry.diagram || index < entry.folio) + { + entry.folio = index; + entry.diagram = diagram; + entry.left = left; + entry.top = top; + } else if (index == entry.folio) { + entry.left = qMin(entry.left, left); + entry.top = qMin(entry.top, top); + } + } + //A cable which stands on no folio the project still knows + //is numbered last rather than first by accident + if (!entry.diagram) { + entry.folio = std::numeric_limits::max(); + } + ordered.append(entry); + } + + //Folio by folio, and on each folio along the axis which leads: + //the other axis is only asked when the leading one says two + //cables are level, so that the numbering never depends on the + //order the cables happen to be stored in + std::sort(ordered.begin(), ordered.end(), + [x_axis_first](const OrderedCable &a, const OrderedCable &b) + { + if (a.folio != b.folio) return a.folio < b.folio; + + const qreal a_first = x_axis_first ? a.left : a.top; + const qreal b_first = x_axis_first ? b.left : b.top; + if (a_first != b_first) return a_first < b_first; + + const qreal a_second = x_axis_first ? a.top : a.left; + const qreal b_second = x_axis_first ? b.top : b.left; + if (a_second != b_second) return a_second < b_second; + + return a.cable->uuid().toString() < b.cable->uuid().toString(); + }); + + //Read the rule from the beginning, once for each cable: its + //counters are put back first, since numbering a project over + //starts at the beginning whatever the last number was + NumerotationContext context = autonum::resetContextCounters(rule); + int current_folio = -1; + + for (const OrderedCable &entry : std::as_const(ordered)) + { + if (!entry.diagram) + { + result.error = QCoreApplication::translate( + "CableRenumber", + "Un câble n'est sur aucun folio du projet : rien n'a été " + "changé."); + return result; + } + + //A counter bound to the folio counts from the beginning of + //that folio: the first cable of a folio gets the first + //number of it, and the folios after it start again rather + //than carrying the number of the folio before on + if (entry.folio != current_folio) + { + context = autonum::resetFolioCounters(context); + current_folio = entry.folio; + } + + const QString candidate = readRule(context, entry.diagram, key); + if (candidate.isEmpty()) + { + result.error = QCoreApplication::translate( + "CableRenumber", + "La règle en cours ne donne aucun numéro ici : rien n'a été " + "changé."); + return result; + } + //A rule which does not count hands out the same number + //twice, and two cables of one project may not be called + //the same thing + if (result.designations.contains(candidate)) + { + result.error = QCoreApplication::translate( + "CableRenumber", + "La règle en cours donne deux fois le même numéro (%1) : " + "elle doit compter pour renuméroter un projet entier. " + "Rien n'a été changé.") + .arg(candidate); + return result; + } + //A name which stays because it was typed in keeps its + //number, and the numbering may not hand that number out + if (taken.contains(candidate)) + { + result.error = QCoreApplication::translate( + "CableRenumber", + "Le numéro %1 est déjà porté par un câble dont le nom est " + "laissé tel quel : rien n'a été changé.") + .arg(candidate); + return result; + } + + taken.insert(candidate); + result.cables.append(entry.cable); + result.designations.append(candidate); + + //Step the rule on for the next cable; a rule which cannot + //step stays where it is and is caught by the check above + NumerotationContextCommands ncc(context, entry.diagram); + const NumerotationContext following = ncc.next(); + if (following.size() == context.size()) { + context = following; + } + } + + //Where the rule stands after the last cable has been numbered: + //the caller writes that into the project, so numbering goes on + //from there instead of from wherever the rule happened to stand + //before this numbering ran + result.after = context; + return result; +} diff --git a/sources/cable/cablerenumber.h b/sources/cable/cablerenumber.h new file mode 100644 index 000000000..c97513e8d --- /dev/null +++ b/sources/cable/cablerenumber.h @@ -0,0 +1,102 @@ +/* + 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 CABLERENUMBER_H +#define CABLERENUMBER_H + +#include +#include +#include + +#include "../autoNum/numerotationcontext.h" + +class Cable; +class QETProject; + +/** + @brief What numbering a whole project would do, worked out before a + single cable is written to. + + Working it out first means the answer can be refused as a whole: a + rule which hands out the same number twice, or a number some cable + which is left alone already carries, stops the whole numbering + rather than numbering half the project and then getting stuck. +*/ +struct CableRenumberPlan +{ + ///The cables to number, in the order they are numbered in + QList cables; + ///The number each of them gets, one for each cable + QStringList designations; + ///The rule as it stands once the last cable has been numbered: + ///its counters are where the next number would be taken from + NumerotationContext after; + ///What stopped the numbering, empty while it may be carried out + QString error; + + bool ok() const {return error.isEmpty();} +}; + +/** + @brief Working out the numbers a whole project should carry. + + Everything here is pure: nothing is written to the project and + nothing is put on the undo stack, so a caller may look at the plan + and decide against it without anything having happened. +*/ +namespace CableRenumber +{ + /** + How many cables of this project carry a name which was typed in + rather than handed out by the numbering rule. Those are the ones + renumbering asks about before it takes their name away. + @param project + @return the number of such cables + */ + int byHandCount(QETProject *project); + + /** + Work the number of every cable of the project out of @p rule. + + The cables are laid out first -- folio by folio, and on each + folio either from the left to the right or from the top to the + bottom, whichever axis leads -- and are then numbered one after + the other from the beginning of the rule, so that a hole left by + a cable which has been deleted closes behind it. + + @param project the project whose cables are numbered + @param rule the numbering rule to read; its counters are put + back to 1 first, since numbering starts from the beginning + @param x_axis_first true to number the cables of a folio from + the left to the right, false to number them from the top to the + bottom + @param include_by_hand false to leave the cables whose name was + typed in exactly as they are + @return the numbers, or what stopped the numbering. The plan + also carries the rule as it stands once every cable has been + given its number, so that the caller may put that into the + project: the counters have to stay where the last number was + taken from, or the next cable drawn would skip over every + number in between + */ + CableRenumberPlan plan(QETProject *project, + const NumerotationContext &rule, + bool x_axis_first, + bool include_by_hand); +} + +#endif // CABLERENUMBER_H diff --git a/sources/cable/cabletextformatdialog.cpp b/sources/cable/cabletextformatdialog.cpp new file mode 100644 index 000000000..4eb318732 --- /dev/null +++ b/sources/cable/cabletextformatdialog.cpp @@ -0,0 +1,278 @@ +/* + 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 "cabletextformatdialog.h" + +#include "../qetapp.h" +#include "../utils/qetutils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + /** + @brief The way a text lines up, as the dialog lists it: right + first, which is where every text stands unless he says otherwise. + @param index the place in the list of the dialog + @return the flags the drawing understands + */ + Qt::Alignment alignmentFromIndex(int index) + { + switch (index) + { + case 1: return Qt::AlignHCenter; + case 2: return Qt::AlignLeft; + default: return Qt::AlignRight; + } + } + + /** + @brief The place in the list of the dialog of an alignment. + @param alignment Qt::AlignLeft, Qt::AlignHCenter or Qt::AlignRight + @return the index, 0 (right) when nothing is named + */ + int alignmentIndex(int alignment) + { + switch (alignment) + { + case int(Qt::AlignHCenter): return 1; + case int(Qt::AlignLeft): return 2; + default: return 0; + } + } + + /** + Whether two fonts would be read the very same way on the sheet: + the family he picks, the size he gives it, and the style he + chose among the four offered. + @param a the font he has chosen + @param b the font the preferences would write it with + @return true when nothing of them differs + */ + bool sameFont(const QFont &a, const QFont &b) + { + return a.family() == b.family() + && qFuzzyCompare(a.pointSizeF(), b.pointSizeF()) + && a.bold() == b.bold() + && a.italic() == b.italic(); + } +} + +/** + @brief CableTextFormatDialog::CableTextFormatDialog + @param format what this cable writes that text with right now + @param cores true for the colours of the cores + @param parent +*/ +CableTextFormatDialog::CableTextFormatDialog(const CableTextFormat &format, + bool cores, + QWidget *parent) : + QDialog(parent), + m_cores(cores) +{ + setWindowTitle(cores ? tr("Couleurs des âmes") + : tr("Format du texte")); + buildUi(); + + //The font this text is written with right now: the one this + //cable asks for, or the one the preferences offer when it asks + //for none -- which is what he sees on the sheet as he opens this. + QFont font; + bool restored = false; + if (!format.font.isEmpty()) { + restored = QETUtils::fontFromString(font, format.font); + } + if (!restored) { + font = cores ? QETApp::cableCoreFont() : QETApp::cableTextsFont(); + } + + //Where this text stands right now, in the same way: the answer + //of the cable itself, or -- when it has none -- the one the + //preferences offer, so that he opens this onto what the sheet + //really shows rather than onto whatever the default used to be. + m_initial_alignment = cores ? 0 + : (format.alignment ? format.alignment + : int(QETApp::cableTextAlignment())); + + m_family->setCurrentFont(font); + m_size->setValue(font.pointSizeF()); + if (font.bold() && font.italic()) m_style->setCurrentIndex(3); + else if (font.italic()) m_style->setCurrentIndex(2); + else if (font.bold()) m_style->setCurrentIndex(1); + else m_style->setCurrentIndex(0); + if (m_alignment) { + m_alignment->setCurrentIndex(alignmentIndex(m_initial_alignment)); + } + + //Going back to the preferences means something only while this + //cable has an answer of its own: when it already follows them, + //there is nothing here to give back. + m_reset->setEnabled(!format.font.isEmpty() || format.alignment != 0); + updatePreview(); +} + +/** + @brief CableTextFormatDialog::buildUi + One row per thing he may set, then what it looks like -- because a + size means nothing as a number alone. +*/ +void CableTextFormatDialog::buildUi() +{ + auto *form = new QFormLayout; + form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + form->setHorizontalSpacing(12); + + m_family = new QFontComboBox(this); + m_family->setFontFilters(QFontComboBox::AllFonts); + form->addRow(tr("Police :"), m_family); + + m_size = new QDoubleSpinBox(this); + m_size->setRange(1.0, 300.0); + m_size->setDecimals(1); + m_size->setSingleStep(0.5); + m_size->setSuffix(tr(" pt")); + form->addRow(tr("Taille :"), m_size); + + m_style = new QComboBox(this); + m_style->addItem(tr("Normal")); + m_style->addItem(tr("Gras")); + m_style->addItem(tr("Italique")); + m_style->addItem(tr("Gras et italique")); + form->addRow(tr("Style :"), m_style); + + //The colours of the cores stand turned by a quarter along their + //own line: there is nothing for them to line up with. + if (!m_cores) + { + m_alignment = new QComboBox(this); + m_alignment->addItem(tr("Aligné à droite")); + m_alignment->addItem(tr("Centré")); + m_alignment->addItem(tr("Aligné à gauche")); + form->addRow(tr("Alignement :"), m_alignment); + } + + m_preview = new QLabel(tr("15W1 · H07V-K 3G1,5"), this); + m_preview->setFrameStyle(QFrame::Box | QFrame::Plain); + m_preview->setMinimumHeight(30); + m_preview->setIndent(6); + form->addRow(tr("Aperçu :"), m_preview); + + auto *buttons = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + m_reset = buttons->addButton(tr("Réinitialiser"), QDialogButtonBox::ResetRole); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(m_reset, &QPushButton::clicked, this, [this]() { + m_reset_chosen = true; + accept(); + }); + + auto *layout = new QVBoxLayout(this); + layout->addLayout(form); + layout->addWidget(buttons); + setLayout(layout); + + connect(m_family, &QFontComboBox::currentFontChanged, + this, &CableTextFormatDialog::updatePreview); + connect(m_size, &QDoubleSpinBox::valueChanged, + this, &CableTextFormatDialog::updatePreview); + connect(m_style, &QComboBox::currentIndexChanged, + this, &CableTextFormatDialog::updatePreview); + if (m_alignment) { + connect(m_alignment, &QComboBox::currentIndexChanged, + this, &CableTextFormatDialog::updatePreview); + } +} + +/** + @brief CableTextFormatDialog::chosenFont + What the dialog offers right now: the family he picked, the size he + gave it and the style he chose among the four offered -- rather than + whatever that family happened to be called in the list. + @return the font +*/ +QFont CableTextFormatDialog::chosenFont() const +{ + QFont font = m_family->currentFont(); + font.setPointSizeF(m_size->value()); + const bool italic = m_style->currentIndex() == 2 || m_style->currentIndex() == 3; + const bool bold = m_style->currentIndex() == 1 || m_style->currentIndex() == 3; + font.setBold(bold); + font.setItalic(italic); + return font; +} + +/** + @brief CableTextFormatDialog::updatePreview + Write the line offered as an example the way it would be written + after what he has just set -- the only place where a size becomes + something one can judge. +*/ +void CableTextFormatDialog::updatePreview() +{ + m_preview->setFont(chosenFont()); + if (m_alignment) { + m_preview->setAlignment( + alignmentFromIndex(m_alignment->currentIndex()) | Qt::AlignVCenter); + } else { + m_preview->setAlignment(Qt::AlignCenter); + } +} + +/** + @brief CableTextFormatDialog::format + @return what the cable is to write that text with from now on. + + An empty answer means "the way the preferences say", and the two + halves of it are worked out apart from each other: he may set the + way one text lines up without taking away its freedom to follow a + new default font later, and the other way round. What already + stands the way the preferences say is left out, so that a cable + which said nothing keeps saying nothing -- and a later change of + what those preferences are still reaches it. +*/ +CableTextFormat CableTextFormatDialog::format() const +{ + if (m_reset_chosen) return CableTextFormat(); + + const QFont font = chosenFont(); + const QFont default_font = m_cores ? QETApp::cableCoreFont() + : QETApp::cableTextsFont(); + + CableTextFormat answer; + if (!sameFont(font, default_font)) { + answer.font = QETUtils::fontToString(font); + } + //The colours of the cores stand turned by a quarter: they have + //no line to line up with, and saying so would only be noise in + //the file. + if (!m_cores) + { + const int alignment = int(alignmentFromIndex(m_alignment->currentIndex())); + if (alignment != int(QETApp::cableTextAlignment())) { + answer.alignment = alignment; + } + } + return answer; +} diff --git a/sources/cable/cabletextformatdialog.h b/sources/cable/cabletextformatdialog.h new file mode 100644 index 000000000..49db070f2 --- /dev/null +++ b/sources/cable/cabletextformatdialog.h @@ -0,0 +1,83 @@ +/* + 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 CABLETEXTFORMATDIALOG_H +#define CABLETEXTFORMATDIALOG_H + +#include "cable.h" + +#include + +class QComboBox; +class QFontComboBox; +class QDoubleSpinBox; +class QLabel; +class QPushButton; + +/** + @brief The small window where one text of one cable is set: the font + it is written with -- family, size and style -- and, for the texts + written next to the trunk line, the way it lines up at the line. + + The colours of the cores come here as well, all of them together + because they are drawn together: they have no alignment, being + turned by a quarter along their own line. + + Saying nothing is the normal case: the cable then follows what the + preferences offer on the "Textes" page, and OK keeps an answer of + its own only when he really changed something -- so that picking + another default later on still reaches this cable. +*/ +class CableTextFormatDialog : public QDialog +{ + Q_OBJECT + + public: + /** + @param format what this cable writes that text with right + now, an empty format when it merely follows the preferences + @param cores true for the colours of the cores: they have + no alignment and follow QETApp::cableCoreFont() + @param parent + */ + CableTextFormatDialog(const CableTextFormat &format, + bool cores, + QWidget *parent = nullptr); + + ///What the cable is to write that text with from now on + CableTextFormat format() const; + + private: + void buildUi(); + void updatePreview(); + QFont chosenFont() const; + + bool m_cores = false; + ///True when he asked to go back to following the preferences + bool m_reset_chosen = false; + ///The alignment this text has right now, 0 for the cores + int m_initial_alignment = int(Qt::AlignRight); + + QFontComboBox *m_family = nullptr; + QDoubleSpinBox *m_size = nullptr; + QComboBox *m_style = nullptr; + QComboBox *m_alignment = nullptr; + QLabel *m_preview = nullptr; + QPushButton *m_reset = nullptr; +}; + +#endif // CABLETEXTFORMATDIALOG_H diff --git a/sources/cable/editcablecommand.cpp b/sources/cable/editcablecommand.cpp new file mode 100644 index 000000000..dff0d2a76 --- /dev/null +++ b/sources/cable/editcablecommand.cpp @@ -0,0 +1,494 @@ +/* + 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 "editcablecommand.h" + +#include "cablemanager.h" +#include "cablepart.h" +#include "../diagram.h" +#include "../qetproject.h" + +#include + +#include + +/** + @brief BatchCommand::BatchCommand + @param first step to run first, kept by this command + @param second step to run right after it, kept by this command +*/ +BatchCommand::BatchCommand(QUndoCommand *first, QUndoCommand *second) : + m_steps({first, second}) +{ + m_steps.removeAll(nullptr); + setText(QCoreApplication::translate("BatchCommand", "Supprimer")); +} + +/** + @brief BatchCommand::BatchCommand + @param steps every step of one single gesture, in the order the + user did them, kept by this command + @param text what the gesture was, as the undo history shows it +*/ +BatchCommand::BatchCommand(const QList &steps, const QString &text) : + m_steps(steps) +{ + m_steps.removeAll(nullptr); + setText(text); +} + +/** + @brief BatchCommand::~BatchCommand + The steps it holds go with it: they were never given to anything else. +*/ +BatchCommand::~BatchCommand() +{ + for (QUndoCommand *step : std::as_const(m_steps)) { + delete step; + } +} + +/** + @brief BatchCommand::redo + Every step, in the order the user did them. +*/ +void BatchCommand::redo() +{ + for (QUndoCommand *step : std::as_const(m_steps)) { + if (step) step->redo(); + } +} + +/** + @brief BatchCommand::undo + Every step taken back in the reverse order, so what one step changed + is still there when the next one is undone. +*/ +void BatchCommand::undo() +{ + for (auto it = m_steps.crbegin(); it != m_steps.crend(); ++it) { + if (*it) (*it)->undo(); + } +} + +/** + @brief MoveCablePartCommand::MoveCablePartCommand + @param cable the cable the section belongs to + @param part the line being dragged + @param before_p1 + @param before_p2 + @param after_p1 + @param after_p2 + @param before_cores where every colour label stood before the drag + @param after_cores where it stands after it +*/ +MoveCablePartCommand::MoveCablePartCommand(Cable *cable, + CablePart *part, + const QPointF &before_p1, + const QPointF &before_p2, + const QPointF &after_p1, + const QPointF &after_p2, + const QList &before_cores, + const QList &after_cores) : + m_cable(cable), + m_part(part), + m_before_p1(before_p1), + m_before_p2(before_p2), + m_after_p1(after_p1), + m_after_p2(after_p2), + m_before_cores(before_cores), + m_after_cores(after_cores) +{ + setText(QCoreApplication::translate("MoveCablePartCommand", "Déplacer un câble")); +} + +void MoveCablePartCommand::undo() +{ + apply(m_before_p1, m_before_p2, m_before_cores); +} + +void MoveCablePartCommand::redo() +{ + apply(m_after_p1, m_after_p2, m_after_cores); +} + +/** + @brief MoveCablePartCommand::apply + Put the line where it belongs and the colour labels with it. The + positions are written last: they are stored in scene coordinates, so + they only make sense once the line itself is back in place. + @param p1 + @param p2 + @param cores +*/ +void MoveCablePartCommand::apply(const QPointF &p1, + const QPointF &p2, + const QList &cores) +{ + if (m_part) { + m_part->setLine(p1, p2); + } + if (m_cable) { + m_cable->setCores(cores); + } +} + +/** + @brief ChangeCablePropertiesCommand::ChangeCablePropertiesCommand + @param cable + @param before every field as it was before the edit + @param after every field as the user left it +*/ +ChangeCablePropertiesCommand::ChangeCablePropertiesCommand(Cable *cable, + const CableProperties &before, + const CableProperties &after) : + m_cable(cable), + m_before(before), + m_after(after) +{ + setText(QCoreApplication::translate("ChangeCablePropertiesCommand", + "Modifier les propriétés d'un câble")); +} + +void ChangeCablePropertiesCommand::undo() +{ + if (m_cable) { + m_cable->setProperties(m_before); + } +} + +void ChangeCablePropertiesCommand::redo() +{ + if (m_cable) { + m_cable->setProperties(m_after); + } +} + +/** + @brief RenumberCablesCommand::RenumberCablesCommand + @param cables the cables to number, in the order they are numbered in + @param designations the number each of them gets + @param project the project whose numbering rule moves along with + the numbering + @param num_key the name that rule is kept under in the project + @param num_before where its counters stood before the numbering ran + @param num_after where the numbering leaves them +*/ +RenumberCablesCommand::RenumberCablesCommand(const QList &cables, + const QStringList &designations, + QETProject *project, + const QString &num_key, + const NumerotationContext &num_before, + const NumerotationContext &num_after) : + m_project(project), + m_num_key(num_key), + m_num_before(num_before), + m_num_after(num_after) +{ + for (int i = 0; i < cables.size() && i < designations.size(); ++i) + { + Cable *cable = cables.at(i); + if (!cable) continue; + + m_cables.append(cable); + m_before.append(cable->designation()); + m_by_hand_before.append(cable->designationByHand()); + m_after.append(designations.at(i)); + //A number the rule has just handed out is the rule's, so + //nothing here stays marked as having been typed in + m_by_hand_after.append(false); + } + + setText(QCoreApplication::translate("RenumberCablesCommand", + "Renumérotter %n câble(s)", + "", m_cables.size())); +} + +void RenumberCablesCommand::undo() +{ + apply(false); +} + +void RenumberCablesCommand::redo() +{ + apply(true); +} + +/** + @brief RenumberCablesCommand::apply + @param use_after true to write the numbers the rule handed out, + false to put the ones back which were there before +*/ +void RenumberCablesCommand::apply(bool use_after) +{ + for (int i = 0; i < m_cables.size(); ++i) + { + Cable *cable = m_cables.at(i).data(); + if (!cable) continue; + + //Where the name comes from is written first: a number the + //rule has just handed out is the rule's whatever the cable + //was called before, and undo has to say so again + cable->setDesignationByHand(use_after ? m_by_hand_after.at(i) + : m_by_hand_before.at(i)); + cable->setDesignation(use_after ? m_after.at(i) : m_before.at(i)); + } + + //Where the numbering rule itself stands moves along with the + //numbering: once it has run, its counters are where the last + //number was taken from, so the next cable drawn goes on from + //there rather than skipping over every number in between. Undo + //puts them back like the names, since numbering a project over + //is one single act. + if (m_project && !m_num_key.isEmpty()) + { + const NumerotationContext context = use_after ? m_num_after + : m_num_before; + //An empty rule is never written: losing the rule altogether + //is not something numbering may do, not even on the way back + if (!context.isEmpty()) + { + m_project->addCableAutoNum(m_num_key, context); + m_project->setCurrentCableAutoNum(m_num_key); + } + } +} + +/** + @brief ChangeCableTypeCommand::ChangeCableTypeCommand + @param cable + @param before the type, the count, the colours and the cores as + they were before the change + @param after the very same four as the chosen type says +*/ +ChangeCableTypeCommand::ChangeCableTypeCommand(Cable *cable, + const CableTypeState &before, + const CableTypeState &after) : + m_cable(cable), + m_before(before), + m_after(after) +{ + setText(QCoreApplication::translate("ChangeCableTypeCommand", + "Changer le type du câble")); +} + +void ChangeCableTypeCommand::undo() +{ + apply(m_before); +} + +void ChangeCableTypeCommand::redo() +{ + apply(m_after); +} + +/** + @brief ChangeCableTypeCommand::apply + One block in one pass: name, colours, count, then the cores. The + count has to stand before the cores, since a cable with fewer cores + cannot hold the others -- and the other way round, undoing a type + with fewer cores has to open the room before the cores come back + into it. + @param state the block to write +*/ +void ChangeCableTypeCommand::apply(const CableTypeState &state) +{ + if (m_cable) { + m_cable->applyTypeState(state); + } +} + +/** + @brief ChangeCableCoresCommand::ChangeCableCoresCommand + @param cable + @param before every core the cable wired before the change + @param after every core it wires after it + @param text what undo says about this step, empty for a core which + is merely moved +*/ +ChangeCableCoresCommand::ChangeCableCoresCommand(Cable *cable, + const QList &before, + const QList &after, + const QString &text) : + m_cable(cable), + m_before(before), + m_after(after) +{ + setText(text.isEmpty() + ? QCoreApplication::translate("ChangeCableCoresCommand", + "Déplacer une âme de câble") + : text); +} + +void ChangeCableCoresCommand::undo() +{ + apply(m_before); +} + +void ChangeCableCoresCommand::redo() +{ + apply(m_after); +} + +/** + @brief ChangeCableCoresCommand::apply + Write one whole set of assignments in place of another. The cable + says it changed, which is what makes the project work out the cable + field of every conductor it feeds again, so the drawing follows. + @param cores +*/ +void ChangeCableCoresCommand::apply(const QList &cores) +{ + if (m_cable) { + m_cable->setCores(cores); + } +} + +/** + @brief RemoveCableCommand::RemoveCableCommand + @param diagram the folios the lines are drawn on + @param parts the lines the user asked to delete +*/ +RemoveCableCommand::RemoveCableCommand(Diagram *diagram, const QList &parts) : + m_diagram(diagram) +{ + setText(QCoreApplication::translate("RemoveCableCommand", "Supprimer un câble")); + + for (CablePart *part : parts) + { + if (!part) continue; + + Entry entry; + entry.part = part; + entry.cable = part->cable(); + entry.data.uuid = part->partUuid(); + entry.data.diagram = m_diagram ? m_diagram->uuid() : QUuid(); + entry.data.p1 = part->firstPoint(); + entry.data.p2 = part->secondPoint(); + m_entries.append(entry); + + if (entry.cable && !m_cables.contains(entry.cable)) { + m_cables.append(entry.cable); + m_cores.insert(entry.cable.data(), entry.cable->usedCores()); + } + } +} + +/** + @brief RemoveCableCommand::~RemoveCableCommand + Whatever the stack leaves behind has to be cleaned up here: a cable + which is not held by the project any more, and a line which never + made it back onto a folio, belong to this command. +*/ +RemoveCableCommand::~RemoveCableCommand() +{ + QETProject *proj = project(); + for (const QPointer &cable : std::as_const(m_cables)) + { + if (cable && (!proj || !proj->cables().contains(cable))) { + delete cable; + } + } + for (const Entry &entry : std::as_const(m_entries)) + { + if (entry.part && !entry.part->scene()) { + delete entry.part; + } + } +} + +/** + @brief RemoveCableCommand::project + @return the project this command works in, or nullptr +*/ +QETProject *RemoveCableCommand::project() const +{ + return m_diagram ? m_diagram->project() : nullptr; +} + +/** + @brief RemoveCableCommand::redo + Take the lines off their folios. Each cable loses the sections which + are gone together with the cores drawn on them, and a cable which has + no section left anywhere leaves the project: a cable nothing draws is + no cable of this project. +*/ +void RemoveCableCommand::redo() +{ + QETProject *proj = project(); + + for (const Entry &entry : std::as_const(m_entries)) + { + if (entry.cable) { + entry.cable->removePart(entry.data.uuid); + } + if (entry.part && entry.part->scene() && m_diagram) { + m_diagram->removeItem(entry.part); + } + } + + if (proj) + { + for (const QPointer &cable : std::as_const(m_cables)) + { + if (cable && cable->parts().isEmpty() && proj->cables().contains(cable)) { + proj->removeCable(cable); + } + } + } + + CableManager::refreshLabels(proj); +} + +/** + @brief RemoveCableCommand::undo + Put the lines back on their folios, the sections back in their cable + and the cores back on those sections: exactly the state the delete + action took away. +*/ +void RemoveCableCommand::undo() +{ + QETProject *proj = project(); + + if (proj) + { + for (const QPointer &cable : std::as_const(m_cables)) + { + if (cable && !proj->cables().contains(cable)) { + proj->addCable(cable); + } + } + } + + for (const Entry &entry : std::as_const(m_entries)) + { + if (entry.cable) + { + if (!entry.cable->hasPart(entry.data.uuid)) { + entry.cable->addPart(entry.data); + } + for (const CableCore &core : m_cores.value(entry.cable.data())) { + entry.cable->setCore(core); + } + } + if (entry.part && !entry.part->scene() && m_diagram) { + m_diagram->addItem(entry.part); + } + } + + CableManager::refreshLabels(proj); +} diff --git a/sources/cable/editcablecommand.h b/sources/cable/editcablecommand.h new file mode 100644 index 000000000..1308b6b66 --- /dev/null +++ b/sources/cable/editcablecommand.h @@ -0,0 +1,285 @@ +/* + 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 EDITCABLECOMMAND_H +#define EDITCABLECOMMAND_H + +#include "cable.h" + +#include "../autoNum/numerotationcontext.h" + +#include +#include +#include +#include + +class CablePart; +class Diagram; +class QETProject; + +/** + @brief Puts a dragged cable line back where it was, or where the user + just dragged it to. + + The colour labels of its cores follow the line -- they are drawn on + it -- and come back with it when the move is undone. Which core lands + on which conductor never depends on where the trunk line stands, so + the assignments are only carried along by this step, never changed. +*/ +class MoveCablePartCommand : public QUndoCommand +{ + public: + MoveCablePartCommand(Cable *cable, + CablePart *part, + const QPointF &before_p1, + const QPointF &before_p2, + const QPointF &after_p1, + const QPointF &after_p2, + const QList &before_cores, + const QList &after_cores); + + void undo() override; + void redo() override; + + private: + void apply(const QPointF &p1, + const QPointF &p2, + const QList &cores); + + private: + QPointer m_cable; + QPointer m_part; + QPointF m_before_p1; + QPointF m_before_p2; + QPointF m_after_p1; + QPointF m_after_p2; + QList m_before_cores; + QList m_after_cores; +}; + +/** + @brief Rewrites which cores a cable wires, the way dragging the slash + of a core does: the whole set of assignments is taken before and + after the gesture, so undoing puts every one of them back -- the core + which was moved, but also the one it swapped colours with. +*/ +class ChangeCableCoresCommand : public QUndoCommand +{ + public: + /** + @param cable the cable whose assignments are rewritten + @param before every core it held before + @param after every core it holds after + @param text what undo says about this step: an empty string + for the usual step of moving a core around, since putting a + core down for the first time is not a move at all + */ + ChangeCableCoresCommand(Cable *cable, + const QList &before, + const QList &after, + const QString &text = QString()); + + void undo() override; + void redo() override; + + private: + void apply(const QList &cores); + + private: + QPointer m_cable; + QList m_before; + QList m_after; +}; + +/** + @brief Writes what the user typed into the fields of a cable -- and + what he ticks off again -- as one single undo step. + + All the fields belong to the cable rather than to one line of it, so + one change puts every line of that cable, on every folio, right. +*/ +class ChangeCablePropertiesCommand : public QUndoCommand +{ + public: + ChangeCablePropertiesCommand(Cable *cable, + const CableProperties &before, + const CableProperties &after); + + void undo() override; + void redo() override; + + private: + QPointer m_cable; + CableProperties m_before; + CableProperties m_after; +}; + +/** + @brief Gives every cable of a project the number the numbering rule + says it should have, laid out in the order the cables are met along + the sheets. + + That is how a project is numbered again after a cable has been taken + away: the hole its number leaves closes because the cables behind it + move up one place, and a rule which has just been changed reaches + every cable of the project at once. + + One command covers them all -- numbering a whole project is one + single act, so undo takes all of it back together. What the + numbering rule itself holds is part of that act: once the cables + are numbered, the counters of the rule stand where the last number + was taken from, and undo puts them back where they stood. +*/ +class RenumberCablesCommand : public QUndoCommand +{ + public: + /** + @param cables the cables to number, in the order they are + numbered in + @param designations the number each of them gets, one for + each cable + @param project the project whose numbering rule moves along + with the numbering + @param num_key the name that rule is kept under in the + project + @param num_before where its counters stood before the + numbering ran + @param num_after where the numbering leaves them + */ + RenumberCablesCommand(const QList &cables, + const QStringList &designations, + QETProject *project, + const QString &num_key, + const NumerotationContext &num_before, + const NumerotationContext &num_after); + + void undo() override; + void redo() override; + + private: + void apply(bool use_after); + + private: + QList> m_cables; + QStringList m_before; + QStringList m_after; + QList m_by_hand_before; + QList m_by_hand_after; + QETProject *m_project = nullptr; + QString m_num_key; + NumerotationContext m_num_before; + NumerotationContext m_num_after; +}; + +/** + @brief Changes which type a cable is -- its name, how many cores it + has and the colour of each of them -- together with the cores it + may lose on the way, as one single step. + + The cores belong to this step rather than to a step of their own: + a type with fewer cores takes marks off the drawing, and undoing has + to bring them back along with the name and the colours, never half + of it. +*/ +class ChangeCableTypeCommand : public QUndoCommand +{ + public: + /** + @param cable the cable whose type is changed + @param before the state the cable was in + @param after the state it is put into + */ + ChangeCableTypeCommand(Cable *cable, + const CableTypeState &before, + const CableTypeState &after); + + void undo() override; + void redo() override; + + private: + void apply(const CableTypeState &state); + + private: + QPointer m_cable; + CableTypeState m_before; + CableTypeState m_after; +}; + +/** + @brief Holds the steps of one single gesture together, so a single + undo takes all of them back -- an ordinary deletion with the cable + deletions which go with it, or a drag which moved several cable + lines at once. + + It keeps its steps rather than handing them to QUndoCommand as + children: Qt6.11 has no addChild() any more. +*/ +class BatchCommand : public QUndoCommand +{ + public: + BatchCommand(QUndoCommand *first, QUndoCommand *second); + BatchCommand(const QList &steps, const QString &text); + ~BatchCommand() override; + + void undo() override; + void redo() override; + + private: + QList m_steps; +}; + +/** + @brief Takes one or more drawn cable lines off their folios -- and the + cable itself with them when no line of it is left anywhere. + + Undo puts everything back: the lines on their folios, the sections in + the cable, the cores on those sections, and the cable in the project + when it had to leave it. +*/ +class RemoveCableCommand : public QUndoCommand +{ + public: + RemoveCableCommand(Diagram *diagram, const QList &parts); + ~RemoveCableCommand() override; + + void undo() override; + void redo() override; + + private: + QETProject *project() const; + + private: + struct Entry + { + ///The line as it is drawn on the folio + QPointer part; + ///Where the cable holds that section + CablePartData data; + ///The cable the section belongs to + QPointer cable; + }; + + QPointer m_diagram; + QList m_entries; + ///Every cable which loses a section here, with all the cores + ///it wires at this moment: removing a section drops the cores + ///drawn on it, and undo has to bring them back + QList> m_cables; + QHash> m_cores; +}; + +#endif // EDITCABLECOMMAND_H diff --git a/sources/cablelist/cableexportdialog.cpp b/sources/cablelist/cableexportdialog.cpp new file mode 100644 index 000000000..da439e63b --- /dev/null +++ b/sources/cablelist/cableexportdialog.cpp @@ -0,0 +1,155 @@ +/* + 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 3 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 "cableexportdialog.h" + +#include "cablelistmodel.h" +#include "cablelistrows.h" +#include "cablequerywidget.h" + +#include "../bomexport.h" +#include "../qet.h" +#include "../qetapp.h" +#include "../qetproject.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + @brief CableExportDialog::CableExportDialog + @param project the project whose cables are exported + @param parent +*/ +CableExportDialog::CableExportDialog(QETProject *project, QWidget *parent) : + QDialog(parent), + m_project(project) +{ + setWindowTitle(tr("Exporter la liste des câbles au format CSV")); + QET::trackDialogGeometry(this); + + auto *main_layout = new QVBoxLayout(this); + + //The same column picker the cable list is built with, so the + //export asks for its columns the same way the list does + m_query_widget = new CableQueryWidget(this); + main_layout->addWidget(m_query_widget); + + auto *page_group = new QGroupBox(tr("Mise en page"), this); + auto *page_layout = new QVBoxLayout(page_group); + m_include_headers = new QCheckBox(tr("inclure les en-têtes"), page_group); + m_include_headers->setChecked(true); + page_layout->addWidget(m_include_headers); + page_layout->addStretch(); + main_layout->addWidget(page_group); + + auto *preview_layout = new QHBoxLayout; + auto *preview_pb = new QPushButton(tr("Aperçu"), this); + preview_layout->addWidget(preview_pb); + preview_layout->addStretch(); + main_layout->addLayout(preview_layout); + + m_preview_table = new QTableView(this); + m_preview_table->setEditTriggers(QAbstractItemView::NoEditTriggers); + main_layout->addWidget(m_preview_table); + + auto *buttons = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + main_layout->addWidget(buttons); + connect(buttons, &QDialogButtonBox::accepted, + this, &CableExportDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, + this, &CableExportDialog::reject); + connect(preview_pb, &QPushButton::clicked, + this, &CableExportDialog::preview); + + m_model = new CableListModel(m_project, this); + m_preview_table->setModel(m_model); + preview(); + + setMinimumSize(QSize(760, 560)); +} + +/** + @brief CableExportDialog::exec + Ask for the file once the user accepted, and write the chosen + columns to it -- the same flow as the material list export + @return +*/ +int CableExportDialog::exec() +{ + const auto r = QDialog::exec(); + if (r == QDialog::Accepted) + { + QString dir = m_project->currentDir(); + if (dir.isEmpty()) dir = QETApp::documentDir(); + const QString file_name = dir + QLatin1Char('/') + + tr("liste_cables_") + m_project->title() + + QStringLiteral(".csv"); + const QString file_path = QFileDialog::getSaveFileName( + this, tr("Enregistrer sous... "), file_name, + tr("Fichiers csv (*.csv)")); + if (!file_path.isEmpty()) + { + QString error; + const auto csv = CableList::toCsv( + m_project, selectedColumns(), + m_include_headers->isChecked()); + if (!BomExport::writeCsv(file_path, csv, &error)) { + QMessageBox::critical( + this, tr("Erreur"), + tr("Impossible d'enregistrer la liste des câbles dans %1.\n%2") + .arg(file_path, error)); + } + } + } + return r; +} + +/** + @brief CableExportDialog::selectedColumns + @return the picked columns, and the default ones when the user + picked nothing -- an export of everything is what a dialog + confirmed without choosing asks for, the same fallback the drawn + table uses +*/ +QStringList CableExportDialog::selectedColumns() const +{ + auto keys = m_query_widget->selectedKeys(); + if (keys.isEmpty()) { + keys = CableList::defaultKeys(); + } + return keys; +} + +/** + @brief CableExportDialog::preview + Show what will be written: the chosen columns, worked out from the + cables of the project as they are right now +*/ +void CableExportDialog::preview() +{ + m_model->setFields(selectedColumns()); + m_preview_table->resizeColumnsToContents(); +} diff --git a/sources/cablelist/cableexportdialog.h b/sources/cablelist/cableexportdialog.h new file mode 100644 index 000000000..e14d63ec7 --- /dev/null +++ b/sources/cablelist/cableexportdialog.h @@ -0,0 +1,58 @@ +/* + 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 3 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 CABLEEXPORTDIALOG_H +#define CABLEEXPORTDIALOG_H + +#include +#include + +class QCheckBox; +class QTableView; + +class CableListModel; +class CableQueryWidget; +class QETProject; + +/** + @brief The CableExportDialog asks which columns of the cable list + are exported and writes them to a CSV file, built the same way the + BOMExportDialog does it for the material list: the column picker + the cable list is built with on top, the page settings under it, a + preview of what will be written, and the file asked for when the + dialog is accepted. +*/ +class CableExportDialog : public QDialog +{ + Q_DECLARE_TR_FUNCTIONS(CableExportDialog) + + public: + explicit CableExportDialog(QETProject *project, QWidget *parent = nullptr); + int exec() override; + + private: + QStringList selectedColumns() const; + void preview(); + + QETProject *m_project; + CableQueryWidget *m_query_widget; + CableListModel *m_model; + QCheckBox *m_include_headers; + QTableView *m_preview_table; +}; + +#endif // CABLEEXPORTDIALOG_H diff --git a/sources/cablelist/cablelistmodel.cpp b/sources/cablelist/cablelistmodel.cpp new file mode 100644 index 000000000..8991a9522 --- /dev/null +++ b/sources/cablelist/cablelistmodel.cpp @@ -0,0 +1,421 @@ +/* + 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 3 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 "cablelistmodel.h" + +#include "cablelistrows.h" + +#include "../cable/cable.h" +#include "../diagram.h" +#include "../qetgraphicsitem/element.h" +#include "../qetproject.h" +#include "../qetxml.h" +#include "../utils/qetutils.h" + +#include +#include + +#include +#include + +/** + @brief CableListModel::CableListModel + @param project : project whose cables this model lists + @param parent : parent QObject +*/ +CableListModel::CableListModel(QETProject *project, QObject *parent) : + QAbstractTableModel(parent), + m_project(project) +{ + //Left is what a table starts with and stays with: the factory + //overwrites it with the alignment chosen in the dialog, and a + //table which never got that far (no column selected at + //placement) still draws its cells left rather than from an + //empty role + m_index_0_0_data.insert(Qt::TextAlignmentRole, int(Qt::AlignLeft)); + //The same gap between the text and the cell lines the creation + //dialog offers for the material list, already in place for a + //table which starts out empty and gets its columns later -- + //the factory overwrites it with the margins chosen in the + //dialog as soon as the table has a cell to configure + m_index_0_0_data.insert(Qt::UserRole+1, + QETUtils::marginsToString(QMargins(5,5,10,5))); + connectProject(); + setFields(CableList::defaultKeys()); +} + +/** + @brief CableListModel::CableListModel + @param other_model +*/ +CableListModel::CableListModel(const CableListModel &other_model) : + QAbstractTableModel(other_model.parent()) +{ + setParent(other_model.parent()); + m_project = other_model.m_project; + m_identifier = other_model.m_identifier; + connectProject(); + setFields(other_model.m_fields); + m_header_data = other_model.m_header_data; + m_index_0_0_data = other_model.m_index_0_0_data; +} + +/** + @brief CableListModel::rowCount + Reimplemented for QAbstractTableModel + @param parent + @return +*/ +int CableListModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + //A table built without a single column shows no rows either: + //the empty frame a nomenclature table makes when it is + //created without entries, not a list of rows nobody can see + if (m_fields.isEmpty()) { + return 0; + } + + return m_rows.count(); +} + +/** + @brief CableListModel::columnCount + Reimplemented for QAbstractTableModel + @param parent + @return +*/ +int CableListModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return m_fields.count(); +} + +/** + @brief CableListModel::setHeaderData + Reimplemented from QAbstractTableModel. + Only horizontal orientation is accepted. + @param section + @param orientation + @param value + @param role + @return +*/ +bool CableListModel::setHeaderData(int section, Qt::Orientation orientation, + const QVariant &value, int role) +{ + if (orientation == Qt::Vertical) { + return false; + } + auto hash_ = m_header_data.value(section); + hash_.insert(role, value); + m_header_data.insert(section, hash_); + emit headerDataChanged(orientation, section, section); + return true; +} + +/** + @brief CableListModel::headerData + Reimplemented for QAbstractTableModel + @param section + @param orientation + @param role + @return +*/ +QVariant CableListModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical) { + return QVariant(); + } + + if (m_header_data.contains(section)) + { + auto hash_ = m_header_data.value(section); + if (role == Qt::DisplayRole && !hash_.contains(Qt::DisplayRole)) { //special case to have the same behavior as Qt + return hash_.value(Qt::EditRole); + } + return m_header_data.value(section).value(role); + } + return QVariant(); +} + +/** + @brief CableListModel::setData + Only store the data for the index 0.0. An empty table has no cells + yet -- index(0,0) does not exist without a single column -- but + its look is still settable: the factory configuring a freshly made + table must not be thrown away just because the table was placed + without entries and gains its columns afterwards. The data is read + as soon as cells exist. + @param index + @param value + @param role + @return +*/ +bool CableListModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.isValid() && (index.row() != 0 || index.column() != 0)) { + return false; + } + m_index_0_0_data.insert(role, value); + if (index.isValid()) { + emit dataChanged(index, index, {role}); + } + return true; +} + +/** + @brief CableListModel::data + Reimplemented for QAbstractTableModel + @param index + @param role + @return +*/ +QVariant CableListModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() == 0 && + index.column() == 0 && + role != Qt::DisplayRole) { + return m_index_0_0_data.value(role); + } + + if (role == Qt::DisplayRole) { + return m_rows.at(index.row()).value(m_fields.at(index.column())); + } + + return QVariant(); +} + +/** + @brief CableListModel::project + @return the project whose cables this model lists +*/ +QETProject *CableListModel::project() const +{ + return m_project.data(); +} + +/** + @brief CableListModel::setFields + Set which columns this table shows, in that order, and work the rows + out along the way. Unknown keys are dropped rather than shown as + empty columns: a file naming a column this program does not know + would draw a blank header nobody can fill. + @param fields the keys of CableList::allColumns() +*/ +void CableListModel::setFields(const QStringList &fields) +{ + QStringList kept; + for (const QString &field : fields) + { + if (kept.contains(field)) { + continue; + } + if (CableList::labelOf(field).isEmpty()) { + continue; + } + kept << field; + } + + beginResetModel(); + m_fields = kept; + //The columns themselves change, their labels are written again + //below -- but HOW a header is drawn (font, alignment, margins) + //belongs to the table, not to the column set, and stays as it + //is across a column edit. Sections which no longer exist are + //dropped, so a stale header of an old column cannot linger + for (auto it = m_header_data.begin(); it != m_header_data.end(); ) { + if (it.key() >= m_fields.count()) { + it = m_header_data.erase(it); + } else { + ++it; + } + } + for (int i = 0; i < m_fields.count(); ++i) { + m_header_data[i].insert(Qt::DisplayRole, CableList::labelOf(m_fields.at(i))); + } + m_rows = m_project ? CableList::rows(m_project) + : QVector>(); + endResetModel(); +} + +/** + @brief CableListModel::queryString + @return the fields joined the way the project file stores them +*/ +QString CableListModel::queryString() const +{ + return m_fields.join(QLatin1Char(',')); +} + +/** + @brief CableListModel::refresh + Work the rows out again, and watch everything they are worked out + from: a cable of the project, a folio, a piece a cable lands on + (its label is the BMK of an end, its Installation and Localisation + are what those two columns of an end read). +*/ +void CableListModel::refresh() +{ + for (const QMetaObject::Connection &connection : std::as_const(m_watchers)) { + QObject::disconnect(connection); + } + m_watchers.clear(); + + if (m_project) + { + const QVector cables = m_project->cables(); + for (Cable *cable : cables) { + m_watchers << connect(cable, &Cable::changed, + this, &CableListModel::refresh); + } + + const QList diagrams = m_project->diagrams(); + for (Diagram *diagram : diagrams) + { + m_watchers << connect(diagram, &Diagram::diagramInformationChanged, + this, &CableListModel::refresh); + const QList elements = diagram->elements(); + for (Element *element : elements) { + m_watchers << connect(element, &Element::elementInfoChange, + this, &CableListModel::refresh); + } + } + } + + beginResetModel(); + m_rows = m_project ? CableList::rows(m_project) + : QVector>(); + endResetModel(); +} + +/** + @brief CableListModel::connectProject + Watch the project itself: cables and folios coming and going reach + this model even though no refresh of ours was running to catch them + (the rows of a project being read from its file are filled cable by + cable, one signal at a time). +*/ +void CableListModel::connectProject() +{ + if (!m_project) { + return; + } + + connect(m_project, &QETProject::cableAdded, this, &CableListModel::refresh); + connect(m_project, &QETProject::cableRemoved, this, &CableListModel::refresh); + connect(m_project, &QETProject::diagramAdded, this, &CableListModel::refresh); + connect(m_project, &QETProject::diagramRemoved, this, &CableListModel::refresh); +} + +/** + @brief CableListModel::toXml + Save the model to xml: which columns are shown, the look of the + cells and of the header -- the rows themselves are not saved, they + are worked out again from the cables whenever the project opens. + @param document + @return +*/ +QDomElement CableListModel::toXml(QDomDocument &document) const +{ + auto dom_element = document.createElement(xmlTagName()); + + //Identifier + auto dom_identifier = document.createElement("identifier"); + auto dom_identifier_text = document.createTextNode(m_identifier); + dom_identifier.appendChild(dom_identifier_text); + dom_element.appendChild(dom_identifier); + + //Fields + auto dom_fields = document.createElement("fields"); + auto dom_fields_text = document.createTextNode(queryString()); + dom_fields.appendChild(dom_fields_text); + dom_element.appendChild(dom_fields); + + //Add index 0,0 data + auto index_00 = document.createElement("index00"); + index_00.setAttribute("font", QETUtils::fontToString(m_index_0_0_data.value(Qt::FontRole).value())); + auto me = QMetaEnum::fromType(); + index_00.setAttribute("alignment", me.valueToKey(m_index_0_0_data.value(Qt::TextAlignmentRole).toInt())); + dom_element.appendChild(index_00); + index_00.setAttribute("margins", m_index_0_0_data.value(Qt::UserRole+1).toString()); + + //header data + QHash> horizontal_; + for (auto key : m_header_data.keys()) + { + //We save all data except the display role, because it is + //generated from the fields above (same reason and same sorted + //order as ProjectDBModel, so the file stays reproducible) + auto list = m_header_data.value(key).keys(); + list.removeAll(Qt::DisplayRole); + std::sort(list.begin(), list.end()); + + horizontal_.insert(key, list); + } + + dom_element.appendChild(QETXML::modelHeaderDataToXml(document, this, horizontal_, QHash>())); + + return dom_element; +} + +/** + @brief CableListModel::fromXml + Restore the model from xml + @param element +*/ +void CableListModel::fromXml(const QDomElement &element) +{ + if (element.tagName() != xmlTagName()) + return; + + setIdentifier(element.firstChildElement("identifier").text()); + setFields(element.firstChildElement("fields").text() + .split(QLatin1Char(','), Qt::SkipEmptyParts)); + + //Index 0,0 + auto index_00 = element.firstChildElement("index00"); + QFont font_; + QETUtils::fontFromString(font_, index_00.attribute("font")); + m_index_0_0_data.insert(Qt::FontRole, font_); + auto me = QMetaEnum::fromType(); + const int alignment = me.keyToValue(index_00.attribute("alignment") + .toStdString().data()); + //A file written without an alignment (an empty or unknown enum + //key) would restore as -1, whose bits draw every cell right- + //aligned -- left is the default instead + m_index_0_0_data.insert(Qt::TextAlignmentRole, + alignment > 0 ? alignment : int(Qt::AlignLeft)); + //A file saved without cell margins (one written before the + //look of an empty table was kept) restores with the same + //default the creation dialog offers, instead of text drawn + //against the cell lines + const QString margins_ = index_00.attribute("margins"); + m_index_0_0_data.insert(Qt::UserRole+1, + margins_.isEmpty() + ? QETUtils::marginsToString(QMargins(5,5,10,5)) + : margins_); + + QETXML::modelHeaderDataFromXml(element.firstChildElement("header_data"), this); +} diff --git a/sources/cablelist/cablelistmodel.h b/sources/cablelist/cablelistmodel.h new file mode 100644 index 000000000..376c790a4 --- /dev/null +++ b/sources/cablelist/cablelistmodel.h @@ -0,0 +1,107 @@ +/* + 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 3 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 CABLELISTMODEL_H +#define CABLELISTMODEL_H + +#include +#include +#include +#include +#include +#include +#include +#include + +class Cable; +class Diagram; +class Element; +class QETProject; + +/** + @brief The CableListModel class + + The rows of a cable list, shown by QetGraphicsTableItem the way the + project database model shows a nomenclature -- and shaped after it + on purpose: same roles, same header behaviour, same way of saving + itself to the project file, so that the table on the sheet treats + this model exactly like any other. + + Whereas ProjectDBModel asks the project database, this model asks + CableList::rows(), straight from the cables of the project. It + refreshes itself whenever anything those rows are worked out from + changes: a cable edited, a cable added or removed, a folio added, + removed or re-informed (its title block carries the installation and + the place), a label typed over on a piece a cable lands on. +*/ +class CableListModel : public QAbstractTableModel +{ + Q_OBJECT + + public: + explicit CableListModel(QETProject *project, QObject *parent = nullptr); + CableListModel(const CableListModel &other_model); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + bool setHeaderData(int section, + Qt::Orientation orientation, + const QVariant &value, + int role = Qt::EditRole) override; + QVariant headerData(int section, + Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, + const QVariant &value, + int role = Qt::EditRole) override; + QVariant data(const QModelIndex &index, + int role = Qt::DisplayRole) const override; + + /// The columns this table shows, in order: keys of CableList + void setFields(const QStringList &fields); + QStringList fields() const {return m_fields;} + /// The same, joined the way the project file stores it + QString queryString() const; + void setIdentifier(const QString &identifier) {m_identifier = identifier;} + QString identifier() const {return m_identifier;} + QETProject *project() const; + + QDomElement toXml(QDomDocument &document) const; + void fromXml(const QDomElement &element); + static QString xmlTagName() {return QStringLiteral("cable_list_model");} + + public slots: + /// Work the rows out again and tell whoever shows them + void refresh(); + + private: + /// Watch everything the rows are worked out from + void connectProject(); + + QPointer m_project; + QStringList m_fields; + QVector> m_rows; + //First int = section, second int = Qt::role, QVariant = value + QHash> m_header_data; + QHash m_index_0_0_data; + QString m_identifier = QStringLiteral("cable_list"); + /// The current watchers of this model, dropped and taken + /// again on every refresh + QList m_watchers; +}; + +#endif // CABLELISTMODEL_H diff --git a/sources/cablelist/cablelistmodelpropertieswidget.cpp b/sources/cablelist/cablelistmodelpropertieswidget.cpp new file mode 100644 index 000000000..217cb85fb --- /dev/null +++ b/sources/cablelist/cablelistmodelpropertieswidget.cpp @@ -0,0 +1,100 @@ +/* + 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 3 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 "cablelistmodelpropertieswidget.h" + +#include "cablelistmodel.h" +#include "cablequerywidget.h" + +#include +#include +#include +#include +#include + +/** + @brief CableListModelPropertiesWidget::CableListModelPropertiesWidget + @param model + @param parent +*/ +CableListModelPropertiesWidget::CableListModelPropertiesWidget( + CableListModel *model, + QWidget *parent) : + PropertiesEditorWidget(parent) +{ + auto *layout = new QHBoxLayout(this); + m_edit_pb = new QPushButton(tr("Modifier les colonnes..."), this); + m_refresh_pb = new QPushButton(tr("Rafraîchir"), this); + layout->addWidget(m_edit_pb); + layout->addWidget(m_refresh_pb); + layout->addStretch(); + + connect(m_edit_pb, &QPushButton::clicked, this, &CableListModelPropertiesWidget::editColumns); + connect(m_refresh_pb, &QPushButton::clicked, this, &CableListModelPropertiesWidget::refreshRows); + + setModel(model); +} + +/** + @brief CableListModelPropertiesWidget::setModel + @param model +*/ +void CableListModelPropertiesWidget::setModel(CableListModel *model) +{ + m_model = model; + m_edit_pb->setEnabled(m_model != nullptr); + m_refresh_pb->setEnabled(m_model != nullptr); +} + +/** + @brief CableListModelPropertiesWidget::editColumns + Ask for the columns again and put them on the table right away -- + the same immediate, not-undoable way the database table changes its + query +*/ +void CableListModelPropertiesWidget::editColumns() +{ + if (!m_model) { + return; + } + + QDialog d(this); + d.setWindowTitle(tr("Colonnes de la liste des câbles")); + auto *layout = new QVBoxLayout(&d); + auto *widget = new CableQueryWidget(&d); + widget->setFields(m_model->fields()); + layout->addWidget(widget); + + auto *button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + layout->addWidget(button_box); + connect(button_box, &QDialogButtonBox::accepted, &d, &QDialog::accept); + connect(button_box, &QDialogButtonBox::rejected, &d, &QDialog::reject); + + if (d.exec()) { + m_model->setFields(widget->selectedKeys()); + } +} + +/** + @brief CableListModelPropertiesWidget::refreshRows +*/ +void CableListModelPropertiesWidget::refreshRows() +{ + if (m_model) { + m_model->refresh(); + } +} diff --git a/sources/cablelist/cablelistmodelpropertieswidget.h b/sources/cablelist/cablelistmodelpropertieswidget.h new file mode 100644 index 000000000..908a8b178 --- /dev/null +++ b/sources/cablelist/cablelistmodelpropertieswidget.h @@ -0,0 +1,55 @@ +/* + 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 3 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 CABLELISTMODELPROPERTIESWIDGET_H +#define CABLELISTMODELPROPERTIESWIDGET_H + +#include "../../PropertiesEditor/propertieseditorwidget.h" + +class CableListModel; +class QPushButton; + +/** + @brief The CableListModelPropertiesWidget class + The editor shown when a cable list table is selected: change which + columns the table shows, or work its rows out again. Shaped after + ProjectDBModelPropertiesWidget, which changes its query the same + immediate, not-undoable way -- the table itself is undoable to move + or delete, its content always follows the cables. +*/ +class CableListModelPropertiesWidget : public PropertiesEditorWidget +{ + Q_OBJECT + + public: + explicit CableListModelPropertiesWidget(CableListModel *model = nullptr, + QWidget *parent = nullptr); + ~CableListModelPropertiesWidget() override = default; + + void setModel(CableListModel *model); + + private slots: + void editColumns(); + void refreshRows(); + + private: + CableListModel *m_model = nullptr; + QPushButton *m_edit_pb = nullptr; + QPushButton *m_refresh_pb = nullptr; +}; + +#endif // CABLELISTMODELPROPERTIESWIDGET_H diff --git a/sources/cablelist/cablelistrows.cpp b/sources/cablelist/cablelistrows.cpp new file mode 100644 index 000000000..6f1e1ac42 --- /dev/null +++ b/sources/cablelist/cablelistrows.cpp @@ -0,0 +1,468 @@ +/* + 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 3 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 "cablelistrows.h" + +#include "../bomexport.h" +#include "../cable/cable.h" +#include "../conductorproperties.h" +#include "../diagram.h" +#include "../qetgraphicsitem/conductor.h" +#include "../qetgraphicsitem/element.h" +#include "../qetgraphicsitem/terminal.h" +#include "../qetinformation.h" +#include "../qetproject.h" + +#include +#include + +#include +#include +#include + +namespace { + +/** + @brief One place where the line of a cable ends. + + An end which lands on a piece carries that piece (its uuid for + grouping and its pointer for reading the label, both valid as long + as the plan lives); an end taken from the drawn line carries none, + and its tie names the section instead. +*/ +struct CableEnd +{ + Diagram *diagram = nullptr; + QPointF scene; + Element *element = nullptr; + QUuid element_uuid; + QString tie; + + bool isLanding() const {return !element_uuid.isNull();} +}; + +/** + @brief endBefore + The order the ends of the cables are read in: the earlier sheet + first, then the higher point of that sheet, then the left one, then + a stable name so that a redraw can never swap two ends and shuffle + the rows around. + @param a first end + @param b second end + @param project the project the sheet numbers come from + @return true when a comes before b +*/ +bool endBefore(const CableEnd &a, const CableEnd &b, const QETProject *project) +{ + const int folio_a = a.diagram ? project->folioIndex(a.diagram) : INT_MAX; + const int folio_b = b.diagram ? project->folioIndex(b.diagram) : INT_MAX; + if (folio_a != folio_b) { + return folio_a < folio_b; + } + if (a.scene.y() != b.scene.y()) { + return a.scene.y() < b.scene.y(); + } + if (a.scene.x() != b.scene.x()) { + return a.scene.x() < b.scene.x(); + } + return a.tie < b.tie; +} + +/** + @brief folioOf + @param end + @return the sheet number the end lies on, empty when it lies on no + sheet at all (a cable nothing was ever drawn for) +*/ +QString folioOf(const CableEnd &end, const QETProject *project) +{ + return end.diagram ? QString::number(project->folioIndex(end.diagram) + 1) + : QString(); +} + +/** + @brief plantOf + @return the Installation the piece the end lands on carries -- the + same field every other list reads for a piece. A piece without one + leaves the cell empty: there is no second place to look for it, and + an end of the drawn line, which lands on no piece at all, fills + nothing either. +*/ +QString plantOf(const CableEnd &end) +{ + if (!end.element) { + return QString(); + } + return end.element->elementInformations() + .value(QETInformation::ELMT_PLANT).toString(); +} + +/** + @brief locmachOf + @return the Localisation the piece the end lands on carries, with + the same rule as the Installation: what the piece carries or + nothing at all. +*/ +QString locmachOf(const CableEnd &end) +{ + if (!end.element) { + return QString(); + } + return end.element->elementInformations() + .value(QETInformation::ELMT_LOCATION).toString(); +} + +/** + @brief labelOfEnd + @return the label (the BMK) of the piece the end lands on, empty for + an end of the drawn line -- there is no piece there to carry one +*/ +QString labelOfEnd(const CableEnd &end) +{ + if (!end.element) { + return QString(); + } + return end.element->elementInformations() + .value(QETInformation::ELMT_LABEL).toString(); +} + +} // namespace + +/** + @brief CableList::allColumns + @return every column of the cable list, in the order they are + offered, drawn and exported +*/ +const QList &CableList::allColumns() +{ + static const QList columns = { + {QStringLiteral("nr"), tr("N°")}, + {QStringLiteral("installation_cable"), tr("Installation câble")}, + {QStringLiteral("location_cable"), tr("Localisation câble")}, + {QStringLiteral("bmk_cable"), tr("Label câble")}, + {QStringLiteral("installation_start"), tr("Installation début")}, + {QStringLiteral("location_start"), tr("Localisation début")}, + {QStringLiteral("bmk_start"), tr("Label début")}, + {QStringLiteral("folio_start"), tr("Folio début")}, + {QStringLiteral("installation_end"), tr("Installation fin")}, + {QStringLiteral("location_end"), tr("Localisation fin")}, + {QStringLiteral("bmk_end"), tr("Label fin")}, + {QStringLiteral("folio_end"), tr("Folio fin")}, + {QStringLiteral("type"), tr("Type de câble")}, + {QStringLiteral("length"), tr("Longueur")}, + {QStringLiteral("cores_used"), tr("Âmes utilisées")}, + }; + return columns; +} + +/** + @brief CableList::defaultKeys + @return the columns a freshly drawn cable list starts with: all of + them +*/ +const QStringList &CableList::defaultKeys() +{ + static const QStringList keys = []() { + QStringList list; + for (const Column &column : allColumns()) { + list << column.key; + } + return list; + }(); + return keys; +} + +/** + @brief CableList::labelOf + @param key + @return the header text of that column, empty when the key names no + column of this list +*/ +QString CableList::labelOf(const QString &key) +{ + for (const Column &column : allColumns()) { + if (column.key == key) { + return column.label; + } + } + return QString(); +} + +/** + @brief CableList::rows + Work out one row per cable, in plan order. + @param project the project whose cables are listed + @return the rows, sorted, the number column already filled in +*/ +QVector> CableList::rows(QETProject *project) +{ + QVector> result; + if (!project) { + return result; + } + + //Every conductor of every cable is walked once, and where one + //lands on a real piece becomes an arrival of that cable. A core + //which ends on a folio report is skipped on that side: the + //cable goes on over there, and the landing on the far sheet is + //what counts as its end. + QHash> arrivals; + for (Diagram *diagram : project->diagrams()) + { + for (Conductor *conductor : diagram->conductors()) + { + const ConductorProperties properties = conductor->properties(); + if (properties.m_cable_uuid.isNull()) { + continue; + } + if (!conductor->terminal1 || !conductor->terminal2) { + continue; + } + + Terminal *terminals[2] = {conductor->terminal1, conductor->terminal2}; + for (Terminal *terminal : terminals) + { + Element *element = terminal->parentElement(); + if (!element) { + //a free end is not an arrival + continue; + } + if (element->linkType() & Element::AllReport) { + //the cable continues on the linked sheet + continue; + } + + CableEnd end; + end.diagram = element->diagram(); + end.scene = terminal->scenePos(); + end.element = element; + end.element_uuid = element->uuid(); + end.tie = conductor->uuid().toString(); + arrivals[properties.m_cable_uuid].append(end); + } + } + } + + struct Entry + { + Cable *cable = nullptr; + CableEnd start; + CableEnd target; + }; + + auto byPlan = [project](const CableEnd &a, const CableEnd &b) { + return endBefore(a, b, project); + }; + + QVector entries; + entries.reserve(project->cables().count()); + + for (Cable *cable : project->cables()) + { + //Several cores landing on one piece are one end: the highest + //of their landings speaks for it. + QHash grouped; + for (const CableEnd &end : arrivals.value(cable->uuid())) + { + auto it = grouped.find(end.element_uuid); + if (it == grouped.end() || byPlan(end, it.value())) { + grouped.insert(end.element_uuid, end); + } + } + QList landings = grouped.values(); + std::sort(landings.begin(), landings.end(), byPlan); + + //The free ends of the drawn line, for the sides no core ever + //landed on + QList line_ends; + for (const CablePartData &part : cable->parts()) + { + Diagram *diagram = project->diagramByUuid(part.diagram); + if (!diagram) { + continue; + } + for (int i = 0; i < 2; ++i) + { + CableEnd end; + end.diagram = diagram; + end.scene = i ? part.p2 : part.p1; + end.tie = part.uuid.toString() + (i ? QStringLiteral("2") + : QStringLiteral("1")); + line_ends.append(end); + } + } + std::sort(line_ends.begin(), line_ends.end(), byPlan); + + Entry entry; + entry.cable = cable; + + if (landings.size() >= 2) + { + //The rule as asked for: the first finding counts for + //both ends. First landing in plan order is the start, + //the very next finding is the target -- not the last + //one. A sheet further along finds the same cable again + //but start and target are already taken by then, so it + //adds nothing: on the first sheet, the upper piece is + //the start and the lower one the target. + entry.start = landings.first(); + entry.target = landings.at(1); + } + else if (landings.size() == 1) + { + //One side is wired: that side carries the label, the + //other side falls back to the drawn line, farthest end + //first so a tie goes to the later point of the plan. + entry.start = landings.first(); + entry.target = landings.first(); + qreal farthest = -1.0; + for (const CableEnd &candidate : std::as_const(line_ends)) + { + const qreal distance = QLineF(entry.start.scene, candidate.scene).length(); + if (distance >= farthest) { + farthest = distance; + entry.target = candidate; + } + } + } + else if (line_ends.size() >= 2) + { + entry.start = line_ends.first(); + entry.target = line_ends.last(); + } + else if (line_ends.size() == 1) + { + entry.start = line_ends.first(); + entry.target = line_ends.first(); + } + //A cable with neither a core nor a line draws no ends at all; + //its own fields still fill the row. + + entries.append(entry); + } + + //Plan order again for the rows themselves: by the sheet the start + //lies on, then from the top of that sheet down. The name and the + //uuid only ever break ties, so the numbering is stable between + //two openings of the same project. + std::sort(entries.begin(), entries.end(), [project](const Entry &a, const Entry &b) { + if (endBefore(a.start, b.start, project)) return true; + if (endBefore(b.start, a.start, project)) return false; + if (endBefore(a.target, b.target, project)) return true; + if (endBefore(b.target, a.target, project)) return false; + if (a.cable->designation() != b.cable->designation()) { + return a.cable->designation() < b.cable->designation(); + } + return a.cable->uuid().toString() < b.cable->uuid().toString(); + }); + + int number = 0; + for (const Entry &entry : std::as_const(entries)) + { + Cable *cable = entry.cable; + QMap row; + row.insert(QStringLiteral("nr"), QString::number(++number)); + row.insert(QStringLiteral("installation_cable"), cable->installation()); + row.insert(QStringLiteral("location_cable"), cable->location()); + row.insert(QStringLiteral("bmk_cable"), cable->designation()); + row.insert(QStringLiteral("installation_start"), plantOf(entry.start)); + row.insert(QStringLiteral("location_start"), locmachOf(entry.start)); + row.insert(QStringLiteral("bmk_start"), labelOfEnd(entry.start)); + row.insert(QStringLiteral("folio_start"), folioOf(entry.start, project)); + row.insert(QStringLiteral("installation_end"), plantOf(entry.target)); + row.insert(QStringLiteral("location_end"), locmachOf(entry.target)); + row.insert(QStringLiteral("bmk_end"), labelOfEnd(entry.target)); + row.insert(QStringLiteral("folio_end"), folioOf(entry.target, project)); + row.insert(QStringLiteral("type"), cable->type()); + row.insert(QStringLiteral("length"), cable->length()); + row.insert(QStringLiteral("cores_used"), + QStringLiteral("%1/%2") + .arg(cable->usedCoreCount()) + .arg(cable->coreCount())); + result.append(row); + } + + return result; +} + +/** + @brief CableList::toCsv + The whole list as CSV bytes: every column of the list, every cable + of the project, in the format every CSV export of this program + writes. + @param project the project whose cables are listed + @return the bytes to write +*/ +/** + @brief CableList::toCsv + @param project the project whose cables are listed + @param keys the columns to write, in that order; unknown keys are + dropped, and a list without a single usable one falls back to the + default columns rather than writing a blank file + @param include_headers whether the line of column names is written + as the first line + @return the CSV bytes in the format every CSV export of this + program writes (UTF-8 BOM, ';', every field quoted) +*/ +QByteArray CableList::toCsv(QETProject *project, const QStringList &keys, + bool include_headers) +{ + QStringList kept; + for (const QString &key : keys) + { + if (kept.contains(key) || labelOf(key).isEmpty()) { + continue; + } + kept << key; + } + if (kept.isEmpty()) { + kept = defaultKeys(); + } + + QByteArray csv("\xEF\xBB\xBF"); + if (include_headers) { + QStringList headers; + for (const QString &key : std::as_const(kept)) { + headers << labelOf(key); + } + csv += BomExport::csvRecord(headers); + } + + const QVector> rows_ = rows(project); + for (const QMap &row : rows_) + { + QStringList values; + for (const QString &key : std::as_const(kept)) { + values << row.value(key); + } + csv += BomExport::csvRecord(values); + } + + return csv; +} + +/** + @brief CableList::toCsv + The whole list as it has always been written: every column, every + cable, the line of column names included + @param project the project whose cables are listed + @return the CSV bytes +*/ +QByteArray CableList::toCsv(QETProject *project) +{ + return toCsv(project, defaultKeys(), true); +} diff --git a/sources/cablelist/cablelistrows.h b/sources/cablelist/cablelistrows.h new file mode 100644 index 000000000..4f3f80988 --- /dev/null +++ b/sources/cablelist/cablelistrows.h @@ -0,0 +1,101 @@ +/* + 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 3 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 CABLELISTROWS_H +#define CABLELISTROWS_H + +#include +#include +#include +#include +#include +#include + +class QETProject; + +/** + @brief The CableList class + + The columns and the rows of the cable list (menu "Listes": the entry + which puts such a table on a sheet, and the CSV export which writes + the same table to a file). + + Everything which decides what a cable list row says lives here, so + that the table drawn on the sheet and the exported CSV can never + disagree: one row per cable, worked out once and read twice. + + The two ends of a cable are worked out the way the user asked for + it: + + - wherever the cores of the cable land on a real piece (a terminal + strip, a device) is an end. Several cores landing on the same + piece are one end. A core which ends on a folio report is not an + end at all -- the cable goes on over there, and the landing on the + far sheet is the end instead, which is what makes a cable running + over several sheets show its start on the earlier sheet and its + target on the later one; + - ends are ordered by sheet first (the earlier sheet wins), then by + height in the plan (the higher one wins), then left to right; + the first end is the start, the last one the target; + - a cable whose cores never landed anywhere (not drawn yet, or only + wired in the middle) falls back to the two free ends of its drawn + line; when there is only one landing, the other side falls back to + the line end farthest away from it. +*/ +class CableList +{ + Q_DECLARE_TR_FUNCTIONS(CableList) + + public: + struct Column + { + /// The name the row hash, the saved file and the CSV use + QString key; + /// What the header over the column says + QString label; + }; + + /// Every column the list can offer, in the order they are + /// offered and exported + static const QList &allColumns(); + /// The columns a fresh list starts with: all of them but the + /// ones marked optional above + static const QStringList &defaultKeys(); + /// The header text of one column, empty when the key is not + /// a column of this list + static QString labelOf(const QString &key); + + /// One row per cable, sorted: first by the sheet its start + /// lies on, then from the top of that sheet down, the number + /// column filled in along the way + static QVector> rows(QETProject *project); + + /// The whole list as CSV bytes, in the format every CSV + /// export of this program writes (UTF-8 BOM, ';', every + /// field quoted): every column, every cable, the line of + /// column names included + static QByteArray toCsv(QETProject *project); + /// The same, but only the given columns, in the given + /// order, and only with the line of column names when asked + /// for -- what the export dialog writes. Unknown keys are + /// dropped, and no usable key at all falls back to the + /// default columns + static QByteArray toCsv(QETProject *project, const QStringList &keys, + bool include_headers); +}; + +#endif // CABLELISTROWS_H diff --git a/sources/cablelist/cablequerywidget.cpp b/sources/cablelist/cablequerywidget.cpp new file mode 100644 index 000000000..0496b3ade --- /dev/null +++ b/sources/cablelist/cablequerywidget.cpp @@ -0,0 +1,319 @@ +/* + 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 3 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 "cablequerywidget.h" + +#include "cablelistrows.h" + +#include "../qetapp.h" +#include "../ui/configsaveloaderwidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +/** + @brief CableQueryWidget::CableQueryWidget + @param parent +*/ +CableQueryWidget::CableQueryWidget(QWidget *parent) : + QWidget(parent) +{ + auto *layout = new QVBoxLayout(this); + + auto *lists = new QHBoxLayout; + + auto *left = new QVBoxLayout; + left->addWidget(new QLabel(tr("Colonnes disponibles"), this)); + m_available_list = new QListWidget(this); + left->addWidget(m_available_list); + + auto *buttons = new QVBoxLayout; + buttons->addStretch(); + auto make_button = [this](QStyle::StandardPixmap icon, const QString &tip) { + auto *button = new QPushButton(this); + button->setIcon(style()->standardIcon(icon)); + button->setToolTip(tip); + return button; + }; + auto *add_pb = make_button(QStyle::SP_ArrowRight, tr("Ajouter la colonne sélectionnée")); + auto *remove_pb = make_button(QStyle::SP_ArrowLeft, tr("Retirer la colonne sélectionnée")); + auto *up_pb = make_button(QStyle::SP_ArrowUp, tr("Monter la colonne sélectionnée")); + auto *down_pb = make_button(QStyle::SP_ArrowDown, tr("Descendre la colonne sélectionnée")); + buttons->addWidget(add_pb); + buttons->addWidget(remove_pb); + buttons->addWidget(up_pb); + buttons->addWidget(down_pb); + buttons->addStretch(); + + auto *right = new QVBoxLayout; + right->addWidget(new QLabel(tr("Colonnes du tableau"), this)); + m_choosen_list = new QListWidget(this); + right->addWidget(m_choosen_list); + + lists->addLayout(left); + lists->addLayout(buttons); + lists->addLayout(right); + layout->addLayout(lists); + + m_config_gb = new ConfigSaveLoaderWidget(this); + layout->addWidget(m_config_gb); + + setMinimumSize(QSize(520, 420)); + + setUpItems(); + fillSavedQuery(); + + connect(m_config_gb, &ConfigSaveLoaderWidget::saveClicked, this, &CableQueryWidget::saveConfig); + connect(m_config_gb, &ConfigSaveLoaderWidget::loadClicked, this, &CableQueryWidget::loadConfig); + + connect(add_pb, &QPushButton::clicked, this, &CableQueryWidget::addField); + connect(remove_pb, &QPushButton::clicked, this, &CableQueryWidget::removeField); + connect(up_pb, &QPushButton::clicked, this, &CableQueryWidget::moveFieldUp); + connect(down_pb, &QPushButton::clicked, this, &CableQueryWidget::moveFieldDown); + + connect(m_available_list, &QListWidget::itemDoubleClicked, + this, &CableQueryWidget::on_m_available_list_itemDoubleClicked); + connect(m_choosen_list, &QListWidget::itemDoubleClicked, + this, &CableQueryWidget::on_m_choosen_list_itemDoubleClicked); +} + +/** + @brief CableQueryWidget::~CableQueryWidget +*/ +CableQueryWidget::~CableQueryWidget() +{ +} + +/** + @brief CableQueryWidget::selectedKeys + @return the chosen columns, in order +*/ +QStringList CableQueryWidget::selectedKeys() const +{ + QStringList keys; + int row = 0; + while (auto *item = m_choosen_list->item(row)) + { + keys.append(item->data(Qt::UserRole).toString()); + ++row; + } + + return keys; +} + +/** + @brief CableQueryWidget::setFields + Put exactly these columns in the right list, in that order. Keys + this list does not know are ignored, which is what keeps a file + coming from a newer program readable here. + @param fields +*/ +void CableQueryWidget::setFields(const QStringList &fields) +{ + //Ugly hack to force to remove all chosen columns, the same way + //SummaryQueryWidget::reset() does it + while (auto *item = m_choosen_list->takeItem(0)) { + m_available_list->addItem(item); + } + + for (const QString &key : fields) + { + for (int row = 0; row < m_available_list->count(); ++row) + { + QListWidgetItem *item = m_available_list->item(row); + if (item->data(Qt::UserRole).toString() == key) { + m_available_list->takeItem(row); + m_choosen_list->addItem(item); + break; + } + } + } +} + +/** + @brief CableQueryWidget::setUpItems + Fill the available list with every column the list offers and + leave them there: a fresh dialog opens with its entries on the + left, the same way the material list dialog does. What a table or + an export starts with when nothing has been picked is the default + set of columns, applied where the table or the file is made. +*/ +void CableQueryWidget::setUpItems() +{ + for (const CableList::Column &column : CableList::allColumns()) + { + auto *item = new QListWidgetItem(column.label, m_available_list); + item->setData(Qt::UserRole, column.key); + } +} + +/** + @brief CableQueryWidget::fillSavedQuery + Fill the combo box of the saved configurations +*/ +void CableQueryWidget::fillSavedQuery() +{ + QFile file(QETApp::configDir() % QStringLiteral("/cable_list.json")); + if (file.open(QFile::ReadOnly)) + { + QJsonDocument jsd(QJsonDocument::fromJson(file.readAll())); + QJsonObject jso = jsd.object(); + + for (auto it = jso.begin() ; it != jso.end() ; ++it) { + m_config_gb->addItem(it.key()); + } + } +} + +/** + @brief CableQueryWidget::on_m_available_list_itemDoubleClicked + @param item +*/ +void CableQueryWidget::on_m_available_list_itemDoubleClicked(QListWidgetItem *item) +{ + Q_UNUSED(item) + addField(); +} + +/** + @brief CableQueryWidget::on_m_choosen_list_itemDoubleClicked + @param item +*/ +void CableQueryWidget::on_m_choosen_list_itemDoubleClicked(QListWidgetItem *item) +{ + Q_UNUSED(item) + removeField(); +} + +/** + @brief CableQueryWidget::addField +*/ +void CableQueryWidget::addField() +{ + if (auto *item = m_available_list->takeItem(m_available_list->currentRow())) { + m_choosen_list->addItem(item); + } +} + +/** + @brief CableQueryWidget::removeField +*/ +void CableQueryWidget::removeField() +{ + if (auto *item = m_choosen_list->takeItem(m_choosen_list->currentRow())) { + m_available_list->addItem(item); + } +} + +/** + @brief CableQueryWidget::moveFieldUp +*/ +void CableQueryWidget::moveFieldUp() +{ + auto row = m_choosen_list->currentRow(); + if (row <= 0) { + return; + } + + auto *item = m_choosen_list->takeItem(row); + m_choosen_list->insertItem(row - 1, item); + m_choosen_list->setCurrentItem(item); +} + +/** + @brief CableQueryWidget::moveFieldDown +*/ +void CableQueryWidget::moveFieldDown() +{ + auto row = m_choosen_list->currentRow(); + if (row == -1) { + return; + } + + auto *item = m_choosen_list->takeItem(row); + m_choosen_list->insertItem(row + 1, item); + m_choosen_list->setCurrentItem(item); +} + +/** + @brief CableQueryWidget::saveConfig +*/ +void CableQueryWidget::saveConfig() +{ + QFile file_(QETApp::configDir() % QStringLiteral("/cable_list.json")); + + if (file_.open(QFile::ReadWrite)) + { + QJsonDocument doc_(QJsonDocument::fromJson(file_.readAll())); + QJsonObject root_object; + + if (!doc_.isEmpty()) + { + root_object = doc_.object(); + if (root_object.contains(m_config_gb->text())) { + root_object.remove(m_config_gb->text()); + } + } + + QJsonObject object_; + object_.insert(QStringLiteral("fields"), selectedKeys().join(QLatin1Char(','))); + root_object[m_config_gb->text()] = object_; + + doc_.setObject(root_object); + file_.resize(0); + file_.write(doc_.toJson()); + } +} + +/** + @brief CableQueryWidget::loadConfig +*/ +void CableQueryWidget::loadConfig() +{ + auto name = m_config_gb->selectedText(); + if (name.isEmpty()) { + return; + } + + QFile file_(QETApp::configDir() % QStringLiteral("/cable_list.json")); + if (!file_.open(QFile::ReadOnly)) { + return; + } + + QJsonDocument doc_(QJsonDocument::fromJson(file_.readAll())); + QJsonObject object_ = doc_.object(); + + auto value = object_.value(name); + if (!value.isObject()) { + return; + } + + auto fields = value.toObject().value(QStringLiteral("fields")); + if (fields.isString()) { + setFields(fields.toString().split(QLatin1Char(','), Qt::SkipEmptyParts)); + } +} diff --git a/sources/cablelist/cablequerywidget.h b/sources/cablelist/cablequerywidget.h new file mode 100644 index 000000000..d1d4d7738 --- /dev/null +++ b/sources/cablelist/cablequerywidget.h @@ -0,0 +1,71 @@ +/* + 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 3 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 CABLEQUERYWIDGET_H +#define CABLEQUERYWIDGET_H + +#include + +class ConfigSaveLoaderWidget; +class QListWidget; +class QListWidgetItem; +class QPushButton; + +/** + @brief The CableQueryWidget class + The content tab of the dialog which puts a cable list on a sheet: + which columns the list shows, and in which order. Shaped after + SummaryQueryWidget -- two lists with arrows between them, a saved + configuration per name -- but the columns are chosen out of the + fixed set of CableList instead of out of a SQL query, so there is no + query line to edit. +*/ +class CableQueryWidget : public QWidget +{ + Q_OBJECT + + public: + explicit CableQueryWidget(QWidget *parent = nullptr); + ~CableQueryWidget(); + + static QString modelIdentifier() {return "cable_list";} + /// The chosen columns, in order + QStringList selectedKeys() const; + /// Put exactly these columns in the right list, in that + /// order; keys this list does not know are ignored + void setFields(const QStringList &fields); + + private slots: + void on_m_available_list_itemDoubleClicked(QListWidgetItem *item); + void on_m_choosen_list_itemDoubleClicked(QListWidgetItem *item); + void addField(); + void removeField(); + void moveFieldUp(); + void moveFieldDown(); + void saveConfig(); + void loadConfig(); + + private: + void setUpItems(); + void fillSavedQuery(); + + QListWidget *m_available_list = nullptr; + QListWidget *m_choosen_list = nullptr; + ConfigSaveLoaderWidget *m_config_gb = nullptr; +}; + +#endif // CABLEQUERYWIDGET_H diff --git a/sources/cablelist/cabletypelist.cpp b/sources/cablelist/cabletypelist.cpp new file mode 100644 index 000000000..cf57cbb3a --- /dev/null +++ b/sources/cablelist/cabletypelist.cpp @@ -0,0 +1,790 @@ +/* + 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 "cabletypelist.h" + +#include "../materiallist/materiallist.h" +#include "../qetinformation.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +/** + @brief The columns of a cable type file -- the whole catalogue, and + the only ones the program reads anything from. + + Three of them: the name of the type, how many cores it has and the + colour of each of them. In that order they are written into a brand + new file, and a label line is matched against them so that a + catalogue read back never depends on the wording of its first line. + + The material file the pattern was copied from holds more columns + (section, sheath colour, manufacturer and so on); they were never + read here and are not listed. A file which has them anyway loses + nothing: a header cell outside this list keeps its own name and its + own data, it carries no meaning, and the machine header line is + recognized by its shape rather than by this list, so an extra + column there never turns that line into a record of cables. +*/ +const QStringList &canonicalColumns() +{ + static const QStringList columns = { + QStringLiteral("designation"), + QStringLiteral("cores"), + QStringLiteral("core_colors") + }; + return columns; +} + +/** + @brief Tell whether a cell of the machine header can be a column key. + + A key is one plain lowercase word -- core_colors and + manufacturer_reference are typical -- written the same whichever + language the file is read in, which is exactly why it can sit under + the label line. A key the program doesn't know is still a key : its + column is kept and shown the way it is written, it carries no + meaning. What the shape really protects is the other side: a cable + ("H07V-K 3G1,5") never looks like this, so a line of cables is + never mistaken for a header. + @param cell + @return true when the cell can be part of the machine header line +*/ +bool isMachineKey(const QString &cell) +{ + if (cell.isEmpty()) { + return false; + } + for (const QChar c : cell) + { + const ushort u = c.unicode(); + const bool key = (u >= 'a' && u <= 'z') + || (u >= '0' && u <= '9') + || u == '_'; + if (!key) { + return false; + } + } + //A key starts with a letter, so that a row of numbers can never + //pass for one. + return cell.at(0).unicode() >= 'a' && cell.at(0).unicode() <= 'z'; +} + +/** + @brief Decode a csv file content. + The cable type file is written in utf-8 with a BOM, the same way the + material file is, so spreadsheets open it with the right encoding. A + file written by hand in the local encoding is still decodable: + fromUtf8() is only used when the bytes are valid utf-8. + @param raw + @return +*/ +QString decode(const QByteArray &raw) +{ + if (raw.startsWith("\xEF\xBB\xBF")) { + return QString::fromUtf8(raw.constData() + 3, raw.size() - 3); + } + + const QByteArray utf8 = QString::fromUtf8(raw).toUtf8(); + if (utf8 == raw) { + return QString::fromUtf8(raw); + } + return QString::fromLocal8Bit(raw); +} + +/** + @brief Tell whether a file exists and holds something. + An existing but empty file is the same as no file at all: it can be + filled with the template without losing anything. + @param path + @return +*/ +bool hasContent(const QString &path) +{ + const QFileInfo info(path); + return info.exists() && info.isFile() && info.size() > 0; +} + +} // namespace + +/** + @brief CableTypeList::settingsKey + @return the QSettings key holding the path of the cable type file +*/ +QString CableTypeList::settingsKey() +{ + return QStringLiteral("cable-management/cable-type-list-path"); +} + +/** + @brief CableTypeList::configuredPath + @return the path of the cable type file as chosen in the preferences, + or a null QString when the user never set one. +*/ +QString CableTypeList::configuredPath() +{ + QSettings settings; + const QString path = settings.value(settingsKey(), QStringLiteral("default")).toString(); + if (path.isEmpty() || path == QLatin1String("default")) { + return QString(); + } + return path; +} + +/** + @brief CableTypeList::setConfiguredPath + Store the path of the cable type file in the preferences. + @param path +*/ +void CableTypeList::setConfiguredPath(const QString &path) +{ + QSettings settings; + settings.setValue(settingsKey(), path.isEmpty() ? QStringLiteral("default") : path); +} + +/** + @brief CableTypeList::defaultFileName + @return the file name used when the cable type file is created without + an explicit location. +*/ +QString CableTypeList::defaultFileName() +{ + return QStringLiteral("qet_cable_types.csv"); +} + +/** + @brief CableTypeList::defaultPath + @return where the cable type file is created when no path is + configured yet : the standard documents folder. +*/ +QString CableTypeList::defaultPath() +{ + QString dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + while (dir.endsWith(QLatin1Char('/'))) { + dir.chop(1); + } + if (dir.isEmpty()) { + dir = QDir::currentPath(); + } + return dir + QLatin1Char('/') + defaultFileName(); +} + +/** + @brief CableTypeList::savedHeaderState + @return the saved column layout of the selection window, empty when the + window was never opened or the layout could not be read back. +*/ +QByteArray CableTypeList::savedHeaderState() +{ + QSettings settings; + return settings.value(QStringLiteral("cable-management/cable-type-list-header")).toByteArray(); +} + +/** + @brief CableTypeList::saveHeaderState + Remember the column layout of the selection window. + @param state the header state as saved by QHeaderView::saveState() +*/ +void CableTypeList::saveHeaderState(const QByteArray &state) +{ + QSettings settings; + settings.setValue(QStringLiteral("cable-management/cable-type-list-header"), state); +} + +/** + @brief CableTypeList::savedDialogSize + @return the size the selection window had the last time it was closed, + an invalid size when it was never opened. +*/ +QSize CableTypeList::savedDialogSize() +{ + QSettings settings; + const QSize size(settings.value(QStringLiteral("cable-management/cable-type-list-width")).toInt(), + settings.value(QStringLiteral("cable-management/cable-type-list-height")).toInt()); + //A size which was never written reads back as 0x0, which Qt still + //counts as valid : it must not be handed to resize(). + return size.isEmpty() ? QSize() : size; +} + +/** + @brief CableTypeList::saveDialogSize + Remember the size of the selection window. + @param size +*/ +void CableTypeList::saveDialogSize(const QSize &size) +{ + if (size.isEmpty()) { + return; + } + QSettings settings; + settings.setValue(QStringLiteral("cable-management/cable-type-list-width"), size.width()); + settings.setValue(QStringLiteral("cable-management/cable-type-list-height"), size.height()); +} + +/** + @brief CableTypeList::defaultColumns + @return the columns of a brand new cable type file, in writing order. +*/ +QStringList CableTypeList::defaultColumns() +{ + return canonicalColumns(); +} + +/** + @brief CableTypeList::translatedColumn + @param column a column of the cable type file + @return that column named the way it is shown to the user, in the + current language. A column QElectroTech doesn't know is returned as it + is, since it can only have been named by the user. +*/ +QString CableTypeList::translatedColumn(const QString &column) +{ + //The columns which are specific to a cable keep their own wording, + //the ones shared with the material file reuse the element + //information wording, so both catalogues read the same. The type + //column is the exception: it is called Désignation here, the way + //the cable form calls it, and not "Numéro d'article", which is + //what an element means by it. + static const QMap own = { + {QStringLiteral("designation"), + QCoreApplication::translate("CableTypeList", "Désignation")}, + {QStringLiteral("cores"), + QCoreApplication::translate("CableTypeList", "Nombre d'âmes")}, + {QStringLiteral("core_colors"), + QCoreApplication::translate("CableTypeList", "Couleurs des âmes")}, + {QStringLiteral("section"), + QCoreApplication::translate("CableTypeList", "Section du câble")}, + {QStringLiteral("color"), + QCoreApplication::translate("CableTypeList", "Couleur du fourreau")} + }; + + const auto it = own.constFind(column); + if (it != own.constEnd()) { + return it.value(); + } + + const QString translated = QETInformation::translatedInfoKey(column); + return translated.isEmpty() ? column : translated; +} + +/** + @brief CableTypeList::translatedHeader + The label line of a cable type file : the columns the way the user + reads them, in the current language. + @param columns + @return +*/ +QStringList CableTypeList::translatedHeader(const QStringList &columns) +{ + QStringList header; + header.reserve(columns.size()); + for (const QString &column : columns) { + header.append(translatedColumn(column)); + } + return header; +} + +/** + @brief CableTypeList::designation + @param record + @return the type name of that cable, the way it shows in the + selection list and on the drawing +*/ +QString CableTypeList::designation(const CableTypeRecord &record) +{ + return record.value(QStringLiteral("designation")); +} + +/** + @brief CableTypeList::coreCount + Tell how many cores a cable type holds. + @param record + @return the number of cores, or 0 when the file doesn't say. 0 means + "unknown", which the selection window treats as usable : a file which + says nothing is no reason to refuse a type. +*/ +int CableTypeList::coreCount(const CableTypeRecord &record) +{ + bool ok = false; + const int cores = record.value(QStringLiteral("cores")).trimmed().toInt(&ok); + return (ok && cores > 0) ? cores : 0; +} + +/** + @brief CableTypeList::coreColors + The colours of the cores, in the order they are written in the file, + which is the order they are given to the cores of a drawn cable. + @param record + @return + + The cell holds the colours separated by |, ; or ,. Writing always + uses | because it can't appear in a colour name and never needs + quoting, reading accepts the three so a hand written file works too. +*/ +QStringList CableTypeList::coreColors(const CableTypeRecord &record) +{ + QStringList colors; + const QString cell = record.value(QStringLiteral("core_colors")); + for (const QString &raw : cell.split(QLatin1Char('|'), Qt::SkipEmptyParts)) + { + for (const QString &part : raw.split(QLatin1Char(';'), Qt::SkipEmptyParts)) + { + for (const QString &color : part.split(QLatin1Char(','), Qt::SkipEmptyParts)) + { + const QString trimmed = color.trimmed(); + if (!trimmed.isEmpty()) { + colors.append(trimmed); + } + } + } + } + return colors; +} + +/** + @brief CableTypeList::headerAliasMap + @return every accepted header cell of a cable type file, lowercased, + mapped to the canonical column it means. + + Beside the key itself, a header may use the translated name of the + column ("Nombre d'âmes" for cores), or the name and translation of an + element information, so a catalogue sharing columns with the material + file is understood in both. +*/ +QMap CableTypeList::headerAliasMap() +{ + QMap map; + + for (const QString &key : canonicalColumns()) + { + map.insert(key.toLower(), key); + const QString translated = translatedColumn(key); + if (!translated.isEmpty()) { + map.insert(translated.toLower(), key); + } + //The element information spells the same column another way + //("Numéro d'article" for designation); both are accepted, so + //that a file written with either wording reads the same. + const QString info_key = QETInformation::translatedInfoKey(key); + if (!info_key.isEmpty() && info_key != translated) { + map.insert(info_key.toLower(), key); + } + } + + //Every element information is an acceptable column name, so that + //columns copied over from the material file keep their meaning. + const QStringList keys = QETInformation::elementInfoKeys(); + for (const QString &key : keys) + { + if (map.contains(key.toLower())) { + continue; + } + map.insert(key.toLower(), key); + const QString translated = QETInformation::translatedInfoKey(key); + if (!translated.isEmpty()) { + map.insert(translated.toLower(), key); + } + } + + return map; +} + +/** + @brief CableTypeList::canonicalColumn + Turn one header cell into a canonical column name. + @param header_cell + @param already_used the names chosen for the previous cells, to keep + the header free of duplicates + @param alias_map lowercased alias -> canonical name + @return +*/ +QString CableTypeList::canonicalColumn(const QString &header_cell, + const QStringList &already_used, + const QMap &alias_map) +{ + const QString raw = header_cell.trimmed(); + + if (raw.isEmpty()) { + //A nameless column keeps its data and its place, it just can't be + //matched to anything. + QString name = QStringLiteral("column_%1").arg(already_used.size() + 1); + int i = already_used.size() + 1; + while (already_used.contains(name)) { + name = QStringLiteral("column_%1").arg(++i); + } + return name; + } + + const auto it = alias_map.constFind(raw.toLower()); + if (it != alias_map.constEnd() && !already_used.contains(it.value())) { + return it.value(); + } + + //Either an unknown name, or a second cell meaning something which is + //already used : the cell is kept as it is written, its data is never + //thrown away. + QString name = raw; + int i = 2; + while (already_used.contains(name)) { + name = raw + QStringLiteral("_%1").arg(i++); + } + return name; +} + +/** + @brief CableTypeList::isMachineHeaderLine + Tell whether the second line of a cable type file is the machine + header QElectroTech writes under the label line : the canonical + column names, kept so that a file written in one language is still + understood in another one. + @param label_line the first line of the file, the one the user reads + @param candidate the line which may be the machine header + @param alias_map lowercased alias -> canonical name + @return true when candidate must not be read as a record +*/ +bool CableTypeList::isMachineHeaderLine(const QStringList &label_line, + const QStringList &candidate, + const QMap &alias_map) +{ + if (candidate.isEmpty() || candidate.size() != label_line.size()) { + return false; + } + + //At least one cell the program really knows : a line made only + //of key shaped words nobody here gave a meaning to is far more + //likely to be a cable than a header. + bool has_known_column = false; + for (int i = 0; i < candidate.size(); ++i) + { + const QString cell = candidate.at(i).trimmed(); + if (cell.isEmpty()) { + continue; + } + if (alias_map.contains(cell.toLower())) + { + has_known_column = true; + continue; + } + if (cell == label_line.at(i).trimmed()) { + continue; + } + //An unknown key is still a key : the column comes back as + //it was written, its data is never thrown away. + if (isMachineKey(cell)) { + continue; + } + return false; + } + + return has_known_column; +} + +/** + @brief CableTypeList::load + Read a cable type file. + @param path + @param data receives the columns and the records + @param error receives a message when the file can't be read + @return true on success +*/ +bool CableTypeList::load(const QString &path, CableTypeListData *data, QString *error) +{ + if (error) { + error->clear(); + } + if (!data) { + if (error) { + *error = QCoreApplication::translate("CableTypeList", "Aucun récepteur pour le fichier des types de câbles."); + } + return false; + } + + data->columns.clear(); + data->records.clear(); + data->separator = QLatin1Char(';'); + + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) + { + if (error) { + *error = file.errorString(); + } + return false; + } + const QByteArray raw = file.readAll(); + file.close(); + + const QString content = decode(raw); + if (content.trimmed().isEmpty()) { + return true; + } + + const int eol = content.indexOf(QLatin1Char('\n')); + data->separator = MaterialList::detectSeparator(eol < 0 ? content : content.left(eol)); + + QList rows = MaterialList::parseCsv(content, data->separator); + if (rows.isEmpty()) { + return true; + } + + const QStringList header = rows.takeFirst(); + const QMap alias_map = headerAliasMap(); + + //The label line is followed by the machine header QElectroTech + //writes itself : the canonical names, so that the file is read + //the same way whatever the language it was written in. + QStringList machine_line; + if (!rows.isEmpty() && isMachineHeaderLine(header, rows.first(), alias_map)) { + machine_line = rows.takeFirst(); + } + + QStringList used; + for (int i = 0; i < header.size(); ++i) + { + QString cell = header.at(i); + if (i < machine_line.size() && !machine_line.at(i).isEmpty()) { + cell = machine_line.at(i); + } + const QString column = canonicalColumn(cell, used, alias_map); + used.append(column); + data->columns.append(column); + } + + for (const QStringList &row : std::as_const(rows)) + { + CableTypeRecord record; + bool empty = true; + for (int i = 0; i < data->columns.size(); ++i) + { + const QString value = i < row.size() ? row.at(i) : QString(); + if (!value.isEmpty()) { + empty = false; + } + record.setValue(data->columns.at(i), value); + } + //Cells written past the header are not thrown away : they are + //written back the way they were read. + for (int i = data->columns.size(); i < row.size(); ++i) + { + if (!row.at(i).isEmpty()) { + empty = false; + } + record.extra.append(row.at(i)); + } + if (!empty) { + data->records.append(record); + } + } + + return true; +} + +/** + @brief CableTypeList::writeFile + Write a whole cable type file atomically : a crash or a full disk + never leaves a half written catalogue behind. + @param path + @param data + @param error receives a message when the file can't be written + @return true on success +*/ +bool CableTypeList::writeFile(const QString &path, const CableTypeListData &data, QString *error) +{ + if (error) { + error->clear(); + } + + QList rows; + rows.reserve(data.records.size()); + for (const CableTypeRecord &record : data.records) + { + QStringList row; + row.reserve(data.columns.size()); + for (const QString &column : data.columns) { + row.append(record.value(column)); + } + row.append(record.extra); + rows.append(row); + } + + QList header_lines; + header_lines.append(translatedHeader(data.columns)); + header_lines.append(data.columns); + const QByteArray content = MaterialList::serializeCsv(header_lines, rows, data.separator); + + QSaveFile file(path); + if (!file.open(QIODevice::WriteOnly) || file.write(content) != content.size()) + { + if (error) { + *error = file.errorString(); + } + file.cancelWriting(); + return false; + } + if (!file.commit()) + { + if (error) { + *error = file.errorString(); + } + return false; + } + return true; +} + +/** + @brief CableTypeList::appendRecord + Add one cable type at the end of a cable type file. + The file is read again first: changes the user just saved from his + spreadsheet are part of what gets rewritten, never dropped. + @param path + @param record + @param error receives a message when the file can't be written + @return true on success +*/ +bool CableTypeList::appendRecord(const QString &path, const CableTypeRecord &record, QString *error) +{ + if (error) { + error->clear(); + } + + CableTypeListData data; + if (hasContent(path)) + { + if (!load(path, &data, error)) { + return false; + } + if (data.columns.isEmpty()) + { + if (error) { + *error = QCoreApplication::translate("CableTypeList", "Le fichier ne contient pas d'en-tête : colonnes manquantes."); + } + return false; + } + } + else + { + data.columns = defaultColumns(); + data.separator = QLatin1Char(';'); + } + + data.records.append(record); + return writeFile(path, data, error); +} + +/** + @brief CableTypeList::updateRecord + Rewrite one line of a cable type file, the way a spreadsheet saves a + row he just edited: the line is found as it stands at this moment, + replaced by what the user left, and the whole file is written again + -- so the changes he saved from his spreadsheet in the meantime are + part of it rather than being thrown away. + @param path the file to write + @param before the line as it stands in the file right now + @param after the line as the user left it + @param error receives a message when the file can't be written, or + when the line he started from is no longer in it + @return true on success +*/ +bool CableTypeList::updateRecord(const QString &path, + const CableTypeRecord &before, + const CableTypeRecord &after, + QString *error) +{ + if (error) { + error->clear(); + } + + CableTypeListData data; + if (!load(path, &data, error)) { + return false; + } + if (data.columns.isEmpty()) + { + if (error) { + *error = QCoreApplication::translate("CableTypeList", "Le fichier ne contient pas d'en-tête : colonnes manquantes."); + } + return false; + } + + int row = -1; + for (int i = 0; i < data.records.size(); ++i) + { + if (data.records.at(i) == before) { + row = i; + break; + } + } + if (row < 0) + { + //The line he started from is gone: rewriting whatever stands + //there now would overwrite somebody else's work. + if (error) { + *error = QCoreApplication::translate("CableTypeList", + "Cette entrée n'existe plus dans le fichier : elle a peut-être été modifiée entre-temps."); + } + return false; + } + + data.records[row] = after; + return writeFile(path, data, error); +} + +/** + @brief CableTypeList::createFile + Create an empty cable type file holding only the header lines. + @param path + @param error receives a message when the file can't be written + @return true on success +*/ +bool CableTypeList::createFile(const QString &path, QString *error) +{ + if (error) { + error->clear(); + } + + if (hasContent(path)) + { + if (error) { + *error = QCoreApplication::translate("CableTypeList", "Le fichier existe déjà et n'est pas vide."); + } + return false; + } + + CableTypeListData data; + data.columns = defaultColumns(); + data.separator = QLatin1Char(';'); + return writeFile(path, data, error); +} + +/** + @brief CableTypeList::isEmptyFile + @param path + @return true when no file, or an empty one, exists at path +*/ +bool CableTypeList::isEmptyFile(const QString &path) +{ + return !hasContent(path); +} diff --git a/sources/cablelist/cabletypelist.h b/sources/cablelist/cabletypelist.h new file mode 100644 index 000000000..a2e5bfcfa --- /dev/null +++ b/sources/cablelist/cabletypelist.h @@ -0,0 +1,134 @@ +/* + 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 CABLETYPELIST_H +#define CABLETYPELIST_H + +#include +#include +#include +#include +#include +#include +#include + +/** + @brief One line of the cable type file. + + Same shape as MaterialRecord : the keys are column names, either one + QElectroTech knows (designation, manufacturer...) or one the user + invented, which is never interpreted but never thrown away either. +*/ +struct CableTypeRecord +{ + QMap values; + //Cells written after the last column of the header, kept as they + //are, exactly like MaterialRecord::extra. + QStringList extra; + + QString value(const QString &column) const {return values.value(column);} + void setValue(const QString &column, const QString &value) {values.insert(column, value);} + bool operator==(const CableTypeRecord &other) const {return values == other.values;} +}; + +/** + @brief The whole content of a cable type file. + + The separator and the column names are read from the file rather than + imposed, so a catalogue written in a spreadsheet keeps working. +*/ +struct CableTypeListData +{ + QStringList columns; + QList records; + QChar separator = QLatin1Char(';'); + + bool isEmpty() const {return columns.isEmpty() && records.isEmpty();} +}; + +/** + @brief The CableTypeList class reads and writes the cable type file (a + CSV file listing the cable kinds a project may use : their name, + how many cores they have and the colour of each of them). + + It is the cable counterpart of MaterialList and follows it step by + step : same settings handling, same two header lines (the labels the + user reads, then the canonical names QElectroTech matches against), + same lenient reading of a hand written file. + + This is a pure data class : it knows nothing about widgets, so it + stays usable from the tests and from the command line. +*/ +class CableTypeList +{ + public: + //Settings + static QString settingsKey(); + static QString configuredPath(); + static void setConfiguredPath(const QString &path); + static QString defaultPath(); + static QString defaultFileName(); + + //Geometry of the selection window, remembered from one opening + //to the next, the same way MaterialList remembers its own. + static QByteArray savedHeaderState(); + static void saveHeaderState(const QByteArray &state); + static QSize savedDialogSize(); + static void saveDialogSize(const QSize &size); + + //Columns + static QStringList defaultColumns(); + static QString translatedColumn(const QString &column); + static QStringList translatedHeader(const QStringList &columns); + + //Cable semantics + static QString designation(const CableTypeRecord &record); + static int coreCount(const CableTypeRecord &record); + static QStringList coreColors(const CableTypeRecord &record); + + //File io + static bool load(const QString &path, CableTypeListData *data, QString *error = nullptr); + static bool writeFile(const QString &path, const CableTypeListData &data, QString *error = nullptr); + static bool appendRecord(const QString &path, const CableTypeRecord &record, QString *error = nullptr); + /** + @brief Rewrite one line of a cable type file, the way a + spreadsheet saves a row he just edited. + @param path the file to write + @param before the line as it stands in the file right now + @param after the line as the user left it + @param error receives a message when the file can't be + written, or when that line is no longer in it + @return true on success + */ + static bool updateRecord(const QString &path, + const CableTypeRecord &before, + const CableTypeRecord &after, + QString *error = nullptr); + static bool createFile(const QString &path, QString *error = nullptr); + static bool isEmptyFile(const QString &path); + + private: + static QMap headerAliasMap(); + static QString canonicalColumn(const QString &header_cell, + const QStringList &already_used, + const QMap &alias_map); + static bool isMachineHeaderLine(const QStringList &label_line, + const QStringList &candidate, + const QMap &alias_map); +}; + +#endif // CABLETYPELIST_H diff --git a/sources/conductorproperties.cpp b/sources/conductorproperties.cpp index 12a55539c..1d62c2c4a 100644 --- a/sources/conductorproperties.cpp +++ b/sources/conductorproperties.cpp @@ -284,6 +284,15 @@ void ConductorProperties::toXml(QDomElement &e) const e.setAttribute("text_color", text_color.name()); e.setAttribute("formula", m_formula); e.setAttribute("cable", m_cable); + if (!m_cable_uuid.isNull()) + { + //Only written when there is a cable: a conductor which is no + //part of one keeps whatever cable text it had and no reference + //at all, so a hand written value is never mistaken for a + //generated one. + e.setAttribute("cable_uuid", m_cable_uuid.toString()); + e.setAttribute("cable_slot", QString::number(m_cable_slot)); + } e.setAttribute("bus", m_bus); e.setAttribute("function", m_function); e.setAttribute("tension_protocol", m_tension_protocol); @@ -343,6 +352,8 @@ void ConductorProperties::fromXml(QDomElement &e) text_color = (xml_text_color.isValid()? xml_text_color : QColor(Qt::black)); m_formula = e.attribute("formula"); m_cable = e.attribute("cable"); + m_cable_uuid = QUuid(e.attribute("cable_uuid")); + m_cable_slot = e.attribute("cable_slot", QString::number(-1)).toInt(); m_bus = e.attribute("bus"); m_function = e.attribute("function"); m_tension_protocol = e.attribute("tension_protocol"); @@ -841,6 +852,8 @@ bool ConductorProperties::operator==(const ConductorProperties &other) const other.text_color == text_color &&\ other.m_formula == m_formula &&\ other.m_cable == m_cable &&\ + other.m_cable_uuid == m_cable_uuid &&\ + other.m_cable_slot == m_cable_slot &&\ other.m_bus == m_bus &&\ other.m_function == m_function &&\ other.m_tension_protocol == m_tension_protocol &&\ diff --git a/sources/conductorproperties.h b/sources/conductorproperties.h index d29bbdbaa..6bb0fd004 100644 --- a/sources/conductorproperties.h +++ b/sources/conductorproperties.h @@ -21,6 +21,7 @@ #include "qet.h" #include #include +#include class QPainter; @@ -97,6 +98,14 @@ class ConductorProperties m_bus, m_cable; + /// The cable of the project this conductor is a core of, null + /// when it belongs to no cable. The text shown in m_cable is + /// generated from the cable and never typed in: this reference + /// plus the slot below is all it takes to work it out again. + QUuid m_cable_uuid; + /// Which core of that cable this conductor shows, -1 when none + int m_cable_slot = -1; + int text_size, m_dash_size = 1; diff --git a/sources/diagram.cpp b/sources/diagram.cpp index f7c336ed6..78d6da912 100644 --- a/sources/diagram.cpp +++ b/sources/diagram.cpp @@ -19,6 +19,8 @@ #include "ElementsCollection/elementcollectionhandler.h" #include "TerminalStrip/GraphicsItem/terminalstripitem.h" +#include "cable/cablecopy.h" +#include "cable/cablepart.h" #include "xml/terminalstripitemxml.h" #include "QPropertyUndoCommand/qpropertyundocommand.h" #include "diagramcontent.h" @@ -1404,9 +1406,10 @@ void Diagram::folioSequentialsToXml(QHash *cables_added) { QDomElement root = document.documentElement(); - return(fromXml(root, position, consider_informations, content_ptr)); + return(fromXml(root, position, consider_informations, content_ptr, cables_added)); } /** @@ -1564,7 +1567,8 @@ Terminal* findTerminal(int conductor_index, bool Diagram::fromXml(QDomElement &document, QPointF position, bool consider_informations, - DiagramContent *content_ptr) + DiagramContent *content_ptr, + QList *cables_added) { const QDomElement& root = document; // The first element must be a diagram @@ -1826,6 +1830,17 @@ bool Diagram::fromXml(QDomElement &document, //Load terminal strip item QVector added_strips { TerminalStripItemXml::fromXml(this, root) }; + //Load the cable lines a copy fragment carries -- built here + //rather than in the caller so that they are placed with the rest + //of the selection, but only when the caller asked to be handed + //them: a folio loaded from a project file holds no cable of its + //own, the cables of a project are read at project level. + QList added_cables; + if (cables_added) { + added_cables = CableCopy::read(this, root); + *cables_added = added_cables; + } + //Translate items if a new position was given in parameter if (position != QPointF()) { @@ -1836,6 +1851,7 @@ bool Diagram::fromXml(QDomElement &document, for (auto image : std::as_const(added_images )) added_items << image; for (auto table : std::as_const(added_tables )) added_items << table; for (const auto &strip : std::as_const(added_strips)) added_items << strip; + for (const auto &cable : std::as_const(added_cables)) added_items << cable; //Get the top left corner of the rectangle that contain all added items QRectF items_rect; diff --git a/sources/diagram.h b/sources/diagram.h index ff7ff1f9b..088a47e55 100644 --- a/sources/diagram.h +++ b/sources/diagram.h @@ -33,6 +33,7 @@ #include class Conductor; +class CablePart; class CustomElement; class DiagramContent; class DiagramPosition; @@ -202,14 +203,21 @@ class Diagram : public QGraphicsScene QPointF = QPointF(), bool = true, DiagramContent * = nullptr); + //The last argument is only for a copy fragment: when it is + //given, the cable lines that fragment carries are built as + //well and handed back through it -- they are not part of a + //DiagramContent, so the caller has to know about them to + //put them on the undo stack (see CableCopy) bool fromXml(QDomDocument &, QPointF = QPointF(), bool = true, - DiagramContent * = nullptr); + DiagramContent * = nullptr, + QList * = nullptr); bool fromXml(QDomElement &, QPointF = QPointF(), bool = true, - DiagramContent * = nullptr); + DiagramContent * = nullptr, + QList * = nullptr); void folioSequentialsToXml(QHash*, QDomElement *, diff --git a/sources/diagramevent/diagrameventaddcable.cpp b/sources/diagramevent/diagrameventaddcable.cpp new file mode 100644 index 000000000..11531ee6c --- /dev/null +++ b/sources/diagramevent/diagrameventaddcable.cpp @@ -0,0 +1,463 @@ +/* + 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 "diagrameventaddcable.h" + +#include "../cable/addcablecommand.h" +#include "../cable/cablecreatedialog.h" +#include "../cable/cablepart.h" +#include "../diagram.h" +#include "../qetapp.h" +#include "../qetdiagrameditor.h" +#include "../qetproject.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + + ///A click closer than this to the start point is not an end point + const qreal min_end_length = 2.0; + +/** + @brief How far apart two grid points are, the same way the snapping + itself works it out -- the direction the line may be locked to has to + be judged against the same step the end point will land on. +*/ +qreal gridStep() +{ + QSettings settings; + const int x = settings.value(QStringLiteral("diagrameditor/Xgrid"), + Diagram::xGrid).toInt(); + return qMax(1, x); +} + +} // namespace + +/** + @brief DiagramEventAddCable::DiagramEventAddCable + @param diagram the folio the cable is drawn on +*/ +DiagramEventAddCable::DiagramEventAddCable(Diagram *diagram) : + DiagramEventInterface(diagram) +{ + m_running = true; + //Done here as well as in init(): Diagram::setEventInterface() + //only calls init() when it replaces another tool, so a fresh + //tool would otherwise never be set up at all. + init(); + //The message itself waits for the next turn of the event loop: + //setEventInterface() destroys the tool which was running before + //this constructor returns, and its destructor clears the status + //bar, which would wipe out anything shown before it is done. + QTimer::singleShot(0, this, [this]() { updateHint(); }); +} + +/** + @brief DiagramEventAddCable::~DiagramEventAddCable +*/ +DiagramEventAddCable::~DiagramEventAddCable() +{ + delete m_preview; + delete m_help_horiz; + delete m_help_verti; + + if (m_diagram) + { + for (QGraphicsView *view : m_diagram->views()) { + view->setContextMenuPolicy(Qt::DefaultContextMenu); + } + } + clearHint(); +} + +/** + @brief DiagramEventAddCable::init + While the tool runs, the right button belongs to it alone: it cancels + the line being drawn or ends the tool. The quick command ring and the + context menu of the folio would both pop up in the middle of the + drawing and take the tool away from under the user, so they are kept + out of the way until the tool is gone. +*/ +void DiagramEventAddCable::init() +{ + m_running = true; + if (m_diagram) + { + for (QGraphicsView *view : m_diagram->views()) { + view->setContextMenuPolicy(Qt::NoContextMenu); + } + } + updateHint(); +} + +/** + @brief DiagramEventAddCable::reset + Put the tool back to where it was before the first click: no line + kept, no preview, nothing remembered. The cross stays, since it + belongs to the tool rather than to one line. +*/ +void DiagramEventAddCable::reset() +{ + if (m_preview && m_diagram) { + m_diagram->removeItem(m_preview); + } + delete m_preview; + m_preview = nullptr; + m_has_anchor = false; + m_anchor = QPointF(); + m_axis = -1; + m_crossings.clear(); + updateHint(); +} + +/** + @brief DiagramEventAddCable::clearHint +*/ +void DiagramEventAddCable::clearHint() const +{ + if (!m_diagram || m_diagram->views().isEmpty()) return; + if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst())) { + editor->statusBar()->clearMessage(); + } +} + +/** + @brief DiagramEventAddCable::updateHint + Say what the next click does, and -- once the line has been pulled + out -- how many conductors it runs across, so the user knows before + the dialog opens what the cable is going to be given. +*/ +void DiagramEventAddCable::updateHint() const +{ + if (!m_diagram || m_diagram->views().isEmpty()) return; + auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()); + if (!editor) return; + + if (!m_has_anchor) + { + editor->statusBar()->showMessage(tr( + "Clic gauche : poser le départ du câble ; " + "glisser ou second clic : terminer ; " + "clic droit : annuler ; Échap : quitter")); + return; + } + + editor->statusBar()->showMessage(tr( + "Clic gauche ou relâcher : terminer la ligne " + "(%n âme(s) reconnue(s) ; toujours horizontale ou verticale) ; " + "clic droit : annuler la ligne", + "how many conductors the line being drawn already crosses", + m_crossings.size())); +} + +/** + @brief DiagramEventAddCable::mousePressEvent + @param event +*/ +void DiagramEventAddCable::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + if (Q_UNLIKELY(!m_diagram || m_diagram->isReadOnly())) return; + + if (event->button() == Qt::RightButton) + { + //Accepted first: emitting finish() makes the diagram destroy + //this very tool, so nothing may touch its members afterwards. + event->setAccepted(true); + + //First right click takes the line being drawn back, second one + //leaves the tool: the user always knows how far he has to press + //to get out, and never lands in a menu he did not ask for. + if (m_has_anchor) { + reset(); + } else { + m_running = false; + emit finish(); + } + return; + } + if (event->button() != Qt::LeftButton) return; + + const QPointF pos = Diagram::snapToGrid(event->scenePos()); + + if (!m_has_anchor) + { + m_has_anchor = true; + m_anchor = pos; + m_axis = -1; + updatePreview(pos); + event->setAccepted(true); + return; + } + + //A second click ends the line wherever it lands: it is snapped + //back onto whichever direction the line already runs along, so + //the user does not have to aim at the line itself. + const QPointF end = finishPoint(pos); + if (QLineF(m_anchor, end).length() >= min_end_length) { + finishLine(end); + } + event->setAccepted(true); +} + +/** + @brief DiagramEventAddCable::mouseMoveEvent + @param event +*/ +void DiagramEventAddCable::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + showGuides(event->scenePos()); + if (!m_has_anchor) return; + + updatePreview(event->scenePos()); + event->setAccepted(true); +} + +/** + @brief DiagramEventAddCable::mouseReleaseEvent + Letting go of a drag which was pulled out far enough lays the line + down straight away, which is how a line is drawn everywhere else in + QElectroTech -- a click without a drag, on the other hand, only puts + the start point down and waits for the next click. + @param event +*/ +void DiagramEventAddCable::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + if (event->button() != Qt::LeftButton || !m_has_anchor) return; + + const QPointF end = finishPoint(Diagram::snapToGrid(event->scenePos())); + if (QLineF(m_anchor, end).length() >= min_end_length) { + finishLine(end); + } + event->setAccepted(true); +} + +/** + @brief DiagramEventAddCable::keyPressEvent + @param event +*/ +void DiagramEventAddCable::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Escape) + { + if (m_has_anchor) + { + reset(); + event->setAccepted(true); + return; + } + } + DiagramEventInterface::keyPressEvent(event); +} + +/** + @brief DiagramEventAddCable::finishPoint + The point the line really ends at: snapped to the grid, and pulled + onto the direction the line already runs along, so a second click + which does not sit exactly on the axis still ends the line instead + of starting a new one. + @param scene_pos where the user clicked + @return the end of the line, in scene coordinates +*/ +QPointF DiagramEventAddCable::finishPoint(const QPointF &scene_pos) const +{ + if (m_axis == 0) { + return Diagram::snapToGrid(QPointF(scene_pos.x(), m_anchor.y())); + } + if (m_axis == 1) { + return Diagram::snapToGrid(QPointF(m_anchor.x(), scene_pos.y())); + } + + //The line was never pulled far enough to have a direction: take + //the one which is clearly longer, so it never ends up diagonal. + const QPointF delta = scene_pos - m_anchor; + if (qAbs(delta.x()) >= qAbs(delta.y())) { + return Diagram::snapToGrid(QPointF(scene_pos.x(), m_anchor.y())); + } + return Diagram::snapToGrid(QPointF(m_anchor.x(), scene_pos.y())); +} + +/** + @brief DiagramEventAddCable::updatePreview + Show the line as it will be laid down. + + The direction is decided again on every move rather than once and + for all: the user pulls, and the line follows whichever way he pulls + harder. A dead band of half a grid step keeps it from flickering + when he moves along the diagonal. + @param scene_pos where the mouse stands +*/ +void DiagramEventAddCable::updatePreview(const QPointF &scene_pos) +{ + if (!m_has_anchor || !m_diagram) return; + + QPointF end = scene_pos; + { + const QPointF delta = end - m_anchor; + const qreal dx = qAbs(delta.x()); + const qreal dy = qAbs(delta.y()); + const qreal dead = gridStep() * 0.5; + + int wanted = -1; + if (dx > dy + dead) { + wanted = 0; + } else if (dy > dx + dead) { + wanted = 1; + } else if (m_axis < 0 && (dx > dead || dy > dead)) { + wanted = dx >= dy ? 0 : 1; + } + if (wanted >= 0) { + m_axis = wanted; + } + } + + if (m_axis == 0) { + end.setY(m_anchor.y()); + } else if (m_axis == 1) { + end.setX(m_anchor.x()); + } + end = Diagram::snapToGrid(end); + + if (!m_preview) + { + m_preview = new QGraphicsLineItem; + QPen pen(QColor(0, 120, 215)); + pen.setWidthF(2.0); + pen.setStyle(Qt::DashLine); + m_preview->setPen(pen); + m_preview->setZValue(1000.0); + m_diagram->addItem(m_preview); + } + m_preview->setLine(QLineF(m_anchor, end)); + + //Keep the count of crossed conductors up to date, so the status + //bar already says what the dialog is about to say. + m_crossings = CableManager::crossedConductors(m_diagram, QLineF(m_anchor, end)); + updateHint(); +} + +/** + @brief DiagramEventAddCable::showGuides + The thin cross QElectroTech draws while any shape is drawn, which + says where the mouse stands in the folio and, for a line, along + which of the two directions it may run. + @param scene_pos where the mouse stands +*/ +void DiagramEventAddCable::showGuides(const QPointF &scene_pos) +{ + if (!m_diagram) return; + + if (!m_help_horiz || !m_help_verti) + { + QPen pen; + pen.setWidthF(0.4); + pen.setCosmetic(true); + pen.setColor(Diagram::background_color == Qt::darkGray ? Qt::lightGray : Qt::darkGray); + + const QRectF rect = m_diagram->border_and_titleblock.insideBorderRect(); + + if (!m_help_horiz) + { + m_help_horiz = new QGraphicsLineItem(rect.topLeft().x(), 0, rect.topRight().x(), 0); + m_help_horiz->setPen(pen); + m_help_horiz->setZValue(999.0); + m_diagram->addItem(m_help_horiz); + } + if (!m_help_verti) + { + m_help_verti = new QGraphicsLineItem(0, rect.topLeft().y(), 0, rect.bottomLeft().y()); + m_help_verti->setPen(pen); + m_help_verti->setZValue(999.0); + m_diagram->addItem(m_help_verti); + } + } + + const QPointF point = Diagram::snapToGrid(scene_pos); + m_help_horiz->setY(point.y()); + m_help_verti->setX(point.x()); +} + +/** + @brief DiagramEventAddCable::finishLine + Put the line down and hand it over to the dialog which turns it into + a cable -- a new one from the cable type file, or one the project + already holds. + @param end where the line stops +*/ +void DiagramEventAddCable::finishLine(const QPointF &end) +{ + if (!m_diagram || !m_diagram->project()) { + reset(); + return; + } + + QPointF to = end; + if (m_axis == 0) { + to.setY(m_anchor.y()); + } else if (m_axis == 1) { + to.setX(m_anchor.x()); + } + to = Diagram::snapToGrid(to); + + const QLineF line(m_anchor, to); + if (line.length() < min_end_length) { + reset(); + return; + } + + m_crossings = CableManager::crossedConductors(m_diagram, line); + + CablePartData part; + part.uuid = QUuid::createUuid(); + part.diagram = m_diagram->uuid(); + part.p1 = line.p1(); + part.p2 = line.p2(); + + //Remember which cable the line was given to: a brand new one was + //brought into being by this very dialog and undo has to take it + //away again, an existing one only gains a section. + const QVector before = m_diagram->project()->cables(); + + { + CableCreateDialog dialog(m_diagram, m_crossings, part, m_diagram->views().isEmpty() + ? nullptr : m_diagram->views().constFirst()); + if (dialog.exec() != QDialog::Accepted || !dialog.cable()) + { + reset(); + return; + } + + const bool own_cable = !before.contains(dialog.cable()); + + auto *item = new CablePart(part); + item->setCable(dialog.cable()); + m_diagram->addItem(item); + + m_diagram->project()->undoStack()->push( + new AddCableCommand(dialog.cable(), item, m_diagram, own_cable, + dialog.takenCores())); + } + + reset(); +} diff --git a/sources/diagramevent/diagrameventaddcable.h b/sources/diagramevent/diagrameventaddcable.h new file mode 100644 index 000000000..40b6cf0b1 --- /dev/null +++ b/sources/diagramevent/diagrameventaddcable.h @@ -0,0 +1,78 @@ +/* + 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 DIAGRAMEVENTADDCABLE_H +#define DIAGRAMEVENTADDCABLE_H + +#include "../cable/cablemanager.h" +#include "diagrameventinterface.h" + +class QLineF; +class QGraphicsLineItem; + +/** + @brief The DiagramEventAddCable class draws the trunk line of a cable. + + It behaves like every other drawing tool of QElectroTech: the first + click puts the start point down, the mouse then shows the line as it + will be, and either a second click or the release of a drag lays it + -- whichever the user does, he does not have to aim at anything. + + The line stays horizontal or vertical, whichever way the user pulls + harder, and it keeps following him: the direction is worked out again + on every mouse move (with a dead band so it does not flicker), never + frozen by the first stray pixel of the gesture -- a cable trunk runs + straight along a row of drawings, but along the row the user meant. +*/ +class DiagramEventAddCable : public DiagramEventInterface +{ + Q_OBJECT + + public: + explicit DiagramEventAddCable(Diagram *diagram); + ~DiagramEventAddCable() override; + + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void init() override; + + private: + void reset(); + void updatePreview(const QPointF &scene_pos); + QPointF finishPoint(const QPointF &scene_pos) const; + void finishLine(const QPointF &end); + void showGuides(const QPointF &scene_pos); + void clearHint() const; + void updateHint() const; + + private: + QGraphicsLineItem *m_preview = nullptr; + ///The cross QElectroTech shows while any shape is drawn + QGraphicsLineItem *m_help_horiz = nullptr; + QGraphicsLineItem *m_help_verti = nullptr; + ///True once the first click has put the line's start down + bool m_has_anchor = false; + QPointF m_anchor; + ///Which way the line is locked: + ///-1 undecided, 0 horizontal, 1 vertical + int m_axis = -1; + QList m_crossings; +}; + +#endif // DIAGRAMEVENTADDCABLE_H diff --git a/sources/diagramevent/diagrameventaddpaste.cpp b/sources/diagramevent/diagrameventaddpaste.cpp index ae408d4b2..30d9e80bb 100644 --- a/sources/diagramevent/diagrameventaddpaste.cpp +++ b/sources/diagramevent/diagrameventaddpaste.cpp @@ -17,6 +17,10 @@ */ #include "diagrameventaddpaste.h" +#include "../cable/addcablecommand.h" +#include "../cable/cablecopy.h" +#include "../cable/cablepart.h" +#include "../cable/editcablecommand.h" #include "../diagram.h" #include "../diagramcommands.h" #include "../qetapp.h" @@ -28,10 +32,34 @@ #include #include +#include #include #include #include +#include + +/** + @brief batchWith + Keep the cable lines of a paste together with the rest of it, so one + Ctrl+Z takes the whole thing back in one go. Mirrors the helper + DiagramView uses for its one-step paste; BatchCommand's own name + says "Supprimer", so the name of what the user actually did is put + back onto the batch. + @param first the step already assembled, may be null + @param second the step to add; ownership goes to the result + @return both steps as one command, or the only one of them +*/ +static QUndoCommand *batchWith(QUndoCommand *first, QUndoCommand *second) +{ + if (!first) return second; + if (!second) return first; + + auto *batched = new BatchCommand(first, second); + if (!first->text().isEmpty()) batched->setText(first->text()); + return batched; +} + /** @brief DiagramEventAddPaste::DiagramEventAddPaste @param diagram : diagram to paste into @@ -63,17 +91,27 @@ db->setUpdateBlocked(true); } - //Load items at their original XML coordinates. - m_diagram->fromXml(document_xml, QPointF(), false, &m_content); + //Load items at their original XML coordinates. A cable line + //comes back through its own list rather than through the + //content: the cable owns it, so it is not one of the folio's + //items -- and it travels as a whole, colour labels and all, + //whether or not the place it is going to holds a wire. + m_diagram->fromXml(document_xml, QPointF(), false, &m_content, &m_cables); if (db) { db->blockSignals(false); db->setUpdateBlocked(false); db->updateDB(); } - if (!m_content.count()) return; + if (!m_content.count() && m_cables.isEmpty()) return; - const QList movable = m_content.items(MovableItems); + QList movable = m_content.items(MovableItems); + for (CablePart *part : std::as_const(m_cables)) { + if (part) { + movable << part; + m_cable_items.insert(part); + } + } if (movable.isEmpty()) return; //Compute the top-left of all items' actual on-screen bounding @@ -260,11 +298,24 @@ void DiagramEventAddPaste::moveTo(const QPointF &scene_pos) const QPointF delta = snapGrid(scene_pos) - m_initial_cursor; + //A cable line is handed a step rather than a place to stand: it + //folds a translation into its own geometry and into the colour + //labels it carries and keeps its own pos() at (0, 0) + //(CablePart::itemChange), so the position stored above -- always + //(0, 0) for it -- would be an addition again on every mouse move + //and the line would run away from the group it was pasted with. + //The difference to the last step keeps the translation the same + //one the rest of the paste gets, expressed their way: a place. + const QPointF cable_step = delta - m_cable_delta; for (auto it = m_relative_pos.constBegin() ; it != m_relative_pos.constEnd() ; ++it) { - if (it.key()) { + if (!it.key()) continue; + if (m_cable_items.contains(it.key())) { + it.key()->setPos(cable_step); + } else { it.key()->setPos(it.value() + delta); } } + m_cable_delta = delta; //Update conductor paths so they follow the moved terminals. const QList conductors = m_content.conductors(DiagramContent::AnyConductor); @@ -328,6 +379,11 @@ void DiagramEventAddPaste::keyPressEvent(QKeyEvent *event) PasteDiagramCommand's first redo() does not add the items to the scene -- it assumes they are already there, which is what Diagram::fromXml did when this started. So pushing it here adopts them rather than duplicating them. + + The cable lines are wired just before that, which is the first moment + the place they will occupy is final: each colour label takes the wire + it happens to stand on, and a line the user refuses to let take wires + away from another cable goes away again on its own. */ void DiagramEventAddPaste::commit() { @@ -335,7 +391,32 @@ void DiagramEventAddPaste::commit() m_finished = true; m_running = false; - m_diagram->undoStack().push(new PasteDiagramCommand(m_diagram, m_content)); + const QList wired = CableCopy::wire( + m_diagram, m_cables, + m_content.conductors(DiagramContent::AnyConductor)); + m_cables.clear(); + + QUndoCommand *command = nullptr; + if (m_content.count()) { + command = new PasteDiagramCommand(m_diagram, m_content); + } + for (const CableCopy::Wired &line : std::as_const(wired)) + { + if (!line.part) continue; + + auto *added = new AddCableCommand(line.part->cable(), line.part.data(), + m_diagram, true, line.taken); + added->setText(QCoreApplication::translate("CableCopy", "Coller un câble")); + command = batchWith(command, added); + } + if (command) { + m_diagram->undoStack().push(command); + } + + //The undo stack has just run its first redo, which may have + //renewed the identity of a pasted conductor: the cores are + //pointed at the wires again so their cable field survives it. + CableCopy::repoint(wired); emit finish(); } @@ -367,6 +448,15 @@ void DiagramEventAddPaste::removeItems() delete item; } + //The cable lines never reached the undo stack either, so they + //are taken back the way they were built: off the folio, out of + //the project and deleted, leaving no trace of having been + //there -- not even a project asking to be saved for nothing. + for (CablePart *part : std::as_const(m_cables)) { + CableCopy::discard(m_diagram, part); + } + m_cables.clear(); + m_content.clear(); m_relative_pos.clear(); } diff --git a/sources/diagramevent/diagrameventaddpaste.h b/sources/diagramevent/diagrameventaddpaste.h index 2cb7eaf9f..1ca681688 100644 --- a/sources/diagramevent/diagrameventaddpaste.h +++ b/sources/diagramevent/diagrameventaddpaste.h @@ -23,7 +23,9 @@ #include #include +#include +class CablePart; class QStatusBar; /** @@ -76,6 +78,25 @@ class DiagramEventAddPaste : public DiagramEventInterface void removeItems(); DiagramContent m_content; + ///The cable lines the same fragment carried, if any. They are + ///not part of a DiagramContent -- the cable owns them -- so + ///they travel beside it: moved with the rest of the paste, + ///wired where it lands, and taken away whole if it never lands. + QList m_cables; + ///The cable lines among them, as plain items. Their setPos() is + ///a translation rather than a move: CablePart works in scene + ///coordinates and keeps itself at the origin, folding whatever + ///it is handed into its own geometry and into the colour labels + ///it carries (CablePart::itemChange). Everything else in + ///m_relative_pos is given a place to stand; these are given a + ///step to take. + QSet m_cable_items; + ///The step already handed to those cable lines. They take the + ///difference to the new one, because for them a setPos() is an + ///addition: handing them the baseline-relative position on every + ///mouse move would add it again each time and let the line run + ///away from the rest of the paste. + QPointF m_cable_delta; ///Each movable item's position relative to the group's top left, ///taken once so repeated moves cannot accumulate rounding drift. QHash m_relative_pos; diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index 0b63fd105..f333801f3 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -22,6 +22,10 @@ #include "qetproject.h" #include "QPropertyUndoCommand/qpropertyundocommand.h" #include "diagramcommands.h" +#include "cable/addcablecommand.h" +#include "cable/cablecopy.h" +#include "cable/cablepart.h" +#include "cable/editcablecommand.h" #include "diagramevent/diagrameventaddelement.h" #include "diagramevent/diagrameventaddmacro.h" #include "dvevent/dveventinterface.h" @@ -60,6 +64,29 @@ static bool hasCabinetInfo(const DiagramContext &infos); +/** + @brief batchWith + Keep two undo steps together so one Ctrl+Z takes the whole thing + back in one go. + + The batch also keeps the name the undo history shows for what the + user actually did: BatchCommand's own name says "Supprimer", which + is what it was first written for and would be misleading for a paste + or a duplicate. + @param first the step already assembled, may be null + @param second the step to add; ownership goes to the result + @return both steps as one command, or the only one of them +*/ +static QUndoCommand *batchWith(QUndoCommand *first, QUndoCommand *second) +{ + if (!first) return second; + if (!second) return first; + + auto *batched = new BatchCommand(first, second); + if (!first->text().isEmpty()) batched->setText(first->text()); + return batched; +} + /** Constructeur @param diagram Schema a afficher ; si diagram vaut 0, un nouveau Diagram est utilise @@ -533,8 +560,29 @@ void DiagramView::cut() { copy(); DiagramContent cut_content(m_diagram); + //A cable line is not held by DiagramContent: it leaves with a + //command of its own, which takes the cable itself away too when + //no line of it is left on the folio. + QList cable_cut; + for (QGraphicsItem *item : m_diagram->selectedItems()) { + if (auto *part = qgraphicsitem_cast(item)) { + cable_cut << part; + } + } + m_diagram -> clearSelection(); - m_diagram -> undoStack().push(new CutDiagramCommand(m_diagram, cut_content)); + + if (cable_cut.isEmpty()) { + m_diagram -> undoStack().push(new CutDiagramCommand(m_diagram, cut_content)); + return; + } + + QUndoCommand *command = nullptr; + if (cut_content.count()) { + command = new CutDiagramCommand(m_diagram, cut_content); + } + command = batchWith(command, new RemoveCableCommand(m_diagram, cable_cut)); + m_diagram -> undoStack().push(command); } /** @@ -544,7 +592,12 @@ void DiagramView::cut() void DiagramView::copy() { QClipboard *clipboard = QApplication::clipboard(); - QString content_clipboard = m_diagram -> toXml(false, true).toString(4); + QDomDocument document = m_diagram -> toXml(false, true); + //A drawn cable line is not one of the folio's items -- the cable + //owns it -- so it travels in a block of its own, beside the XML + //of the folio rather than in it. + CableCopy::write(m_diagram, document); + QString content_clipboard = document.toString(4); if (clipboard -> supportsSelection()) clipboard -> setText(content_clipboard, QClipboard::Selection); clipboard -> setText(content_clipboard); } @@ -565,13 +618,52 @@ void DiagramView::paste(const QPointF &pos, QClipboard::Mode clipboard_mode) { if (!document_xml.setContent(text_clipboard)) return; DiagramContent content_pasted; - m_diagram->fromXml(document_xml, pos, false, &content_pasted); + //A cable line is not part of a DiagramContent: it comes back + //through its own list, carrying the colour labels it was copied + //with, and each of them is given the wire it happens to stand + //on at the place it has just been put -- none at all, when that + //place holds no wire, in which case the labels go on standing + //on the line to be wired when it is pulled into place. + QList cable_pasted; + m_diagram->fromXml(document_xml, pos, false, &content_pasted, &cable_pasted); + const QList wired = CableCopy::wire( + m_diagram, cable_pasted, + content_pasted.conductors(DiagramContent::AnyConductor)); //If something was really added to diagram, we create an undo object. - if (content_pasted.count()) + if (content_pasted.count() || !wired.isEmpty()) { m_diagram -> clearSelection(); - m_diagram -> undoStack().push(new PasteDiagramCommand(m_diagram, content_pasted)); + + //One single step for the whole paste, cable lines included: + //undoing has to take back everything which came out of the + //clipboard in one go, and redoing has to put it all back. + QUndoCommand *command = nullptr; + if (content_pasted.count()) { + command = new PasteDiagramCommand(m_diagram, content_pasted); + } + for (const CableCopy::Wired &line : std::as_const(wired)) { + if (!line.part) continue; + auto *added = new AddCableCommand(line.part->cable(), line.part.data(), + m_diagram, true, line.taken); + added->setText(QCoreApplication::translate("CableCopy", "Coller un câble")); + command = batchWith(command, added); + } + //The copies end up selected, like everything else a paste + //just added, so the next Ctrl+V or Ctrl+D carries on from them + for (const CableCopy::Wired &line : std::as_const(wired)) { + if (line.part) line.part->setSelected(true); + } + + if (command) { + m_diagram -> undoStack().push(command); + } + //The stack has just renewed the identity of a pasted + //conductor: the cores are pointed at the wires again and + //the cable fields written out once more, so the entries + //the copy was given as it was put down are on the wires it + //has actually landed on. + CableCopy::repoint(wired); adjustSceneRect(); } } @@ -643,21 +735,51 @@ void DiagramView::duplicate(const QPoint &stepOffset) // Mirrors copy(), but does not touch the system clipboard: Ctrl+D // should not clobber whatever the user last copied with Ctrl+C. QDomDocument document = m_diagram->toXml(false, true); + CableCopy::write(m_diagram, document); DiagramContent pasted; + QList cable_pasted; // No position argument -- see the function comment above for why // the offset is not passed here. - m_diagram->fromXml(document, QPointF(), false, &pasted); - if (!pasted.count()) return; + m_diagram->fromXml(document, QPointF(), false, &pasted, &cable_pasted); + if (!pasted.count() && cable_pasted.isEmpty()) return; const int movable = DiagramContent::Elements | DiagramContent::TextFields | DiagramContent::Images | DiagramContent::Shapes | DiagramContent::Tables | DiagramContent::TerminalStrip; for (QGraphicsItem *item : pasted.items(movable)) item->setPos(item->pos() + offset); + //The new line is stamped beside the original like everything + //else -- and only after that, because where each colour label + //ends up decides which wire it is going to take + for (CablePart *part : std::as_const(cable_pasted)) + if (part) part->setPos(part->pos() + offset); + + const QList wired = CableCopy::wire( + m_diagram, cable_pasted, + pasted.conductors(DiagramContent::AnyConductor)); m_diagram->clearSelection(); - m_diagram->undoStack().push(new PasteDiagramCommand(m_diagram, pasted)); + + QUndoCommand *command = nullptr; + if (pasted.count()) { + command = new PasteDiagramCommand(m_diagram, pasted); + } + for (const CableCopy::Wired &line : std::as_const(wired)) { + if (!line.part) continue; + auto *added = new AddCableCommand(line.part->cable(), line.part.data(), + m_diagram, true, line.taken); + added->setText(QCoreApplication::translate("CableCopy", "Dupliquer un câble")); + command = batchWith(command, added); + } + for (const CableCopy::Wired &line : std::as_const(wired)) { + if (line.part) line.part->setSelected(true); + } + if (command) m_diagram->undoStack().push(command); + //Same as a paste: the stack has renewed the identity of the + //pasted conductors, so the cores are pointed at the wires again + //and the cable fields written out once more. + CableCopy::repoint(wired); adjustSceneRect(); } @@ -687,44 +809,71 @@ void DiagramView::mousePressEvent(QMouseEvent *e) m_press_pos = e->position().toPoint(); } + //A line which waits for the click putting one of its cores down + //owns the right button as well: that click says "no" to the core, + //so it neither starts the quick command ring nor reaches the + //sheet. It is the context menu of this very click which ends the + //wait (see contextMenuEvent()), which is why nothing is ended + //here: the menu is then cancelled instead of opened, and no menu + //can open out of a click meant to say "no" -- neither QET's own + //nor the platform's, which arrives with the press on X11 and with + //the release on Windows. + if (e->button() == Qt::RightButton && CablePart::placingPart()) { + e->accept(); + return; + } + //Right button: a click opens the context menu on release, a drag is //a gesture (DiagramGestureOverlay). Left alone while a text is edited. m_swallow_native_menu = false; m_gesture_over_tool = false; + + //A right click on a running tool belongs to the tool alone: it + //cancels the line being drawn or ends the tool, and ending the + //tool puts the context menu policy of the views back before this + //very click is over. The platform then still sends its own + //context menu for the same click -- on X11 with the press, on + //Windows with the release -- and the menu would open on the + //click which was meant to cancel. So it is swallowed here, the + //way a gesture swallows it further down; the next press which no + //tool handles puts this flag back to false. + if (e->button() == Qt::RightButton && m_diagram + && m_diagram->eventInterfaceIsRunning()) { + m_swallow_native_menu = true; + } if (e->button() == Qt::RightButton && DiagramGestureOverlay::isEnabled() - && !m_diagram->focusItem()) + && !m_diagram->focusItem() + && !m_diagram->eventInterfaceIsRunning()) { + //No tool is running: the right button draws the quick command + //ring, or opens the context menu on release. When a tool *is* + //running, no gesture is tracked at all: the tool owns the right + //button (it cancels the line being drawn or ends itself), and + //tracking one would both pop the ring up in the middle of the + //drawing and take the tool away from under the user (see + //mouseMoveEvent, which ends the tool as soon as a gesture is + //recognised). m_gesture_tracking = true; m_gesture_origin = e->position().toPoint(); m_context_toolbar->hide(); - //A tool is running, often one a gesture just started. A right - //click still goes to it -- it cancels or finishes the tool -- so - //the press carries on to the scene. A drag ends the tool and - //shows the ring (see mouseMoveEvent). - if (m_diagram->eventInterfaceIsRunning()) { - m_gesture_over_tool = true; - } - else - { - m_swallow_native_menu = true; + m_swallow_native_menu = true; - //Select what is under the mouse, as the context menu does, so - //a gesture acts on it - if (QGraphicsItem *item = m_diagram->itemAt(mapToScene(m_gesture_origin), transform())) { - if (!item->isSelected()) { - m_diagram->clearSelection(); - //Clearing the selection can delete handler items, so - //look the item up again (see contextMenuEvent) - if (QGraphicsItem *again = m_diagram->itemAt(mapToScene(m_gesture_origin), transform())) { - again->setSelected(true); - } + //Select what is under the mouse, as the context menu does, so + //a gesture acts on it + if (QGraphicsItem *item = m_diagram->itemAt(mapToScene(m_gesture_origin), transform())) { + if (!item->isSelected()) { + m_diagram->clearSelection(); + //Clearing the selection can delete handler items, so + //look the item up again (see contextMenuEvent) + if (QGraphicsItem *again = m_diagram->itemAt(mapToScene(m_gesture_origin), transform())) { + again->setSelected(true); } } - e->accept(); - return; } + e->accept(); + return; } //Start drag view when hold the middle button @@ -1137,6 +1286,15 @@ void DiagramView::keyPressEvent(QKeyEvent *e) if (m_diagram && m_diagram->eventInterfaceIsRunning()) { QGraphicsView::keyPressEvent(e); // let the active tool see it return; + } + //A line waiting for the click putting a core down has the + //first call on Escape: it lets that core go back to being + //a free one and lets the mouse go, while everything else + //-- the selection included -- stays as it is, so that + //Escape keeps doing one thing at a time. + if (CablePart::cancelPlacing()) { + e->accept(); + return; } if (m_diagram && !m_diagram->selectedItems().isEmpty()) { m_diagram->clearSelection(); @@ -1854,12 +2012,67 @@ QList DiagramView::contextMenuActions() const return list; } +/** + @brief DiagramView::cableCoreMenu + The menu of one single core, opened where he right-clicked: taking + that core off its line, which is the exact opposite of the menu the + free cores offer in the panel, of putting one down on a line. + + Only the colour labels count -- a right-click anywhere else on the + line is still about the line itself, and opens the folio's own menu + instead. Nothing is changed until he picks the entry, so opening the + menu and clicking it away leaves everything standing as it was. + @param menu_pos where he right-clicked, in view coordinates + @param global_pos the same place, in global coordinates + @return true when that menu was the one to open +*/ +bool DiagramView::cableCoreMenu(const QPoint &menu_pos, const QPoint &global_pos) +{ + if (!m_diagram || m_diagram->isReadOnly()) return false; + + const QPointF scene_pos = mapToScene(menu_pos); + auto *part = qgraphicsitem_cast( + m_diagram->itemAt(scene_pos, transform())); + if (!part) return false; + + const int core = part->coreAt(scene_pos); + if (core < 0) return false; + + //The line he takes a core off is the one on the sheet, so it is + //also the one the panel beside it edits from now on. + if (!part->isSelected()) { + m_diagram->clearSelection(); + part->setSelected(true); + } + + QMenu menu(this); + QAction *take = menu.addAction(tr("Retirer cette âme de la ligne")); + take->setIcon(QET::Icons::Cable); + + if (menu.exec(global_pos) == take) { + part->takeCoreOffLine(core); + } + return true; +} + /** @brief DiagramView::contextMenuEvent @param e */ void DiagramView::contextMenuEvent(QContextMenuEvent *e) { + //A line which waits for the click putting one of its cores down + //takes this very event: the right button ends that wait and no + //menu opens out of a click meant to say "no". Being asked here + //first is what makes it work for every way a menu can come up -- + //the gesture ring's own menu, the platform's (with the press on + //X11, with the release on Windows) and the one raised from the + //keyboard -- so the menu never has to be closed after the fact. + if (CablePart::cancelPlacing()) { + e->accept(); + return; + } + QPoint menu_pos = e->pos(); QPoint menu_global_pos = e->globalPos(); @@ -1908,6 +2121,16 @@ void DiagramView::contextMenuEvent(QContextMenuEvent *e) } else { + //A colour label standing on a line asks about that one core + //rather than about the whole sheet: the very opposite of the + //menu the free cores offer, of putting one down on a line. + //It comes first, so that a core drawn over something else + //still belongs to its own line. + if (cableCoreMenu(menu_pos, menu_global_pos)) { + e->accept(); + return; + } + QGraphicsView::contextMenuEvent(e); if(e->isAccepted()) return; diff --git a/sources/diagramview.h b/sources/diagramview.h index ebf3fd1f8..aa7d7834b 100644 --- a/sources/diagramview.h +++ b/sources/diagramview.h @@ -143,6 +143,20 @@ class DiagramView : public PaletteGraphicsView private: void handleElementDrop(QDropEvent *); void updateFolioReportMenu(); + /** + The menu of one single core: the right button coming down + on a colour label which stands on a line asks about that + core rather than about the whole folio -- taking it off the + line, which is the exact opposite of the menu the free + cores offer, of putting one down on a line. + + Nothing opens when the point carries no colour label: the + folio's own menu is then still the right one. + @param menu_pos where he right-clicked, in view coordinates + @param global_pos the same place, in global coordinates + @return true when that menu was the one to open + */ + bool cableCoreMenu(const QPoint &menu_pos, const QPoint &global_pos); void handleTitleBlockDrop(QDropEvent *); void handleTextDrop(QDropEvent *); void scrollOnMovement(QKeyEvent *); diff --git a/sources/elementsmover.cpp b/sources/elementsmover.cpp index d8127cae9..2e0e6a576 100644 --- a/sources/elementsmover.cpp +++ b/sources/elementsmover.cpp @@ -19,6 +19,8 @@ #include "qetproject.h" #include "autobreakconductor.h" #include "conductorautonumerotation.h" +#include "cable/cablepart.h" +#include "cable/editcablecommand.h" #include "diagram.h" #include "itemgroups.h" #include "qetgraphicsitem/conductor.h" @@ -116,6 +118,54 @@ int ElementsMover::beginMovement(Diagram *diagram, QGraphicsItem *driver_item) if (!m_moved_content.count()) return(-1); + //Steps left over by a gesture which never reached its end are + //never worth keeping: whatever starts here starts from nothing. + qDeleteAll(m_extra_commands); + m_extra_commands.clear(); + + //The cable lines he marked together with the elements come along + //too: a selection of elements and lines is one thing to move. + //When the finger is on a line itself, that line carries its own + //kind -- every line of the selection is begun by the one being + //dragged (see CablePart) -- so there is nothing to collect here. + m_moved_cables.clear(); + if (!qgraphicsitem_cast(m_movement_driver)) + { + for (QGraphicsItem *item : diagram->selectedItems()) + { + auto *cable = qgraphicsitem_cast(item); + if (!cable || !cable->cable()) continue; + + cable->beginLineGesture(); + m_moved_cables.append(cable); + } + } + + //Where every one of them stands at this moment: calling the + //movement off puts them all back here, recorded rather than + //worked out from the movement, since every step of that movement + //was snapped to the grid on its way and need not land on the + //same place coming back. + m_start_positions.clear(); + typedef DiagramContent dc; + for (auto &qgi : m_moved_content.items(dc::Elements + | dc::TextFields + | dc::Images + | dc::Shapes + | dc::ElementTextFields + | dc::TextGroup)) + { + m_start_positions.insert(qgi, qgi->pos()); + } + for (auto *conductor : m_moved_content.m_conductors_to_move) + { + if (conductor && conductor->textItem() + && conductor->textItem()->wasMovedByUser()) { + m_start_positions.insert(conductor->textItem(), + conductor->textItem()->pos()); + } + } + /* At this point, we've got all info to manage movement. * There is now a move in progress */ m_movement_running = true; @@ -177,9 +227,25 @@ void ElementsMover::continueMovement(const QPointF &movement) conductor->updatePath(); } + //The cable lines he marked together with the elements come along + //by the very same step: one gesture moves the whole selection + //instead of the lines standing still while the elements go, or + //the other way round. They are drawn where the gesture brings + //them, exactly as if the finger had been on the line itself, and + //settled once at the end (see endMovement). + for (CablePart *cable : std::as_const(m_moved_cables)) + { + if (cable) cable->continueLineGesture(movement); + } + if (m_status_bar && m_movement_driver) { - const auto point_{m_movement_driver->scenePos()}; + //A line does not stand at a point of its own -- it is drawn + //straight on the sheet -- so its own end is what is worth + //showing rather than its origin, which never moves. + auto *cable = qgraphicsitem_cast(m_movement_driver); + const auto point_{cable ? cable->firstPoint() + : m_movement_driver->scenePos()}; m_status_bar->showMessage(QString("x %1 : y %2").arg(QString::number(point_.x()), QString::number(point_.y()))); } } @@ -195,6 +261,30 @@ void ElementsMover::endMovement() // A movement must be inited if (!m_movement_running) return; + //The lines he marked together with the elements settle first: + //they may have to be asked something before they can rest, and + //their answer decides whether the whole gesture is written at + //all -- one of them calling it off calls off everything, the + //elements included, and there is nothing to undo because nothing + //at all has been written. + QList cable_steps; + for (CablePart *cable : std::as_const(m_moved_cables)) + { + if (!cable) continue; + if (!cable->settleLineGesture(cable_steps)) + { + qDeleteAll(cable_steps); + cable_steps.clear(); + cancelMovement(); + return; + } + } + for (CablePart *cable : std::as_const(m_moved_cables)) + { + if (cable) cable->doneLineGesture(); + } + m_moved_cables.clear(); + //empty command to be used has parent of commands below QUndoCommand *undo_object{new QUndoCommand()}; @@ -270,18 +360,118 @@ void ElementsMover::endMovement() } } - //Add undo_object if have child - if (undo_object->childCount()) - m_diagram->undoStack().push(undo_object); + //Add undo_object if have child -- or, when the lines of this + //selection brought their own undo over (see addExtraCommands), + //one single step for the whole gesture: taking it back has to + //take all of it back, or half the selection would stay where + //the gesture left it. + QList steps = m_extra_commands; + m_extra_commands.clear(); + steps.append(cable_steps); + + if (steps.isEmpty()) + { + if (undo_object->childCount()) + m_diagram->undoStack().push(undo_object); + else + delete undo_object; + } else - delete undo_object; + { + QString text = undo_object->text(); + if (text.isEmpty()) + text = steps.first()->text(); + + if (undo_object->childCount()) + steps.prepend(undo_object); + else + delete undo_object; + + if (steps.size() == 1) + m_diagram->undoStack().push(steps.takeFirst()); + else + m_diagram->undoStack().push(new BatchCommand(steps, text)); + } // There is no movement in progress now m_movement_running = false; m_moved_content.clear(); + m_moved_cables.clear(); + m_start_positions.clear(); //Keep saying why a held group did not move if (m_status_bar && !m_driver_held) { m_status_bar->clearMessage(); } } + +/** + @brief ElementsMover::addExtraCommands + Take over undo steps of items this mover does not move itself -- + the cable lines the user dragged himself -- so that the end of the + movement pushes them together with its own: one gesture, one step + in the history, whatever the selection is made of. + + They are thrown away by cancelMovement() if the gesture is called + off, and pushed by endMovement() otherwise. + @param steps the commands, given away with them +*/ +void ElementsMover::addExtraCommands(const QList &steps) +{ + for (QUndoCommand *step : steps) + { + if (step) m_extra_commands.append(step); + } +} + +/** + @brief ElementsMover::cancelMovement + Call the whole movement off without writing anything: every element + goes back to the place the gesture found it, every line of the + selection puts itself back where it stood and the undo steps which + were waiting for this movement are thrown away. + + One gesture is written as a whole or not at all, which is what a + "Annuler" answer to a question asked while the lines settle means. +*/ +void ElementsMover::cancelMovement() +{ + if (!m_movement_running) return; + + //Back to the places the gesture found them in: the recorded + //ones rather than the movement taken back, since every step of + //that movement was snapped to the grid on its way and coming + //back by the same amount need not land on the same place. + for (auto it = m_start_positions.constBegin(); + it != m_start_positions.constEnd(); ++it) + { + if (it.key()) it.key()->setPos(it.value()); + } + m_start_positions.clear(); + + for (auto *conductor : m_moved_content.m_conductors_to_move) + { + if (conductor) conductor->updatePath(); + } + + //Every line of the selection puts itself back where it stood + for (CablePart *cable : std::as_const(m_moved_cables)) + { + if (cable) cable->abortLineGesture(); + } + m_moved_cables.clear(); + + //Steps handed over by a line he dragged himself go with + //everything else: nothing has been written, so there is nothing + //to take back. + qDeleteAll(m_extra_commands); + m_extra_commands.clear(); + + m_current_movement -= m_current_movement; + m_movement_running = false; + m_moved_content.clear(); + + if (m_status_bar && !m_driver_held) { + m_status_bar->clearMessage(); + } +} diff --git a/sources/elementsmover.h b/sources/elementsmover.h index 2074a88eb..5a210d905 100644 --- a/sources/elementsmover.h +++ b/sources/elementsmover.h @@ -18,13 +18,16 @@ #ifndef ELEMENTS_MOVER_H #define ELEMENTS_MOVER_H +#include #include #include #include "diagramcontent.h" +class CablePart; class ConductorTextItem; class Diagram; class QStatusBar; +class QUndoCommand; /** This class manages the interactive movement of different items (elements, @@ -53,6 +56,27 @@ class ElementsMover { void continueMovement(const QPointF &); void endMovement(); bool holds(const QGraphicsItem *item) const; + /** + Steps of items this mover does not move itself -- the + cable lines the user dragged himself, which know how to + carry their own colour labels along -- handed over so + that the end of this movement pushes them together with + its own: one gesture, one undo step, whatever the + selection is made of. + @param steps the commands, given away with them + */ + void addExtraCommands(const QList &steps); + /** + Call the whole movement off without writing anything: + every element goes back to the place the gesture found it + and every line of the selection puts itself back too (see + CablePart), so nothing at all has to be undone. + + Answering "Annuler" to a question asked while the lines + settle is what calls it: one gesture is written as a + whole or not at all. + */ + void cancelMovement(); // attributes private: @@ -63,6 +87,17 @@ class ElementsMover { bool m_driver_held{false}; DiagramContent m_moved_content; QPointer m_status_bar; + ///The cable lines marked together with the elements, moved + ///by this mover from one gesture to the next (see + ///CablePart::beginLineGesture) -- empty whenever the finger + ///is on a line itself, since a line then carries its own + QList m_moved_cables; + ///Steps handed over by a line the user dragged himself, to + ///be pushed together with the rest of the movement + QList m_extra_commands; + ///Where every item of the movement stood when it began, so + ///that calling it off puts them all back exactly there + QHash m_start_positions; }; #endif diff --git a/sources/factory/propertieseditorfactory.cpp b/sources/factory/propertieseditorfactory.cpp index 48a39738b..2fc4176e7 100644 --- a/sources/factory/propertieseditorfactory.cpp +++ b/sources/factory/propertieseditorfactory.cpp @@ -18,6 +18,10 @@ #include "propertieseditorfactory.h" #include "../PropertiesEditor/propertieseditorwidget.h" +#include "../cable/cablepart.h" +#include "../cable/cablepropertieseditorwidget.h" +#include "../cablelist/cablelistmodel.h" +#include "../cablelist/cablelistmodelpropertieswidget.h" #include "../qetgraphicsitem/ViewItem/projectdbmodel.h" #include "../qetgraphicsitem/ViewItem/qetgraphicstableitem.h" #include "../qetgraphicsitem/ViewItem/ui/graphicstablepropertieseditor.h" @@ -54,7 +58,7 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor( PropertiesEditorWidget *editor, QWidget *parent) { - if (auto m = static_cast(model)) + if (auto m = dynamic_cast(model)) { if (editor && editor->metaObject()->className() @@ -65,6 +69,17 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor( } return new ProjectDBModelPropertiesWidget(m, parent); } + if (auto m = dynamic_cast(model)) + { + if (editor && + editor->metaObject()->className() + == CableListModelPropertiesWidget::staticMetaObject.className()) + { + static_cast(editor)->setModel(m); + return editor; + } + return new CableListModelPropertiesWidget(m, parent); + } return nullptr; } @@ -239,6 +254,27 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor( } return new GraphicsTablePropertiesEditor(table, parent); } + case CablePart::Type: //1012 + { + if (count_ > 1) { + return nullptr; + } + auto part = static_cast(item); + //The fields belong to the cable, never to one of its + //lines: whichever line is picked, it is the cable which + //is edited. + Cable *cable = part->cable(); + if (!cable) { + return nullptr; + } + + if (class_name == CablePropertiesEditorWidget::staticMetaObject.className()) + { + static_cast(editor)->setCable(cable, part); + return editor; + } + return new CablePropertiesEditorWidget(cable, part, parent); + } default: return nullptr; } diff --git a/sources/factory/qetgraphicstablefactory.cpp b/sources/factory/qetgraphicstablefactory.cpp index 6fe99971b..66e81b2a2 100644 --- a/sources/factory/qetgraphicstablefactory.cpp +++ b/sources/factory/qetgraphicstablefactory.cpp @@ -17,6 +17,8 @@ */ #include "qetgraphicstablefactory.h" +#include "../cablelist/cablelistmodel.h" +#include "../cablelist/cablequerywidget.h" #include "../dataBase/ui/elementquerywidget.h" #include "../dataBase/ui/summaryquerywidget.h" #include "../diagram.h" @@ -71,6 +73,26 @@ void QetGraphicsTableFactory::createAndAddSummary(Diagram *diagram) } } +/** + @brief QetGraphicsTableFactory::createAndAddCableList + Open a dialog for ask user the config of the table, + create a cable list table and add it to diagram + @param diagram +*/ +void QetGraphicsTableFactory::createAndAddCableList(Diagram *diagram) +{ + QScopedPointer d( + new AddTableDialog( + new CableQueryWidget(), + diagram->views().first())); + d->setWindowTitle(QObject::tr("Ajouter une liste de câbles")); + d->setTableName(QObject::tr("Liste des câbles")); + + if (d->exec()) { + create(diagram, d.data()); + } +} + void QetGraphicsTableFactory::create(Diagram *diagram, AddTableDialog *dialog) { auto table_ = newTable(diagram, dialog); @@ -124,20 +146,33 @@ QetGraphicsTableItem *QetGraphicsTableFactory::newTable(Diagram *diagram, AddTab if (!previous_table) { - QString identifier_; - QString query_; + QAbstractTableModel *model = nullptr; if (auto query_widget = dynamic_cast(dialog->contentWidget())) { - identifier_ = query_widget->modelIdentifier(); - query_ = query_widget->queryStr(); + auto *db_model = new ProjectDBModel(diagram->project(), diagram->project()); + db_model->setIdentifier(query_widget->modelIdentifier()); + db_model->setQuery(query_widget->queryStr()); + model = db_model; } else if (auto query_widget = dynamic_cast(dialog->contentWidget())) { - identifier_ = query_widget->modelIdentifier(); - query_ = query_widget->queryStr(); + auto *db_model = new ProjectDBModel(diagram->project(), diagram->project()); + db_model->setIdentifier(query_widget->modelIdentifier()); + db_model->setQuery(query_widget->queryStr()); + model = db_model; + } else if (auto query_widget = dynamic_cast(dialog->contentWidget())) { + auto *cable_model = new CableListModel(diagram->project(), diagram->project()); + cable_model->setIdentifier(CableQueryWidget::modelIdentifier()); + //No entries chosen is no mistake to correct: an empty + //set of columns makes an empty table, the same grey + //frame a nomenclature table makes without entries + cable_model->setFields(query_widget->selectedKeys()); + model = cable_model; + } else { + //A content widget this factory does not know leaves an + //empty database model behind, which is what it has + //always done + model = new ProjectDBModel(diagram->project(), diagram->project()); } - auto model = new ProjectDBModel(diagram->project(), diagram->project()); - model->setIdentifier(identifier_); - model->setQuery(query_); model->setData(model->index(0,0), int(dialog->tableAlignment()), Qt::TextAlignmentRole); model->setData(model->index(0,0), dialog->tableFont(), Qt::FontRole); model->setData(model->index(0,0), QETUtils::marginsToString(dialog->headerMargins()), Qt::UserRole+1); diff --git a/sources/factory/qetgraphicstablefactory.h b/sources/factory/qetgraphicstablefactory.h index 0a93b0b5b..5c3118310 100644 --- a/sources/factory/qetgraphicstablefactory.h +++ b/sources/factory/qetgraphicstablefactory.h @@ -32,6 +32,7 @@ class QetGraphicsTableFactory static void createAndAddNomenclature(Diagram *diagram); static void createAndAddSummary(Diagram *diagram); + static void createAndAddCableList(Diagram *diagram); // Public so a caller that has already built and configured an // AddTableDialog itself (never shown or exec'd -- the two // methods above always exec() one, which the scripting API diff --git a/sources/pdf_links.cpp b/sources/pdf_links.cpp index 6bdf563c1..96664f87f 100644 --- a/sources/pdf_links.cpp +++ b/sources/pdf_links.cpp @@ -17,6 +17,7 @@ */ #include "pdf_links.h" +#include "cable/cablepart.h" #include "diagram.h" #include "qetgraphicsitem/crossrefitem.h" #include "qetgraphicsitem/dynamicelementtextitem.h" @@ -50,10 +51,13 @@ void injectCrossRefLinks(QPdfEngine *engine, Diagram *diagram, const QRectF &pageBounds = geom.pageBounds; // Compute, in PDF points on its OWN page, the rectangle to frame for a - // target element (used as a /FitR destination so the link zooms onto it). - auto destRectPdf = [&](Element *tgt) -> QRectF { - Diagram *dg = tgt ? tgt->diagram() : nullptr; - if (!dg) return QRectF(); + // target (used as a /FitR destination so the link zooms onto it). + // The target is given as a rectangle in the scene of @p dg, since what + // is pointed at is not always an element: a cable section is a plain + // drawn line somewhere on another folio. + auto frameRectPdf = [&](Diagram *dg, const QRectF &targetScene) -> QRectF { + if (!dg || targetScene.width() <= 0.0 || targetScene.height() <= 0.0) + return QRectF(); const QRectF srcT = geom.sourceRectOf(dg); if (srcT.width() <= 0.0 || srcT.height() <= 0.0) return QRectF(); const qreal sT = qMin(target.width() / srcT.width(), @@ -63,35 +67,38 @@ void injectCrossRefLinks(QPdfEngine *engine, Diagram *diagram, fitT.scale(sT, sT); fitT.translate(-srcT.x(), -srcT.y()); - QRectF elemScene = tgt->mapRectToScene(tgt->boundingRect()); - // Frame the element with a little context, and enforce a minimum + // Frame the target with a little context, and enforce a minimum // framed size so tiny contacts don't zoom in extremely. + QRectF framed = targetScene; const qreal pad = 25.0; - elemScene.adjust(-pad, -pad, pad, pad); + framed.adjust(-pad, -pad, pad, pad); const qreal minSide = 160.0; - if (elemScene.width() < minSide) - elemScene.adjust(-(minSide - elemScene.width()) / 2.0, 0, - (minSide - elemScene.width()) / 2.0, 0); - if (elemScene.height() < minSide) - elemScene.adjust(0, -(minSide - elemScene.height()) / 2.0, - 0, (minSide - elemScene.height()) / 2.0); + if (framed.width() < minSide) + framed.adjust(-(minSide - framed.width()) / 2.0, 0, + (minSide - framed.width()) / 2.0, 0); + if (framed.height() < minSide) + framed.adjust(0, -(minSide - framed.height()) / 2.0, + 0, (minSide - framed.height()) / 2.0); - const QRectF devT = fitT.mapRect(elemScene); + const QRectF devT = fitT.mapRect(framed); const QPointF a = geom.devToPdf(devT.topLeft()); const QPointF b = geom.devToPdf(devT.bottomRight()); return QRectF(QPointF(qMin(a.x(), b.x()), qMin(a.y(), b.y())), QPointF(qMax(a.x(), b.x()), qMax(a.y(), b.y()))); }; - auto injectLink = [&](const QRectF &sceneRect, Element *targetElmt) { - if (!targetElmt || !targetElmt->diagram()) return; - const int targetPage = pageMap.value(targetElmt->diagram(), -1); + // One clickable link: @p sceneRect on THIS folio leads to + // @p targetDiagram, framed around @p targetScene over there. + auto injectLinkTo = [&](const QRectF &sceneRect, Diagram *targetDiagram, + const QRectF &targetScene) { + if (!targetDiagram) return; + const int targetPage = pageMap.value(targetDiagram, -1); if (targetPage < 1) return; const QRectF devRect = fit.mapRect(sceneRect); if (!devRect.isValid() || !pageBounds.intersects(devRect)) return; QString frag = QString("page=%1").arg(targetPage); - const QRectF d = destRectPdf(targetElmt); // /FitR L_B_R_T + const QRectF d = frameRectPdf(targetDiagram, targetScene); // /FitR L_B_R_T if (d.isValid()) frag += QString("&fitr=%1_%2_%3_%4") .arg(qRound(d.left())).arg(qRound(d.top())) @@ -102,6 +109,12 @@ void injectCrossRefLinks(QPdfEngine *engine, Diagram *diagram, engine->drawHyperlink(devRect, url); }; + auto injectLink = [&](const QRectF &sceneRect, Element *targetElmt) { + if (!targetElmt || !targetElmt->diagram()) return; + injectLinkTo(sceneRect, targetElmt->diagram(), + targetElmt->mapRectToScene(targetElmt->boundingRect())); + }; + for (auto *item : diagram->items()) { // --- CrossRefItem links --- @@ -117,6 +130,15 @@ void injectCrossRefLinks(QPdfEngine *engine, Diagram *diagram, continue; } + // --- Cable cross-reference links (the lines under a cable which + // name the folio it goes on to) --- + if (auto *cable = dynamic_cast(item)) { + for (const CablePart::PdfRef &ref : cable->pdfRefs()) { + injectLinkTo(ref.here, ref.diagram, ref.there); + } + continue; + } + // --- Folio report links (DynamicElementTextItem) --- if (auto *deti = dynamic_cast(item)) { Element *parent = deti->parentElement(); diff --git a/sources/properties/xrefproperties.cpp b/sources/properties/xrefproperties.cpp index 601fb02ef..cc6ca5e93 100644 --- a/sources/properties/xrefproperties.cpp +++ b/sources/properties/xrefproperties.cpp @@ -36,6 +36,7 @@ XRefProperties::XRefProperties() m_prefix_keys << "power" << "delay" << "switch"; m_master_label = "%f-%l%c"; m_slave_label = "(%f-%l%c)"; + m_font = QString(); m_offset = 0; m_slave_offset = 0; m_xref_pos = Qt::AlignBottom; @@ -65,6 +66,7 @@ void XRefProperties::toSettings(QSettings &settings, settings.setValue(prefix % "master_label", master_label); QString slave_label = m_slave_label; settings.setValue(prefix % "slave_label", slave_label); + settings.setValue(prefix % "font", m_font); QMetaEnum var = QMetaEnum::fromType(); @@ -95,6 +97,7 @@ void XRefProperties::fromSettings(const QSettings &settings, m_slave_offset = settings.value(prefix % "slave_offset", "0").toInt(); m_master_label = settings.value(prefix % "master_label", "%f-%l%c").toString(); m_slave_label = settings.value(prefix % "slave_label", "(%f-%l%c)").toString(); + m_font = settings.value(prefix % "font").toString(); QMetaEnum var = QMetaEnum::fromType(); m_xref_pos = Qt::AlignmentFlag(var.keyToValue((settings.value(prefix % "xrefpos", "AlignBottom").toString()).toStdString().data())); @@ -136,6 +139,7 @@ QDomElement XRefProperties::toXml(QDomDocument &xml_document) const xml_element.setAttribute("master_label", master_label); QString slave_label = m_slave_label; xml_element.setAttribute("slave_label", slave_label); + xml_element.setAttribute("font", m_font); foreach (QString key, m_prefix.keys()) { xml_element.setAttribute(key % "prefix", m_prefix.value(key)); } @@ -170,6 +174,7 @@ bool XRefProperties::fromXml(const QDomElement &xml_element) { m_slave_offset = xml_element.attribute("slave_offset", "0").toInt(); m_master_label = xml_element.attribute("master_label", "%f-%l%c"); m_slave_label = xml_element.attribute("slave_label","(%f-%l%c)"); + m_font = xml_element.attribute("font"); foreach (QString key, m_prefix_keys) { m_prefix.insert(key, xml_element.attribute(key % "prefix")); } @@ -179,15 +184,17 @@ bool XRefProperties::fromXml(const QDomElement &xml_element) { /** @brief XRefProperties::defaultProperties @return the default properties stored in the setting file - For the xref, there are 2 properties. - For coil, stored with the string "coil" in the returned QHash. - For protection, stored with the string "protection" in the returned QHash. + One entry per kind of element which shows a cross reference, stored + under its own key: "coil", "protection", "commutator", "plc" -- and + "cable", which is not an element at all: it is the reference a cable + writes in the label of each of its sections when the cable runs on + several folios. */ QHash XRefProperties::defaultProperties() { QHash hash; QStringList keys; - keys << "coil" << "protection" << "commutator" << "plc"; + keys << "coil" << "protection" << "commutator" << "plc" << "cable"; QSettings settings; @@ -196,6 +203,13 @@ QHash XRefProperties::defaultProperties() XRefProperties properties; QString str("diagrameditor/defaultxref"); properties.fromSettings(settings, str += key); + + //A cable takes the very same default text as a relay master + //and the other types, "%f-%l%c": what it names is then the + //other section of the cable -- the folio it stands on and + //the place it stands at there, which is what an element + //points at as well. No default of its own, so that setting + //the five of them apart needs no thought at all. hash.insert(key, properties); } @@ -213,7 +227,8 @@ bool XRefProperties::operator ==(const XRefProperties &xrp) const{ && m_offset == xrp.m_offset && m_slave_offset== xrp.m_slave_offset && m_xref_pos == xrp.m_xref_pos - && m_slave_label == xrp.m_slave_label); + && m_slave_label == xrp.m_slave_label + && m_font == xrp.m_font); } bool XRefProperties::operator !=(const XRefProperties &xrp) const diff --git a/sources/properties/xrefproperties.h b/sources/properties/xrefproperties.h index e2faa015e..6fd0a97c9 100644 --- a/sources/properties/xrefproperties.h +++ b/sources/properties/xrefproperties.h @@ -77,6 +77,14 @@ class XRefProperties : public PropertiesInterface void setMasterLabel (const QString master) {m_master_label = master;} QString masterLabel () const {return m_master_label;} + /** + The font the reference of a cable is written with, as the + settings describe it -- empty when he has not set one, which + leaves the reference with the font of the cable texts. + */ + void setFont (const QString &font) {m_font = font;} + QString font () const {return m_font;} + void setSlaveLabel(const QString slave) {m_slave_label = slave;} QString slaveLabel () const {return m_slave_label;} @@ -99,6 +107,7 @@ class XRefProperties : public PropertiesInterface QStringList m_prefix_keys; QString m_master_label; QString m_slave_label; + QString m_font; int m_offset; int m_slave_offset; QString m_key; diff --git a/sources/qetapp.cpp b/sources/qetapp.cpp index 615428f07..62abbace0 100644 --- a/sources/qetapp.cpp +++ b/sources/qetapp.cpp @@ -1522,6 +1522,97 @@ QFont QETApp::indiTextsItemFont(qreal size) return(font_); } +/** + @brief QETApp::cableTextsFont + @return the font the texts of a cable (designation, type, + installation, location, length) are written with when the cable + itself has been given no font of its own. + + That is what the preferences offer on the "Textes" page, and -- when + nothing has been chosen there yet -- the font the drawing writes its + own texts with, a shade smaller: the way every cable has always been + drawn, so that opening the preferences changes nothing until he + picks something. +*/ +QFont QETApp::cableTextsFont() +{ + QSettings settings; + if (settings.contains("cable-management/cable-text-font")) + { + QFont font; + if (QETUtils::fontFromString( + font, settings.value("cable-management/cable-text-font").toString())) { + return font; + } + } + + QFont font = QApplication::font(); + font.setPointSizeF(font.pointSizeF() * 0.9); + return font; +} + +/** + @brief QETApp::cableCoreFont + @return the font the colour labels of the cores are written with + when the cable itself has been given no font of its own. + + That is what the preferences offer on the "Textes" page; until he + picks something there, the colours follow the texts of the cable, + so one setting alone already resizes the whole label of a cable. +*/ +QFont QETApp::cableCoreFont() +{ + QSettings settings; + if (settings.contains("cable-management/cable-core-font")) + { + QFont font; + if (QETUtils::fontFromString( + font, settings.value("cable-management/cable-core-font").toString())) { + return font; + } + } + return cableTextsFont(); +} + +/** + @brief QETApp::cableTextAlignment + @return how the texts of a cable line up at the left end of their + line when the cable itself asks for nothing: what the preferences + offer on the "Textes" page, and -- until he has picked something + there -- the way every text has always stood, with its right end at + the place where the line begins. +*/ +Qt::Alignment QETApp::cableTextAlignment() +{ + const QSettings settings; + const QString name = settings.value("cable-management/cable-align").toString(); + if (name == QLatin1String("left")) { + return Qt::AlignLeft; + } + if (name == QLatin1String("center")) { + return Qt::AlignHCenter; + } + return Qt::AlignRight; +} + +/** + @brief QETApp::setCableTextAlignment + Keep what the preferences chose as the way every cable lines up its + texts, for the cables which say nothing about it themselves. + @param alignment Qt::AlignRight, Qt::AlignHCenter or Qt::AlignLeft +*/ +void QETApp::setCableTextAlignment(Qt::Alignment alignment) +{ + QString name = QLatin1String("right"); + if (alignment == Qt::AlignLeft) { + name = QLatin1String("left"); + } else if (alignment == Qt::AlignHCenter) { + name = QLatin1String("center"); + } + QSettings settings; + settings.setValue("cable-management/cable-align", name); +} + /** @brief QETApp::diagramEditors @return schema editors diff --git a/sources/qetapp.h b/sources/qetapp.h index 2c2fa1d3e..a029c8e66 100644 --- a/sources/qetapp.h +++ b/sources/qetapp.h @@ -174,6 +174,24 @@ class QETApp : public QObject static QFont diagramTextsItemFont(qreal = -1.0); static QFont dynamicTextsItemFont(qreal = -1.0); static QFont indiTextsItemFont (qreal = -1.0); + /** + @brief The font the texts of a cable (designation, type, + installation, location, length) are written with when the + cable itself asks for none. + */ + static QFont cableTextsFont(); + /** + @brief The font the colour labels of the cores of a cable + are written with when the cable itself asks for none. + */ + static QFont cableCoreFont(); + /** + @brief How the texts of a cable line up at the left end of + their line when the cable asks for nothing itself: right, + centred or left of where the line begins. + */ + static Qt::Alignment cableTextAlignment(); + static void setCableTextAlignment(Qt::Alignment alignment); static QETDiagramEditor *diagramEditorForFile(const QString &); static QETDiagramEditor *diagramEditorAncestorOf (const QWidget *child); static QList diagramEditors(); diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index 7cbfb4ed2..34ce1b81e 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -38,7 +38,10 @@ #endif #include "diagramevent/diagrameventaddshape.h" #include "diagramevent/diagrameventaddpath.h" +#include "diagramevent/diagrameventaddcable.h" #include "diagramevent/diagrameventaddtext.h" +#include "cable/cablepart.h" +#include "cable/editcablecommand.h" #include "diagramevent/diagrameventaddpaste.h" #include "diagramview.h" #include "elementspanelwidget.h" @@ -46,6 +49,7 @@ #include "factory/qetgraphicstablefactory.h" #include "print/projectprintwindow.h" #include "project/projectpropertieshandler.h" +#include "ui/projectpropertiesdialog.h" #include "projectview.h" #include "qetproject.h" #include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h" @@ -56,6 +60,7 @@ #include "recentfiles.h" #include "textgrid.h" #include "shortcutmanager.h" +#include "cablelist/cableexportdialog.h" #include "ui/bomexportdialog.h" #include "ui/conductorcolortoolbutton.h" #include "ui/diagrambgcolorbutton.h" @@ -78,6 +83,7 @@ #include "wiringlistexport.h" #include "ui/wiringlistdialog.h" #include "ui/terminalnumberingdialog.h" +#include "ui/cablenumberingdialog.h" #include #include #include @@ -639,6 +645,27 @@ void QETDiagramEditor::setUpActions() m_project_terminalBloc = new QAction(QET::Icons::TerminalStrip, tr("Lancer le plugin de création de borniers"), this); connect(m_project_terminalBloc, &QAction::triggered, this, &QETDiagramEditor::generateTerminalBlock); + //Add a cable list + m_add_cable_list = new QAction(QET::Icons::TableOfContent, tr("Ajouter une liste de câbles"), this); + connect(m_add_cable_list, &QAction::triggered, this, [this]() { + if (this->currentDiagramView()) { + QetGraphicsTableFactory::createAndAddCableList(this->currentDiagramView()->diagram()); + } + }); + + //Export the whole cable list to CSV, every column of the list + m_export_cable_list = new QAction(QET::Icons::DocumentSpreadsheet, tr("Exporter la liste des câbles au format CSV"), this); + connect(m_export_cable_list, &QAction::triggered, [this]() { + QETProject *project = this->currentProject(); + if (!project) { + return; + } + //The same dialog the material list is exported with, only + //with the columns of the cable list + CableExportDialog dialog(project, this); + dialog.exec(); + }); + //Export conductor num to csv m_project_export_conductor_num = new QAction(QET::Icons::DocumentSpreadsheet, tr("Exporter la liste des noms de conducteurs"), this); connect(m_project_export_conductor_num, &QAction::triggered, [this]() { @@ -675,6 +702,14 @@ void QETDiagramEditor::setUpActions() m_terminal_numbering = new QAction(QET::Icons::TerminalStrip, tr("Numérotation automatique des bornes"), this); connect(m_terminal_numbering, &QAction::triggered, this, &QETDiagramEditor::slot_terminalNumbering); + // Cable numbering: the rule the project numbers its cables with, and + // the numbering of every cable of that project at once + m_cable_numbering = new QAction(QET::Icons::AutoNum, tr("Numérotation des câbles"), this); + m_cable_numbering->setStatusTip( + tr("Affiche la règle de numérotation des câbles du projet et " + "renumérote ses câbles d'après elle")); + connect(m_cable_numbering, &QAction::triggered, this, &QETDiagramEditor::slot_cableNumbering); + // Reload element drawings from their current definition (bugtracker #802) m_reload_element_drawings = new QAction(QET::Icons::ViewRefresh, tr("Recharger les dessins des éléments"), this); m_reload_element_drawings->setStatusTip( @@ -1052,6 +1087,9 @@ void QETDiagramEditor::setUpActions() QAction *add_polyline = m_add_item_actions_group.addAction(QET::Icons::PartPolygon, tr("Ajouter une polyligne")); QAction *add_path = m_add_item_actions_group.addAction(QET::Icons::PartBezier, tr("Ajouter une courbe")); QAction *add_terminal_strip = m_add_item_actions_group.addAction(QET::Icons::TerminalStrip, tr("Ajouter un plan de bornes")); + //Not an element of the drawing but a line drawn across the + //conductors: what it crosses becomes the cores of a cable. + m_add_cable = m_add_item_actions_group.addAction(QET::Icons::Cable, tr("Tracer un câble")); add_text ->setStatusTip(tr("Ajoute un champ de texte sur le folio actuel")); add_image ->setStatusTip(tr("Ajoute une image sur le folio actuel")); @@ -1065,6 +1103,7 @@ void QETDiagramEditor::setUpActions() add_polyline ->setStatusTip(tr("Ajoute une polyligne sur le folio actuel")); add_path ->setStatusTip(tr("Ajoute une courbe de Bézier sur le folio actuel")); add_terminal_strip->setStatusTip(tr("Ajoute un plan de bornier sur le folio actuel")); + m_add_cable ->setStatusTip(tr("Trace la ligne maîtresse d'un câble : chaque conducteur traversé devient une de ses âmes")); add_text ->setData(QStringLiteral("text")); add_image ->setData(QStringLiteral("image")); @@ -1078,6 +1117,7 @@ void QETDiagramEditor::setUpActions() add_polyline ->setData(QStringLiteral("polyline")); add_path ->setData(QStringLiteral("path")); add_terminal_strip->setData(QStringLiteral("terminal_strip")); + m_add_cable ->setData(QStringLiteral("cable")); add_text->setCheckable(true); add_line->setCheckable(true); @@ -1086,6 +1126,7 @@ void QETDiagramEditor::setUpActions() add_arc->setCheckable(true); add_polyline->setCheckable(true); add_path->setCheckable(true); + m_add_cable->setCheckable(true); connect(&m_add_item_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::addItemGroupTriggered); //No default key, but an id: they can then be found by the command @@ -1176,6 +1217,11 @@ void QETDiagramEditor::setUpToolBar() diagram_tool_bar -> addAction (m_conductor_reset); diagram_tool_bar -> addAction (m_auto_conductor); diagram_tool_bar -> addAction (m_auto_break_conductor); + //Drawn across the conductors those two actions work on, so it + //sits with them rather than with the shapes of the "Ajouter" + //toolbar, where nobody would think to look for it. + diagram_tool_bar -> addSeparator(); + diagram_tool_bar -> addAction (m_add_cable); //Sits with the conductor actions it works alongside: it colours //the selected conductors and sets the colour of the next one drawn. m_conductor_color_button = new ConductorColorToolButton(this, this); @@ -1184,6 +1230,9 @@ void QETDiagramEditor::setUpToolBar() m_add_item_tool_bar = new QToolBar(tr("Ajouter"), this); m_add_item_tool_bar->setObjectName("adding"); m_add_item_tool_bar->addActions(m_add_item_actions_group.actions()); + if (m_add_cable) { + m_add_item_tool_bar->removeAction(m_add_cable); + } m_depth_tool_bar = new QToolBar(tr("Profondeur", "toolbar title")); m_depth_tool_bar->setObjectName("diagram_depth_toolbar"); @@ -1205,6 +1254,10 @@ void QETDiagramEditor::setUpMenu() QMenu* menu_fichier = new QMenu(tr("&Fichier"), this); QMenu* menu_edition = new QMenu(tr("&Édition"), this); QMenu* menu_project = new QMenu(tr("&Projet"), this); + //Numbering of everything the project numbers, kept together + //and sitting right behind the project it belongs to + QMenu* menu_numbering = new QMenu(tr("Numérotation"), this); + QMenu* menu_lists = new QMenu(tr("Listes"), this); QMenu* menu_affichage = new QMenu(tr("Afficha&ge"), this); // QMenu *menu_outils = new QMenu(tr("O&utils"), this); windows_menu = new QMenu(tr("Fe&nêtres"), this); @@ -1212,6 +1265,8 @@ void QETDiagramEditor::setUpMenu() insertMenu(settings_menu_, menu_fichier); insertMenu(settings_menu_, menu_edition); insertMenu(settings_menu_, menu_project); + insertMenu(settings_menu_, menu_numbering); + insertMenu(settings_menu_, menu_lists); insertMenu(settings_menu_, menu_affichage); insertMenu(help_menu_, windows_menu); @@ -1303,15 +1358,11 @@ void QETDiagramEditor::setUpMenu() menu_project -> addAction(m_remove_diagram_from_project); menu_project -> addAction(m_clean_project); menu_project -> addSeparator(); - menu_project -> addAction(m_add_summary); - menu_project -> addAction(m_add_nomenclature); menu_project -> addAction(m_csv_export); + menu_project -> addAction(m_export_cable_list); menu_project -> addAction(m_project_export_conductor_num); - menu_project -> addAction(m_terminal_strip_dialog); - menu_project -> addAction(m_project_terminalBloc); menu_project -> addAction(m_project_export_wiring_list); menu_project -> addAction(m_project_wiring_list_view); - menu_project -> addAction(m_terminal_numbering); menu_project -> addAction(m_reload_element_drawings); #ifdef QET_HAS_SCRIPTING menu_project -> addAction(m_run_script); @@ -1321,6 +1372,27 @@ void QETDiagramEditor::setUpMenu() menu_project -> addAction(m_export_project_db); #endif + // menu Numérotation + //Everything the project numbers sits here together: the + //terminals have come out of the project menu, the cables are + //the new one + menu_numbering -> addAction(m_terminal_numbering); + menu_numbering -> addAction(m_cable_numbering); + + // menu Listes + //Everything which drops a table or a list onto the sheets has + //come out of the project menu and sits here together: the + //summary, the material list, the terminal strip tools and the + //cable list. The CSV exports stay with the other exports in the + //project menu, the cable list's among them. + menu_lists -> addAction(m_add_summary); + menu_lists -> addAction(m_add_nomenclature); + menu_lists -> addSeparator(); + menu_lists -> addAction(m_terminal_strip_dialog); + menu_lists -> addAction(m_project_terminalBloc); + menu_lists -> addSeparator(); + menu_lists -> addAction(m_add_cable_list); + main_tool_bar -> toggleViewAction() -> setStatusTip(tr("Affiche ou non la barre d'outils principale")); view_tool_bar -> toggleViewAction() -> setStatusTip(tr("Affiche ou non la barre d'outils Affichage")); diagram_tool_bar -> toggleViewAction() -> setStatusTip(tr("Affiche ou non la barre d'outils Schéma")); @@ -1984,6 +2056,79 @@ void QETDiagramEditor::selectGroupTriggered(QAction *action) diagram->selectAllTextFields(); } +/** + @brief QETDiagramEditor::cableNumberingGate + Decide whether the cable tool may start, asking about the numbering + rule when the project numbers its cables by no rule at all. + + A project which does have one is asked nothing: the tool starts and + that rule gives the numbers. A project which has none is asked once + whether one should be defined now: + + - Oui opens the window where the rule is written, straight onto the + cable page of its automatic numbering; whatever he does in there, + the tool only starts if a rule really exists by the time that + window is closed. + - Non says no, and says it for good: it is remembered in the program + settings so the question does not come up on the next start + either. Drawing goes on all the same, with W as the cable's name: + no number is worked out where none has been defined. + - Annuler leaves everything as it was, the tool does not start, and + the question comes again the next time he picks the tool up. + + @param diagram the folio the cable would be drawn on + @return true when the cable tool may start +*/ +bool QETDiagramEditor::cableNumberingGate(Diagram *diagram) +{ + if (!diagram || !diagram->project()) return true; + if (diagram->project()->hasCableAutoNum()) return true; + + QSettings settings; + const QString ask_key = QStringLiteral("cable-management/ask_numbering_rule"); + if (!settings.value(ask_key, true).toBool()) { + return true; + } + + QMessageBox box(QMessageBox::Question, + tr("Numérotation automatique", "window title"), + tr("Aucune numérotation automatique des câbles n'est définie.", + "shown when a project has no cable numbering rule"), + QMessageBox::NoButton, + this); + box.setInformativeText(tr( + "Voulez-vous en définir une maintenant ?\n\n" + "Vous pourrez aussi numéroter les câbles automatiquement plus " + "tard : cette règle se règle à tout moment dans les propriétés " + "du projet, page « Numérotation auto ».", + "ask whether a cable numbering rule should be defined now")); + QPushButton *define_button = box.addButton(tr("Oui", "yes"), QMessageBox::YesRole); + QPushButton *later_button = box.addButton(tr("Non", "no"), QMessageBox::NoRole); + box.addButton(tr("Annuler", "cancel"), QMessageBox::RejectRole); + box.setDefaultButton(later_button); + box.exec(); + + if (box.clickedButton() == later_button) { + //For good, in the program settings: he has answered this + //once and should not be asked again at every start. + settings.setValue(ask_key, false); + return true; + } + if (box.clickedButton() != define_button) { + return false; + } + + //Oui: the window itself, opened where the rule is written. It + //has to really be there by the time that window is closed -- + //a Cancel in it must not leave the tool running all the same. + ProjectPropertiesDialog dialog(diagram->project(), this); + dialog.setCurrentPage(ProjectPropertiesDialog::Autonum); + dialog.changeToCable(); + dialog.exec(); + + return diagram->project()->hasCableAutoNum(); +} + /** @brief QETDiagramEditor::addItemGroupTriggered This slot is called when an item must be added to the current diagram, @@ -2036,6 +2181,22 @@ void QETDiagramEditor::addItemGroupTriggered(QAction *action) { diagram_event = new DiagramEventAddPath (d); } + else if (value == QLatin1String("cable")) + { + //The rule the number comes from -- or the word about there + //being no rule yet -- comes before anything is drawn. + if (!cableNumberingGate(d)) + { + //Nothing is drawn, so the button must not go on looking + //like a tool which is running, and whichever tool was + //active before this click must not keep going with its + //own button unchecked either. + action->setChecked(false); + d->clearEventInterface(); + return; + } + diagram_event = new DiagramEventAddCable(d); + } else if (value == "image") { DiagramEventAddImage *deai = new DiagramEventAddImage(d); @@ -2124,8 +2285,30 @@ void QETDiagramEditor::selectionGroupTriggered(QAction *action) "Déponter et/ou supprimer les niveaux des bornes concerné " "afin de pouvoir les supprimer")); } else { + //A cable line is not held by DiagramContent: it is + //removed by its own command, which also lets the cable + //it belongs to go when no line of it is left anywhere. + QList cable_parts; + for (QGraphicsItem *item : diagram->selectedItems()) { + if (auto *part = qgraphicsitem_cast(item)) { + cable_parts << part; + } + } + const bool other_items = dc.hasDeletableItems(); + diagram->clearSelection(); - diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, dc)); + + if (cable_parts.isEmpty()) { + diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, dc)); + } else if (!other_items) { + diagram->undoStack().push(new RemoveCableCommand(diagram, cable_parts)); + } else { + //One single step, so a selection holding both a + //cable and ordinary drawings comes back whole. + diagram->undoStack().push(new BatchCommand( + new DeleteQGraphicsItemCommand(diagram, dc), + new RemoveCableCommand(diagram, cable_parts))); + } dv->adjustSceneRect(); } } @@ -2319,6 +2502,9 @@ void QETDiagramEditor::slot_updateActions() m_project_export_wiring_list -> setEnabled(opened_project); m_project_wiring_list_view -> setEnabled(opened_project); m_terminal_numbering -> setEnabled(editable_project); + m_cable_numbering -> setEnabled(editable_project); + m_add_cable_list -> setEnabled(editable_project); + m_export_cable_list -> setEnabled(editable_project); m_reload_element_drawings -> setEnabled(opened_project); #ifdef QET_HAS_SCRIPTING m_run_script -> setEnabled(opened_project); @@ -2404,8 +2590,20 @@ void QETDiagramEditor::slot_updateComplexActions() m_find_element->setEnabled(selected_elements_count == 1); //Actions that need items (elements, conductors, texts...) selected, to be enabled - bool copiable_items = dc.hasCopiableItems(); - bool deletable_items = dc.hasDeletableItems(); + //A cable line is drawn on the folio but held by the cable, not + //by DiagramContent: it counts on its own for deleting and editing. + int selected_cables = 0; + for (QGraphicsItem *item : diagram_->selectedItems()) { + if (item && item->type() == CablePart::Type) { + selected_cables++; + } + } + + //A drawn cable line is copyable, cuttable and duplicatable on + //its own, although it belongs to its cable rather than to the + //folio and DiagramContent never sees it. + bool copiable_items = dc.hasCopiableItems() || selected_cables > 0; + bool deletable_items = dc.hasDeletableItems() || selected_cables > 0; m_cut -> setEnabled(!ro && copiable_items); m_copy -> setEnabled(copiable_items); m_duplicate -> setEnabled(!ro && copiable_items); @@ -2460,7 +2658,8 @@ void QETDiagramEditor::slot_updateComplexActions() - selected_dynamic_elmt_text) + selected_image + selected_shape - + selected_conductors_count; + + selected_conductors_count + + selected_cables; if (selected_editable == 1) { @@ -2486,6 +2685,13 @@ void QETDiagramEditor::slot_updateComplexActions() "edit image")); m_edit_selection -> setIcon(QET::Icons::resize_image); } + //edit cable + else if (selected_cables) + { + m_edit_selection -> setText(tr("Éditer le câble", + "edit cable")); + m_edit_selection -> setIcon(QET::Icons::Cable); + } //edit conductor else if (selected_conductors_count) { @@ -3601,6 +3807,21 @@ void QETDiagramEditor::slot_terminalNumbering() { } } +/** + * @brief QETDiagramEditor::slot_cableNumbering + * Opens the window holding the numbering rule of the project's cables + * and the button which numbers every one of them again. The window + * writes the project and pushes its own undo step itself, so nothing + * is done with its result here beyond letting it work. + */ +void QETDiagramEditor::slot_cableNumbering() { + QETProject *project = currentProject(); + if (!project) return; + + CableNumberingDialog dialog(project, this); + dialog.exec(); +} + /** @brief QETDiagramEditor::slot_reloadElementDrawings Redraw every placed element of the current project from its current diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h index d8c59e29d..ffd54794f 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -98,6 +98,18 @@ class QETDiagramEditor : public QETMainWindow void setUpMenu (); bool addProject(QETProject *, bool = true); + /** + Checked while the cable tool is about to start, when the + project has no numbering rule for its cables. He is then + asked whether one should be defined now (the window which + opens goes straight to the cable page of the automatic + numbering) or not -- and having answered no, he is not + asked again, whatever he draws from then on. A project + which does number its cables is never asked at all, and + one which lost the rule again is. + @return true when the cable tool may start. + */ + bool cableNumberingGate(Diagram *diagram); DiagramView *currentDiagramView() const; Element *currentElement() const; ProjectView *findProject(DiagramView *) const; @@ -151,6 +163,7 @@ class QETDiagramEditor : public QETMainWindow void editProjectProperties(ProjectView *); void editProjectProperties(QETProject *); void slot_terminalNumbering(); + void slot_cableNumbering(); void slot_reloadElementDrawings(); #ifdef QET_HAS_SCRIPTING void slot_runScript(); @@ -224,6 +237,7 @@ class QETDiagramEditor : public QETMainWindow *m_configure_duplicate, ///< Reopen the duplicate offset/direction dialog (#991) *m_auto_conductor, ///< Enable/Disable the use of auto conductor *m_auto_break_conductor, ///< Enable/Disable the use of auto break conductor + *m_add_cable = nullptr, ///< Draw the trunk line of a cable *m_draw_grid, ///< Switch the background grid display or not *m_draw_guides = nullptr, ///< Switch the custom guides display or not *m_cell_rulers = nullptr, ///< Keep the folio column/row headers in sight or not @@ -241,6 +255,9 @@ class QETDiagramEditor : public QETMainWindow *m_project_export_wiring_list, ///< Action to export the wiring list *m_project_wiring_list_view, ///< Action to show the wiring list read from the project database *m_terminal_numbering, ///< Action to launch terminal numbering + *m_cable_numbering, ///< Action to open the cable numbering window (rule + numbering of every cable) + *m_add_cable_list, ///< Add a cable list graphics item + *m_export_cable_list, ///< Export the cable list of the project to CSV *m_reload_element_drawings, ///< Action to redraw every placed element from its current definition #ifdef QET_HAS_SCRIPTING *m_run_script, ///< Action to run a JavaScript macro against the current project diff --git a/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp b/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp index fd9bbe6ba..0bc05792c 100644 --- a/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp +++ b/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp @@ -18,6 +18,7 @@ #include "qetgraphicstableitem.h" #include "../../qetproject.h" #include "../../QPropertyUndoCommand/qpropertyundocommand.h" +#include "../../cablelist/cablelistmodel.h" #include "../../createdxf.h" #include "../../diagram.h" #include "../../elementprovider.h" @@ -448,7 +449,13 @@ void QetGraphicsTableItem::setPreviousTable(QetGraphicsTableItem *table) } else //Copie the model of old previous table { - setModel(new ProjectDBModel(*static_cast(old_previous_table->model()))); + auto *old_model = old_previous_table ? old_previous_table->model() : nullptr; + if (auto *db_model = dynamic_cast(old_model)) { + setModel(new ProjectDBModel(*db_model)); + } + else if (auto *cable_model = dynamic_cast(old_model)) { + setModel(new CableListModel(*cable_model)); + } } if (old_previous_table && @@ -605,8 +612,12 @@ QDomElement QetGraphicsTableItem::toXml(QDomDocument &dom_document) const { //Add model auto dom_model = dom_document.createElement("model"); - auto project_db_model = static_cast(m_model.data()); - dom_model.appendChild(project_db_model->toXml(dom_document)); + if (auto project_db_model = dynamic_cast(m_model.data())) { + dom_model.appendChild(project_db_model->toXml(dom_document)); + } + else if (auto cable_model = dynamic_cast(m_model.data())) { + dom_model.appendChild(cable_model->toXml(dom_document)); + } dom_table.appendChild(dom_model); } @@ -658,16 +669,30 @@ void QetGraphicsTableItem::fromXml(const QDomElement &dom_element) } else if (this->diagram()) //The table haven't got a previous table, so there should be a model save to xml { - //Get table - auto model_ = new ProjectDBModel( - this->diagram()->project(), - this->diagram()->project()); - model_->fromXml( - dom_element - .firstChildElement("model") - .firstChildElement( - ProjectDBModel::xmlTagName())); - this->setModel(model_); + //Get table: which model wrote itself in there decides which + //class reads it back -- a cable list is not a database query + auto model_element = dom_element.firstChildElement("model"); + auto project_model_element = model_element.firstChildElement( + ProjectDBModel::xmlTagName()); + auto cable_model_element = model_element.firstChildElement( + CableListModel::xmlTagName()); + + if (!cable_model_element.isNull()) + { + auto model_ = new CableListModel( + this->diagram()->project(), + this->diagram()->project()); + model_->fromXml(cable_model_element); + this->setModel(model_); + } + else + { + auto model_ = new ProjectDBModel( + this->diagram()->project(), + this->diagram()->project()); + model_->fromXml(project_model_element); + this->setModel(model_); + } } //Restore the header from xml diff --git a/sources/qetgraphicsitem/conductor.cpp b/sources/qetgraphicsitem/conductor.cpp index 4d658129f..59ffd4a83 100644 --- a/sources/qetgraphicsitem/conductor.cpp +++ b/sources/qetgraphicsitem/conductor.cpp @@ -1786,6 +1786,32 @@ ConductorProperties Conductor::properties() const return(m_properties); } +/** + @brief Conductor::setCableReference + Record the cable core this conductor shows, together with the label + that assignment generates. Called only by CableManager, which works + the values out from the cable itself: the user never types them. + @param label the generated cable field, e.g. "15W1:br", empty to + detach the conductor from its cable + @param cable_uuid the cable this conductor is a core of + @param slot which core of that cable it shows, -1 when none +*/ +void Conductor::setCableReference(const QString &label, + const QUuid &cable_uuid, + int slot) +{ + if (m_properties.m_cable == label + && m_properties.m_cable_uuid == cable_uuid + && m_properties.m_cable_slot == slot) { + return; + } + + m_properties.m_cable = label; + m_properties.m_cable_uuid = cable_uuid; + m_properties.m_cable_slot = slot; + emit propertiesChange(); +} + /** @return true si le conducteur est mis en evidence */ diff --git a/sources/qetgraphicsitem/conductor.h b/sources/qetgraphicsitem/conductor.h index 46976129a..a13225239 100644 --- a/sources/qetgraphicsitem/conductor.h +++ b/sources/qetgraphicsitem/conductor.h @@ -122,6 +122,15 @@ class Conductor : public QGraphicsObject bool only_text = false); void setProperties(const ConductorProperties &property); ConductorProperties properties() const; + /// Records which cable core this conductor shows and writes the + /// label that assignment generates (15W1:br) into its cable + /// field. Deliberately not routed through + /// setPropertyToPotential(): a core of a cable belongs to this + /// conductor alone and must never spill over a pass-through + /// terminal onto the neighbouring half conductor. + void setCableReference(const QString &label, + const QUuid &cable_uuid, + int slot); void setProfile(const ConductorProfile &, Qt::Corner); ConductorProfile profile(Qt::Corner) const; diff --git a/sources/qeticons.cpp b/sources/qeticons.cpp index 244de3a98..0ba44f360 100644 --- a/sources/qeticons.cpp +++ b/sources/qeticons.cpp @@ -37,6 +37,7 @@ namespace QET { QIcon ArrowRightDouble; QIcon Autoconnect; QIcon BringForward; + QIcon Cable; QIcon Cancel; QIcon Company; QIcon Conductor; @@ -416,6 +417,7 @@ void QET::Icons::initIcons() ArrowRightDouble = QIcon::fromTheme("arrow-right-double"); Autoconnect = QIcon::fromTheme("autoconnect"); BringForward = QIcon::fromTheme("bring_forward"); + Cable = QIcon::fromTheme("cable"); Cancel = QIcon::fromTheme("item-cancel"); Company = QIcon::fromTheme("go-company"); Conductor = QIcon::fromTheme("conductor"); diff --git a/sources/qeticons.h b/sources/qeticons.h index 31a4d0aa8..0306870d8 100644 --- a/sources/qeticons.h +++ b/sources/qeticons.h @@ -44,6 +44,7 @@ namespace QET { extern QIcon ArrowRightDouble; extern QIcon Autoconnect; extern QIcon BringForward; + extern QIcon Cable; extern QIcon Cancel; extern QIcon Company; extern QIcon Conductor; diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index 21c6f83ce..fe2d88a48 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -23,6 +23,9 @@ #include "autoNum/numerotationcontext.h" #include "autoNum/numerotationcontextcommands.h" #include "autoNum/renumberelementscommand.h" +#include "cable/cable.h" +#include "cable/cablemanager.h" +#include "cable/cablepart.h" #include "diagram.h" #include "qetgraphicsitem/element.h" #include "qetapp.h" @@ -43,27 +46,6 @@ #include #include -namespace { - -/** - * @brief Reset numeric fields of a NumerotationContext so renumbering starts at 1. - * Keeps non-numeric parts (string/idfolio/folio/plant/locmach/elementline/elementcolumn/elementprefix) unchanged. - */ -NumerotationContext resetContextForRenumber(const NumerotationContext &tmpl) -{ - NumerotationContext out = tmpl; - 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 (out.keyIsNumber(type)) { - out.replaceValue(i, QStringLiteral("1")); - } - } - return out; -} - -} // namespace #include #include #include @@ -131,6 +113,17 @@ m_project_properties_handler{this} for (auto it = folioData.first.constBegin(); it != folioData.first.constEnd(); ++it) { addFolioAutoNum(it.key(), it.value()); } + + //The cable numbering rule a project starts with, so a project + //made from these defaults numbers its cables the way the + //global settings say without having to be opened first. + auto cableData = NumerotationContext::loadFromSettings(settings, QStringLiteral("autonum/cable")); + for (auto it = cableData.first.constBegin(); it != cableData.first.constEnd(); ++it) { + addCableAutoNum(it.key(), it.value()); + } + if (!cableData.second.isEmpty()) { + setCurrentCableAutoNum(cableData.second); + } } } @@ -840,6 +833,16 @@ QHash QETProject::elementAutoNum() const return m_element_autonum; } +/** + @brief QETProject::cableAutoNum + @return the cable numerotation stored in the project -- one entry at + most, since cables are numbered by a single rule +*/ +QHash QETProject::cableAutoNum() const +{ + return m_cable_autonum; +} + /** @brief QETProject::elementAutoNumFormula @param key : autonum title @@ -880,6 +883,83 @@ void QETProject::setCurrrentElementAutonum(QString autoNum) { m_current_element_autonum = std::move(autoNum); } +/** + @brief QETProject::cableAutoNumFormula + @param key : autonum title + @return Formula of the cable numerotation stored with key +*/ +QString QETProject::cableAutoNumFormula(const QString& key) const +{ + if (m_cable_autonum.contains(key)) { + return autonum::numerotationContextToFormula(m_cable_autonum.value(key)); + } + + return QString(); +} + +/** + @brief QETProject::hasCableAutoNum + @return true when a numbering rule for cables is set: a context + stored under the name which is in use. A project which has never been + given one answers false, and that is what makes the cable tool ask + whether one should be defined (see QETDiagramEditor). +*/ +bool QETProject::hasCableAutoNum() const +{ + if (m_current_cable_autonum.isEmpty()) return false; + return !cableAutoNum(m_current_cable_autonum).isEmpty(); +} + +/** + @brief QETProject::cableCurrentAutoNum + @return title of the cable numerotation in use (one rule only) +*/ +QString QETProject::cableCurrentAutoNum() const +{ + return m_current_cable_autonum; +} + +/** + @brief QETProject::setCurrentCableAutoNum + @param autoNum : the rule which from now on numbers the cables +*/ +void QETProject::setCurrentCableAutoNum(QString autoNum) +{ + m_current_cable_autonum = std::move(autoNum); +} + +/** + @brief QETProject::cableAutoNumRuleName + @return the name the one and only cable numbering rule is kept under +*/ +QString QETProject::cableAutoNumRuleName() +{ + return QStringLiteral("Cable"); +} + +/** + @brief QETProject::cableXAxisFirst + @return true when the cables are numbered along the X axis (the + leftmost first), false when they are numbered along the Y axis (the + topmost first) +*/ +bool QETProject::cableXAxisFirst() const +{ + return m_cable_axis_x_first; +} + +/** + @brief QETProject::setCableXAxisFirst + Which axis the whole project numbers its cables along. Stored with + the rule rather than on its own, since it only means anything while + a rule is being applied to every cable at once. + @param x_axis_first +*/ +void QETProject::setCableXAxisFirst(bool x_axis_first) +{ + m_cable_axis_x_first = x_axis_first; +} + /** @brief QETProject::renumberElementsBySchemeTitle Renumber existing elements by element autonumbering scheme title. @@ -942,7 +1022,7 @@ void QETProject::renumberElementsBySchemeTitle(const QString &scheme_title) std::sort(elements.begin(), elements.end(), [](Element *a, Element *b){ return comparPos(a, b); }); NumerotationContext base_tmpl = m_element_autonum.value(key); - NumerotationContext nc = resetContextForRenumber(base_tmpl); + NumerotationContext nc = autonum::resetContextCounters(base_tmpl); NumerotationContextCommands ncc(nc); for (Element *el : elements) { @@ -1060,6 +1140,32 @@ void QETProject::addFolioAutoNum(const QString& key, const NumerotationContext& emit autoNumContextUpdated(); } +/** + @brief QETProject::addCableAutoNum + Store the cable numbering rule. There is room for exactly one: saving + it under a name drops every other entry, so a project can never end up + with a list of cable rules to choose from. + @param key + @param context +*/ +void QETProject::addCableAutoNum(const QString& key, const NumerotationContext& context) +{ + if (!key.isEmpty()) { + const QStringList others = m_cable_autonum.keys(); + for (const QString &other : others) { + if (other != key) m_cable_autonum.remove(other); + } + } + m_cable_autonum.insert(key, context); + //The rule in use must always be one which exists: after a save + //under another name the old entry is gone, so follow the new one. + if (!key.isEmpty() && (m_current_cable_autonum.isEmpty() + || !m_cable_autonum.contains(m_current_cable_autonum))) { + m_current_cable_autonum = key; + } + emit autoNumContextUpdated(); +} + /** @brief QETProject::removeConductorAutoNum Remove Conductor Numerotation Context stored with key @@ -1089,6 +1195,21 @@ void QETProject::removeFolioAutoNum(const QString& key) { m_folio_autonum.remove(key); } +/** + @brief QETProject::removeCableAutoNum + Forget the cable numbering rule stored with key + @param key +*/ +void QETProject::removeCableAutoNum(const QString& key) +{ + m_cable_autonum.remove(key); + if (m_current_cable_autonum == key) { + m_current_cable_autonum = m_cable_autonum.isEmpty() + ? QString() : m_cable_autonum.keys().first(); + } + emit autoNumContextUpdated(); +} + /** @brief QETProject::conductorAutoNum Return conductor numerotation context stored with key. @@ -1124,6 +1245,18 @@ NumerotationContext QETProject::folioAutoNum (const QString &key) const else return NumerotationContext(); } +/** + @brief QETProject::cableAutoNum + Return the cable numerotation context stored with key. + If key is not found, return an empty numerotation context + @param key +*/ +NumerotationContext QETProject::cableAutoNum(const QString &key) const +{ + if (m_cable_autonum.contains(key)) return m_cable_autonum.value(key); + else return NumerotationContext(); +} + /** @brief QETProject::freezeExistentConductorLabel Freeze Existent Conductors in the selected folios @@ -1402,6 +1535,18 @@ QDomDocument QETProject::toXml() project_root.appendChild(xml_strip); } + //Write the cables to xml. Their sections are written here too, + //beside the cable they belong to, so a cable and everything drawn + //for it are read back together and can never come apart. + if (m_cables.count()) + { + auto xml_cables = xml_doc.createElement(QStringLiteral("cables")); + for (auto &cable : m_cables) { + xml_cables.appendChild(cable->toXml(xml_doc)); + } + project_root.appendChild(xml_cables); + } + // Write the elements collection. project_root.appendChild(m_elements_collection->root().cloneNode(true)); @@ -1905,10 +2050,23 @@ void QETProject::readProjectXml(QDomDocument &xml_project) readTerminalStripXml(xml_project); const qint64 strips_ms = phase_timer.restart(); + //Load the cables, after the diagrams their sections are drawn on + readCableXml(xml_project); + const qint64 cables_ms = phase_timer.restart(); + //Now that all are loaded we refresh content of the project. refresh(); const qint64 refresh_ms = phase_timer.restart(); + //refresh() above resolved the folio report links for the first + //time (refreshContents() -> initLink). The cable label pass at + //the end of readCableXml() ran before that, while no element + //knew its partners yet, so a core running through a report could + //not be carried over there -- it was even cleared off the far + //wire again. Run the pass once more now that the links exist: + //it recomputes everything from the project as it now stands. + CableManager::refreshLabels(this); + m_data_base.blockSignals(false); m_data_base.setUpdateBlocked(false); m_data_base.updateDB(xml_project); @@ -1916,11 +2074,12 @@ void QETProject::readProjectXml(QDomDocument &xml_project) qInfo().nospace() << "Project content built in " - << (elements_ms + diagrams_ms + strips_ms + << (elements_ms + diagrams_ms + strips_ms + cables_ms + refresh_ms + database_ms) / 1000.0 << " seconds (elements collection " << elements_ms / 1000.0 << ", diagrams " << diagrams_ms / 1000.0 << ", terminal strips " << strips_ms / 1000.0 + << ", cables " << cables_ms / 1000.0 << ", refresh " << refresh_ms / 1000.0 << ", database " << database_ms / 1000.0 << ")"; @@ -2092,7 +2251,7 @@ void QETProject::readDefaultPropertiesXml(QDomDocument &xml_project) m_default_xref_properties = XRefProperties:: defaultProperties(); //Read values indicate in project - QDomElement border_elmt, titleblock_elmt, conductors_elmt, report_elmt, xref_elmt, conds_autonums, folio_autonums, element_autonums, guides_elmt; + QDomElement border_elmt, titleblock_elmt, conductors_elmt, report_elmt, xref_elmt, conds_autonums, folio_autonums, element_autonums, cable_autonums, guides_elmt; for (QDomNode child = newdiagrams_elmt.firstChild() ; !child.isNull() ; child = child.nextSibling()) { @@ -2115,6 +2274,8 @@ void QETProject::readDefaultPropertiesXml(QDomDocument &xml_project) folio_autonums = child_elmt; else if (child_elmt.tagName()== QLatin1String("element_autonums")) element_autonums = child_elmt; + else if (child_elmt.tagName()== QLatin1String("cable_autonums")) + cable_autonums = child_elmt; else if (child_elmt.tagName() == QLatin1String("guides")) guides_elmt = child_elmt; } @@ -2165,6 +2326,30 @@ void QETProject::readDefaultPropertiesXml(QDomDocument &xml_project) m_element_autonum.insert(elmt.attribute(QStringLiteral("title")), nc); } } + if (!cable_autonums.isNull()) + { + m_current_cable_autonum = cable_autonums.attribute(QStringLiteral("current_autonum")); + //Which axis the cables are numbered along. A file written + //before this existed has no say in it and stays with the + //axis the terminals have always started from. + m_cable_axis_x_first = cable_autonums.attribute( + QStringLiteral("axis_priority"), QStringLiteral("x")) + != QLatin1String("y"); + for (auto elmt : QET::findInDomElement(cable_autonums, QStringLiteral("cable_autonum"))) + { + NumerotationContext nc; + nc.fromXml(elmt); + if (elmt.attribute(QStringLiteral("title")).isEmpty()) continue; + m_cable_autonum.insert(elmt.attribute(QStringLiteral("title")), nc); + } + //A file written by hand, or written before this rule existed, + //may name a rule which is not in the list: the numbering must + //never be left pointing at something which is not there. + if (!m_cable_autonum.contains(m_current_cable_autonum)) { + m_current_cable_autonum = m_cable_autonum.isEmpty() + ? QString() : m_cable_autonum.keys().first(); + } + } // Read guides from XML (if missing, e.g. in old projects, list stays empty) m_default_guides.clear(); @@ -2199,6 +2384,55 @@ void QETProject::readTerminalStripXml(const QDomDocument &xml_project) } } +/** + * @brief QETProject::readCableXml + * Read the cables of the project, and draw a section for each of them on + * the folio it belongs to. + * + * The diagrams have already been read at this point, which is what makes + * it possible to put the sections back where they were: a folio is + * identified by its uuid, so reordering or renumbering pages never moves + * a cable onto the wrong one. + * @param xml_project + */ +void QETProject::readCableXml(const QDomDocument &xml_project) +{ + auto xml_elmt = xml_project.documentElement(); + auto xml_cables = xml_elmt.firstChildElement(QStringLiteral("cables")); + if (xml_cables.isNull()) return; + + for (auto xml_cable = xml_cables.firstChildElement(QStringLiteral("cable")); + !xml_cable.isNull(); + xml_cable = xml_cable.nextSiblingElement(QStringLiteral("cable"))) + { + auto cable = new Cable(this); + cable->fromXml(xml_cable); + addCable(cable); + + for (const CablePartData &part_data : cable->parts()) + { + Diagram *diagram = diagramByUuid(part_data.diagram); + if (!diagram) + { + //The folio this section was drawn on has been removed: + //the section goes with it, and so do the cores which + //were only ever shown on it. + cable->removePart(part_data.uuid); + continue; + } + + auto part = new CablePart(part_data); + part->setCable(cable); + diagram->addItem(part); + } + } + + //The file holds the generated cable field of every conductor as + //well as the cables themselves; working it out again here makes + //sure the two agree even if a file was edited by hand. + CableManager::refreshLabels(this); +} + /** Export project properties under the \a xml_element XML element. */ @@ -2331,6 +2565,24 @@ void QETProject::writeDefaultPropertiesXml(QDomElement &xml_element) } xml_element.appendChild(element_autonums); + //Export Cable Autonum -- one rule, but stored exactly like the others + //so it travels with the project and survives a re-save untouched. + QDomElement cable_autonums = xml_document.createElement("cable_autonums"); + cable_autonums.setAttribute("current_autonum", m_current_cable_autonum); + cable_autonums.setAttribute("axis_priority", + m_cable_axis_x_first ? "x" : "y"); + QStringList cable_autonum_keys = cableAutoNum().keys(); + cable_autonum_keys.sort(); + for (const QString &key : std::as_const(cable_autonum_keys)) { + QDomElement cable_autonum = cableAutoNum(key).toXml(xml_document, "cable_autonum"); + if (key != "" && cableAutoNumFormula(key) != "") { + cable_autonum.setAttribute("title", key); + cable_autonum.setAttribute("formula", cableAutoNumFormula(key)); + cable_autonums.appendChild(cable_autonum); + } + } + xml_element.appendChild(cable_autonums); + // Export default guides QDomElement guides_elmt = xml_document.createElement("guides"); for (const auto &g : m_default_guides) { @@ -2516,6 +2768,86 @@ bool QETProject::removeTerminalStrip(TerminalStrip *strip) { return m_terminal_strip_vector.removeOne(strip); } +/** + * @brief QETProject::cables + * @return every cable of this project + */ +QVector QETProject::cables() const { + return m_cables; +} + +/** + * @brief QETProject::newCable + * Create a cable owned by this project and watch it: whenever it changes, + * the cable field of every conductor it feeds is worked out again. + * @return the new cable + */ +Cable *QETProject::newCable() +{ + auto cable = new Cable(this); + addCable(cable); + return cable; +} + +/** + * @brief QETProject::addCable + * @param cable + * @return true when the cable is now held by this project + */ +bool QETProject::addCable(Cable *cable) +{ + if (!cable || cable->parent() != this) + return false; + + if (m_cables.contains(cable)) + return true; + + m_cables.append(cable); + connect(cable, &Cable::changed, this, [this]() { + CableManager::refreshLabels(this); + setModified(true); + }); + emit cableAdded(cable); + return true; +} + +/** + * @brief QETProject::removeCable + * Cable is removed from the list but not deleted, so an undo command can + * put it back. + * @param cable + * @return true when the cable was in the list + */ +bool QETProject::removeCable(Cable *cable) { + if (!m_cables.removeOne(cable)) + return false; + + disconnect(cable, nullptr, this, nullptr); + + //A cable which is no longer one of ours stops writing its labels + //into the conductors it used to feed. + CableManager::refreshLabels(this); + setModified(true); + emit cableRemoved(cable); + return true; +} + +/** + * @brief QETProject::diagramByUuid + * @param uuid + * @return the folio with that identity, or nullptr. A cable section says + * which folio it is drawn on by identity, since folios are reordered and + * renumbered freely. + */ +Diagram *QETProject::diagramByUuid(const QUuid &uuid) +{ + if (uuid.isNull()) return nullptr; + for (Diagram *diagram : m_diagrams_list) { + if (diagram && diagram->uuid() == uuid) return diagram; + } + return nullptr; +} + /** Cette methode sert a reperer un projet vide, c-a-d un projet identique a ce que l'on obtient en faisant Fichier > Nouveau. diff --git a/sources/qetproject.h b/sources/qetproject.h index d2fe564f2..5ebb50c0b 100644 --- a/sources/qetproject.h +++ b/sources/qetproject.h @@ -52,6 +52,7 @@ class QUndoStack; class XmlElementCollection; class QTimer; class TerminalStrip; +class Cable; #include @@ -178,6 +179,37 @@ class QETProject : public QObject QString elementCurrentAutoNum() const; void setCurrrentElementAutonum(QString autoNum); + /// The cable numbering rule. Only one of them is ever kept: + /// a cable is numbered by one rule and that is all there is, + /// so the hash holds a single entry (see the cable tab of + /// the automatic numbering pages). + QHash cableAutoNum() const; + void addCableAutoNum (const QString& key, const NumerotationContext& context); + void removeCableAutoNum (const QString& key); + NumerotationContext cableAutoNum(const QString &key) const; + + QString cableAutoNumFormula(const QString& key) const; + bool hasCableAutoNum() const; + QString cableCurrentAutoNum() const; + void setCurrentCableAutoNum(QString autoNum); + /** + Which axis the cables of the project are laid out along + when the whole project is numbered again: true (the + default) for the X axis, so the leftmost cable of a folio + comes first, false for the Y axis, so the topmost does. + It belongs to the numbering and is written into the + project file next to the rule itself. + */ + bool cableXAxisFirst() const; + void setCableXAxisFirst(bool x_axis_first); + /** + The name the single cable numbering rule is kept under. + Fixed and never translated: it is a key which goes into + the settings and into the project file, so it must read + the same whatever language the program runs in. + */ + static QString cableAutoNumRuleName(); + /** * @brief Renumber existing elements by element autonumbering scheme. * @@ -239,6 +271,12 @@ class QETProject : public QObject bool addTerminalStrip(TerminalStrip *strip); bool removeTerminalStrip(TerminalStrip *strip); + QVector cables() const; + Cable *newCable(); + bool addCable(Cable *cable); + bool removeCable(Cable *cable); + Diagram *diagramByUuid(const QUuid &uuid); + public slots: Diagram *addNewDiagram(int pos = -1); void removeDiagram(Diagram *); @@ -272,6 +310,12 @@ class QETProject : public QObject void folioAutoNumRemoved(); void defaultTitleBlockPropertiesChanged(); void conductorAutoNumChanged(); + /// A cable joined or left this project. Views over the + /// cables -- the cable list table -- listen to both to work + /// their rows out again; during a load the cables arrive one + /// signal at a time, long after the folios were read. + void cableAdded(Cable *cable); + void cableRemoved(Cable *cable); private slots: void updateDiagramsFolioData(); @@ -306,6 +350,7 @@ class QETProject : public QObject void readProjectPropertiesXml(QDomDocument &xml_project); void readDefaultPropertiesXml(QDomDocument &xml_project); void readTerminalStripXml(const QDomDocument &xml_project); + void readCableXml(const QDomDocument &xml_project); void readUsageXml(QDomDocument &xml_project); void readWireHopsXml(QDomDocument &xml_project); @@ -369,6 +414,13 @@ class QETProject : public QObject /// Element Auto Numbering QHash m_element_autonum; //Title and NumContext hash QString m_current_element_autonum; + /// Cable auto numbering: exactly one rule, kept the same way + /// the others are so it stores and travels with the project + /// alike, but never a list to choose from. + QHash m_cable_autonum; + QString m_current_cable_autonum; + ///< which axis the cables are numbered along (see cableXAxisFirst) + bool m_cable_axis_x_first = true; bool m_auto_conductor = true; WireHops::Mode m_wire_hops = WireHops::Mode::None; bool m_auto_break_conductor = false; @@ -386,6 +438,7 @@ class QETProject : public QObject QSet m_saved_item_uuids; //symbol and wire uuids the file carries, see derivedItemUuid() projectDataBase m_data_base; QVector m_terminal_strip_vector; + QVector m_cables; ProjectPropertiesHandler m_project_properties_handler; }; diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index 53f3a41aa..2bf1acab2 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -24,6 +24,9 @@ #include "../QPropertyUndoCommand/qpropertyundocommand.h" #include "../cli_export.h" #include "../diagram.h" +#include "../bomexport.h" +#include "../cablelist/cablelistrows.h" +#include "../cablelist/cablequerywidget.h" #include "../dataBase/ui/elementquerywidget.h" #include "../dataBase/ui/summaryquerywidget.h" #include "../diagramcontent.h" @@ -260,6 +263,31 @@ bool QetScriptApi::exportBom(const QString &output) return runFlag(QStringLiteral("--export-bom"), {output}); } +/** + @brief QetScriptApi::exportCableList + Write the cable list of this project to @p output as CSV -- the same + bytes the "Exporter la liste des câbles au format CSV" action writes, + every column of CableList, every cable of the project. Written from + the project in memory rather than through runFlag(): the rows are + worked out from the cables themselves, there is no CLI flag and no + reopening of the file to do it through. + @param output path of the file to write + @return true when the file was written +*/ +bool QetScriptApi::exportCableList(const QString &output) +{ + if (!m_project) { + log(QStringLiteral("qet.exportCableList: no project")); + return false; + } + QString error; + if (!BomExport::writeCsv(output, CableList::toCsv(m_project), &error)) { + log(QStringLiteral("qet.exportCableList: %1").arg(error)); + return false; + } + return true; +} + bool QetScriptApi::exportWiring(const QString &output) { return runFlag(QStringLiteral("--export-wiring"), {output}); @@ -2679,12 +2707,16 @@ QStringList QetScriptApi::tables(int folioIndex) const file, so one call creates exactly the one table asked for. A script that wants either behaviour can resize the result or add a folio itself. - kind is "nomenclature" (an ElementQueryWidget, over placed elements) or - "summary" (a SummaryQueryWidget, over folios); query is required, since + kind is "nomenclature" (an ElementQueryWidget, over placed elements), + "summary" (a SummaryQueryWidget, over folios) or "cable_list" (a + CableQueryWidget, over the cables of the project); query is required + for the first two, since both widgets otherwise build their own from a set of checkboxes that default to none checked, and "SELECT with no columns" is not a useful table -- query() against element_nomenclature_view or - project_summary_view is the way to find one that is. + project_summary_view is the way to find one that is. For + "cable_list" the query is comma-joined column keys of CableList, and + empty means the default columns. Not undoable: newTable(), which create() calls, calls Diagram::addItem() directly, with no undo command of its own, in the @@ -2708,7 +2740,7 @@ int QetScriptApi::addTable(int folioIndex, const QString &kind, const QString &n log(QStringLiteral("qet.%1: no folio at index %2").arg(caller).arg(folioIndex)); return -1; } - if (query.isEmpty()) { + if (query.isEmpty() && kind != QLatin1String("cable_list")) { // Both widgets build their query from a set of checkboxes that // default to none checked, so "no query" is not "the sensible // default" here the way it might look -- it is SELECT with no @@ -2717,6 +2749,9 @@ int QetScriptApi::addTable(int folioIndex, const QString &kind, const QString &n // instead, over query() -- the same project database and the same // two views this project already exposes, element_nomenclature_view // for a nomenclature table and project_summary_view for a summary. + // A cable list is not read from that database (its rows come + // straight from the cables of the project), so for that kind an + // empty query is allowed and simply means "the default columns". log(QStringLiteral("qet.%1: a query is required -- try qet.query() against " "element_nomenclature_view or project_summary_view first " "to find one that returns what is wanted").arg(caller)); @@ -2732,8 +2767,27 @@ int QetScriptApi::addTable(int folioIndex, const QString &kind, const QString &n auto *w = new SummaryQueryWidget(); w->setQuery(query); content = w; + } else if (kind == QLatin1String("cable_list")) { + auto *w = new CableQueryWidget(); + if (!query.isEmpty()) { + // Comma-joined column keys; keys the list does not know are + // ignored by setFields(), and an empty list of usable keys + // leaves the default columns rather than a blank table. + const QStringList fields = query.split(QLatin1Char(','), Qt::SkipEmptyParts); + w->setFields(fields); + if (w->selectedKeys().isEmpty()) { + w->setFields(CableList::defaultKeys()); + } + } else { + // No query for this kind means the default columns, as + // promised above. The dialog a user opens leaves an empty + // table when nothing is picked, but a script asking for + // "a cable list" asks for the columns everybody gets. + w->setFields(CableList::defaultKeys()); + } + content = w; } else { - log(QStringLiteral("qet.%1: unknown kind '%2'; expected nomenclature or summary").arg(caller, kind)); + log(QStringLiteral("qet.%1: unknown kind '%2'; expected nomenclature, summary or cable_list").arg(caller, kind)); return -1; } diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index dff3225e7..c294cc616 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -189,7 +189,8 @@ class QetGraphicsTableItem; button applies, not a rule reimplemented here. sortTerminalStrip() reorders the strip's physical positions into the canonical order the editor's own sort button computes. - - @b Tables: a BOM/nomenclature or a summary (table of contents) placed + - @b Tables: a BOM/nomenclature, a summary (table of contents) or a + cable list placed on a folio, through QetGraphicsTableFactory::create() -- the same factory call the "add table" menu action makes, minus the modal AddTableDialog it collects its settings from first. That dialog is @@ -201,7 +202,11 @@ class QetGraphicsTableItem; calling addTable() once should create exactly the one table it asked for, not possibly several spread across folios it never asked to add. A script that wants either behaviour can resize the result itself or - add its own folio. + add its own folio. kind is "nomenclature" (over placed elements), + "summary" (over folios) or "cable_list" (over the cables of the + project): the first two require a real SQL query, the last takes + comma-joined column keys instead and accepts an empty query, which + means its default columns. Neither creating nor deleting a table is undoable: QetGraphicsTableFactory::newTable(), which create() calls, calls @@ -368,6 +373,7 @@ class QetScriptApi : public QObject Q_INVOKABLE bool exportCables(const QString &output); Q_INVOKABLE bool exportWires(const QString &output); Q_INVOKABLE bool exportBom(const QString &output); + Q_INVOKABLE bool exportCableList(const QString &output); Q_INVOKABLE bool exportWiring(const QString &output); Q_INVOKABLE bool exportNets(const QString &output); Q_INVOKABLE bool exportLinks(const QString &output); diff --git a/sources/ui/cablenumberingdialog.cpp b/sources/ui/cablenumberingdialog.cpp new file mode 100644 index 000000000..6c2fadb30 --- /dev/null +++ b/sources/ui/cablenumberingdialog.cpp @@ -0,0 +1,386 @@ +/* + 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 "cablenumberingdialog.h" + +#include "../autoNum/ui/selectautonumw.h" +#include "../cable/cablerenumber.h" +#include "../cable/editcablecommand.h" +#include "../qetproject.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + @brief CableNumberingDialog::CableNumberingDialog + Build the window around the numbering rule of the project: the rule + itself, the axis the cables are laid out along when the whole + project is numbered again, and the button which does that numbering. + @param project the project whose cables are numbered + @param parent +*/ +CableNumberingDialog::CableNumberingDialog(QETProject *project, QWidget *parent) : + QDialog(parent), + m_project(project) +{ + setWindowTitle(tr("Numérotation des câbles")); + //The rule editor needs room to be read: this window opens with + //the size it needs rather than with whatever its contents + //happen to add up to, so nothing of the rule is cramped from + //the first moment + setMinimumSize(QSize(900, 620)); + + auto *layout = new QVBoxLayout(this); + + auto *note = new QLabel(tr( + "Cette fenêtre montre la règle de numérotation automatique des " + "câbles de ce projet et la modifie. Une règle changée ici est " + "celle que le projet garde : elle est écrite dans le projet quand " + "vous validez."), this); + note->setWordWrap(true); + layout->addWidget(note); + + m_rule = new SelectAutonumW(3, this); + //One rule only, so the row which picks between several named + //numberings is hidden: what is on show is the project's rule + m_rule->setSingleRuleMode(true); + layout->addWidget(m_rule); + + auto *axis_group = new QGroupBox(tr("Priorité des axes"), this); + auto *axis_layout = new QHBoxLayout(axis_group); + m_axis_x = new QRadioButton(tr("Priorité à l'axe X (horizontal)"), axis_group); + m_axis_y = new QRadioButton(tr("Priorité à l'axe Y (vertical)"), axis_group); + axis_layout->addWidget(m_axis_x); + axis_layout->addWidget(m_axis_y); + layout->addWidget(axis_group); + + auto *axis_note = new QLabel(tr( + "L'axe choisi décide l'ordre dans lequel les câbles sont " + "renumérotés : X les numérote de gauche à droite, Y les numérote " + "de haut en bas."), this); + axis_note->setWordWrap(true); + layout->addWidget(axis_note); + + auto *actions = new QHBoxLayout(); + m_renumber_pb = new QPushButton(tr("Renumérotter tous les câbles"), this); + m_renumber_pb->setToolTip(tr( + "Remet le numéro de chaque câble du projet d'après la règle, en " + "partant du début : un numéro manquant après la suppression d'un " + "câble se referme ainsi.")); + actions->addWidget(m_renumber_pb); + actions->addStretch(); + + //No confirm button: this window is there to number the cables + //again and to show the rule which does it. What the window + //holds is written by the editor's own Apply button, and by the + //numbering button which writes it before numbering -- so a + //second button only saving and closing would say the same + //thing twice. + m_box = new QDialogButtonBox(this); + m_box->addButton(QDialogButtonBox::Cancel); + m_box->button(QDialogButtonBox::Cancel)->setText(tr("Annuler")); + actions->addWidget(m_box); + layout->addLayout(actions); + + connect(m_box, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(m_renumber_pb, &QPushButton::clicked, this, &CableNumberingDialog::renumber); + //The editor's own buttons are wired the same way the project + //properties wire them, so this window saves exactly what that + //page would have saved + connect(m_rule, &SelectAutonumW::applyPressed, this, &CableNumberingDialog::applyRule); + connect(m_rule, &SelectAutonumW::removeClicked, this, &CableNumberingDialog::removeRule); + //Filling a rule in lights the numbering button up as soon as + //there is something to number with + connect(m_rule, &SelectAutonumW::contextEdited, this, + &CableNumberingDialog::updateEnabling); + + if (m_project) + { + const QString key = m_project->cableCurrentAutoNum(); + const bool has_rule = !key.isEmpty() + && !m_project->cableAutoNum(key).isEmpty(); + m_rule->setContext(has_rule ? m_project->cableAutoNum(key) + : NumerotationContext()); + //The button which takes the rule away is only worth showing + //when there is one, and only while the project may be + //written to + m_rule->setRuleRemovable(has_rule && !m_project->isReadOnly()); + + m_axis_x->setChecked(m_project->cableXAxisFirst()); + m_axis_y->setChecked(!m_project->cableXAxisFirst()); + + if (m_project->isReadOnly()) + { + m_rule->setEnabled(false); + m_axis_x->setEnabled(false); + m_axis_y->setEnabled(false); + } + } + updateEnabling(); +} + +/** + @brief CableNumberingDialog::applyRule + What the editor's own Apply button does: write the rule into the + project and leave the window open, the way that button behaves on + the numbering pages of the settings. +*/ +void CableNumberingDialog::applyRule() +{ + save(); + updateEnabling(); +} + +/** + @brief CableNumberingDialog::save + Write the rule and the axis into the project. Nothing is written + when nothing was changed, so confirming a window which was only + ever looked at leaves the project exactly as it was. + @return true when the window holds nothing which could not be written +*/ +bool CableNumberingDialog::save() +{ + if (!m_project || m_project->isReadOnly()) return false; + + bool changed = false; + + if (m_rule->isModified()) + { + if (!m_rule->isValid()) + { + QMessageBox::warning(this, tr("Numérotation des câbles"), + tr("La règle n'est pas encore complète : " + "il reste un champ vide dedans.")); + return false; + } + + const QString title = QETProject::cableAutoNumRuleName(); + m_project->addCableAutoNum(title, m_rule->toNumContext()); + m_project->setCurrentCableAutoNum(title); + //What was edited is written now, so there is nothing of it + //left for a second save to write again + m_rule->setContext(m_rule->toNumContext()); + m_rule->setRuleRemovable(true); + changed = true; + } + + const bool axis_first = m_axis_x->isChecked(); + if (m_project->cableXAxisFirst() != axis_first) changed = true; + m_project->setCableXAxisFirst(axis_first); + + if (changed) m_project->setModified(true); + return true; +} + +/** + @brief CableNumberingDialog::removeRule + Take the cable numbering rule out of the project. The project then + numbers its cables by no rule at all -- they are called W until one + is defined again -- and the cable tool asks once more whether one + should be defined. +*/ +void CableNumberingDialog::removeRule() +{ + if (!m_project || m_project->isReadOnly()) return; + + const QString title = m_project->cableCurrentAutoNum(); + if (title.isEmpty()) return; + + m_project->removeCableAutoNum(title); + m_rule->setContext(NumerotationContext()); + //No rule left, so the button which takes one away goes too. And + //having just taken the rule away means numbering cables by hand + //again: the question of defining one comes back the next time a + //cable is drawn, so it is not held back any more. + m_rule->setRuleRemovable(false); + QSettings settings; + settings.setValue(QStringLiteral("cable-management/ask_numbering_rule"), + true); + m_project->setModified(true); + updateEnabling(); +} + +/** + @brief CableNumberingDialog::updateEnabling + Renumbering needs something to number with: a rule filled in well + enough to be one, and a project which may be written to. +*/ +void CableNumberingDialog::updateEnabling() +{ + if (!m_project || m_project->isReadOnly() || !m_rule) { + m_renumber_pb->setEnabled(false); + return; + } + + const QString key = m_project->cableCurrentAutoNum(); + const bool saved_rule = !key.isEmpty() + && !m_project->cableAutoNum(key).isEmpty(); + //A rule which is still being filled in counts as soon as it is + //written, and the numbering writes it together with the numbers + //it hands out + const bool drawn_rule = m_rule->isModified() && m_rule->isValid() + && !m_rule->toNumContext().isEmpty(); + + m_renumber_pb->setEnabled((saved_rule || drawn_rule) + && m_rule->isValid() + && !m_rule->toNumContext().isEmpty()); +} + +/** + @brief CableNumberingDialog::askAboutHandWritten + Some cables carry a name he typed in himself rather than one the + rule handed out. Taking such a name away is his to allow: yes and + they are numbered like all the others, no and they keep their name + while the rest is numbered around them, and cancelling leaves every + cable exactly where it stands. + @param count how many such names the project holds + @return 1 to number them too, 0 to leave them alone, -1 for nothing +*/ +int CableNumberingDialog::askAboutHandWritten(int count) +{ + if (count <= 0) return 1; + + QMessageBox box(QMessageBox::Question, tr("Numérotation des câbles"), + tr("%n nom(s) de câble(s) de ce projet ont été saisis à " + "la main.\n\n" + "Les renuméroter aussi ?", "", count), + QMessageBox::NoButton, this); + QPushButton *take = box.addButton( + tr("Oui", "Les noms saisis à la main sont remplacés eux aussi."), + QMessageBox::YesRole); + QPushButton *leave = box.addButton( + tr("Non", "Les noms saisis à la main sont laissés tels quels."), + QMessageBox::NoRole); + QPushButton *cancel = box.addButton( + tr("Annuler", "Ne renumérote rien du tout."), + QMessageBox::RejectRole); + //Keeping his own names is the safe answer under his finger + box.setDefaultButton(leave); + box.setEscapeButton(cancel); + box.exec(); + + const QAbstractButton *clicked = box.clickedButton(); + if (clicked == take) return 1; + if (clicked == leave) return 0; + return -1; +} + +/** + @brief CableNumberingDialog::renumber + Number every cable of the project again with the rule on show, + from the beginning of that rule and in the order the cables stand + along the chosen axis. One single undo step takes it all back, and + the window closes once the numbering has been written so that he + can see what became of his cables. +*/ +void CableNumberingDialog::renumber() +{ + if (!m_project || m_project->isReadOnly()) return; + + //A rule which is not complete yet is refused before anything is + //worked out at all. What the window holds is written only together + //with the numbering itself, so every way out coming before that + //point -- a rule which does not fit, him saying no, nothing to + //number -- leaves the project exactly as it stands. + const NumerotationContext rule = m_rule->toNumContext(); + if (!m_rule->isValid() || rule.isEmpty()) + { + QMessageBox::warning(this, tr("Numérotation des câbles"), + tr("La règle n'est pas encore complète.")); + return; + } + const bool axis_first = m_axis_x->isChecked(); + + //Whether he lets his own names be taken away or not, a project + //with nothing to number with says so before he is asked + //anything: the rule has to be one first + CableRenumberPlan plan = CableRenumber::plan(m_project, rule, axis_first, + true); + if (!plan.ok()) + { + QMessageBox::warning(this, tr("Numérotation des câbles"), plan.error); + return; + } + + const int answer = askAboutHandWritten(CableRenumber::byHandCount(m_project)); + if (answer < 0) return; + + //Leaving his own names alone means the numbering runs around + //them, which can hand out a number somebody else keeps: that + //is worked out again here and refused if it does not fit + if (answer == 0) + { + plan = CableRenumber::plan(m_project, rule, axis_first, false); + if (!plan.ok()) + { + QMessageBox::warning(this, tr("Numérotation des câbles"), plan.error); + return; + } + } + if (plan.cables.isEmpty()) + { + QMessageBox::information(this, tr("Numérotation des câbles"), + tr("Aucun câble à renuméroter dans ce projet.")); + return; + } + + //Every cable already carries the number the rule says: there is + //nothing to write, and an undo step which undoes nothing would + //only be in the way + bool changed = false; + for (int i = 0; i < plan.cables.size(); ++i) + { + if (plan.cables.at(i)->designation() != plan.designations.at(i) + || plan.cables.at(i)->designationByHand()) { + changed = true; + break; + } + } + if (!changed) + { + QMessageBox::information(this, tr("Numérotation des câbles"), + tr("Tous les câbles portent déjà le numéro " + "que la règle leur donne.")); + return; + } + + //The rule on show is written now, right with the numbering it + //belongs to, and the numbering then leaves the counters of that + //rule where the last number was taken from: both go into the + //project with the names, as one single act which undo takes + //back together. Nothing was written before this point, which is + //why he may still back out of here without the project having + //been touched. + if (!save()) return; + + const QString num_key = QETProject::cableAutoNumRuleName(); + if (QUndoStack *stack = m_project->undoStack()) { + stack->push(new RenumberCablesCommand( + plan.cables, plan.designations, m_project, num_key, + m_project->cableAutoNum(num_key), plan.after)); + } + accept(); +} diff --git a/sources/ui/cablenumberingdialog.h b/sources/ui/cablenumberingdialog.h new file mode 100644 index 000000000..246940c22 --- /dev/null +++ b/sources/ui/cablenumberingdialog.h @@ -0,0 +1,86 @@ +/* + 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 CABLENUMBERINGDIALOG_H +#define CABLENUMBERINGDIALOG_H + +#include + +class QDialogButtonBox; +class QETProject; +class QPushButton; +class QRadioButton; +class SelectAutonumW; + +/** + @brief The window behind the "Numérotation des câbles" entry of the + numbering menu: the one numbering rule the project numbers its + cables with, the axis those cables are laid out along when the whole + project is numbered again, and the button which does that numbering. + + The rule is the very editor the project properties hold on their + cable tab, read from the project when the window opens and written + back into the project by that editor's own Apply button -- and by + the numbering button, which writes what the window holds before it + numbers, so numbering always happens with the rule on show: a rule + which is changed here is the rule the project keeps, not a rule of + its own. There is no confirm button of this window's own: what the + window holds is either applied by hand or left behind when it is + closed. + + Renumbering works every cable of the project out from the beginning + of that rule, so the hole a deleted cable leaves in the numbering + closes -- and it only happens when he asks for it. +*/ +class CableNumberingDialog : public QDialog +{ + Q_OBJECT + + public: + explicit CableNumberingDialog(QETProject *project, QWidget *parent = nullptr); + + private slots: + ///Put the rule and the axis into the project + void applyRule(); + ///Take the cable numbering rule out of the project + void removeRule(); + ///Number every cable of the project with the rule on show + void renumber(); + ///Let the button say whether there is a rule to number with + void updateEnabling(); + + private: + ///Write what the window holds into the project + bool save(); + /** + Ask what to do with the names he typed in himself. + @param count how many such names the project holds + @return 1 to number them too, 0 to leave them alone, + -1 to do nothing at all + */ + int askAboutHandWritten(int count); + + private: + QETProject *m_project = nullptr; + SelectAutonumW *m_rule = nullptr; + QRadioButton *m_axis_x = nullptr; + QRadioButton *m_axis_y = nullptr; + QPushButton *m_renumber_pb = nullptr; + QDialogButtonBox *m_box = nullptr; +}; + +#endif // CABLENUMBERINGDIALOG_H diff --git a/sources/ui/configpage/configpages.cpp b/sources/ui/configpage/configpages.cpp index 7c2db969d..1d2ed41de 100644 --- a/sources/ui/configpage/configpages.cpp +++ b/sources/ui/configpage/configpages.cpp @@ -108,19 +108,26 @@ NewDiagramPage::NewDiagramPage(QETProject *project, auto saw_conductor = new SelectAutonumW(1); auto saw_element = new SelectAutonumW(0); auto saw_folio = new SelectAutonumW(2); + auto saw_cable = new SelectAutonumW(3); + //A cable is numbered by one rule and that is all there is: + //the row which lists the available numberings is hidden. + saw_cable->setSingleRuleMode(true); initAutoNumTab(m_autonum_conductor, saw_conductor, QStringLiteral("autonum/conductor")); initAutoNumTab(m_autonum_element, saw_element, QStringLiteral("autonum/element")); initAutoNumTab(m_autonum_folio, saw_folio, QStringLiteral("autonum/folio")); + initAutoNumTab(m_autonum_cable, saw_cable, QStringLiteral("autonum/cable")); + m_autonum_cable.fixed_name = QETProject::cableAutoNumRuleName(); QSettings autonum_settings; loadAutoNumTab(m_autonum_conductor, autonum_settings); loadAutoNumTab(m_autonum_element, autonum_settings); loadAutoNumTab(m_autonum_folio, autonum_settings); + loadAutoNumTab(m_autonum_cable, autonum_settings); // Intercept Return key in the combo line edits so it doesn't // activate the dialog's default button (OK). - for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) { + for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio, &m_autonum_cable}) { if (QComboBox *combo = tab->widget->contextComboBox()) { if (combo->lineEdit()) { combo->lineEdit()->installEventFilter(this); @@ -165,6 +172,7 @@ NewDiagramPage::NewDiagramPage(QETProject *project, autonum_inner_tab->addTab(m_autonum_conductor.widget, tr("Conducteurs")); autonum_inner_tab->addTab(m_autonum_element.widget, tr("Eléments")); autonum_inner_tab->addTab(m_autonum_folio.widget, tr("Folios")); + autonum_inner_tab->addTab(m_autonum_cable.widget, tr("Câbles")); autonum_layout->addWidget(autonum_inner_tab); tab_widget -> addTab (autonum_widget, tr("Numérotation auto")); } @@ -274,6 +282,20 @@ void NewDiagramPage::applyConf() settings.endArray(); // save global auto-numbering defaults + // + // The cable rule is written here as well as by its own Apply + // button. The cable tool asks a project for that rule the + // moment he tries to draw a cable, and a project built out of + // settings he has just filled in and confirmed must not turn + // up without one -- which is what would happen if only the + // Apply button of that one tab could save it. A rule nobody + // has finished filling in is left alone, and a tab he has not + // touched writes nothing at all. + if (m_autonum_cable.widget + && m_autonum_cable.widget->isModified() + && m_autonum_cable.widget->isValid()) { + saveAutoNumContext(m_autonum_cable); + } persistAutonumSettings(); } } @@ -387,6 +409,12 @@ void NewDiagramPage::loadAutoNumTab(AutoNumTab &tab, QSettings &settings) tab.widget->contextComboBox()->setCurrentText(data.second); tab.widget->setContext(tab.contexts.value(data.second)); } + //A tab which keeps one single rule can take it away again with + //a button of its own -- the row the others delete from is + //hidden here -- but only when there really is one to take away. + if (!tab.fixed_name.isEmpty()) { + tab.widget->setRuleRemovable(tab.contexts.contains(tab.fixed_name)); + } } /** @@ -396,15 +424,25 @@ void NewDiagramPage::loadAutoNumTab(AutoNumTab &tab, QSettings &settings) */ void NewDiagramPage::saveAutoNumContext(AutoNumTab &tab) { - QString name = tab.widget->contextComboBox()->currentText().trimmed(); - if (name.isEmpty() || isPlaceholder(tab.widget->contextComboBox(), name)) { - return; + // A tab which keeps one single rule has no visible combo box to name + // it with (the row is hidden): it saves under its fixed name. + QString name = tab.fixed_name; + if (name.isEmpty()) { + name = tab.widget->contextComboBox()->currentText().trimmed(); + if (name.isEmpty() || isPlaceholder(tab.widget->contextComboBox(), name)) { + return; + } } tab.contexts.insert(name, tab.widget->toNumContext()); if (tab.widget->contextComboBox()->findText(name) == -1) { tab.widget->contextComboBox()->addItem(name); } + tab.widget->contextComboBox()->setCurrentText(name); persistAutonumSettings(); + //There is a rule now, so there is something to take away again. + if (!tab.fixed_name.isEmpty()) { + tab.widget->setRuleRemovable(true); + } } /** @@ -413,17 +451,37 @@ void NewDiagramPage::saveAutoNumContext(AutoNumTab &tab) */ void NewDiagramPage::removeAutoNumContext(AutoNumTab &tab) { - QString name = tab.widget->contextComboBox()->currentText().trimmed(); - if (name.isEmpty() || isPlaceholder(tab.widget->contextComboBox(), name)) { - return; + QString name = tab.fixed_name; + if (name.isEmpty()) { + name = tab.widget->contextComboBox()->currentText().trimmed(); + if (name.isEmpty() || isPlaceholder(tab.widget->contextComboBox(), name)) { + return; + } } int idx = tab.widget->contextComboBox()->findText(name); - if (idx == -1) return; + // A name which is not in the list was never saved, so there is + // nothing to take out -- unless it is a single rule, which is + // always under its fixed name whether the combo knows it or not. + if (idx == -1 && tab.fixed_name.isEmpty()) return; + if (idx != -1) { + tab.widget->contextComboBox()->removeItem(idx); + tab.widget->contextComboBox()->setCurrentText(QString()); + } tab.contexts.remove(name); - tab.widget->contextComboBox()->removeItem(idx); - tab.widget->contextComboBox()->setCurrentText(QString()); tab.widget->setContext(NumerotationContext()); persistAutonumSettings(); + + //A tab which keeps one single rule has no list to delete it + //from, so this is where the button of its own goes away too. + //And having just taken the rule away means numbering cables by + //hand again: the question of defining one comes back the next + //time a cable is drawn, so it is not held back any more. + if (!tab.fixed_name.isEmpty()) { + tab.widget->setRuleRemovable(false); + QSettings settings; + settings.setValue(QStringLiteral("cable-management/ask_numbering_rule"), + true); + } } /** @@ -433,12 +491,21 @@ void NewDiagramPage::removeAutoNumContext(AutoNumTab &tab) void NewDiagramPage::persistAutonumSettings() { QSettings settings; - for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) { + for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio, &m_autonum_cable}) { QString current; - QComboBox *combo = tab->widget->contextComboBox(); - if (!isPlaceholder(combo, combo->currentText().trimmed()) - && tab->contexts.contains(combo->currentText().trimmed())) { - current = combo->currentText().trimmed(); + if (!tab->fixed_name.isEmpty()) { + // The one rule of a single-rule tab is in use as soon as + // it exists: there is nothing to choose between. + if (tab->contexts.contains(tab->fixed_name)) { + current = tab->fixed_name; + } + } + else { + QComboBox *combo = tab->widget->contextComboBox(); + if (!isPlaceholder(combo, combo->currentText().trimmed()) + && tab->contexts.contains(combo->currentText().trimmed())) { + current = combo->currentText().trimmed(); + } } NumerotationContext::saveToSettings(tab->contexts, current, settings, tab->prefix); @@ -456,7 +523,7 @@ bool NewDiagramPage::eventFilter(QObject *obj, QEvent *event) auto *ke = static_cast(event); if ((ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter)) { // Check if this is a line edit inside one of our autonum combos - for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) { + for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio, &m_autonum_cable}) { if (QComboBox *combo = tab->widget->contextComboBox()) { if (combo->lineEdit() && combo->lineEdit() == obj) { return true; // eat the event diff --git a/sources/ui/configpage/configpages.h b/sources/ui/configpage/configpages.h index 9ddb2a523..e5676611a 100644 --- a/sources/ui/configpage/configpages.h +++ b/sources/ui/configpage/configpages.h @@ -82,10 +82,16 @@ public slots: SelectAutonumW *widget = nullptr; QHash contexts; QString prefix; + /// When set, this tab keeps one single rule, always saved + /// and read under this name: there is no visible combo box + /// to name a rule with (see SelectAutonumW:: + /// setSingleRuleMode). The cable numbering works that way. + QString fixed_name; }; AutoNumTab m_autonum_conductor; AutoNumTab m_autonum_element; AutoNumTab m_autonum_folio; + AutoNumTab m_autonum_cable; void initAutoNumTab(AutoNumTab &tab, SelectAutonumW *w, const QString &prefix); void loadAutoNumTab(AutoNumTab &tab, QSettings &settings); diff --git a/sources/ui/configpage/generalconfigurationpage.cpp b/sources/ui/configpage/generalconfigurationpage.cpp index 206735c64..1d94b606a 100644 --- a/sources/ui/configpage/generalconfigurationpage.cpp +++ b/sources/ui/configpage/generalconfigurationpage.cpp @@ -21,6 +21,7 @@ #include "../../qeticons.h" #include "ui_generalconfigurationpage.h" #include "../../materiallist/materiallist.h" +#include "../../cablelist/cabletypelist.h" #include "../../utils/qetsettings.h" #include "../../utils/qetutils.h" #include "../../qetmessagebox.h" @@ -29,6 +30,7 @@ #include "../../ElementsCollection/qetlabelsfile.h" #include "../prefixconfigurationdialog.h" #include "../nokde/kcolorbutton.h" +#include #include #include #include @@ -36,6 +38,35 @@ #include #include +#include "../../diagram.h" +#include "../../qetproject.h" + +namespace { + /** + @brief What one of those font buttons says: the family, the size + and the style, the way it would read on the sheet. + @param font the font that text is written with + @return e.g. "Liberation Sans 8,1 (Regular)" + */ + QString fontButtonLabel(const QFont &font) + { + QString style = font.styleName(); + if (font.bold()) { + style = font.italic() + ? QCoreApplication::translate("GeneralConfigurationPage", + "Gras et italique") + : QCoreApplication::translate("GeneralConfigurationPage", "Gras"); + } else if (font.italic()) { + style = QCoreApplication::translate("GeneralConfigurationPage", "Italique"); + } else if (style.isEmpty()) { + style = QCoreApplication::translate("GeneralConfigurationPage", "Normal"); + } + return font.family() + QLatin1Char(' ') + + QString::number(font.pointSizeF(), 'g', 3) + + QLatin1String(" (") + style + QLatin1Char(')'); + } +} + /** @brief GeneralConfigurationPage::GeneralConfigurationPage @param parent @@ -168,6 +199,17 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) : font.styleName() + ")"; ui->m_indi_text_font_pb->setText(fontInfos); } else { ui->m_indi_text_font_pb->setText("Liberation Sans 9 (Regular)"); } + + //Cable texts: what every cable writes along its own line when + //it has not been given a font of its own. + ui->m_cable_text_font_pb->setText(fontButtonLabel(QETApp::cableTextsFont())); + ui->m_cable_core_font_pb->setText(fontButtonLabel(QETApp::cableCoreFont())); + + //Where those texts stop in relation to the beginning of their + //own line, for the cables which say nothing about it themselves + const int align_index = int(QETApp::cableTextAlignment()) == int(Qt::AlignLeft) ? 2 + : (int(QETApp::cableTextAlignment()) == int(Qt::AlignHCenter) ? 1 : 0); + ui->m_cable_align_cb->setCurrentIndex(align_index); ui->m_highlight_integrated_elements->setChecked(settings.value("diagrameditor/highlight-integrated-elements", true).toBool()); ui->m_default_elements_info->setPlainText(settings.value("elementeditor/default-informations", "").toString()); @@ -247,6 +289,17 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) : .arg(MaterialList::defaultPath())); } + //CABLE TYPE FILE + path = CableTypeList::configuredPath(); + ui->m_cable_type_list_path_le->setText(path); + if (path.isEmpty()) + { + ui->m_cable_type_list_path_le->setPlaceholderText( + tr("Non configuré (par défaut : %1)", + "hint shown in the cable type file field when no file is configured yet") + .arg(CableTypeList::defaultPath())); + } + fillLang(); } @@ -438,6 +491,30 @@ void GeneralConfigurationPage::applyConf() //kept as chosen even when it doesn't exist yet : the user may well //point QElectroTech at a file he intends to write later. MaterialList::setConfiguredPath(ui->m_material_list_path_le->text().trimmed()); + + //CABLE TYPE FILE + //Same rule as the material file : it is a plain file, kept as + //chosen even when it doesn't exist yet. + CableTypeList::setConfiguredPath(ui->m_cable_type_list_path_le->text().trimmed()); + + //The way a cable lines up its texts at the beginning of its + //line, for every cable which says nothing about it itself -- + //written now, so the sheets drawn again just below show it. + const int align_index = ui->m_cable_align_cb->currentIndex(); + QETApp::setCableTextAlignment(align_index == 2 ? Qt::AlignLeft + : (align_index == 1 + ? Qt::AlignHCenter + : Qt::AlignRight)); + + //Every folio is drawn again: the texts of the cables are written + //from the settings, so that what he just picked reaches the + //sheets without his having to touch anything on them first. + for (QETProject *project : QETApp::registeredProjects()) + { + for (Diagram *diagram : project->diagrams()) { + diagram->update(); + } + } } /** @@ -556,6 +633,45 @@ void GeneralConfigurationPage::on_m_dyn_text_font_pb_clicked() } } +/** + @brief GeneralConfigurationPage::on_m_cable_text_font_pb_clicked + Let him pick the font the texts of a cable are written with by + default: designation, type, installation, location and length. + + Every cable which has not been given a font of its own follows it, + from now on: one which has keeps the one it was given, which is why + those are set on the cable itself rather than here. +*/ +void GeneralConfigurationPage::on_m_cable_text_font_pb_clicked() +{ + bool ok = false; + const QFont font = QFontDialog::getFont(&ok, QETApp::cableTextsFont(), this, + tr("Police des textes de câble")); + if (!ok) return; + + QSettings settings; + settings.setValue("cable-management/cable-text-font", QETUtils::fontToString(font)); + ui->m_cable_text_font_pb->setText(fontButtonLabel(QETApp::cableTextsFont())); +} + +/** + @brief GeneralConfigurationPage::on_m_cable_core_font_pb_clicked + Let him pick the font the colours of the cores are written with by + default -- all of them together, the way they are drawn: one row of + colours beside the line, never set apart from each other. +*/ +void GeneralConfigurationPage::on_m_cable_core_font_pb_clicked() +{ + bool ok = false; + const QFont font = QFontDialog::getFont(&ok, QETApp::cableCoreFont(), this, + tr("Police des couleurs des âmes")); + if (!ok) return; + + QSettings settings; + settings.setValue("cable-management/cable-core-font", QETUtils::fontToString(font)); + ui->m_cable_core_font_pb->setText(fontButtonLabel(QETApp::cableCoreFont())); +} + @@ -791,6 +907,70 @@ void GeneralConfigurationPage::on_m_material_list_create_pb_clicked() ui->m_material_list_path_le->setText(path); } +/** + @brief GeneralConfigurationPage::on_m_cable_type_list_browse_pb_clicked + Let the user pick an existing cable type file. +*/ +void GeneralConfigurationPage::on_m_cable_type_list_browse_pb_clicked() +{ + QString start_dir = ui->m_cable_type_list_path_le->text(); + start_dir = start_dir.isEmpty() + ? QETApp::documentDir() + : QFileInfo(start_dir).absolutePath(); + + const QString path = QFileDialog::getOpenFileName( + this, + tr("Sélectionner le fichier de la liste des types de câbles"), + start_dir, + tr("Fichiers csv (*.csv)")); + + if (!path.isEmpty()) { + ui->m_cable_type_list_path_le->setText(path); + } +} + +/** + @brief GeneralConfigurationPage::on_m_cable_type_list_create_pb_clicked + Create the cable type file with its header line, so the columns are + known before the user fills them from his spreadsheet. +*/ +void GeneralConfigurationPage::on_m_cable_type_list_create_pb_clicked() +{ + QString path = ui->m_cable_type_list_path_le->text(); + path = path.isEmpty() + ? CableTypeList::defaultPath() + : QFileInfo(path).absolutePath() + QLatin1Char('/') + CableTypeList::defaultFileName(); + + path = QFileDialog::getSaveFileName( + this, + tr("Créer le fichier de la liste des types de câbles"), + path, + tr("Fichiers csv (*.csv)")); + if (path.isEmpty()) { + return; + } + if (QFileInfo(path).suffix().isEmpty()) { + path += QStringLiteral(".csv"); + } + + //An existing file is kept as it is : this button creates the + //header, it never overwrites a catalogue. + if (CableTypeList::isEmptyFile(path)) + { + QString error; + if (!CableTypeList::createFile(path, &error)) + { + QET::QetMessageBox::critical(this, + tr("Création impossible"), + tr("Impossible de créer le fichier :\n%1\n%2") + .arg(path, error)); + return; + } + } + + ui->m_cable_type_list_path_le->setText(path); +} + void GeneralConfigurationPage::on_m_indi_text_font_pb_clicked() { bool ok; diff --git a/sources/ui/configpage/generalconfigurationpage.h b/sources/ui/configpage/generalconfigurationpage.h index 4954a6d3c..e613c7b88 100644 --- a/sources/ui/configpage/generalconfigurationpage.h +++ b/sources/ui/configpage/generalconfigurationpage.h @@ -50,7 +50,11 @@ class GeneralConfigurationPage : public ConfigPage void on_m_prefix_pb_clicked(); void on_m_material_list_browse_pb_clicked(); void on_m_material_list_create_pb_clicked(); + void on_m_cable_type_list_browse_pb_clicked(); + void on_m_cable_type_list_create_pb_clicked(); void on_m_indi_text_font_pb_clicked(); + void on_m_cable_text_font_pb_clicked(); + void on_m_cable_core_font_pb_clicked(); void on_MaxPartsElementEditorList_sb_valueChanged(int value); void on_DiagramEditor_Grid_PointSize_min_sb_valueChanged(int value); void on_ElementEditor_Grid_PointSize_min_sb_valueChanged(int value); diff --git a/sources/ui/configpage/generalconfigurationpage.ui b/sources/ui/configpage/generalconfigurationpage.ui index 2298006ee..f3baa3085 100644 --- a/sources/ui/configpage/generalconfigurationpage.ui +++ b/sources/ui/configpage/generalconfigurationpage.ui @@ -510,6 +510,47 @@ + + + + Fichier de la liste des types de câbles + + + + + + + + + Répertoire des types de câbles utilisé pour définir un câble (nombre d'âmes, couleurs, section) + + + true + + + Non configuré + + + + + + + Parcourir... + + + + + + + Créer le fichier de la liste des types de câbles avec ses en-têtes + + + Créer... + + + + + @@ -775,6 +816,113 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments + + + + Textes de câble + + + + + + Désignation, type, installation, emplacement, longueur : + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Police des textes écrits le long de la ligne d'un câble. Un câble réglé sur mesure garde la sienne. + + + + + + + + + + Couleurs des âmes (toutes ensemble) : + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Police des couleurs des âmes d'un câble, écrites le long de la ligne. Un câble réglé sur mesure garde la sienne. + + + + + + + + + + Alignement des textes : + + + true + + + + + + + Où les textes d'un câble s'arrêtent par rapport au début de sa ligne. Un câble réglé sur mesure garde le sien. + + + + Aligné à droite + + + + + Centré + + + + + Aligné à gauche + + + + + + + diff --git a/sources/ui/configpage/projectconfigpages.cpp b/sources/ui/configpage/projectconfigpages.cpp index feae7a034..f2745bf21 100644 --- a/sources/ui/configpage/projectconfigpages.cpp +++ b/sources/ui/configpage/projectconfigpages.cpp @@ -33,6 +33,8 @@ #include "../titleblockpropertieswidget.h" #include "../xrefpropertieswidget.h" +#include + //#include "ui_autonumberingmanagementw.h" #include @@ -353,9 +355,20 @@ QIcon ProjectAutoNumConfigPage::icon() const /** @brief ProjectAutoNumConfigPage::applyProjectConf + Save what this page has to save when the window is confirmed. + The cable rule is written from here as well as by its own Apply + button: the cable tool asks the project for that rule the moment + he tries to draw a cable, so filling it in and confirming this + window only to be asked once more would be a surprise. A rule he + has not finished filling in is left alone, and a page he has not + touched leaves the project exactly as it was. */ void ProjectAutoNumConfigPage::applyProjectConf() -{} +{ + if (m_saw_cable && m_saw_cable->isModified() && m_saw_cable->isValid()) { + saveContextCable(); + } +} /** @brief ProjectAutoNumConfigPage::initWidgets @@ -364,6 +377,7 @@ void ProjectAutoNumConfigPage::applyProjectConf() void ProjectAutoNumConfigPage::initWidgets() { QTabWidget *tab_widget = new QTabWidget(this); + m_tab_widget = tab_widget; //Management tab m_amw = new AutoNumberingManagementW(project()); @@ -380,6 +394,12 @@ void ProjectAutoNumConfigPage::initWidgets() //Folio Tab m_saw_folio = new SelectAutonumW(2); tab_widget->addTab(m_saw_folio, tr("Folios")); + + //Cable tab: one rule only, so the row which lists the available + //numberings stays hidden and there is nothing to choose between. + m_saw_cable = new SelectAutonumW(3); + m_saw_cable->setSingleRuleMode(true); + tab_widget->addTab(m_saw_cable, tr("Câbles")); //AutoNumbering Tab m_faw = new FolioAutonumberingW(project()); @@ -414,6 +434,7 @@ void ProjectAutoNumConfigPage::readValuesFromProject() m_saw_conductor->contextComboBox()->clear(); m_saw_element->contextComboBox()->clear(); m_saw_folio->contextComboBox()->clear(); + m_saw_cable->contextComboBox()->clear(); //Conductor Tab const QStringList strlc(m_project->conductorAutoNum().keys()); @@ -426,6 +447,25 @@ void ProjectAutoNumConfigPage::readValuesFromProject() //Folio Tab const QStringList strlf(m_project->folioAutoNum().keys()); m_saw_folio->contextComboBox()->addItems(strlf); + + //Cable tab: the rule goes straight into the editor, since with the + //row of available numberings hidden he can never pick it out of + //the combo box himself. With no rule at all the editor shows an + //empty one to fill in, exactly like a numbering which has just + //been opened for the first time. + const QStringList strlk(m_project->cableAutoNum().keys()); + m_saw_cable->contextComboBox()->addItems(strlk); + const QString cable_rule = m_project->cableCurrentAutoNum(); + const bool cable_rule_ok = !cable_rule.isEmpty() + && !m_project->cableAutoNum(cable_rule).isEmpty(); + if (cable_rule_ok) { + m_saw_cable->setContext(m_project->cableAutoNum(cable_rule)); + } else { + m_saw_cable->setContext(NumerotationContext()); + } + //The button which takes the rule away is only worth showing when + //there is one, and only clickable when the project allows changes. + m_saw_cable->setRuleRemovable(cable_rule_ok && !m_project->isReadOnly()); //Folio AutoNumbering Tab m_faw->setContext(m_project->folioAutoNum().keys()); @@ -440,6 +480,15 @@ void ProjectAutoNumConfigPage::adjustReadOnly() if (m_import_pb && m_project) { m_import_pb->setDisabled(m_project->isReadOnly()); } + //The button which takes the cable numbering rule away does the + //same thing as the import button: nothing while the project + //cannot be written. + if (m_saw_cable && m_project) { + const QString title = m_project->cableCurrentAutoNum(); + m_saw_cable->setRuleRemovable(!m_project->isReadOnly() + && !title.isEmpty() + && !m_project->cableAutoNum(title).isEmpty()); + } } /** @@ -466,6 +515,11 @@ void ProjectAutoNumConfigPage::buildConnections() connect(m_saw_folio, &SelectAutonumW::removeClicked, this, &ProjectAutoNumConfigPage::removeContextFolio); connect(m_saw_folio->contextComboBox(), &QComboBox::textActivated, this, &ProjectAutoNumConfigPage::updateContextFolio); + //Cable tab: one rule, so there is no name to pick in the hidden + //combo box -- applying always writes the single rule. + connect(m_saw_cable, &SelectAutonumW::applyPressed, this, &ProjectAutoNumConfigPage::saveContextCable); + connect(m_saw_cable, &SelectAutonumW::removeClicked, this, &ProjectAutoNumConfigPage::removeContextCable); + // Auto Folio Numbering connect(m_faw, &FolioAutonumberingW::applyPressed, this, &ProjectAutoNumConfigPage::applyAutoNum); @@ -541,6 +595,68 @@ void ProjectAutoNumConfigPage::saveContextElement() } } +/** + @brief ProjectAutoNumConfigPage::saveContextCable + Save the cable numbering rule into the project. + + There is one rule only, and it has no name the user could have typed + (the row which holds those names is hidden): it always goes in under + the fixed name, and it is the rule in use from the moment it exists. +*/ +void ProjectAutoNumConfigPage::saveContextCable() +{ + if (!m_project || m_project->isReadOnly()) return; + + const QString title = QETProject::cableAutoNumRuleName(); + m_project->addCableAutoNum(title, m_saw_cable->toNumContext()); + m_project->setCurrentCableAutoNum(title); + + //Keep the hidden combo box in step with what the project holds, + //so the next reading of this page shows the rule just saved. + if (m_saw_cable->contextComboBox()->findText(title) == -1) { + m_saw_cable->contextComboBox()->addItem(title); + } + m_saw_cable->contextComboBox()->setCurrentText(title); + //There is a rule now, so there is something to take away again. + m_saw_cable->setRuleRemovable(true); + //This is applied straight away rather than at the end of the + //window, so the project has to say so itself: otherwise the rule + //would be dropped by a Cancel which comes after it. + m_project->setModified(true); +} + +/** + @brief ProjectAutoNumConfigPage::removeContextCable + Take the cable numbering rule out of the project. The project then + numbers its cables by no rule at all -- they are called W until one + is defined again -- and the cable tool asks once more whether one + should be defined. +*/ +void ProjectAutoNumConfigPage::removeContextCable() +{ + if (!m_project || m_project->isReadOnly()) return; + + const QString title = m_project->cableCurrentAutoNum(); + if (title.isEmpty()) return; + + m_project->removeCableAutoNum(title); + const int idx = m_saw_cable->contextComboBox()->findText(title); + if (idx != -1) { + m_saw_cable->contextComboBox()->removeItem(idx); + } + m_saw_cable->contextComboBox()->setCurrentText(QString()); + m_saw_cable->setContext(NumerotationContext()); + //No rule left, so the button which takes one away goes too. And + //having just taken the rule away means numbering cables by hand + //again: the question of defining one comes back the next time a + //cable is drawn, so it is not held back any more. + m_saw_cable->setRuleRemovable(false); + QSettings settings; + settings.setValue(QStringLiteral("cable-management/ask_numbering_rule"), + true); + m_project->setModified(true); +} + /** @brief ProjectAutoNumConfigPage::importFromProject Read the automatic numbering rules stored in another .qet project and @@ -966,5 +1082,18 @@ void ProjectAutoNumConfigPage::removeContextFolio() */ void ProjectAutoNumConfigPage::changeToTab(int i) { - qDebug()<<"Q_UNUSED"<= m_tab_widget->count()) return; + m_tab_widget->setCurrentIndex(i); +} + +/** + @brief ProjectAutoNumConfigPage::changeToCableTab + Show the cable tab -- the one the cable tool opens when it finds out + that the project has no numbering rule yet. +*/ +void ProjectAutoNumConfigPage::changeToCableTab() +{ + //Tabs in order : Management, Conducteurs, Eléments, Folios, Câbles + changeToTab(4); } diff --git a/sources/ui/configpage/projectconfigpages.h b/sources/ui/configpage/projectconfigpages.h index 7375d83ff..b16a99c6a 100644 --- a/sources/ui/configpage/projectconfigpages.h +++ b/sources/ui/configpage/projectconfigpages.h @@ -24,6 +24,7 @@ class QComboBox; class QLineEdit; class QCheckBox; class QPushButton; +class QTabWidget; class QETProject; class BorderPropertiesWidget; class ConductorPropertiesWidget; @@ -142,6 +143,9 @@ class ProjectAutoNumConfigPage : public ProjectConfigPage { QIcon icon() const override; void applyProjectConf() override; virtual void changeToTab(int); + ///Jump straight to the cable tab (used when the cable tool + ///asks whether a numbering rule should be defined) + void changeToCableTab(); protected: void initWidgets() override; void initLayout() override {} @@ -159,6 +163,8 @@ class ProjectAutoNumConfigPage : public ProjectConfigPage { void updateContextElement(const QString&);//element void saveContextElement(); void removeContextElement(); + void saveContextCable(); //cable: one rule, no name to pick + void removeContextCable(); void importFromProject(); void applyAutoNum(); @@ -175,9 +181,12 @@ class ProjectAutoNumConfigPage : public ProjectConfigPage { SelectAutonumW *m_saw_conductor; SelectAutonumW *m_saw_folio; SelectAutonumW *m_saw_element; + SelectAutonumW *m_saw_cable; FolioAutonumberingW *m_faw; AutoNumberingManagementW *m_amw; QPushButton *m_import_pb = nullptr; + /// The tabs of this page, kept to switch between them + QTabWidget *m_tab_widget = nullptr; }; diff --git a/sources/ui/projectpropertiesdialog.cpp b/sources/ui/projectpropertiesdialog.cpp index f427d923e..e95658777 100644 --- a/sources/ui/projectpropertiesdialog.cpp +++ b/sources/ui/projectpropertiesdialog.cpp @@ -93,3 +93,17 @@ void ProjectPropertiesDialog::changeToFolio() m_properties_dialog->pages.at(2)); autoNumPage->changeToTab(3); } + +/** + @brief ProjectPropertiesDialog::changeToCable + Change the current displayed tab to the cable numbering tab. + The caller is the cable tool, which opens this window when it finds + that the project numbers its cables by no rule at all. +*/ +void ProjectPropertiesDialog::changeToCable() +{ + ProjectAutoNumConfigPage *autoNumPage = + static_cast ( + m_properties_dialog->pages.at(2)); + autoNumPage->changeToCableTab(); +} diff --git a/sources/ui/projectpropertiesdialog.h b/sources/ui/projectpropertiesdialog.h index 096656e33..f571be810 100644 --- a/sources/ui/projectpropertiesdialog.h +++ b/sources/ui/projectpropertiesdialog.h @@ -41,6 +41,8 @@ class ProjectPropertiesDialog : public QObject { void exec(); void setCurrentPage(ProjectPropertiesDialog::Page); void changeToFolio(); + ///Show the automatic numbering page with the cable tab on top + void changeToCable(); private: ConfigDialog *m_properties_dialog; diff --git a/sources/ui/xrefpropertieswidget.cpp b/sources/ui/xrefpropertieswidget.cpp index c113eda45..f7476af45 100644 --- a/sources/ui/xrefpropertieswidget.cpp +++ b/sources/ui/xrefpropertieswidget.cpp @@ -17,12 +17,16 @@ */ #include +#include #include #include +#include #include "xrefpropertieswidget.h" #include "ui_xrefpropertieswidget.h" #include "qdebug.h" +#include "../qetapp.h" +#include "../utils/qetutils.h" /** @brief XRefPropertiesWidget::XRefPropertiesWidget @@ -37,10 +41,12 @@ XRefPropertiesWidget::XRefPropertiesWidget(QHash prope { ui->setupUi(this); buildUi(); + fillMissingTypes(); connect(ui->m_display_has_cross_rb, &QRadioButton::toggled, ui->m_cross_properties_gb, &QWidget::setEnabled); connect(ui->m_display_has_contacts_rb, &QRadioButton::toggled, ui->m_show_all_slaves_cb, &QWidget::setEnabled); connect(ui->m_type_cb, qOverload(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::typeChanged); connect(ui->m_snap_to_cb, qOverload(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::enableOffsetSB); + connect(ui->m_font_pb, &QPushButton::clicked, this, &XRefPropertiesWidget::chooseXRefFont); updateDisplay(); } @@ -54,6 +60,7 @@ XRefPropertiesWidget::~XRefPropertiesWidget() disconnect(ui->m_display_has_contacts_rb, &QRadioButton::toggled, ui->m_show_all_slaves_cb, &QWidget::setEnabled); disconnect(ui->m_type_cb, qOverload(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::typeChanged); disconnect(ui->m_snap_to_cb, qOverload(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::enableOffsetSB); + disconnect(ui->m_font_pb, &QPushButton::clicked, this, &XRefPropertiesWidget::chooseXRefFont); delete ui; } @@ -65,10 +72,33 @@ XRefPropertiesWidget::~XRefPropertiesWidget() void XRefPropertiesWidget::setProperties(const QHash &properties) { m_properties = properties; + fillMissingTypes(); updateDisplay(); m_previous_type_index = ui->m_type_cb->currentIndex(); } +/** + @brief XRefPropertiesWidget::fillMissingTypes + Make sure every type the combo box offers has properties of its own. + + A project built before a type existed does not hold that type, and + neither does a settings file written before it. Without an entry the + form would make one up out of thin air, and for a cable that would be + the very default of the coil ("%f-%l%c"), which reads as a stray dash + behind the folio number and two empty numbers after it. +*/ +void XRefPropertiesWidget::fillMissingTypes() +{ + const QHash defaults = XRefProperties::defaultProperties(); + for (int i = 0; i < ui->m_type_cb->count(); ++i) + { + const QString type = ui->m_type_cb->itemData(i).toString(); + if (!m_properties.contains(type)) { + m_properties.insert(type, defaults.value(type)); + } + } +} + /** @brief XRefPropertiesWidget::properties @return the properties edited by this widget @@ -88,6 +118,9 @@ void XRefPropertiesWidget::setReadOnly(bool ro) { ui->m_type_cb->setDisabled(ro); ui->m_display_gb->setDisabled(ro); ui->m_cross_properties_gb->setDisabled(ro); + //The texts stand on their own since they are needed for a cable + //too, and they are as much a setting as the rest. + ui->m_labels_gb->setDisabled(ro); if (!ro && ui->m_display_has_contacts_rb->isChecked()) { ui->m_cross_properties_gb->setDisabled(true); @@ -104,6 +137,9 @@ void XRefPropertiesWidget::buildUi() ui -> m_type_cb -> addItem(tr("Organe de protection"), "protection"); ui -> m_type_cb -> addItem(tr("Commutateur / bouton"), "commutator"); ui -> m_type_cb -> addItem(tr("Automate (PLC)"), "plc"); + //Not an element: the type of the cross references a cable writes + //in its own label when it runs on several folios. + ui -> m_type_cb -> addItem(tr("Câble"), "cable"); ui -> m_snap_to_cb -> addItem(tr("En bas de page"), "bottom"); ui -> m_snap_to_cb -> addItem(tr("Sous le label de l'élément"), "label"); @@ -151,6 +187,7 @@ void XRefPropertiesWidget::saveProperties(int index) { xrp.setPrefix("switch", ui->m_switch_prefix_le->text()); xrp.setMasterLabel(ui->m_master_le->text()); xrp.setSlaveLabel(ui->m_slave_le->text()); + xrp.setFont(m_current_font); xrp.setOffset(ui->m_offset_sb->value()); xrp.setSlaveOffset(ui->m_slave_offset_sb->value()); @@ -177,6 +214,10 @@ void XRefPropertiesWidget::updateDisplay() QString master = xrp.masterLabel(); ui->m_master_le->setText(master); + //Which font that type's reference is written with: the button + //offers it for a cable alone, but every type carries its own. + m_current_font = xrp.font(); + QString slave = xrp.slaveLabel(); ui->m_slave_le->setText(slave); @@ -226,7 +267,37 @@ void XRefPropertiesWidget::updateDisplay() ui->m_display_has_cross_rb->setVisible(!is_plc); ui->m_show_terminal_name_cb->setVisible(!is_plc); ui->m_show_all_slaves_cb->setVisible(!is_plc); - ui->m_cross_properties_gb->setVisible(!is_plc); + + //A cable is not an element: it draws no cross of its own and stands + //at no place of the sheet to be pointed at. Its reference is a line + //of the label of the cable itself, written once per other folio the + //cable runs on, so the whole presentation (where a cross is put, how + //it is drawn) and the text of the slave have no meaning for it: what + //is left is the one text which is written into that line. + const bool is_cable = type == QLatin1String("cable"); + ui->m_display_gb->setVisible(!is_cable); + ui->label_7->setVisible(!is_cable); + ui->m_slave_le->setVisible(!is_cable); + ui->label_6->setText(is_cable ? tr("Texte :") : tr("Maitre")); + + //Only a cable writes its reference into its own label, so only a + //cable has a text whose font to choose here: the cross references + //of the elements keep the font they have always been drawn with. + ui->m_font_pb->setVisible(is_cable); + if (m_current_font.isEmpty()) + { + ui->m_font_pb->setToolTip(tr("Police et taille : celles des textes de câble")); + } + else + { + QFont font; + if (QETUtils::fontFromString(font, m_current_font)) { + ui->m_font_pb->setToolTip(tr("Police et taille : %1, %2 pt") + .arg(font.family()).arg(font.pointSizeF())); + } + } + + ui->m_cross_properties_gb->setVisible(!is_plc && !is_cable); } /** @@ -255,3 +326,31 @@ void XRefPropertiesWidget::enableOffsetSB(int i){ else ui->m_offset_sb->setEnabled(true); } + +/** + @brief XRefPropertiesWidget::chooseXRefFont + Let him pick the font -- family, size and style, all three at once -- + the reference of a cable is written with. + + The font is only kept when he presses OK: cancelling the dialog + leaves the previous one standing. What he picked is written into the + properties by saveProperties(), the way every other field of this + form is, so leaving the type or pressing OK in the settings window + is what really stores it. +*/ +void XRefPropertiesWidget::chooseXRefFont() +{ + QFont initial; + if (m_current_font.isEmpty() || !QETUtils::fontFromString(initial, m_current_font)) { + initial = QETApp::cableTextsFont(); + } + + bool ok = false; + const QFont font = QFontDialog::getFont(&ok, initial, this, + tr("Police du texte de querverweis")); + if (!ok) return; + + m_current_font = QETUtils::fontToString(font); + ui->m_font_pb->setToolTip(tr("Police et taille : %1, %2 pt") + .arg(font.family()).arg(font.pointSizeF())); +} diff --git a/sources/ui/xrefpropertieswidget.h b/sources/ui/xrefpropertieswidget.h index 8bd255b74..c10a845af 100644 --- a/sources/ui/xrefpropertieswidget.h +++ b/sources/ui/xrefpropertieswidget.h @@ -46,7 +46,9 @@ class XRefPropertiesWidget : public QWidget private: void buildUi(); + void fillMissingTypes(); void saveProperties(int index); + void chooseXRefFont(); private slots: void updateDisplay(); @@ -58,6 +60,8 @@ class XRefPropertiesWidget : public QWidget Ui::XRefPropertiesWidget *ui; QHash m_properties; int m_previous_type_index; + ///The font picked for the type shown, as the settings describe it + QString m_current_font; }; #endif // XREFPROPERTIESWIDGET_H diff --git a/sources/ui/xrefpropertieswidget.ui b/sources/ui/xrefpropertieswidget.ui index e79070539..a4b56cc0c 100644 --- a/sources/ui/xrefpropertieswidget.ui +++ b/sources/ui/xrefpropertieswidget.ui @@ -90,56 +90,56 @@ - - - - - - XRef slave position - - - - - - - - - - - - - - Distance label - slave : - - - - - - - Distance in pixels between the label and the slave cross reference - - - px - - - QAbstractSpinBox::CorrectToNearestValue - - - -50 - - - 100 - - - 1 - - - 0 - - - - - + + + + + + XRef slave position + + + + + + + + + + + + + + Distance label - slave : + + + + + + + Distance in pixels between the label and the slave cross reference + + + px + + + QAbstractSpinBox::CorrectToNearestValue + + + -50 + + + 100 + + + 1 + + + 0 + + + + + @@ -179,100 +179,110 @@ - - + + + + + + + + + Label des références croisées + + - - - Label des références croisées - - - - - - - - - 0 - 0 - - - - - 48 - 0 - - - - Maitre - - - - - - - - - - %f-%l%c - - - - - - - - - - - - 0 - 0 - - - - - 48 - 0 - - - - Esclave - - - false - - - - - - - (%f-%l%c) - - - - - - - - - Créer votre propre texte en vous aidant des variables suivantes : + + + + + + 0 + 0 + + + + + 48 + 0 + + + + Maitre + + + + + + + + + + %f-%l%c + + + + + + + Choisir la police et la taille du texte du querverweis du câble + + + Police… + + + + + + + + + + + + 0 + 0 + + + + + 48 + 0 + + + + Esclave + + + false + + + + + + + (%f-%l%c) + + + + + + + + + Créer votre propre texte en vous aidant des variables suivantes : %f : le numéro de folio %F: le label de folio %l : le numéro de ligne %c : le numéro de colonne %M: Installation %LM: Localisation - - - - + - - - + + + diff --git a/sources/undocommand/deleteqgraphicsitemcommand.cpp b/sources/undocommand/deleteqgraphicsitemcommand.cpp index a2db0baf8..dc6ca7ca6 100644 --- a/sources/undocommand/deleteqgraphicsitemcommand.cpp +++ b/sources/undocommand/deleteqgraphicsitemcommand.cpp @@ -17,6 +17,7 @@ */ #include "deleteqgraphicsitemcommand.h" +#include "../cable/cablemanager.h" #include "../diagram.h" #include "addgraphicsobjectcommand.h" #include "../qetdiagrameditor.h" @@ -307,6 +308,15 @@ void DeleteQGraphicsItemCommand::undo() } } + //Back on the sheet and linked again: the cables which run over + //this report claim their wire on the other side once more. + for (Element *e : m_removed_contents.m_elements) { + if (e && (e->linkType() & Element::AllReport)) { + CableManager::refreshLabels(m_diagram->project()); + break; + } + } + QUndoCommand::undo(); } @@ -359,5 +369,16 @@ void DeleteQGraphicsItemCommand::redo() for(QGraphicsItem *item : m_removed_contents.items()) m_diagram->removeItem(item); + //A folio report carries a cable core over to the next sheet: + //taking one away (putting it back is undo, below) changes which + //wires belong to which cable, so the cable fields are worked + //out again straight away. + for (Element *e : m_removed_contents.m_elements) { + if (e && (e->linkType() & Element::AllReport)) { + CableManager::refreshLabels(m_diagram->project()); + break; + } + } + QUndoCommand::redo(); } diff --git a/sources/undocommand/linkelementcommand.cpp b/sources/undocommand/linkelementcommand.cpp index b84ad5761..854ce91fb 100644 --- a/sources/undocommand/linkelementcommand.cpp +++ b/sources/undocommand/linkelementcommand.cpp @@ -17,6 +17,7 @@ */ #include "linkelementcommand.h" +#include "../cable/cablemanager.h" #include "../conductorautonumerotation.h" #include "../diagram.h" #include "../diagramposition.h" @@ -59,6 +60,23 @@ static QString plcCrossRefText(Element *master, Element *slave) xrp.slaveLabel(), seq, master->diagram(), master); } +/** + @brief refreshCableLabelsForReport + A folio report carries a cable core over to the next sheet, so + making or breaking such a link changes which wires belong to which + cable. Work the cable fields out again straight away, so the wire + coming out of the linked arrow shows the cable right after the link + is made -- and drops it right after the link is taken away. + @param element the element the link was made or broken on +*/ +static void refreshCableLabelsForReport(Element *element) +{ + if (!element || !(element->linkType() & Element::AllReport)) return; + if (Diagram *diagram = element->diagram()) { + CableManager::refreshLabels(diagram->project()); + } +} + /** @brief LinkElementCommand::LinkElementCommand Constructor @@ -302,6 +320,7 @@ void LinkElementCommand::undo() } makeLink(m_linked_before); + refreshCableLabelsForReport(m_element); QUndoCommand::undo(); } @@ -329,6 +348,7 @@ void LinkElementCommand::redo() } m_first_redo = false; } + refreshCableLabelsForReport(m_element); QUndoCommand::redo(); } diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index e339f3721..93fea1bbe 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -147,6 +147,23 @@ add_test(NAME tst_legacyelementtypes COMMAND tst_legacyelementtypes) target_include_directories(tst_legacyelementtypes PRIVATE ${QET_DIR}/sources) target_link_libraries(tst_legacyelementtypes PRIVATE Qt::Test) +# The cable type catalogue is plain data (it reads and writes the CSV file +# holding the cable kinds), so it is compiled here on its own, together with +# the material file helpers it shares and the element information wording. +add_executable( + tst_cabletypelist + tst_cabletypelist.cpp + ${QET_DIR}/sources/cablelist/cabletypelist.cpp + ${QET_DIR}/sources/cablelist/cabletypelist.h + ${QET_DIR}/sources/materiallist/materiallist.cpp + ${QET_DIR}/sources/materiallist/materiallist.h + ${QET_DIR}/sources/qetinformation.cpp + ${QET_DIR}/sources/qetinformation.h) +add_test(NAME tst_cabletypelist COMMAND tst_cabletypelist) +target_include_directories(tst_cabletypelist PRIVATE ${QET_DIR}/sources) +# qetinformation.h uses QRegularExpressionValidator, which lives in QtGui. +target_link_libraries(tst_cabletypelist PRIVATE Qt::Test Qt::Gui) + add_executable( tst_qetpalette tst_qetpalette.cpp diff --git a/tests/qttest/tst_cabletypelist.cpp b/tests/qttest/tst_cabletypelist.cpp new file mode 100644 index 000000000..77f36ad35 --- /dev/null +++ b/tests/qttest/tst_cabletypelist.cpp @@ -0,0 +1,186 @@ +/* + 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 "cablelist/cabletypelist.h" + +#include +#include +#include + +/** + The cable type catalogue is read back the way it was written, and the + three columns it really uses are the only ones a new file gets: + + - a catalogue carrying the material file's whole shape (ten columns, + two header lines) reads as ten columns, its machine header line is + not mistaken for a cable, and the type comes out of it : this is + the catalogue a user already has on disk; + - a file the program creates itself holds the two header lines and + the three columns, no more; + - a file written by hand with the label line alone still gives the + type, so dropping the machine header never costs a column; + - and a line of cables is never taken for the machine header, even + when it happens to be written in key shaped words. +*/ +class tst_cabletypelist : public QObject +{ + Q_OBJECT + + QTemporaryDir m_dir; + + QString write(const QString &name, const QString &content) + { + const QString path = m_dir.filePath(name); + QFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + return {}; + } + file.write(content.toUtf8()); + file.close(); + return path; + } + + bool load(const QString &path, CableTypeListData *data) + { + QString error; + const bool ok = CableTypeList::load(path, data, &error); + if (!ok) { + qWarning("%s", qPrintable(error)); + } + return ok; + } + + private slots: + void tenColumnCatalogueReadsBack() + { + const QString path = write(QStringLiteral("ten.csv"), + QStringLiteral( + "Désignation;Nombre d'âmes;Couleurs des âmes;Section du câble;Couleur du fourreau;Fabricant;Numéro de commande;Fournisseur;Description textuelle;Notes\n" + "designation;cores;core_colors;section;color;manufacturer;manufacturer_reference;supplier;description;notes\n" + "H07V-K 3G1,5;3;wh|bn|bl;1.5;PVC;Nexans;X123;Rexel;courant fort;note\n" + "NYM-J 3G2,5;3;wh|bn|bl;2.5;PVC;;;;;\n")); + QVERIFY(!path.isEmpty()); + + CableTypeListData data; + QVERIFY2(load(path, &data), "the catalogue could not be read"); + + //The machine header line is not one of the cables... + QCOMPARE(data.records.size(), 2); + //... and every column it names is read under that name, even the + //ones this program never gave a meaning to. + QCOMPARE(data.columns, QStringList({ + QStringLiteral("designation"), QStringLiteral("cores"), + QStringLiteral("core_colors"), QStringLiteral("section"), + QStringLiteral("color"), QStringLiteral("manufacturer"), + QStringLiteral("manufacturer_reference"), + QStringLiteral("supplier"), QStringLiteral("description"), + QStringLiteral("notes")})); + + QCOMPARE(CableTypeList::designation(data.records.at(0)), + QStringLiteral("H07V-K 3G1,5")); + QCOMPARE(CableTypeList::coreCount(data.records.at(0)), 3); + QCOMPARE(CableTypeList::coreColors(data.records.at(0)), + QStringList({QStringLiteral("wh"), QStringLiteral("bn"), + QStringLiteral("bl")})); + //A column outside the three the program uses keeps its name and + //its data. + QCOMPARE(data.records.at(0).value(QStringLiteral("section")), + QStringLiteral("1.5")); + QCOMPARE(CableTypeList::designation(data.records.at(1)), + QStringLiteral("NYM-J 3G2,5")); + } + + void newFileHasThreeColumns() + { + const QString path = m_dir.filePath(QStringLiteral("new.csv")); + QVERIFY2(CableTypeList::createFile(path), "the file was not created"); + + CableTypeListData data; + QVERIFY(load(path, &data)); + QCOMPARE(data.columns, QStringList({ + QStringLiteral("designation"), QStringLiteral("cores"), + QStringLiteral("core_colors")})); + QVERIFY(data.records.isEmpty()); + //The label line reads the way the cable form names the type. + QCOMPARE(CableTypeList::translatedHeader(data.columns), QStringList({ + QStringLiteral("Désignation"), + QStringLiteral("Nombre d'âmes"), + QStringLiteral("Couleurs des âmes")})); + + CableTypeRecord record; + record.setValue(QStringLiteral("designation"), + QStringLiteral("H07V-K 3G1,5")); + record.setValue(QStringLiteral("cores"), QStringLiteral("3")); + record.setValue(QStringLiteral("core_colors"), + QStringLiteral("wh|bn|bl")); + QVERIFY2(CableTypeList::appendRecord(path, record), + "the record was not appended"); + + CableTypeListData read_back; + QVERIFY(load(path, &read_back)); + QCOMPARE(read_back.columns, data.columns); + QCOMPARE(read_back.records.size(), 1); + QCOMPARE(CableTypeList::designation(read_back.records.first()), + QStringLiteral("H07V-K 3G1,5")); + QCOMPARE(CableTypeList::coreCount(read_back.records.first()), 3); + QCOMPARE(CableTypeList::coreColors(read_back.records.first()), + QStringList({QStringLiteral("wh"), QStringLiteral("bn"), + QStringLiteral("bl")})); + } + + void labelLineAloneStillGivesTheType() + { + const QString path = write(QStringLiteral("hand.csv"), + QStringLiteral( + "Désignation;Nombre d'âmes;Couleurs des âmes\n" + "H07V-K 3G1,5;3;wh|bn|bl\n")); + QVERIFY(!path.isEmpty()); + + CableTypeListData data; + QVERIFY2(load(path, &data), "the hand written file could not be read"); + QCOMPARE(data.columns, QStringList({ + QStringLiteral("designation"), QStringLiteral("cores"), + QStringLiteral("core_colors")})); + QCOMPARE(data.records.size(), 1); + QCOMPARE(CableTypeList::designation(data.records.first()), + QStringLiteral("H07V-K 3G1,5")); + QCOMPARE(CableTypeList::coreCount(data.records.first()), 3); + } + + void aCableIsNeverMistakenForTheHeader() + { + //Three cells which all look like column keys, none of which means + //anything here : far more likely to be a cable than a header. + const QString path = write(QStringLiteral("unknown.csv"), + QStringLiteral( + "Désignation;Nombre d'âmes;Couleurs des âmes\n" + "ymj;;wh\n" + "H07V-K 3G1,5;3;wh|bn|bl\n")); + QVERIFY(!path.isEmpty()); + + CableTypeListData data; + QVERIFY2(load(path, &data), "the file could not be read"); + QCOMPARE(data.records.size(), 2); + QCOMPARE(CableTypeList::designation(data.records.first()), + QStringLiteral("ymj")); + QCOMPARE(CableTypeList::designation(data.records.last()), + QStringLiteral("H07V-K 3G1,5")); + } +}; + +QTEST_APPLESS_MAIN(tst_cabletypelist) +#include "tst_cabletypelist.moc"