Merge pull request #1438 from Kellermorph/wiring-manager
Add cable management: drawing, cores, numbering, cross-references and cable list
@@ -345,6 +345,7 @@ target_include_directories(
|
||||
${QET_DIR}/sources/print
|
||||
${QET_DIR}/sources/svg
|
||||
${QET_DIR}/sources/materiallist
|
||||
${QET_DIR}/sources/cablelist
|
||||
)
|
||||
|
||||
if(NOT BUILD_WITH_KF)
|
||||
|
||||
@@ -387,6 +387,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/diagramevent/diagrameventfillet.h
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddpath.cpp
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddpath.h
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddcable.cpp
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddcable.h
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddtext.cpp
|
||||
${QET_DIR}/sources/diagramevent/diagrameventaddtext.h
|
||||
${QET_DIR}/sources/diagramevent/diagrameventinterface.cpp
|
||||
@@ -811,6 +813,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
|
||||
@@ -936,6 +940,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
|
||||
|
||||
@@ -257,6 +257,21 @@
|
||||
<file alias="themes/qet/24x24/bring_forward.svg">themes/generated/traced-24/bring_forward.svg</file>
|
||||
<file alias="themes/qet/24x24@2/bring_forward.svg">themes/generated/traced-large-24/bring_forward.svg</file>
|
||||
<file alias="themes/qet/scalable/bring_forward.svg">qet/traced/large22/bring_forward.svg</file>
|
||||
<file alias="themes/qet/16x16/cable.svg">qet/traced/16/cable.svg</file>
|
||||
<file alias="themes/qet/16x16@2/cable.svg">qet/traced/large16/cable.svg</file>
|
||||
<file alias="themes/qet/22x22/cable.svg">qet/traced/22/cable.svg</file>
|
||||
<file alias="themes/qet/22x22@2/cable.svg">qet/traced/large22/cable.svg</file>
|
||||
<file alias="themes/qet/32x32/cable.svg">qet/traced/large16/cable.svg</file>
|
||||
<file alias="themes/qet/32x32@2/cable.svg">qet/traced/large16/cable.svg</file>
|
||||
<file alias="themes/qet/48x48/cable.svg">themes/generated/traced-large-24/cable.svg</file>
|
||||
<file alias="themes/qet/48x48@2/cable.svg">themes/generated/traced-large-24/cable.svg</file>
|
||||
<file alias="themes/qet/64x64/cable.svg">qet/traced/large16/cable.svg</file>
|
||||
<file alias="themes/qet/64x64@2/cable.svg">qet/traced/large16/cable.svg</file>
|
||||
<file alias="themes/qet/128x128/cable.svg">qet/traced/large16/cable.svg</file>
|
||||
<file alias="themes/qet/128x128@2/cable.svg">qet/traced/large16/cable.svg</file>
|
||||
<file alias="themes/qet/24x24/cable.svg">themes/generated/traced-24/cable.svg</file>
|
||||
<file alias="themes/qet/24x24@2/cable.svg">themes/generated/traced-large-24/cable.svg</file>
|
||||
<file alias="themes/qet/scalable/cable.svg">qet/traced/large22/cable.svg</file>
|
||||
<file alias="themes/qet/16x16/circle.svg">qet/traced/16/circle.svg</file>
|
||||
<file alias="themes/qet/16x16@2/circle.svg">qet/traced/large16/circle.svg</file>
|
||||
<file alias="themes/qet/22x22/circle.svg">qet/traced/22/circle.svg</file>
|
||||
@@ -2699,6 +2714,12 @@
|
||||
<file>themes/qet-dark/traced/large22/bring_forward.svg</file>
|
||||
<file>themes/qet-dark/traced-large-24/bring_forward.svg</file>
|
||||
<file>themes/qet-dark/traced-24/bring_forward.svg</file>
|
||||
<file>themes/qet-dark/traced/16/cable.svg</file>
|
||||
<file>themes/qet-dark/traced/large16/cable.svg</file>
|
||||
<file>themes/qet-dark/traced/22/cable.svg</file>
|
||||
<file>themes/qet-dark/traced/large22/cable.svg</file>
|
||||
<file>themes/qet-dark/traced-large-24/cable.svg</file>
|
||||
<file>themes/qet-dark/traced-24/cable.svg</file>
|
||||
<file>themes/qet-dark/traced/16/circle.svg</file>
|
||||
<file>themes/qet-dark/traced/large16/circle.svg</file>
|
||||
<file>themes/qet-dark/traced/22/circle.svg</file>
|
||||
@@ -3516,6 +3537,21 @@
|
||||
<file alias="themes/qet-dark/24x24/bring_forward.svg">themes/qet-dark/traced-24/bring_forward.svg</file>
|
||||
<file alias="themes/qet-dark/24x24@2/bring_forward.svg">themes/qet-dark/traced-large-24/bring_forward.svg</file>
|
||||
<file alias="themes/qet-dark/scalable/bring_forward.svg">themes/qet-dark/traced/large22/bring_forward.svg</file>
|
||||
<file alias="themes/qet-dark/16x16/cable.svg">themes/qet-dark/traced/16/cable.svg</file>
|
||||
<file alias="themes/qet-dark/16x16@2/cable.svg">themes/qet-dark/traced/large16/cable.svg</file>
|
||||
<file alias="themes/qet-dark/22x22/cable.svg">themes/qet-dark/traced/22/cable.svg</file>
|
||||
<file alias="themes/qet-dark/22x22@2/cable.svg">themes/qet-dark/traced/large22/cable.svg</file>
|
||||
<file alias="themes/qet-dark/32x32/cable.svg">themes/qet-dark/traced/large16/cable.svg</file>
|
||||
<file alias="themes/qet-dark/32x32@2/cable.svg">themes/qet-dark/traced/large16/cable.svg</file>
|
||||
<file alias="themes/qet-dark/48x48/cable.svg">themes/qet-dark/traced-large-24/cable.svg</file>
|
||||
<file alias="themes/qet-dark/48x48@2/cable.svg">themes/qet-dark/traced-large-24/cable.svg</file>
|
||||
<file alias="themes/qet-dark/64x64/cable.svg">themes/qet-dark/traced/large16/cable.svg</file>
|
||||
<file alias="themes/qet-dark/64x64@2/cable.svg">themes/qet-dark/traced/large16/cable.svg</file>
|
||||
<file alias="themes/qet-dark/128x128/cable.svg">themes/qet-dark/traced/large16/cable.svg</file>
|
||||
<file alias="themes/qet-dark/128x128@2/cable.svg">themes/qet-dark/traced/large16/cable.svg</file>
|
||||
<file alias="themes/qet-dark/24x24/cable.svg">themes/qet-dark/traced-24/cable.svg</file>
|
||||
<file alias="themes/qet-dark/24x24@2/cable.svg">themes/qet-dark/traced-large-24/cable.svg</file>
|
||||
<file alias="themes/qet-dark/scalable/cable.svg">themes/qet-dark/traced/large22/cable.svg</file>
|
||||
<file alias="themes/qet-dark/16x16/circle.svg">themes/qet-dark/traced/16/circle.svg</file>
|
||||
<file alias="themes/qet-dark/16x16@2/circle.svg">themes/qet-dark/traced/large16/circle.svg</file>
|
||||
<file alias="themes/qet-dark/22x22/circle.svg">themes/qet-dark/traced/22/circle.svg</file>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path fill="none" stroke="currentColor" stroke-width="2" d="M2 5h12"/>
|
||||
<path fill="none" stroke="currentColor" stroke-width="1" d="M11 1v14"/>
|
||||
<path fill="none" stroke="currentColor" stroke-width="1.5" d="M9 9L13 1"/>
|
||||
<path fill="currentColor" d="M1 9h5v1H1zM1 11h3v1H1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 350 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<path fill="none" stroke="currentColor" stroke-width="3" d="M3 7h16"/>
|
||||
<path fill="none" stroke="currentColor" stroke-width="1.5" d="M15 1v20"/>
|
||||
<path fill="none" stroke="currentColor" stroke-width="2" d="M12.5 12.5L17.5 2.5"/>
|
||||
<path fill="currentColor" d="M2 13h7v1.5H2zM2 16.5h4v1.5H2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 367 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M2 5h12"/>
|
||||
<path fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" d="M11 1v14"/>
|
||||
<path fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" d="M9 9L13 1"/>
|
||||
<path fill="currentColor" d="M1 9h5v1H1zM1 11h3v1H1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 419 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<path fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" d="M3 7h16"/>
|
||||
<path fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" d="M15 1v20"/>
|
||||
<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M12.5 12.5L17.5 2.5"/>
|
||||
<path fill="currentColor" d="M2 13h7v1.5H2zM2 16.5h4v1.5H2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 436 B |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g transform="translate(1,1)">
|
||||
<path fill="none" stroke="currentColor" stroke-width="3" d="M3 7h16"/>
|
||||
<path fill="none" stroke="currentColor" stroke-width="1.5" d="M15 1v20"/>
|
||||
<path fill="none" stroke="currentColor" stroke-width="2" d="M12.5 12.5L17.5 2.5"/>
|
||||
<path fill="currentColor" d="M2 13h7v1.5H2zM2 16.5h4v1.5H2z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 407 B |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g transform="translate(1,1)">
|
||||
<path fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" d="M3 7h16"/>
|
||||
<path fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" d="M15 1v20"/>
|
||||
<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M12.5 12.5L17.5 2.5"/>
|
||||
<path fill="currentColor" d="M2 13h7v1.5H2zM2 16.5h4v1.5H2z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 476 B |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g transform="translate(1,1)">
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="3" d="M3 7h16"/>
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="1.5" d="M15 1v20"/>
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="2" d="M12.5 12.5L17.5 2.5"/>
|
||||
<path fill="#dcdcdc" d="M2 13h7v1.5H2zM2 16.5h4v1.5H2z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 387 B |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g transform="translate(1,1)">
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="3" stroke-linecap="round" d="M3 7h16"/>
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="1.5" stroke-linecap="round" d="M15 1v20"/>
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="2" stroke-linecap="round" d="M12.5 12.5L17.5 2.5"/>
|
||||
<path fill="#dcdcdc" d="M2 13h7v1.5H2zM2 16.5h4v1.5H2z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 456 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="2" d="M2 5h12"/>
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="1" d="M11 1v14"/>
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="1.5" d="M9 9L13 1"/>
|
||||
<path fill="#dcdcdc" d="M1 9h5v1H1zM1 11h3v1H1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 330 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="3" d="M3 7h16"/>
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="1.5" d="M15 1v20"/>
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="2" d="M12.5 12.5L17.5 2.5"/>
|
||||
<path fill="#dcdcdc" d="M2 13h7v1.5H2zM2 16.5h4v1.5H2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 347 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="2" stroke-linecap="round" d="M2 5h12"/>
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="1" stroke-linecap="round" d="M11 1v14"/>
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="1.5" stroke-linecap="round" d="M9 9L13 1"/>
|
||||
<path fill="#dcdcdc" d="M1 9h5v1H1zM1 11h3v1H1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 399 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="3" stroke-linecap="round" d="M3 7h16"/>
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="1.5" stroke-linecap="round" d="M15 1v20"/>
|
||||
<path fill="none" stroke="#dcdcdc" stroke-width="2" stroke-linecap="round" d="M12.5 12.5L17.5 2.5"/>
|
||||
<path fill="#dcdcdc" d="M2 13h7v1.5H2zM2 16.5h4v1.5H2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 416 B |
@@ -649,6 +649,74 @@ namespace autonum
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief resetFolioCounters
|
||||
@param nc the rule to read
|
||||
@return the same rule, with the parts whose counter belongs to a
|
||||
single folio put back to the beginning that folio is numbered
|
||||
from -- the initial value the rule carries for it -- while the
|
||||
parts counting over the whole project are left alone
|
||||
*/
|
||||
NumerotationContext resetFolioCounters(const NumerotationContext &nc)
|
||||
{
|
||||
NumerotationContext out = nc;
|
||||
for (int i = 0; i < out.size(); ++i)
|
||||
{
|
||||
const QStringList parts = out.itemAt(i);
|
||||
if (parts.isEmpty()) continue;
|
||||
|
||||
const QString &type = parts.at(0);
|
||||
if (type == QLatin1String("unitfolio")
|
||||
|| type == QLatin1String("tenfolio")
|
||||
|| type == QLatin1String("hundredfolio"))
|
||||
{
|
||||
//The beginning a folio is numbered from is the one
|
||||
//written into the rule, never a hard 1: a counter
|
||||
//starting at 5 starts every folio at 5.
|
||||
const bool has_start = parts.size() > 3
|
||||
&& !parts.at(3).isEmpty();
|
||||
out.replaceValue(i, has_start ? parts.at(3)
|
||||
: QStringLiteral("1"));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief resetContextCounters
|
||||
@param nc the rule to read
|
||||
@return the same rule, with every part which counts set back to
|
||||
the value it started from
|
||||
*/
|
||||
NumerotationContext resetContextCounters(const NumerotationContext &nc)
|
||||
{
|
||||
NumerotationContext out = resetFolioCounters(nc);
|
||||
for (int i = 0; i < out.size(); ++i)
|
||||
{
|
||||
const QStringList parts = out.itemAt(i);
|
||||
if (parts.isEmpty()) continue;
|
||||
|
||||
const QString &type = parts.at(0);
|
||||
if (type == QLatin1String("unit")
|
||||
|| type == QLatin1String("ten")
|
||||
|| type == QLatin1String("hundred"))
|
||||
{
|
||||
out.replaceValue(i, QStringLiteral("1"));
|
||||
}
|
||||
else if (type == QLatin1String("wrap"))
|
||||
{
|
||||
//A cyclic part walks over [0, modulus), so it goes
|
||||
//back to 0 rather than to 1
|
||||
out.replaceValue(i, QStringLiteral("0"));
|
||||
}
|
||||
else if (type == QLatin1String("alpha"))
|
||||
{
|
||||
out.replaceValue(i, QStringLiteral("a"));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief numerotationContextToFormula
|
||||
@param nc
|
||||
|
||||
@@ -117,6 +117,31 @@ namespace autonum
|
||||
void setFolioSequentialToHash(QStringList &list, QHash<QString, QStringList> &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);
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,16 @@ NumPartEditorW::NumPartEditorW (NumerotationContext &context,
|
||||
if (strl.at(0)=="wrap" && strl.size() > 4)
|
||||
ui -> modulus_spinBox -> setValue(strl.at(4).toInt());
|
||||
ui -> format_le -> setText(NumerotationContext::formatOf(strl));
|
||||
//A counter bound to the folio keeps the start it was
|
||||
//configured with: value_field shows the live counter, and
|
||||
//writing that back as the start would make every folio
|
||||
//begin at the last number the previous numbering gave out
|
||||
if ((strl.at(0)==QLatin1String("unitfolio")
|
||||
|| strl.at(0)==QLatin1String("tenfolio")
|
||||
|| strl.at(0)==QLatin1String("hundredfolio"))
|
||||
&& strl.size() > 3 && !strl.at(3).isEmpty()) {
|
||||
m_initial_value = strl.at(3).toInt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,8 +141,12 @@ void NumPartEditorW::setVisibleItems()
|
||||
<< tr("Alphabetical")
|
||||
<< tr("Text");
|
||||
}
|
||||
else if (m_edited_type == 1)
|
||||
else if (m_edited_type == 1 || m_edited_type == 3)
|
||||
{
|
||||
//Type 3 is a cable: it is drawn along a folio just like a
|
||||
//conductor is, so it takes the same kinds of variable and
|
||||
//none of the ones an element has and a cable has not (its
|
||||
//line, its column, its prefix).
|
||||
items << tr("number format 1")
|
||||
<< tr("number format 1 - Sheet")
|
||||
<< tr("number format 01")
|
||||
@@ -232,7 +246,8 @@ NumerotationContext NumPartEditorW::toNumContext()
|
||||
nc.addValue(type_str,
|
||||
ui -> value_field -> displayText(),
|
||||
ui -> increase_spinBox -> value(),
|
||||
ui->value_field->displayText().toInt(),
|
||||
m_initial_value >= 0 ? m_initial_value
|
||||
: ui->value_field->displayText().toInt(),
|
||||
0,
|
||||
number_format);
|
||||
else if (type_str == "wrap")
|
||||
@@ -315,6 +330,10 @@ void NumPartEditorW::on_type_cb_activated(int) {
|
||||
*/
|
||||
void NumPartEditorW::on_value_field_textEdited()
|
||||
{
|
||||
//What the user types in is a deliberate start for a counter
|
||||
//bound to the folio; merely showing the live counter (as
|
||||
//setContext() does) never changes the start.
|
||||
m_initial_value = ui -> value_field -> text().toInt();
|
||||
emit changed();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -37,22 +37,41 @@ QList<ElementAutoNumSchemeCommand::Scheme> PasteNumberingImport::copiedBy(const
|
||||
elements follow and the project does not have.
|
||||
@param parent : parent of the question
|
||||
@param copied : the numberings the copy carries
|
||||
@param extra : commands the copy brings beside the paste itself
|
||||
(the cable lines of a fragment, for example); they are pushed inside
|
||||
the same macro, so one Ctrl+Z undoes all of it at once.
|
||||
*/
|
||||
void PasteNumberingImport::push(QWidget *parent,
|
||||
Diagram *diagram,
|
||||
const DiagramContent &content,
|
||||
const QList<ElementAutoNumSchemeCommand::Scheme> &copied)
|
||||
const QList<ElementAutoNumSchemeCommand::Scheme> &copied,
|
||||
const QList<QUndoCommand *> &extra)
|
||||
{
|
||||
QETProject *project = diagram->project();
|
||||
auto *paste = new PasteDiagramCommand(diagram, content);
|
||||
|
||||
//The paste and the commands beside it are one step: only the
|
||||
//macros really need more than the single push.
|
||||
const auto push_all = [&]() {
|
||||
if (extra.isEmpty()) {
|
||||
diagram->undoStack().push(paste);
|
||||
return;
|
||||
}
|
||||
diagram->undoStack().beginMacro(paste->text());
|
||||
diagram->undoStack().push(paste);
|
||||
for (QUndoCommand *cmd : extra) {
|
||||
diagram->undoStack().push(cmd);
|
||||
}
|
||||
diagram->undoStack().endMacro();
|
||||
};
|
||||
|
||||
QList<ElementAutoNumSchemeCommand::Scheme> missing;
|
||||
if (project && !copied.isEmpty()
|
||||
&& QSettings().value("diagramcommands/autonumber-pasted-elements", true).toBool()) {
|
||||
missing = ElementAutoNumSchemeCommand::missingForPaste(project, copied, content.m_elements);
|
||||
}
|
||||
if (missing.isEmpty()) {
|
||||
diagram->undoStack().push(paste);
|
||||
push_all();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,7 +90,7 @@ void PasteNumberingImport::push(QWidget *parent,
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::Yes);
|
||||
if (answer != QMessageBox::Yes) {
|
||||
diagram->undoStack().push(paste);
|
||||
push_all();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,5 +110,8 @@ void PasteNumberingImport::push(QWidget *parent,
|
||||
}
|
||||
}
|
||||
diagram->undoStack().push(paste);
|
||||
for (QUndoCommand *cmd : extra) {
|
||||
diagram->undoStack().push(cmd);
|
||||
}
|
||||
diagram->undoStack().endMacro();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QList>
|
||||
#include <QUndoCommand>
|
||||
|
||||
class Diagram;
|
||||
class QWidget;
|
||||
@@ -40,6 +41,11 @@ class QWidget;
|
||||
without a numbering.
|
||||
|
||||
The import and the paste are one undo step.
|
||||
|
||||
A paste which brings more than itself (the cable lines of the copy,
|
||||
for instance, which are no part of a DiagramContent) hands those
|
||||
commands over through @p extra: they are pushed inside the very
|
||||
same macro, so one Ctrl+Z still takes the whole of it back.
|
||||
*/
|
||||
class PasteNumberingImport
|
||||
{
|
||||
@@ -49,7 +55,8 @@ class PasteNumberingImport
|
||||
static void push(QWidget *parent,
|
||||
Diagram *diagram,
|
||||
const DiagramContent &content,
|
||||
const QList<ElementAutoNumSchemeCommand::Scheme> &copied);
|
||||
const QList<ElementAutoNumSchemeCommand::Scheme> &copied,
|
||||
const QList<QUndoCommand *> &extra = QList<QUndoCommand *>());
|
||||
|
||||
static QList<ElementAutoNumSchemeCommand::Scheme> copiedBy(const QDomDocument &clipboard);
|
||||
};
|
||||
|
||||
@@ -138,6 +138,9 @@ void SelectAutonumW::setContext(const NumerotationContext &context)
|
||||
|
||||
updatePartButtons();
|
||||
applyEnable(false);
|
||||
//Everything the rule held before is read now, so nothing of it
|
||||
//is left to write: what is on show is what there is.
|
||||
m_dirty = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,7 +150,11 @@ void SelectAutonumW::setContext(const NumerotationContext &context)
|
||||
*/
|
||||
void SelectAutonumW::insertPartRow(NumPartEditorW *part)
|
||||
{
|
||||
connect(part, &NumPartEditorW::changed, this, [this]() { applyEnable(); });
|
||||
connect(part, &NumPartEditorW::changed, this, [this]() {
|
||||
m_dirty = true;
|
||||
applyEnable();
|
||||
emit contextEdited();
|
||||
});
|
||||
|
||||
PartRow r;
|
||||
r.part = part;
|
||||
@@ -263,7 +270,10 @@ NumerotationContext SelectAutonumW::toNumContext() const
|
||||
void SelectAutonumW::on_add_button_clicked()
|
||||
{
|
||||
insertPartRow(new NumPartEditorW(m_edited_type, this));
|
||||
m_dirty = true;
|
||||
ui -> remove_button -> setEnabled(true);
|
||||
applyEnable();
|
||||
emit contextEdited();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -272,9 +282,16 @@ void SelectAutonumW::on_add_button_clicked()
|
||||
*/
|
||||
void SelectAutonumW::on_remove_button_clicked()
|
||||
{
|
||||
if (!m_rows.isEmpty()) {
|
||||
//remove if @num_part_list contains more than one item
|
||||
if (num_part_list_.size() > 1) {
|
||||
m_dirty = true;
|
||||
removePartRow(m_rows.last().row);
|
||||
if (num_part_list_.size() == 1) {
|
||||
ui -> remove_button -> setDisabled(true);
|
||||
}
|
||||
}
|
||||
applyEnable();
|
||||
emit contextEdited();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -296,6 +313,78 @@ QComboBox *SelectAutonumW::contextComboBox() const
|
||||
return ui->m_comboBox;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief SelectAutonumW::setSingleRuleMode
|
||||
@param single true to show this editor as the one and only rule of
|
||||
its kind.
|
||||
|
||||
The row which lists the available numberings -- its label, its combo
|
||||
box and the button which deletes one -- is hidden, so there is
|
||||
nothing to name a rule with and nothing to choose between: the rule
|
||||
being edited is all there is. The combo box itself stays where it is
|
||||
and keeps holding the rule's name, so everything which reads or
|
||||
writes the rule through contextComboBox() carries on untouched; he
|
||||
just cannot see it.
|
||||
*/
|
||||
void SelectAutonumW::setSingleRuleMode(bool single)
|
||||
{
|
||||
m_single_rule = single;
|
||||
ui->label->setVisible(!single);
|
||||
ui->m_comboBox->setVisible(!single);
|
||||
ui->m_remove_pb->setVisible(!single);
|
||||
|
||||
//A single rule has no list to be deleted from, so it needs a
|
||||
//button of its own to get out of the numbering again. It is
|
||||
//only shown when there is really a rule to take away -- see
|
||||
//setRuleRemovable.
|
||||
if (single && !m_rule_remove_pb) {
|
||||
m_rule_remove_pb = new QPushButton(tr("Delete the rule"), this);
|
||||
m_rule_remove_pb->setToolTip(tr(
|
||||
"Delete the cable numbering rule: cables are "
|
||||
"numbered again as before, and the question of "
|
||||
"defining one comes back on the next drawing."));
|
||||
m_rule_remove_pb->setObjectName(QStringLiteral("m_rule_remove_pb"));
|
||||
if (ui->m_definition_groupe && ui->m_definition_groupe->layout()) {
|
||||
ui->m_definition_groupe->layout()->addWidget(m_rule_remove_pb);
|
||||
}
|
||||
connect(m_rule_remove_pb, &QPushButton::clicked,
|
||||
this, &SelectAutonumW::removeClicked);
|
||||
}
|
||||
if (m_rule_remove_pb) {
|
||||
m_rule_remove_pb->setVisible(m_single_rule && m_rule_removable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief SelectAutonumW::setRuleRemovable
|
||||
Whether the button which takes the whole rule away is worth showing:
|
||||
it has nothing to do while this editor holds no rule at all, and it
|
||||
would only be a button which does nothing.
|
||||
@param can_remove true when a rule is there to be taken away
|
||||
*/
|
||||
void SelectAutonumW::setRuleRemovable(bool can_remove)
|
||||
{
|
||||
m_rule_removable = can_remove;
|
||||
if (m_rule_remove_pb) {
|
||||
m_rule_remove_pb->setVisible(m_single_rule && can_remove);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief SelectAutonumW::isValid
|
||||
@return true when there is at least one variable in the editor and
|
||||
every one of them holds what it needs. An editor which has only just
|
||||
been opened holds an empty rule, and an empty rule is not one.
|
||||
*/
|
||||
bool SelectAutonumW::isValid()
|
||||
{
|
||||
if (num_part_list_.isEmpty()) return false;
|
||||
for (NumPartEditorW *npe : num_part_list_) {
|
||||
if (!npe || !npe->isValid()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief SelectAutonumW::on_buttonBox_clicked
|
||||
Action on button clicked
|
||||
@@ -364,6 +453,32 @@ void SelectAutonumW::on_buttonBox_clicked(QAbstractButton *button)
|
||||
));
|
||||
break;
|
||||
}
|
||||
else if (m_edited_type == 3)
|
||||
{
|
||||
QMessageBox::information (
|
||||
this,
|
||||
tr("Cable Auto Numbering",
|
||||
"title window"),
|
||||
tr("This is where you can define how new cables will be numbered.\n"
|
||||
"-A numbering is composed of a minimum variable.\n"
|
||||
"-You can add or delete a dialing variable through the - and + buttons.\n"
|
||||
"-A numbering variable includes: a type, a value and an increment.\n"
|
||||
|
||||
"\n-the \"Digit 1\", \"Digit 01\" and \"Digit 001\" types represent a numeric type defined in the \"Value\" field, "
|
||||
"which is incremented to each new cable by the value of the \"Increment\" field.\n"
|
||||
"-\"Digit 01\" and \"Digit 001\", are respectively represented on the diagram by two and three digits minimum.\n"
|
||||
"If the digit defined in the Value field has fewer digits than the chosen type, it will be preceded by one or two 0s in "
|
||||
"order to respect its type.\n"
|
||||
|
||||
"\n-Type \"Text\", represents a fixed text.\nThe \"Increment\" field is not used.\n"
|
||||
|
||||
"\n-The \"Sheet no.\" type represents the number of the current sheet.\nThe other fields are not used.\n"
|
||||
|
||||
"\n-The \"Sheet\" type represents the name of the current sheet.\nThe other fields are not used.",
|
||||
"help dialog about the cable autonumerotation"
|
||||
));
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
QMessageBox::information (
|
||||
@@ -397,6 +512,9 @@ void SelectAutonumW::on_buttonBox_clicked(QAbstractButton *button)
|
||||
case QDialogButtonBox::ApplyRole:
|
||||
applyEnable(false);
|
||||
emit applyPressed();
|
||||
//Whatever was edited is written now, so there is
|
||||
//nothing of it left for a later save to write again.
|
||||
m_dirty = false;
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ class NumPartEditorW;
|
||||
class QAbstractButton;
|
||||
class FormulaAutonumberingW;
|
||||
class QComboBox;
|
||||
class QPushButton;
|
||||
class QToolButton;
|
||||
class QHBoxLayout;
|
||||
|
||||
@@ -54,11 +55,55 @@ class SelectAutonumW : public QWidget
|
||||
void contextToFormula ();
|
||||
QString formula();
|
||||
QComboBox *contextComboBox() const;
|
||||
/**
|
||||
Show this editor as the only numbering rule there is: the
|
||||
row which lets several named numberings be kept and chosen
|
||||
between is hidden, so the rule being edited is the one and
|
||||
only. Everything else -- the definition itself, the buttons
|
||||
around it -- stays exactly as it is for the other kinds.
|
||||
*/
|
||||
void setSingleRuleMode(bool single);
|
||||
bool isSingleRuleMode() const { return m_single_rule; }
|
||||
/**
|
||||
True while everything the editor holds is filled in well
|
||||
enough to be a rule -- no variable left with nothing in it.
|
||||
The Apply button says the same thing; this is so that a page
|
||||
which saves on its own way knows it too, rather than writing
|
||||
a rule nobody has finished filling in.
|
||||
*/
|
||||
bool isValid();
|
||||
/**
|
||||
True while the rule on show has been touched since it was
|
||||
read or since it was last applied: a page which saves on
|
||||
its own way (the cable rule does) writes it only when there
|
||||
is something to write, so that confirming a window with
|
||||
nothing changed in it leaves the project alone.
|
||||
*/
|
||||
bool isModified() const { return m_dirty; }
|
||||
/**
|
||||
Show the button which takes the rule itself away, which is
|
||||
the only way to stop numbering cables automatically again:
|
||||
the row the other numberings delete from is hidden here, so
|
||||
a single rule would have no way out at all. The page behind
|
||||
the editor says whether there is a rule to take away at the
|
||||
moment -- without one the button has nothing to do and is
|
||||
kept out of sight.
|
||||
@param can_remove true when a rule is really there
|
||||
*/
|
||||
void setRuleRemovable(bool can_remove);
|
||||
void setExplicitNaming();
|
||||
|
||||
signals:
|
||||
void applyPressed();
|
||||
void removeClicked();
|
||||
/**
|
||||
Sent whenever what the rule holds is edited -- a
|
||||
field of a part changed, a part added, a part taken
|
||||
away -- so that a window holding this editor can let
|
||||
its own buttons say whether there is something to
|
||||
write now.
|
||||
*/
|
||||
void contextEdited();
|
||||
/// Only with setExplicitNaming()
|
||||
void newClicked();
|
||||
void renameClicked();
|
||||
@@ -96,7 +141,19 @@ class SelectAutonumW : public QWidget
|
||||
NumerotationContext m_context;
|
||||
FormulaAutonumberingW *m_feaw;
|
||||
FormulaAutonumberingW *m_fcaw;
|
||||
int m_edited_type = -1; ///<0 == element : 1 == conductor : 2 == folio
|
||||
int m_edited_type = -1; ///<0 == element : 1 == conductor : 2 == folio : 3 == cable
|
||||
///< when true, the row of available numberings is hidden and
|
||||
///< this editor holds the one rule there is (see setSingleRuleMode)
|
||||
bool m_single_rule = false;
|
||||
///< true from the moment the rule on show is touched until
|
||||
///< it is read again or applied (see isModified)
|
||||
bool m_dirty = false;
|
||||
///< the button which takes the whole rule away, built for
|
||||
///< the one kind of numbering which has no list to delete
|
||||
///< it from (see setRuleRemovable)
|
||||
QPushButton *m_rule_remove_pb = nullptr;
|
||||
///< whether such a rule is there to be taken away
|
||||
bool m_rule_removable = false;
|
||||
};
|
||||
|
||||
#endif // SELECTAUTONUMW_H
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -30,6 +30,11 @@ namespace BomExport
|
||||
QString defaultQuery(bool include_slaves = true,
|
||||
bool include_junctions = true);
|
||||
QString junctionFilter();
|
||||
/// One CSV record: every field quoted, ';' between them, '\n'
|
||||
/// after -- the format every CSV export of this program writes.
|
||||
/// Public so a list which is not read from the project database
|
||||
/// (the cable list) writes the same bytes.
|
||||
QByteArray csvRecord(const QStringList &values);
|
||||
QByteArray toCsv(QSqlQuery &query, const QStringList &headers,
|
||||
bool include_headers = true, int *row_count = nullptr);
|
||||
bool writeCsv(const QString &file_path, const QByteArray &csv,
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "addcablecommand.h"
|
||||
|
||||
#include "cablemanager.h"
|
||||
#include "cablepart.h"
|
||||
#include "../autoNum/numerotationcontextcommands.h"
|
||||
#include "../diagram.h"
|
||||
#include "../qetproject.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
@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<CableCoreTaken> &taken) :
|
||||
m_cable(cable),
|
||||
m_part(part),
|
||||
m_diagram(diagram),
|
||||
m_taken(taken),
|
||||
m_own_cable(own_cable)
|
||||
{
|
||||
if (!m_cable || !m_part) return;
|
||||
|
||||
m_part_data.uuid = m_part->partUuid();
|
||||
m_part_data.diagram = m_diagram ? m_diagram->uuid() : QUuid();
|
||||
m_part_data.p1 = m_part->firstPoint();
|
||||
m_part_data.p2 = m_part->secondPoint();
|
||||
|
||||
for (const CableCore &core : m_cable->usedCores()) {
|
||||
if (core.part == m_part_data.uuid) {
|
||||
m_cores.append(core);
|
||||
}
|
||||
}
|
||||
|
||||
//A cable this action brought into being takes the next number
|
||||
//the project's numbering rule hands out. The number itself was
|
||||
//worked out before this command existed; what has to happen here
|
||||
//is moving the rule on, and moving it is undoable just like the
|
||||
//cable is -- so undoing gives the number back and the cable
|
||||
//drawn after that takes it again instead of skipping over it.
|
||||
//
|
||||
//Only the rule as it stands now is taken down. Where the rule
|
||||
//stands after this cable is worked out when the cable really
|
||||
//comes into being, because several cables pasted together are
|
||||
//all made before any of them is added, and each of them has to
|
||||
//move the rule on from wherever the one before it left it.
|
||||
if (m_own_cable && m_diagram && project() && project()->hasCableAutoNum())
|
||||
{
|
||||
const QString key = project()->cableCurrentAutoNum();
|
||||
const NumerotationContext current = project()->cableAutoNum(key);
|
||||
if (!current.isEmpty())
|
||||
{
|
||||
m_num_key = key;
|
||||
m_num_before = current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AddCableCommand::~AddCableCommand
|
||||
Whatever the stack leaves behind has to be cleaned up here: a cable
|
||||
which is not held by the project any more belongs to this command,
|
||||
and so does a line which never made it onto a folio.
|
||||
|
||||
The cable is a QPointer, so if another command took it away first it
|
||||
is already null here and there is nothing left to do -- which is
|
||||
exactly the point, since following a raw pointer here meant freeing
|
||||
the same cable a second time.
|
||||
*/
|
||||
AddCableCommand::~AddCableCommand()
|
||||
{
|
||||
if (m_cable && m_own_cable) {
|
||||
QETProject *proj = project();
|
||||
if (!proj || !proj->cables().contains(m_cable.data())) {
|
||||
delete m_cable.data();
|
||||
}
|
||||
}
|
||||
if (m_part && !m_part->scene()) {
|
||||
delete m_part;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AddCableCommand::project
|
||||
@return the project this command works in, or nullptr
|
||||
*/
|
||||
QETProject *AddCableCommand::project() const
|
||||
{
|
||||
return m_diagram ? m_diagram->project() : nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AddCableCommand::redo
|
||||
Put the line on the folio, together with everything the cable holds
|
||||
for it.
|
||||
*/
|
||||
void AddCableCommand::redo()
|
||||
{
|
||||
QETProject *proj = project();
|
||||
if (!proj || !m_cable || !m_part || !m_diagram) return;
|
||||
|
||||
if (m_own_cable) {
|
||||
proj->addCable(m_cable);
|
||||
} else {
|
||||
if (!m_cable->hasPart(m_part_data.uuid)) {
|
||||
m_cable->addPart(m_part_data);
|
||||
}
|
||||
for (const CableCore &core : m_cores) {
|
||||
m_cable->setCore(core);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_part->scene() != m_diagram) {
|
||||
m_diagram->addItem(m_part);
|
||||
}
|
||||
|
||||
//Take the cores away from the cables which held them before this
|
||||
//line claimed those conductors. On the very first run they are
|
||||
//already gone, so nothing happens then; redoing after an undo
|
||||
//has to take them again.
|
||||
for (const CableCoreTaken &taken : std::as_const(m_taken))
|
||||
{
|
||||
if (taken.cable && taken.cable != m_cable) {
|
||||
taken.cable->removeCore(taken.core.core);
|
||||
}
|
||||
}
|
||||
//The cable field of every conductor is worked out again either
|
||||
//way: redoing after an undo puts the cable back into the project
|
||||
//without it having said anything, and the wires it feeds would go
|
||||
//on showing a blank cable field although the cable which
|
||||
//generates that field is right there again.
|
||||
//
|
||||
//And the number this cable was given is spent now: the rule
|
||||
//moves on to the one the next cable will be handed. Worked out
|
||||
//here rather than when the command was made, because a run of
|
||||
//cables pasted together is made from end to end before any of
|
||||
//it is added -- each of them has to move the rule on from where
|
||||
//the one before it left it.
|
||||
if (!m_num_key.isEmpty()) {
|
||||
NumerotationContextCommands ncc(proj->cableAutoNum(m_num_key),
|
||||
m_diagram.data());
|
||||
proj->addCableAutoNum(m_num_key, ncc.next());
|
||||
proj->setCurrentCableAutoNum(m_num_key);
|
||||
}
|
||||
CableManager::refreshLabels(proj);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AddCableCommand::undo
|
||||
Take the line back off the folio. An existing cable loses the
|
||||
section and the cores it was carrying on it -- and with them the
|
||||
cable field of the conductors, which CableManager works out again as
|
||||
soon as the cable says it changed.
|
||||
|
||||
The cores this line had taken away from other cables go back to
|
||||
those cables afterwards: without them the conductors they belong to
|
||||
would be left with a blank cable field although the cable which
|
||||
generates that field is still there.
|
||||
*/
|
||||
void AddCableCommand::undo()
|
||||
{
|
||||
QETProject *proj = project();
|
||||
if (!proj || !m_cable || !m_part || !m_diagram) return;
|
||||
|
||||
if (m_part->scene()) {
|
||||
m_diagram->removeItem(m_part);
|
||||
}
|
||||
|
||||
if (m_own_cable) {
|
||||
//The cable keeps everything it knows: it is only taken out of
|
||||
//the project, so redo finds it exactly as it was.
|
||||
proj->removeCable(m_cable);
|
||||
} else {
|
||||
m_cable->removePart(m_part_data.uuid);
|
||||
}
|
||||
|
||||
for (const CableCoreTaken &taken : std::as_const(m_taken))
|
||||
{
|
||||
if (taken.cable && taken.cable != m_cable) {
|
||||
taken.cable->setCore(taken.core);
|
||||
}
|
||||
}
|
||||
//The number this cable was given goes back into the rule, so
|
||||
//the cable drawn after an undo takes that number rather than
|
||||
//the one after it: nothing of it is left spent.
|
||||
if (!m_num_key.isEmpty()) {
|
||||
proj->addCableAutoNum(m_num_key, m_num_before);
|
||||
proj->setCurrentCableAutoNum(m_num_key);
|
||||
}
|
||||
|
||||
//Same as in redo(): worked out again whatever happened above,
|
||||
//so a cable section or a whole cable taken away never leaves
|
||||
//the wires it used to feed with a cable field it still writes.
|
||||
CableManager::refreshLabels(proj);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef ADDCABLECOMMAND_H
|
||||
#define ADDCABLECOMMAND_H
|
||||
|
||||
#include "cable.h"
|
||||
|
||||
#include "../autoNum/numerotationcontext.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QPointer>
|
||||
#include <QUndoCommand>
|
||||
|
||||
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<CableCoreTaken> &taken = QList<CableCoreTaken>());
|
||||
~AddCableCommand() override;
|
||||
|
||||
void undo() override;
|
||||
void redo() override;
|
||||
|
||||
private:
|
||||
QETProject *project() const;
|
||||
|
||||
private:
|
||||
/**
|
||||
The cable the line belongs to.
|
||||
|
||||
A pointer, not a raw one: RemoveCableCommand deletes the
|
||||
cable when it goes, and both of them are torn down together
|
||||
when the stack takes a new command. Whoever of the two runs
|
||||
first frees the cable, so the other one must not follow it
|
||||
into the freed memory -- QPointer goes null instead, and
|
||||
this command then simply has nothing left to clean up.
|
||||
*/
|
||||
QPointer<Cable> m_cable;
|
||||
///The line itself, owned by the folio once it is drawn
|
||||
QPointer<CablePart> m_part;
|
||||
QPointer<Diagram> 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<CableCore> m_cores;
|
||||
///The cores this line took away from other cables: redo takes
|
||||
///them again, undo gives them back
|
||||
QList<CableCoreTaken> 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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "cable.h"
|
||||
|
||||
#include <QDomDocument>
|
||||
#include <QDomElement>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
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<CableCore> Cable::usedCores() const
|
||||
{
|
||||
QList<CableCore> 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<CableCore> &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<CablePartData> 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);
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CABLE_H
|
||||
#define CABLE_H
|
||||
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QPointF>
|
||||
#include <QStringList>
|
||||
#include <QUuid>
|
||||
|
||||
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<QString, CableTextFormat> 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<CableCore> 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<CableCore> 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<CableCore> &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<CablePartData> 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<CableCore> m_cores;
|
||||
QList<CablePartData> 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<QString, CableTextFormat> 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> cable;
|
||||
///The core exactly as the other cable held it
|
||||
CableCore core;
|
||||
};
|
||||
|
||||
#endif // CABLE_H
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#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 <QDomDocument>
|
||||
#include <QSet>
|
||||
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
@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<CablePart *> parts;
|
||||
for (QGraphicsItem *item : diagram->selectedItems()) {
|
||||
if (auto *part = qgraphicsitem_cast<CablePart *>(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<CablePart *> CableCopy::read(Diagram *diagram, const QDomElement &root)
|
||||
{
|
||||
QList<CablePart *> 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<CableCore> 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::Wired> CableCopy::wire(Diagram *diagram,
|
||||
const QList<CablePart *> &parts,
|
||||
const QList<Conductor *> &fresh)
|
||||
{
|
||||
QList<Wired> 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> offered;
|
||||
QSet<QUuid> wires;
|
||||
|
||||
const QLineF line(part->firstPoint(), part->secondPoint());
|
||||
const QList<CableCrossing> 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<CableCrossing> 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> 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> &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;
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CABLECOPY_H
|
||||
#define CABLECOPY_H
|
||||
|
||||
#include "cable.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QPointer>
|
||||
|
||||
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<CablePart *> 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<CablePart> part;
|
||||
QList<CableCoreTaken> taken;
|
||||
///The wires the colour labels of this line were given,
|
||||
///so they can be pointed at them again afterwards
|
||||
QList<Bound> 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<Wired> wire(Diagram *diagram,
|
||||
const QList<CablePart *> &parts,
|
||||
const QList<Conductor *> &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> &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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CABLECREATEDIALOG_H
|
||||
#define CABLECREATEDIALOG_H
|
||||
|
||||
#include "../cablelist/cabletypelist.h"
|
||||
#include "cable.h"
|
||||
#include "cablemanager.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QList>
|
||||
#include <QPointer>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
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<QWidget *> 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<CableCrossing> &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<CableCoreTaken> 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<int> &core_slots);
|
||||
QList<CableCoreTaken> coresTakenFromOthers(const QUuid &target) const;
|
||||
QList<int> 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<Diagram> m_diagram;
|
||||
QList<CableCrossing> m_crossings;
|
||||
CablePartData m_part;
|
||||
Cable *m_cable = nullptr;
|
||||
///The cable whose type is changed, null while a line is drawn
|
||||
QPointer<Cable> m_change;
|
||||
///The cores other cables lost to this one, given back by undo
|
||||
QList<CableCoreTaken> 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<Cable *> 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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#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 <QHash>
|
||||
#include <QPainterPath>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
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<CableCrossing> CableManager::crossedConductors(Diagram *diagram, const QLineF &line)
|
||||
{
|
||||
QList<CableCrossing> 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<qreal>::max();
|
||||
}
|
||||
|
||||
qreal best = std::numeric_limits<qreal>::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<QUuid, Binding> 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<QUuid, Conductor *> conductors_by_uuid;
|
||||
QHash<Element *, QList<Conductor *>> 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<Conductor *> &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<Conductor *> 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<Conductor *> 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();
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CABLEMANAGER_H
|
||||
#define CABLEMANAGER_H
|
||||
|
||||
#include <QLineF>
|
||||
#include <QList>
|
||||
#include <QPointF>
|
||||
#include <QUuid>
|
||||
|
||||
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<CableCrossing> 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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CABLEPART_H
|
||||
#define CABLEPART_H
|
||||
|
||||
#include "cable.h"
|
||||
#include "cablemanager.h"
|
||||
#include "../qetgraphicsitem/qetgraphicsitem.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QFont>
|
||||
#include <QPointer>
|
||||
#include <QSet>
|
||||
#include <QUuid>
|
||||
|
||||
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<CableCrossing> &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<QUndoCommand *> &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<PdfRef> pdfRefs() const;
|
||||
|
||||
protected:
|
||||
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
One line of the label at the left of the trunk line:
|
||||
what is written, which field of the cable it comes from
|
||||
and -- worked out from what that field asks for -- the
|
||||
font it is written with, the way it lines up and the
|
||||
rectangle it sits in.
|
||||
|
||||
Every line carries its own font, because every field
|
||||
may have been given one of its own: Anlage, Ort and
|
||||
Désignation stand in one block, and their lines may
|
||||
very well no longer all be the same size once he has
|
||||
set them apart.
|
||||
*/
|
||||
struct LabelLine
|
||||
{
|
||||
QString text;
|
||||
QString key;
|
||||
QFont font;
|
||||
Qt::Alignment align = Qt::AlignRight;
|
||||
qreal width = 0.0;
|
||||
qreal height = 0.0;
|
||||
QRectF rect;
|
||||
};
|
||||
|
||||
/**
|
||||
One line of the label which points at a folio this cable
|
||||
runs on besides this one: that folio, the section of the
|
||||
cable standing there, and the text this project asks for
|
||||
such a reference -- the format chosen for the type
|
||||
"Cable" under Cross-references, written out once per folio,
|
||||
in the order the folios turn.
|
||||
*/
|
||||
struct CrossRef
|
||||
{
|
||||
///The other folio, which always exists while listed
|
||||
Diagram *diagram = nullptr;
|
||||
///Which section of the cable stands on it
|
||||
QUuid part;
|
||||
///The text as it is written on the drawing
|
||||
QString text;
|
||||
};
|
||||
|
||||
QFont textFont() const;
|
||||
///The font the colours of the cores are written with
|
||||
QFont coreFont() const;
|
||||
///The font one named text of this cable is written with
|
||||
QFont fontOf(const QString &key) const;
|
||||
///How one named text of this cable lines up at the line
|
||||
Qt::Alignment alignOf(const QString &key) const;
|
||||
/**
|
||||
How far the whole label at the left of the line is
|
||||
nudged on the drawing: over the line, under it, left
|
||||
or right of the end of it. A nudge only, which moves
|
||||
the texts and never the line.
|
||||
@return the distance in scene units
|
||||
*/
|
||||
QPointF textOffset() const;
|
||||
/**
|
||||
How far the row of colours is nudged on the drawing:
|
||||
again a distance on the texts only, the slashes and
|
||||
the entries behind them staying where they stand.
|
||||
@return the distance in scene units
|
||||
*/
|
||||
QPointF coreOffset() const;
|
||||
/**
|
||||
Where a text of this width starts, at the left end of
|
||||
the trunk line: before the line when it is set to end
|
||||
there (the way it always did), on the line when it is
|
||||
centred, after it when it starts there.
|
||||
@param key the field the text comes from
|
||||
@param width how wide the text is, in scene units
|
||||
@return the x of the left of the text
|
||||
*/
|
||||
qreal textLeft(const QString &key, qreal width) const;
|
||||
bool trunkIsHorizontal() const;
|
||||
QList<LabelLine> 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<LabelLine> 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<CrossRef> crossRefs() const;
|
||||
/**
|
||||
Which of those references stands at that very place, so
|
||||
that both the hand of the mouse and a double click know
|
||||
which line they are about.
|
||||
@param scene_pos a point of the sheet, in scene coordinates
|
||||
@return the index of that reference, -1 when none stands there
|
||||
*/
|
||||
int crossRefAt(const QPointF &scene_pos) const;
|
||||
/**
|
||||
Send the mouse to the section of the cable which stands
|
||||
on the folio the reference at that place points at -- the
|
||||
very leap an element's cross reference takes, straight to
|
||||
the place to look at.
|
||||
@param scene_pos a point of the sheet, in scene coordinates
|
||||
@return true when the click stood on a reference
|
||||
*/
|
||||
bool goToCrossRef(const QPointF &scene_pos);
|
||||
/**
|
||||
Keep the reference lines up to date with the project: a
|
||||
folio which is added, removed or moved changes what they
|
||||
say, and so does the format chosen under Cross-references.
|
||||
*/
|
||||
void setUpXrefHooks();
|
||||
QString typeText() const;
|
||||
QString lengthText() const;
|
||||
QRectF labelRect() const;
|
||||
qreal underTop() const;
|
||||
QRectF typeRect() const;
|
||||
QRectF lengthRect() const;
|
||||
QRectF coreLabelRect(const QPointF &at, const QString &color) const;
|
||||
QPointF coreLabelPivot(const QPointF &at, const QString &color) const;
|
||||
QPointF coreDrawPosition(const CableCore &core) const;
|
||||
bool coreDrawn(const CableCore &core) const;
|
||||
QPointF dragPosition(const QPointF &scene_pos) const;
|
||||
/**
|
||||
Where the mouse would put down a core which waits to be
|
||||
put down: the mouse projected onto the line itself, at a
|
||||
grid step, and never past either end of the line -- the
|
||||
same coordinates a dragged label would be drawn at, only
|
||||
worked out from the mouse rather than from a gesture.
|
||||
@param scene_pos where the mouse stands, in scene coordinates
|
||||
@return where the slash would be drawn
|
||||
*/
|
||||
QPointF placePosition(const QPointF &scene_pos) const;
|
||||
/**
|
||||
Snap a distance measured along the line to a grid step, in
|
||||
the coordinates of the sheet -- so a horizontal line steps
|
||||
along x and a vertical one along y. Holding Ctrl lets go of
|
||||
the grid, as everywhere else on the sheet.
|
||||
@param along how far from the first end of the line
|
||||
@return that distance at a grid step
|
||||
*/
|
||||
qreal stepAlong(qreal along) const;
|
||||
void drawLabel(QPainter *painter, const QColor &ink) const;
|
||||
void drawCores(QPainter *painter, const QColor &ink) const;
|
||||
///Draw the slash of one core and the colour written beside it
|
||||
void drawCoreMark(QPainter *painter, const QColor &ink,
|
||||
const QPointF &at, const QString &color) const;
|
||||
void syncGeometry();
|
||||
bool labelAt(int core, const QPointF &scene_pos) const;
|
||||
///Answer to the question of taking a wire another cable holds
|
||||
enum class ClaimAnswer {
|
||||
///Take it: the other cable lets it go
|
||||
Take,
|
||||
///Leave it: nothing changes hands, the rest still happens
|
||||
Leave,
|
||||
///Cancel: this whole gesture is called off, as if undone
|
||||
Cancel
|
||||
};
|
||||
/**
|
||||
@param core which core is being let go
|
||||
@param dropped_at where its label comes to rest
|
||||
@param before every core the cable held before this one was
|
||||
added to it, so that the one step of undo takes the addition
|
||||
back as well; nullptr while a core which is already on the
|
||||
sheet is merely moved
|
||||
@return false when the user called the whole thing off
|
||||
*/
|
||||
bool rebindCore(int core, const QPointF &dropped_at,
|
||||
const QList<CableCore> *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<CableCore> &cores, const QPointF &delta) const;
|
||||
bool settleCores(QList<CableCore> &cores) const;
|
||||
ClaimAnswer mayClaim(Conductor *conductor) const;
|
||||
ClaimAnswer mayClaimSeveral(const QList<Conductor *> &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<CablePart> s_placing;
|
||||
QPointer<Cable> 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<CableCore> 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<CableCore> 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<CableCore> cores;
|
||||
};
|
||||
QList<CarriedLine> 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<QMetaObject::Connection> 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<QUndoCommand *> &steps);
|
||||
void putCompanionsBack();
|
||||
void readyToSettle(QList<CableCore> &cores) const;
|
||||
};
|
||||
|
||||
#endif // CABLEPART_H
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "cablepropertiesdialog.h"
|
||||
|
||||
#include "cablecreatedialog.h"
|
||||
#include "cablepart.h"
|
||||
#include "cablepropertieswidget.h"
|
||||
#include "editcablecommand.h"
|
||||
#include "../diagram.h"
|
||||
#include "../qeticons.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QMenu>
|
||||
#include <QUndoStack>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
/**
|
||||
@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("Cable"));
|
||||
|
||||
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("Unplaced cores:"), this);
|
||||
m_free_cores = new QListWidget(this);
|
||||
m_free_cores->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_free_cores->setAlternatingRowColors(true);
|
||||
m_free_cores->setToolTip(tr("Double-click a core to place it on the "
|
||||
"line: the line follows the mouse and a "
|
||||
"click leaves it where you aimed."));
|
||||
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("No cable."));
|
||||
return;
|
||||
}
|
||||
|
||||
m_cores->setText(tr("%1 of %2 cores wired to a conductor",
|
||||
"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("Core %1").arg(i + 1)
|
||||
: tr("Core %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<CablePart> 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("Place this core on the line"));
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CABLEPROPERTIESDIALOG_H
|
||||
#define CABLEPROPERTIESDIALOG_H
|
||||
|
||||
#include "cable.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPointer>
|
||||
|
||||
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<Cable> m_cable;
|
||||
Diagram *m_diagram = nullptr;
|
||||
///The line whose dialog this is, and the one a core chosen
|
||||
///here is put down on
|
||||
QPointer<CablePart> 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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "cablepropertieseditorwidget.h"
|
||||
|
||||
#include "cablecreatedialog.h"
|
||||
#include "cablepart.h"
|
||||
#include "cablepropertieswidget.h"
|
||||
#include "editcablecommand.h"
|
||||
#include "../qeticons.h"
|
||||
#include "../qetproject.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QMenu>
|
||||
#include <QSizePolicy>
|
||||
#include <QUndoStack>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
/**
|
||||
@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("Unplaced cores:"));
|
||||
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-click a core to place it on the "
|
||||
"line: the line follows the mouse and a "
|
||||
"click leaves it where you aimed."));
|
||||
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<QETProject *>(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 of %2 cores wired to a conductor",
|
||||
"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("Core %1").arg(i + 1)
|
||||
: tr("Core %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("Place this core on the line"));
|
||||
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("Cable");
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CABLEPROPERTIESEDITORWIDGET_H
|
||||
#define CABLEPROPERTIESEDITORWIDGET_H
|
||||
|
||||
#include "cable.h"
|
||||
|
||||
#include "../PropertiesEditor/propertieseditorwidget.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QMetaObject>
|
||||
#include <QPointer>
|
||||
|
||||
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<Cable> 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<CablePart> 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<Cable> m_live_cable;
|
||||
QMetaObject::Connection m_live_connection;
|
||||
bool m_updating = false;
|
||||
};
|
||||
|
||||
#endif // CABLEPROPERTIESEDITORWIDGET_H
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "cablepropertieswidget.h"
|
||||
|
||||
#include "cabletextformatdialog.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qeticons.h"
|
||||
#include "../utils/qetutils.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QSignalBlocker>
|
||||
#include <QSpinBox>
|
||||
|
||||
/**
|
||||
@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("Drawing",
|
||||
"column header: the drawing shows this field"), this);
|
||||
plan->setAlignment(Qt::AlignCenter);
|
||||
plan->setToolTip(tr("Tick to write this field on the drawing.",
|
||||
"tooltip of the tick before each field"));
|
||||
grid->addWidget(plan, 0, 1, Qt::AlignCenter);
|
||||
|
||||
int row = 1;
|
||||
addField(grid, row++, QStringLiteral("designation"),
|
||||
tr("Designation"), &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("Choose the type of this cable from the list of "
|
||||
"cable types…"));
|
||||
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("Plant (=)"), &m_installation, &m_show_installation);
|
||||
addField(grid, row++, QStringLiteral("location"),
|
||||
tr("Location (+)"), &m_location, &m_show_location);
|
||||
addField(grid, row++, QStringLiteral("length"),
|
||||
tr("Length"), &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("Core colors"), this);
|
||||
cores_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
cores_label->setToolTip(tr("The font of the colors of all the cores "
|
||||
"of this cable, written along the line."));
|
||||
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("Text position"),
|
||||
tr("Shifts all the cable text: plant, location "
|
||||
"and designation on one side, type and length "
|
||||
"on the other. The line, the slashes and the "
|
||||
"wiring stay in place. 10 px make one grid cell."),
|
||||
&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("Core colors position"),
|
||||
tr("Shifts the core colors relative to their slashes. "
|
||||
"The slashes, the entries and the wiring stay in place. "
|
||||
"10 px make one grid cell."),
|
||||
&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("Tick to write this field on the drawing.",
|
||||
"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("Font, size and style of the core colors.")
|
||||
: tr("Font, size, style and alignment of this text.");
|
||||
const CableTextFormat format = m_formats.value(key);
|
||||
const bool its_own = !format.font.isEmpty() || format.alignment != 0;
|
||||
button->setToolTip(tip + QLatin1Char(' ') + (its_own
|
||||
? tr("Set for this cable.")
|
||||
: tr("Value defined in the preferences.")));
|
||||
}
|
||||
|
||||
/**
|
||||
@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));
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CABLEPROPERTIESWIDGET_H
|
||||
#define CABLEPROPERTIESWIDGET_H
|
||||
|
||||
#include "cable.h"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
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<QString, CableTextFormat> m_formats;
|
||||
///The button opening the window of each text, by name
|
||||
QHash<QString, QPushButton *> 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
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "cablerenumber.h"
|
||||
|
||||
#include "cable.h"
|
||||
#include "../autoNum/assignvariables.h"
|
||||
#include "../autoNum/numerotationcontextcommands.h"
|
||||
#include "../diagram.h"
|
||||
#include "../qetproject.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QSet>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
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", "No project is open.");
|
||||
return result;
|
||||
}
|
||||
if (project->isReadOnly())
|
||||
{
|
||||
result.error = QCoreApplication::translate(
|
||||
"CableRenumber",
|
||||
"This project is read-only: its cables cannot be "
|
||||
"renumbered.");
|
||||
return result;
|
||||
}
|
||||
//The rule may be one the numbering window is drawing right now
|
||||
//and has not written into the project yet: the numbering writes
|
||||
//it together with the numbers themselves, so the project does
|
||||
//not have to hold it beforehand. What the counters are kept
|
||||
//under is the name the rule is stored with, which for cables is
|
||||
//fixed either way -- so numbering starts from a project which
|
||||
//has no rule saved yet just as well.
|
||||
QString key = project->cableCurrentAutoNum();
|
||||
if (key.isEmpty()) {
|
||||
key = QETProject::cableAutoNumRuleName();
|
||||
}
|
||||
if (rule.isEmpty())
|
||||
{
|
||||
result.error = QCoreApplication::translate(
|
||||
"CableRenumber",
|
||||
"No cable numbering rule is defined for this "
|
||||
"project. Define one in this window before "
|
||||
"renumbering its cables.");
|
||||
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<OrderedCable> ordered;
|
||||
QSet<QString> 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<int>::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",
|
||||
"A cable is not on any sheet of the project: "
|
||||
"nothing was changed.");
|
||||
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",
|
||||
"The current rule gives no number here: "
|
||||
"nothing was changed.");
|
||||
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",
|
||||
"The current rule gives the same number (%1) twice: "
|
||||
"it must count to renumber a whole project. "
|
||||
"Nothing was changed.")
|
||||
.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",
|
||||
"Number %1 is already used by a cable whose name "
|
||||
"is left as it is: nothing was changed.")
|
||||
.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;
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CABLERENUMBER_H
|
||||
#define CABLERENUMBER_H
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#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<Cable *> 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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "cabletextformatdialog.h"
|
||||
|
||||
#include "../qetapp.h"
|
||||
#include "../utils/qetutils.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFontComboBox>
|
||||
#include <QFormLayout>
|
||||
#include <QFrame>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
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("Core colors")
|
||||
: tr("Text format"));
|
||||
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("Font:"), 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("Size:"), m_size);
|
||||
|
||||
m_style = new QComboBox(this);
|
||||
m_style->addItem(tr("Normal"));
|
||||
m_style->addItem(tr("Bold"));
|
||||
m_style->addItem(tr("Italic"));
|
||||
m_style->addItem(tr("Bold italic"));
|
||||
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("Right aligned"));
|
||||
m_alignment->addItem(tr("Centred"));
|
||||
m_alignment->addItem(tr("Left aligned"));
|
||||
form->addRow(tr("Alignment:"), 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("Preview:"), m_preview);
|
||||
|
||||
auto *buttons = new QDialogButtonBox(
|
||||
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
m_reset = buttons->addButton(tr("Reset"), 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;
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CABLETEXTFORMATDIALOG_H
|
||||
#define CABLETEXTFORMATDIALOG_H
|
||||
|
||||
#include "cable.h"
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "editcablecommand.h"
|
||||
|
||||
#include "cablemanager.h"
|
||||
#include "cablepart.h"
|
||||
#include "../diagram.h"
|
||||
#include "../qetproject.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
@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", "Delete"));
|
||||
}
|
||||
|
||||
/**
|
||||
@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<QUndoCommand *> &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<CableCore> &before_cores,
|
||||
const QList<CableCore> &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", "Move a cable"));
|
||||
}
|
||||
|
||||
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<CableCore> &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",
|
||||
"Edit the properties of a cable"));
|
||||
}
|
||||
|
||||
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<Cable *> &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",
|
||||
"Renumber %n cable(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",
|
||||
"Change the cable type"));
|
||||
}
|
||||
|
||||
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<CableCore> &before,
|
||||
const QList<CableCore> &after,
|
||||
const QString &text) :
|
||||
m_cable(cable),
|
||||
m_before(before),
|
||||
m_after(after)
|
||||
{
|
||||
setText(text.isEmpty()
|
||||
? QCoreApplication::translate("ChangeCableCoresCommand",
|
||||
"Move a cable core")
|
||||
: 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<CableCore> &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<CablePart *> &parts) :
|
||||
m_diagram(diagram)
|
||||
{
|
||||
setText(QCoreApplication::translate("RemoveCableCommand", "Delete a cable"));
|
||||
|
||||
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> &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> &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> &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);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef EDITCABLECOMMAND_H
|
||||
#define EDITCABLECOMMAND_H
|
||||
|
||||
#include "cable.h"
|
||||
|
||||
#include "../autoNum/numerotationcontext.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QPointer>
|
||||
#include <QUndoCommand>
|
||||
|
||||
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<CableCore> &before_cores,
|
||||
const QList<CableCore> &after_cores);
|
||||
|
||||
void undo() override;
|
||||
void redo() override;
|
||||
|
||||
private:
|
||||
void apply(const QPointF &p1,
|
||||
const QPointF &p2,
|
||||
const QList<CableCore> &cores);
|
||||
|
||||
private:
|
||||
QPointer<Cable> m_cable;
|
||||
QPointer<CablePart> m_part;
|
||||
QPointF m_before_p1;
|
||||
QPointF m_before_p2;
|
||||
QPointF m_after_p1;
|
||||
QPointF m_after_p2;
|
||||
QList<CableCore> m_before_cores;
|
||||
QList<CableCore> 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<CableCore> &before,
|
||||
const QList<CableCore> &after,
|
||||
const QString &text = QString());
|
||||
|
||||
void undo() override;
|
||||
void redo() override;
|
||||
|
||||
private:
|
||||
void apply(const QList<CableCore> &cores);
|
||||
|
||||
private:
|
||||
QPointer<Cable> m_cable;
|
||||
QList<CableCore> m_before;
|
||||
QList<CableCore> 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<Cable> 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<Cable *> &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<QPointer<Cable>> m_cables;
|
||||
QStringList m_before;
|
||||
QStringList m_after;
|
||||
QList<bool> m_by_hand_before;
|
||||
QList<bool> 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<Cable> 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<QUndoCommand *> &steps, const QString &text);
|
||||
~BatchCommand() override;
|
||||
|
||||
void undo() override;
|
||||
void redo() override;
|
||||
|
||||
private:
|
||||
QList<QUndoCommand *> 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<CablePart *> &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<CablePart> part;
|
||||
///Where the cable holds that section
|
||||
CablePartData data;
|
||||
///The cable the section belongs to
|
||||
QPointer<Cable> cable;
|
||||
};
|
||||
|
||||
QPointer<Diagram> m_diagram;
|
||||
QList<Entry> 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<QPointer<Cable>> m_cables;
|
||||
QHash<Cable *, QList<CableCore>> m_cores;
|
||||
};
|
||||
|
||||
#endif // EDITCABLECOMMAND_H
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#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 <QAbstractItemView>
|
||||
#include <QCheckBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFileDialog>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QTableView>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
/**
|
||||
@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("Export the cable list as 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("Layout"), this);
|
||||
auto *page_layout = new QVBoxLayout(page_group);
|
||||
m_include_headers = new QCheckBox(tr("include headers"), 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("Preview"), 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("cable_list_") + m_project->title()
|
||||
+ QStringLiteral(".csv");
|
||||
const QString file_path = QFileDialog::getSaveFileName(
|
||||
this, tr("Save As... "), file_name,
|
||||
tr("CSV files (*.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("Error"),
|
||||
tr("Cannot save the cable list to %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();
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CABLEEXPORTDIALOG_H
|
||||
#define CABLEEXPORTDIALOG_H
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDialog>
|
||||
|
||||
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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#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 <QFont>
|
||||
#include <QMetaEnum>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
@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<QMap<QString, QString>>();
|
||||
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<Cable *> cables = m_project->cables();
|
||||
for (Cable *cable : cables) {
|
||||
m_watchers << connect(cable, &Cable::changed,
|
||||
this, &CableListModel::refresh);
|
||||
}
|
||||
|
||||
const QList<Diagram *> diagrams = m_project->diagrams();
|
||||
for (Diagram *diagram : diagrams)
|
||||
{
|
||||
m_watchers << connect(diagram, &Diagram::diagramInformationChanged,
|
||||
this, &CableListModel::refresh);
|
||||
const QList<Element *> 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<QMap<QString, QString>>();
|
||||
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<QFont>()));
|
||||
auto me = QMetaEnum::fromType<Qt::Alignment>();
|
||||
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<int, QList<int>> 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<int, QList<int>>()));
|
||||
|
||||
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<Qt::Alignment>();
|
||||
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);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CABLELISTMODEL_H
|
||||
#define CABLELISTMODEL_H
|
||||
|
||||
#include <QAbstractTableModel>
|
||||
#include <QDomElement>
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QPointer>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
|
||||
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<QETProject> m_project;
|
||||
QStringList m_fields;
|
||||
QVector<QMap<QString, QString>> m_rows;
|
||||
//First int = section, second int = Qt::role, QVariant = value
|
||||
QHash<int, QHash<int, QVariant>> m_header_data;
|
||||
QHash<int, QVariant> 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<QMetaObject::Connection> m_watchers;
|
||||
};
|
||||
|
||||
#endif // CABLELISTMODEL_H
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "cablelistmodelpropertieswidget.h"
|
||||
|
||||
#include "cablelistmodel.h"
|
||||
#include "cablequerywidget.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
/**
|
||||
@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("Edit columns..."), this);
|
||||
m_refresh_pb = new QPushButton(tr("Refresh"), 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("Cable list columns"));
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#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 <QHash>
|
||||
#include <QLineF>
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <utility>
|
||||
|
||||
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::Column> &CableList::allColumns()
|
||||
{
|
||||
static const QList<Column> columns = {
|
||||
{QStringLiteral("nr"), tr("No.")},
|
||||
{QStringLiteral("installation_cable"), tr("Plant (cable)")},
|
||||
{QStringLiteral("location_cable"), tr("Location (cable)")},
|
||||
{QStringLiteral("bmk_cable"), tr("Label (cable)")},
|
||||
{QStringLiteral("installation_start"), tr("Plant (start)")},
|
||||
{QStringLiteral("location_start"), tr("Location (start)")},
|
||||
{QStringLiteral("bmk_start"), tr("Label (start)")},
|
||||
{QStringLiteral("folio_start"), tr("Sheet (start)")},
|
||||
{QStringLiteral("installation_end"), tr("Plant (end)")},
|
||||
{QStringLiteral("location_end"), tr("Location (end)")},
|
||||
{QStringLiteral("bmk_end"), tr("Label (end)")},
|
||||
{QStringLiteral("folio_end"), tr("Sheet (end)")},
|
||||
{QStringLiteral("type"), tr("Cable type")},
|
||||
{QStringLiteral("length"), tr("Length")},
|
||||
{QStringLiteral("cores_used"), tr("Used cores")},
|
||||
};
|
||||
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<QMap<QString, QString>> CableList::rows(QETProject *project)
|
||||
{
|
||||
QVector<QMap<QString, QString>> 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<QUuid, QVector<CableEnd>> 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<Entry> 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<QUuid, CableEnd> 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<CableEnd> 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<CableEnd> 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<QString, QString> 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<QMap<QString, QString>> rows_ = rows(project);
|
||||
for (const QMap<QString, QString> &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);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CABLELISTROWS_H
|
||||
#define CABLELISTROWS_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QCoreApplication>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
|
||||
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<Column> &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<QMap<QString, QString>> 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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "cablequerywidget.h"
|
||||
|
||||
#include "cablelistrows.h"
|
||||
|
||||
#include "../qetapp.h"
|
||||
#include "../ui/configsaveloaderwidget.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QHBoxLayout>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QListWidgetItem>
|
||||
#include <QPushButton>
|
||||
#include <QStyle>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <QStringBuilder>
|
||||
|
||||
/**
|
||||
@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("Available columns"), 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("Add the selected column"));
|
||||
auto *remove_pb = make_button(QStyle::SP_ArrowLeft, tr("Remove the selected column"));
|
||||
auto *up_pb = make_button(QStyle::SP_ArrowUp, tr("Move the selected column up"));
|
||||
auto *down_pb = make_button(QStyle::SP_ArrowDown, tr("Move the selected column down"));
|
||||
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("Table columns"), 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));
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CABLEQUERYWIDGET_H
|
||||
#define CABLEQUERYWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
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
|
||||
@@ -0,0 +1,824 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "cabletypelist.h"
|
||||
|
||||
#include "../materiallist/materiallist.h"
|
||||
#include "../qetinformation.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QSaveFile>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include <utility>
|
||||
|
||||
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 Designation here, the way
|
||||
//the cable form calls it, and not "Article number", which is
|
||||
//what an element means by it.
|
||||
static const QMap<QString, QString> own = {
|
||||
{QStringLiteral("designation"),
|
||||
QCoreApplication::translate("CableTypeList", "Designation")},
|
||||
{QStringLiteral("cores"),
|
||||
QCoreApplication::translate("CableTypeList", "Number of cores")},
|
||||
{QStringLiteral("core_colors"),
|
||||
QCoreApplication::translate("CableTypeList", "Core colors")},
|
||||
{QStringLiteral("section"),
|
||||
QCoreApplication::translate("CableTypeList", "Cable section")},
|
||||
{QStringLiteral("color"),
|
||||
QCoreApplication::translate("CableTypeList", "Sheath color")}
|
||||
};
|
||||
|
||||
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<QString, QString> CableTypeList::headerAliasMap()
|
||||
{
|
||||
QMap<QString, QString> 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
|
||||
//("Article number" 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);
|
||||
}
|
||||
}
|
||||
|
||||
//The wording older files were written with : these French names
|
||||
//were the ones the label line carried before the interface text
|
||||
//became English, and a catalogue written with them must keep
|
||||
//reading, so each of them still means its column here.
|
||||
static const QMap<QString, QString> older_wording = {
|
||||
{QStringLiteral("désignation"), QStringLiteral("designation")},
|
||||
{QStringLiteral("nombre d'âmes"), QStringLiteral("cores")},
|
||||
{QStringLiteral("couleurs des âmes"), QStringLiteral("core_colors")},
|
||||
{QStringLiteral("section du câble"), QStringLiteral("section")},
|
||||
{QStringLiteral("couleur du fourreau"), QStringLiteral("color")}
|
||||
};
|
||||
for (auto it = older_wording.constBegin(); it != older_wording.constEnd(); ++it)
|
||||
{
|
||||
if (!map.contains(it.key())) {
|
||||
map.insert(it.key(), it.value());
|
||||
}
|
||||
}
|
||||
|
||||
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<QString, QString> &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<QString, QString> &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", "No receiver for the cable types file.");
|
||||
}
|
||||
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<QStringList> rows = MaterialList::parseCsv(content, data->separator);
|
||||
if (rows.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const QStringList header = rows.takeFirst();
|
||||
const QMap<QString, QString> 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<QStringList> 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<QStringList> header_lines;
|
||||
header_lines.append(translatedHeader(data.columns));
|
||||
header_lines.append(data.columns);
|
||||
const QByteArray content = MaterialList::serializeCsv(header_lines, rows, data.separator);
|
||||
|
||||
//The folder may not be there yet: a fresh profile has no
|
||||
//documents folder, and a path typed into the settings may name a
|
||||
//folder which was never created. QSaveFile cannot write into a
|
||||
//folder which does not exist, so without this the whole write
|
||||
//fails and the message says nothing about the real reason.
|
||||
const QString folder = QFileInfo(path).absolutePath();
|
||||
if (!folder.isEmpty() && !QDir(folder).exists() && !QDir().mkpath(folder))
|
||||
{
|
||||
if (error) {
|
||||
*error = QCoreApplication::translate(
|
||||
"CableTypeList",
|
||||
"Cannot create the folder %1 for the cable types file.").arg(folder);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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", "The file contains no header: missing columns.");
|
||||
}
|
||||
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", "The file contains no header: missing columns.");
|
||||
}
|
||||
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",
|
||||
"This entry no longer exists in the file: it may have been modified in the meantime.");
|
||||
}
|
||||
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", "The file already exists and is not empty.");
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CABLETYPELIST_H
|
||||
#define CABLETYPELIST_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QChar>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
/**
|
||||
@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<QString, QString> 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<CableTypeRecord> 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<QString, QString> headerAliasMap();
|
||||
static QString canonicalColumn(const QString &header_cell,
|
||||
const QStringList &already_used,
|
||||
const QMap<QString, QString> &alias_map);
|
||||
static bool isMachineHeaderLine(const QStringList &label_line,
|
||||
const QStringList &candidate,
|
||||
const QMap<QString, QString> &alias_map);
|
||||
};
|
||||
|
||||
#endif // CABLETYPELIST_H
|
||||
@@ -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");
|
||||
@@ -844,6 +855,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 &&\
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "qet.h"
|
||||
#include <QColor>
|
||||
#include <QSettings>
|
||||
#include <QUuid>
|
||||
|
||||
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;
|
||||
|
||||
@@ -20,9 +20,12 @@
|
||||
|
||||
#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"
|
||||
#include "diagramevent/diagrameventaddcable.h"
|
||||
#include "diagramevent/diagrameventinterface.h"
|
||||
#include "diagramposition.h"
|
||||
#include "factory/elementfactory.h"
|
||||
@@ -900,6 +903,23 @@ bool Diagram::eventInterfaceIsRunning() const
|
||||
return m_event_interface && m_event_interface->isRunning();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Diagram::cableToolIsRunning
|
||||
@return true while the cable drawing tool is the running tool.
|
||||
|
||||
That tool is the one which takes the right mouse button away from
|
||||
everything else: while it runs, a right click cancels the line being
|
||||
drawn and must not open the quick command ring nor any context menu
|
||||
with it. Every other tool lets go of the button again and is
|
||||
surrounded by the ring the way it always was.
|
||||
*/
|
||||
bool Diagram::cableToolIsRunning() const
|
||||
{
|
||||
return m_event_interface
|
||||
&& m_event_interface->isRunning()
|
||||
&& qobject_cast<DiagramEventAddCable *>(m_event_interface);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Diagram::conductorsAutonumName
|
||||
@return the name of autonum to use.
|
||||
@@ -1420,9 +1440,10 @@ void Diagram::folioSequentialsToXml(QHash<QString,
|
||||
bool Diagram::fromXml(QDomDocument &document,
|
||||
QPointF position,
|
||||
bool consider_informations,
|
||||
DiagramContent *content_ptr) {
|
||||
DiagramContent *content_ptr,
|
||||
QList<CablePart *> *cables_added) {
|
||||
QDomElement root = document.documentElement();
|
||||
return(fromXml(root, position, consider_informations, content_ptr));
|
||||
return(fromXml(root, position, consider_informations, content_ptr, cables_added));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1662,7 +1683,8 @@ QPair<Terminal *, Terminal *> pickEnds(const QDomElement &f,
|
||||
bool Diagram::fromXml(QDomElement &document,
|
||||
QPointF position,
|
||||
bool consider_informations,
|
||||
DiagramContent *content_ptr)
|
||||
DiagramContent *content_ptr,
|
||||
QList<CablePart *> *cables_added)
|
||||
{
|
||||
const QDomElement& root = document;
|
||||
// The first element must be a diagram
|
||||
@@ -1928,6 +1950,17 @@ bool Diagram::fromXml(QDomElement &document,
|
||||
//Load terminal strip item
|
||||
QVector<TerminalStripItem *> 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<CablePart *> 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())
|
||||
{
|
||||
@@ -1938,6 +1971,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;
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include <QtXml>
|
||||
|
||||
class Conductor;
|
||||
class CablePart;
|
||||
class CustomElement;
|
||||
class DiagramContent;
|
||||
class DiagramPosition;
|
||||
@@ -203,14 +204,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<CablePart *> * = nullptr);
|
||||
bool fromXml(QDomElement &,
|
||||
QPointF = QPointF(),
|
||||
bool = true,
|
||||
DiagramContent * = nullptr);
|
||||
DiagramContent * = nullptr,
|
||||
QList<CablePart *> * = nullptr);
|
||||
void folioSequentialsToXml(QHash<QString,
|
||||
QStringList>*,
|
||||
QDomElement *,
|
||||
@@ -231,6 +239,7 @@ class Diagram : public QGraphicsScene
|
||||
virtual void addItem (QGraphicsItem *item);
|
||||
virtual void removeItem (QGraphicsItem *item);
|
||||
bool eventInterfaceIsRunning() const;
|
||||
bool cableToolIsRunning() const;
|
||||
|
||||
// methods related to graphics options
|
||||
ExportProperties applyProperties(const ExportProperties &);
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#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 <QGraphicsLineItem>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QGraphicsView>
|
||||
#include <QKeyEvent>
|
||||
#include <QLineF>
|
||||
#include <QSettings>
|
||||
#include <QStatusBar>
|
||||
#include <QTimer>
|
||||
#include <QUndoStack>
|
||||
|
||||
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(
|
||||
"Left click: place the cable start; "
|
||||
"drag or second click: finish; "
|
||||
"right click: cancel; Esc: quit"));
|
||||
return;
|
||||
}
|
||||
|
||||
editor->statusBar()->showMessage(tr(
|
||||
"Left click or release: finish the line "
|
||||
"(%n core(s) recognized; always horizontal or vertical); "
|
||||
"right click: cancel the line",
|
||||
"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<Cable *> 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();
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#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<CableCrossing> m_crossings;
|
||||
};
|
||||
|
||||
#endif // DIAGRAMEVENTADDCABLE_H
|
||||
@@ -18,6 +18,10 @@
|
||||
#include "diagrameventaddpaste.h"
|
||||
#include "../autoNum/ui/pastenumberingimport.h"
|
||||
|
||||
#include "../cable/addcablecommand.h"
|
||||
#include "../cable/cablecopy.h"
|
||||
#include "../cable/cablepart.h"
|
||||
#include "../cable/editcablecommand.h"
|
||||
#include "../diagram.h"
|
||||
#include "../foliogrid.h"
|
||||
#include "../diagramcommands.h"
|
||||
@@ -30,12 +34,15 @@
|
||||
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QCoreApplication>
|
||||
#include <QCursor>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QGraphicsView>
|
||||
#include <QKeyEvent>
|
||||
#include <QStatusBar>
|
||||
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPaste::DiagramEventAddPaste
|
||||
@param diagram : diagram to paste into
|
||||
@@ -74,8 +81,12 @@
|
||||
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);
|
||||
m_copied_schemes = PasteNumberingImport::copiedBy(document_xml);
|
||||
|
||||
if (db) {
|
||||
@@ -83,9 +94,15 @@
|
||||
db->setUpdateBlocked(false);
|
||||
db->updateDB();
|
||||
}
|
||||
if (!m_content.count()) return;
|
||||
if (!m_content.count() && m_cables.isEmpty()) return;
|
||||
|
||||
const QList<QGraphicsItem *> movable = m_content.items(MovableItems);
|
||||
QList<QGraphicsItem *> 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
|
||||
@@ -309,11 +326,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<Conductor *> conductors = m_content.conductors(DiagramContent::AnyConductor);
|
||||
@@ -377,6 +407,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()
|
||||
{
|
||||
@@ -384,10 +419,35 @@ void DiagramEventAddPaste::commit()
|
||||
m_finished = true;
|
||||
m_running = false;
|
||||
|
||||
const QList<CableCopy::Wired> wired = CableCopy::wire(
|
||||
m_diagram, m_cables,
|
||||
m_content.conductors(DiagramContent::AnyConductor));
|
||||
m_cables.clear();
|
||||
|
||||
//The cable lines the copy brings are not part of the paste
|
||||
//itself: they are handed to the paste so that one Ctrl+Z takes
|
||||
//the whole of it back in one go.
|
||||
QList<QUndoCommand *> cable_commands;
|
||||
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", "Paste a cable"));
|
||||
cable_commands << added;
|
||||
}
|
||||
|
||||
//Asks whether to import the numberings the copy brings, if the
|
||||
//project has not got them
|
||||
//project has not got them; the paste itself and the cable lines
|
||||
//travel inside the same step of the undo stack.
|
||||
PasteNumberingImport::push(m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
|
||||
m_diagram, m_content, m_copied_schemes);
|
||||
m_diagram, m_content, m_copied_schemes, cable_commands);
|
||||
|
||||
//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();
|
||||
}
|
||||
|
||||
@@ -419,6 +479,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();
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
|
||||
#include <QHash>
|
||||
#include <QPointer>
|
||||
#include <QSet>
|
||||
|
||||
class CablePart;
|
||||
class QStatusBar;
|
||||
|
||||
/**
|
||||
@@ -91,6 +93,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<CablePart *> 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<QGraphicsItem *> 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;
|
||||
/// The numberings the pasted copy carries
|
||||
QList<ElementAutoNumSchemeCommand::Scheme> m_copied_schemes;
|
||||
///Each movable item's position relative to the group's top left,
|
||||
|
||||
@@ -193,6 +193,11 @@ QStringList DiagramToolbarSettings::defaultIds(const QString &name)
|
||||
QStringLiteral("diagrameditor.conductor_reset"),
|
||||
QStringLiteral("diagrameditor.auto_conductor"),
|
||||
QStringLiteral("diagrameditor.auto_break_conductor"),
|
||||
//The cable tool is drawn across the conductors those two
|
||||
//actions work on, so it sits with them rather than with
|
||||
//the shapes of the "Ajouter" toolbar.
|
||||
sep,
|
||||
QStringLiteral("diagrameditor.add_cable"),
|
||||
QStringLiteral("widget:conductor_color")};
|
||||
}
|
||||
if (name == QLatin1String("adding")) {
|
||||
|
||||
@@ -23,6 +23,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"
|
||||
@@ -67,6 +71,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
|
||||
@@ -638,8 +665,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<CablePart *> cable_cut;
|
||||
for (QGraphicsItem *item : m_diagram->selectedItems()) {
|
||||
if (auto *part = qgraphicsitem_cast<CablePart *>(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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -649,7 +697,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);
|
||||
}
|
||||
@@ -670,16 +723,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<CablePart *> cable_pasted;
|
||||
m_diagram->fromXml(document_xml, pos, false, &content_pasted, &cable_pasted);
|
||||
const QList<CableCopy::Wired> 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();
|
||||
|
||||
//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.
|
||||
QList<QUndoCommand *> cable_commands;
|
||||
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", "Paste a cable"));
|
||||
cable_commands << 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);
|
||||
}
|
||||
|
||||
//Asks whether to import the numberings the copy brings, if the
|
||||
//project has not got them
|
||||
//project has not got them; the paste itself and the cable lines
|
||||
//travel inside the same step of the undo stack.
|
||||
PasteNumberingImport::push(this, m_diagram, content_pasted,
|
||||
PasteNumberingImport::copiedBy(document_xml));
|
||||
PasteNumberingImport::copiedBy(document_xml),
|
||||
cable_commands);
|
||||
//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();
|
||||
}
|
||||
}
|
||||
@@ -749,21 +838,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<CablePart *> 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<CableCopy::Wired> 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", "Duplicate a cable"));
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -793,22 +912,59 @@ 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 the running cable tool belongs to the tool
|
||||
//alone: it cancels the line being drawn, 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. No other tool is touched: they hand the
|
||||
//button back and are surrounded by the ring, as they always were.
|
||||
if (e->button() == Qt::RightButton && m_diagram
|
||||
&& m_diagram->cableToolIsRunning()) {
|
||||
m_swallow_native_menu = true;
|
||||
}
|
||||
if (e->button() == Qt::RightButton
|
||||
&& DiagramGestureOverlay::isEnabled()
|
||||
&& !m_diagram->focusItem())
|
||||
&& !m_diagram->focusItem()
|
||||
&& !m_diagram->cableToolIsRunning())
|
||||
{
|
||||
//The right button draws the quick command ring, or opens the
|
||||
//context menu on release. The cable tool alone does not let go
|
||||
//of it: while it runs no gesture is tracked at all, because
|
||||
//tracking one would both pop the ring up in the middle of the
|
||||
//drawing and take the tool away from the click meant to
|
||||
//cancel it (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).
|
||||
//Another 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;
|
||||
}
|
||||
@@ -1277,6 +1433,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();
|
||||
@@ -1995,12 +2160,67 @@ QList<QAction *> 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<CablePart *>(
|
||||
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("Remove this core from the line"));
|
||||
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();
|
||||
|
||||
@@ -2049,6 +2269,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;
|
||||
|
||||
@@ -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 handleImageFilesDrop(QDropEvent *);
|
||||
|
||||
@@ -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<CablePart *>(m_movement_driver))
|
||||
{
|
||||
for (QGraphicsItem *item : diagram->selectedItems())
|
||||
{
|
||||
auto *cable = qgraphicsitem_cast<CablePart *>(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<CablePart *>(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<QUndoCommand *> 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<QUndoCommand *> 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<QUndoCommand *> &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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,16 @@
|
||||
#ifndef ELEMENTS_MOVER_H
|
||||
#define ELEMENTS_MOVER_H
|
||||
|
||||
#include <QHash>
|
||||
#include <QPointF>
|
||||
#include <QPointer>
|
||||
#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<QUndoCommand *> &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<QStatusBar> 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<CablePart *> m_moved_cables;
|
||||
///Steps handed over by a line the user dragged himself, to
|
||||
///be pushed together with the rest of the movement
|
||||
QList<QUndoCommand *> 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<QGraphicsItem *, QPointF> m_start_positions;
|
||||
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -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"
|
||||
@@ -55,7 +59,7 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor(
|
||||
PropertiesEditorWidget *editor,
|
||||
QWidget *parent)
|
||||
{
|
||||
if (auto m = static_cast<ProjectDBModel *>(model))
|
||||
if (auto m = dynamic_cast<ProjectDBModel *>(model))
|
||||
{
|
||||
if (editor &&
|
||||
editor->metaObject()->className()
|
||||
@@ -66,6 +70,17 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor(
|
||||
}
|
||||
return new ProjectDBModelPropertiesWidget(m, parent);
|
||||
}
|
||||
if (auto m = dynamic_cast<CableListModel *>(model))
|
||||
{
|
||||
if (editor &&
|
||||
editor->metaObject()->className()
|
||||
== CableListModelPropertiesWidget::staticMetaObject.className())
|
||||
{
|
||||
static_cast<CableListModelPropertiesWidget *>(editor)->setModel(m);
|
||||
return editor;
|
||||
}
|
||||
return new CableListModelPropertiesWidget(m, parent);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -247,6 +262,27 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor(
|
||||
}
|
||||
return new GraphicsTablePropertiesEditor(table, parent);
|
||||
}
|
||||
case CablePart::Type: //1012
|
||||
{
|
||||
if (count_ > 1) {
|
||||
return nullptr;
|
||||
}
|
||||
auto part = static_cast<CablePart *>(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<CablePropertiesEditorWidget *>(editor)->setCable(cable, part);
|
||||
return editor;
|
||||
}
|
||||
return new CablePropertiesEditorWidget(cable, part, parent);
|
||||
}
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -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<AddTableDialog> d(
|
||||
new AddTableDialog(
|
||||
new CableQueryWidget(),
|
||||
diagram->views().first()));
|
||||
d->setWindowTitle(QObject::tr("Add a cable list"));
|
||||
d->setTableName(QObject::tr("Cable list"));
|
||||
|
||||
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<ElementQueryWidget *>(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<SummaryQueryWidget *>(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<CableQueryWidget *>(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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
#include "pdf_links.h"
|
||||
|
||||
#include "cable/cablepart.h"
|
||||
#include "diagram.h"
|
||||
#include "qetgraphicsitem/crossrefitem.h"
|
||||
#include "qetgraphicsitem/dynamicelementtextitem.h"
|
||||
@@ -53,10 +54,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(),
|
||||
@@ -66,20 +70,20 @@ 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())),
|
||||
@@ -94,15 +98,18 @@ void injectCrossRefLinks(QPdfEngine *engine, Diagram *diagram,
|
||||
struct Link { QRectF rect; QUrl url; };
|
||||
QList<Link> links;
|
||||
|
||||
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()))
|
||||
@@ -113,6 +120,12 @@ void injectCrossRefLinks(QPdfEngine *engine, Diagram *diagram,
|
||||
links.append({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 ---
|
||||
@@ -128,6 +141,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<CablePart*>(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<DynamicElementTextItem*>(item)) {
|
||||
Element *parent = deti->parentElement();
|
||||
|
||||
@@ -67,6 +67,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;
|
||||
@@ -97,6 +98,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<Qt::Alignment>();
|
||||
@@ -128,6 +130,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();
|
||||
|
||||
m_xref_pos = xrefPosFromKey(settings.value(prefix % "xrefpos").toString());
|
||||
|
||||
@@ -169,6 +172,14 @@ 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);
|
||||
//Only the cable cross-reference has a font of its own, and only
|
||||
//it ever sets one. Writing the attribute unconditionally put
|
||||
//font="" on every cross-reference type of every project the
|
||||
//program saved; a file which does not hold the attribute reads
|
||||
//back as no font, which is what all of them are.
|
||||
if (!m_font.isEmpty()) {
|
||||
xml_element.setAttribute("font", m_font);
|
||||
}
|
||||
foreach (QString key, m_prefix.keys()) {
|
||||
xml_element.setAttribute(key % "prefix", m_prefix.value(key));
|
||||
}
|
||||
@@ -197,6 +208,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"));
|
||||
}
|
||||
@@ -206,15 +218,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<QString, XRefProperties> XRefProperties::defaultProperties()
|
||||
{
|
||||
QHash <QString, XRefProperties> hash;
|
||||
QStringList keys;
|
||||
keys << "coil" << "protection" << "commutator" << "plc";
|
||||
keys << "coil" << "protection" << "commutator" << "plc" << "cable";
|
||||
|
||||
QSettings settings;
|
||||
|
||||
@@ -223,6 +237,13 @@ QHash<QString, XRefProperties> 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);
|
||||
}
|
||||
|
||||
@@ -241,7 +262,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
|
||||
|
||||
@@ -80,6 +80,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;}
|
||||
|
||||
@@ -103,6 +111,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;
|
||||
|
||||
@@ -1536,6 +1536,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
|
||||
|
||||
@@ -175,6 +175,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<QETDiagramEditor *> diagramEditors();
|
||||
|
||||
@@ -46,8 +46,11 @@
|
||||
#endif
|
||||
#include "diagramevent/diagrameventaddshape.h"
|
||||
#include "diagramevent/diagrameventaddpath.h"
|
||||
#include "diagramevent/diagrameventaddcable.h"
|
||||
#include "diagramevent/diagrameventfillet.h"
|
||||
#include "diagramevent/diagrameventaddtext.h"
|
||||
#include "cable/cablepart.h"
|
||||
#include "cable/editcablecommand.h"
|
||||
#include "diagramevent/diagrameventaddpaste.h"
|
||||
#include "diagramview.h"
|
||||
#include "elementspanelwidget.h"
|
||||
@@ -55,6 +58,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"
|
||||
@@ -65,6 +69,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"
|
||||
@@ -90,6 +95,7 @@
|
||||
#include "wiringlistexport.h"
|
||||
#include "ui/wiringlistdialog.h"
|
||||
#include "ui/terminalnumberingdialog.h"
|
||||
#include "ui/cablenumberingdialog.h"
|
||||
#include "toolbarsettings.h"
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
@@ -744,6 +750,27 @@ void QETDiagramEditor::setUpActions()
|
||||
ShortcutManager::instance().registerAction(m_project_terminalBloc, "diagrameditor.terminal_strip_plugin", tr("Diagram editors"), QKeySequence());
|
||||
connect(m_project_terminalBloc, &QAction::triggered, this, &QETDiagramEditor::generateTerminalBlock);
|
||||
|
||||
//Add a cable list
|
||||
m_add_cable_list = new QAction(QET::Icons::TableOfContent, tr("Add a cable list"), 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("Export the cable list to CSV format"), 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("Export the list of names of wires"), this);
|
||||
ShortcutManager::instance().registerAction(m_project_export_conductor_num, "diagrameditor.export_conductor_names", tr("Diagram editors"), QKeySequence());
|
||||
@@ -784,6 +811,14 @@ void QETDiagramEditor::setUpActions()
|
||||
ShortcutManager::instance().registerAction(m_terminal_numbering, "diagrameditor.terminal_numbering", tr("Diagram editors"), QKeySequence());
|
||||
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("Cable numbering"), this);
|
||||
m_cable_numbering->setStatusTip(
|
||||
tr("Shows the cable numbering rule of the project and "
|
||||
"renumbers its cables with it"));
|
||||
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("Reload element drawings"), this);
|
||||
ShortcutManager::instance().registerAction(m_reload_element_drawings, "diagrameditor.reload_element_drawings", tr("Diagram editors"), QKeySequence());
|
||||
@@ -1243,6 +1278,9 @@ void QETDiagramEditor::setUpActions()
|
||||
QAction *add_fillet = m_add_item_actions_group.addAction(QET::Icons::DrawFillet, tr("Add a fillet"));
|
||||
QAction *add_terminal_strip = m_add_item_actions_group.addAction(QET::Icons::TerminalStrip, tr("Add a terminal plan"));
|
||||
QAction *add_generic_device = m_add_item_actions_group.addAction(QET::Icons::GenericDevice, tr("Add a generic device…"));
|
||||
//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("Draw a cable"));
|
||||
|
||||
add_text ->setStatusTip(tr("Adds a text field to the current sheet"));
|
||||
add_image ->setStatusTip(tr("Add an image to the current sheet"));
|
||||
@@ -1258,6 +1296,7 @@ void QETDiagramEditor::setUpActions()
|
||||
add_fillet ->setStatusTip(tr("Rounds the corner between two lines of the current sheet"));
|
||||
add_terminal_strip->setStatusTip(tr("Adds a terminal plan to the current sheet"));
|
||||
add_generic_device->setStatusTip(tr("Makes a box symbol with terminals on any side and places it on the current sheet"));
|
||||
m_add_cable ->setStatusTip(tr("Draws the main line of a cable: every conductor crossed becomes one of its cores"));
|
||||
|
||||
add_text ->setData(QStringLiteral("text"));
|
||||
add_image ->setData(QStringLiteral("image"));
|
||||
@@ -1273,6 +1312,7 @@ void QETDiagramEditor::setUpActions()
|
||||
add_fillet ->setData(QStringLiteral("fillet"));
|
||||
add_terminal_strip->setData(QStringLiteral("terminal_strip"));
|
||||
add_generic_device->setData(QStringLiteral("generic_device"));
|
||||
m_add_cable ->setData(QStringLiteral("cable"));
|
||||
|
||||
add_text->setCheckable(true);
|
||||
add_line->setCheckable(true);
|
||||
@@ -1281,6 +1321,7 @@ void QETDiagramEditor::setUpActions()
|
||||
add_arc->setCheckable(true);
|
||||
add_polyline->setCheckable(true);
|
||||
add_path->setCheckable(true);
|
||||
m_add_cable->setCheckable(true);
|
||||
add_fillet->setCheckable(true);
|
||||
|
||||
connect(&m_add_item_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::addItemGroupTriggered);
|
||||
@@ -1442,6 +1483,10 @@ void QETDiagramEditor::setUpMenu()
|
||||
QMenu* menu_fichier = new QMenu(tr("&File"), this);
|
||||
QMenu* menu_edition = new QMenu(tr("&Edit"), this);
|
||||
QMenu* menu_project = new QMenu(tr("&Project"), this);
|
||||
//Numbering of everything the project numbers, kept together
|
||||
//and sitting right behind the project it belongs to
|
||||
QMenu* menu_numbering = new QMenu(tr("Numbering"), this);
|
||||
QMenu* menu_lists = new QMenu(tr("Lists"), this);
|
||||
QMenu* menu_affichage = new QMenu(tr("Displ&ay"), this);
|
||||
// QMenu *menu_outils = new QMenu(tr("O&utils"), this);
|
||||
windows_menu = new QMenu(tr("Wi&ndows"), this);
|
||||
@@ -1449,6 +1494,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);
|
||||
|
||||
@@ -1556,15 +1603,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
|
||||
m_scripts_menu = menu_project -> addMenu(tr("Scripts"));
|
||||
@@ -1578,6 +1621,27 @@ void QETDiagramEditor::setUpMenu()
|
||||
menu_project -> addAction(m_export_project_db);
|
||||
#endif
|
||||
|
||||
// menu Numbering
|
||||
//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("Display or hide the main toolbar"));
|
||||
view_tool_bar -> toggleViewAction() -> setStatusTip(tr("Display or hide the Display toolbar"));
|
||||
diagram_tool_bar -> toggleViewAction() -> setStatusTip(tr("Display or hide the Diagram toolbar"));
|
||||
@@ -2242,6 +2306,90 @@ 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;
|
||||
QETProject *project = diagram->project();
|
||||
if (project->hasCableAutoNum()) return true;
|
||||
|
||||
//A read-only project is left alone without a question: there is
|
||||
//nowhere to define a rule in it, so asking whether he wants to
|
||||
//would be a way to nowhere. Its cables are drawn called W, the
|
||||
//same as any project without a rule.
|
||||
if (project->isReadOnly()) return true;
|
||||
|
||||
//Whether he has once answered no in THIS project. The answer is
|
||||
//kept with the project rather than in the program settings, so
|
||||
//one project being answered with no does not stop every other
|
||||
//project from being asked.
|
||||
if (!project->cableAskNumbering()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QMessageBox box(QMessageBox::Question,
|
||||
tr("Automatic numbering", "window title"),
|
||||
tr("No automatic cable numbering is defined.",
|
||||
"shown when a project has no cable numbering rule"),
|
||||
QMessageBox::NoButton,
|
||||
this);
|
||||
box.setInformativeText(tr(
|
||||
"Do you want to define one now?\n\n"
|
||||
"You can also number the cables automatically later: the rule "
|
||||
"is set at any time in the project properties, on the "
|
||||
"\"Auto Numbering\" page.",
|
||||
"ask whether a cable numbering rule should be defined now"));
|
||||
QPushButton *define_button = box.addButton(tr("Yes", "yes"), QMessageBox::YesRole);
|
||||
QPushButton *later_button = box.addButton(tr("No", "no"), QMessageBox::NoRole);
|
||||
box.addButton(tr("Cancel", "cancel"), QMessageBox::RejectRole);
|
||||
box.setDefaultButton(later_button);
|
||||
box.exec();
|
||||
|
||||
if (box.clickedButton() == later_button) {
|
||||
//With this project only: he has answered once here and
|
||||
//should not be asked again every time he picks the tool
|
||||
//up in this project -- another project is free to ask.
|
||||
project->setCableAskNumbering(false);
|
||||
project->setModified(true);
|
||||
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,
|
||||
@@ -2294,6 +2442,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 == "fillet")
|
||||
diagram_event = new DiagramEventFillet (d);
|
||||
else if (value == "image")
|
||||
@@ -2388,8 +2552,30 @@ void QETDiagramEditor::selectionGroupTriggered(QAction *action)
|
||||
"Unbridge and/or remove the levels from the affected terminals so that they can be "
|
||||
"deleted"));
|
||||
} 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<CablePart *> cable_parts;
|
||||
for (QGraphicsItem *item : diagram->selectedItems()) {
|
||||
if (auto *part = qgraphicsitem_cast<CablePart *>(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();
|
||||
}
|
||||
}
|
||||
@@ -2593,6 +2779,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);
|
||||
@@ -2686,8 +2875,20 @@ void QETDiagramEditor::slot_updateComplexActions()
|
||||
m_mirror_vertical->setEnabled(!ro && selected_elements_count);
|
||||
|
||||
//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);
|
||||
@@ -2743,7 +2944,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)
|
||||
{
|
||||
@@ -2769,6 +2971,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("Edit the cable",
|
||||
"edit cable"));
|
||||
m_edit_selection -> setIcon(QET::Icons::Cable);
|
||||
}
|
||||
//edit conductor
|
||||
else if (selected_conductors_count)
|
||||
{
|
||||
@@ -4011,6 +4220,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
|
||||
|
||||
@@ -102,6 +102,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;
|
||||
@@ -158,6 +170,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();
|
||||
@@ -241,6 +254,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
|
||||
@@ -258,6 +272,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
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "../../shownkinds.h"
|
||||
#include "../../qetproject.h"
|
||||
#include "../../QPropertyUndoCommand/qpropertyundocommand.h"
|
||||
#include "../../cablelist/cablelistmodel.h"
|
||||
#include "../../createdxf.h"
|
||||
#include "../../diagram.h"
|
||||
#include "../../elementprovider.h"
|
||||
@@ -450,7 +451,13 @@ void QetGraphicsTableItem::setPreviousTable(QetGraphicsTableItem *table)
|
||||
}
|
||||
else if (old_previous_table->model()) //Copie the model of old previous table
|
||||
{
|
||||
setModel(new ProjectDBModel(*static_cast<ProjectDBModel *>(old_previous_table->model())));
|
||||
auto *old_model = old_previous_table ? old_previous_table->model() : nullptr;
|
||||
if (auto *db_model = dynamic_cast<ProjectDBModel *>(old_model)) {
|
||||
setModel(new ProjectDBModel(*db_model));
|
||||
}
|
||||
else if (auto *cable_model = dynamic_cast<CableListModel *>(old_model)) {
|
||||
setModel(new CableListModel(*cable_model));
|
||||
}
|
||||
}
|
||||
|
||||
if (old_previous_table &&
|
||||
@@ -619,8 +626,12 @@ QDomElement QetGraphicsTableItem::toXml(QDomDocument &dom_document) const
|
||||
{
|
||||
//Add model
|
||||
auto dom_model = dom_document.createElement("model");
|
||||
auto project_db_model = static_cast<ProjectDBModel *>(m_model.data());
|
||||
dom_model.appendChild(project_db_model->toXml(dom_document));
|
||||
if (auto project_db_model = dynamic_cast<ProjectDBModel *>(m_model.data())) {
|
||||
dom_model.appendChild(project_db_model->toXml(dom_document));
|
||||
}
|
||||
else if (auto cable_model = dynamic_cast<CableListModel *>(m_model.data())) {
|
||||
dom_model.appendChild(cable_model->toXml(dom_document));
|
||||
}
|
||||
dom_table.appendChild(dom_model);
|
||||
|
||||
}
|
||||
@@ -672,16 +683,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
|
||||
|
||||
@@ -1845,6 +1845,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
|
||||
*/
|
||||
|
||||
@@ -125,6 +125,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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 "autoNum/elementautonumschemecommand.h"
|
||||
#include "diagram.h"
|
||||
#include "qetgraphicsitem/element.h"
|
||||
@@ -117,6 +120,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,6 +840,16 @@ QHash <QString, NumerotationContext> 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 <QString, NumerotationContext> QETProject::cableAutoNum() const
|
||||
{
|
||||
return m_cable_autonum;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::elementAutoNumFormula
|
||||
@param key : autonum title
|
||||
@@ -866,6 +890,109 @@ 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::cableAskNumbering
|
||||
@return true when the cable tool may still ask this project whether
|
||||
a numbering rule should be defined. The answer to that question is
|
||||
kept with the project rather than in the program settings, so that
|
||||
one project being answered with no does not stop every other
|
||||
project from asking.
|
||||
*/
|
||||
bool QETProject::cableAskNumbering() const
|
||||
{
|
||||
return m_cable_ask_numbering;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::setCableAskNumbering
|
||||
Take the answer he gave to the numbering question down with the
|
||||
project. False means he has said no once here and is not to be
|
||||
asked again in this project; taking the rule away puts it back to
|
||||
true so the question comes again.
|
||||
@param ask
|
||||
*/
|
||||
void QETProject::setCableAskNumbering(bool ask)
|
||||
{
|
||||
m_cable_ask_numbering = ask;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::renumberElementsBySchemeTitle
|
||||
Renumber existing elements by element autonumbering scheme title.
|
||||
@@ -1187,6 +1314,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
|
||||
@@ -1217,6 +1370,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.
|
||||
@@ -1252,6 +1420,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
|
||||
@@ -1596,6 +1776,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));
|
||||
|
||||
@@ -2109,10 +2301,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);
|
||||
@@ -2120,11 +2325,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 << ")";
|
||||
|
||||
@@ -2318,7 +2524,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())
|
||||
{
|
||||
@@ -2341,6 +2547,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;
|
||||
}
|
||||
@@ -2409,6 +2617,37 @@ void QETProject::readDefaultPropertiesXml(QDomDocument &xml_project)
|
||||
m_current_element_autonum = current_title;
|
||||
}
|
||||
}
|
||||
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");
|
||||
//Whether the numbering question was already answered with
|
||||
//no in this project. A file which does not hold the answer
|
||||
//yet asks again rather than staying silent: it is his own
|
||||
//answer per project which is kept, not a default saying no.
|
||||
m_cable_ask_numbering = cable_autonums.attribute(
|
||||
QStringLiteral("ask_numbering_rule"), QStringLiteral("true"))
|
||||
!= QLatin1String("false");
|
||||
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();
|
||||
|
||||
@@ -2443,6 +2682,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.
|
||||
*/
|
||||
@@ -2607,6 +2895,52 @@ 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.
|
||||
//
|
||||
//Only when it holds something, though. An element which is not there
|
||||
//reads back as exactly the defaults a project which never touched
|
||||
//cable numbering already has, so writing it every time put
|
||||
//<cable_autonums> into every file the program saved -- into the
|
||||
//example projects and into projects without a single cable in them.
|
||||
//
|
||||
//That is not the same as writing it only for projects with cables,
|
||||
//which would cost data: a rule named before the first cable is
|
||||
//drawn, an axis chosen before it, and the numbering question
|
||||
//answered with no are answers only this element can carry.
|
||||
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");
|
||||
//Only the no is written down: a project which has not answered
|
||||
//the numbering question yet asks again, so silence in the file
|
||||
//means asking rather than holding back.
|
||||
if (!m_cable_ask_numbering) {
|
||||
cable_autonums.setAttribute("ask_numbering_rule", "false");
|
||||
}
|
||||
int written_cable_rules = 0;
|
||||
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);
|
||||
++written_cable_rules;
|
||||
}
|
||||
}
|
||||
//Nothing to keep when the project never named a rule, never
|
||||
//chose an axis other than the default and has not answered the
|
||||
//numbering question with no -- then this element would say
|
||||
//nothing the reader does not assume anyway.
|
||||
if (!m_current_cable_autonum.isEmpty()
|
||||
|| !m_cable_ask_numbering
|
||||
|| !m_cable_axis_x_first
|
||||
|| written_cable_rules > 0) {
|
||||
xml_element.appendChild(cable_autonums);
|
||||
}
|
||||
|
||||
// Export default guides
|
||||
QDomElement guides_elmt = xml_document.createElement("guides");
|
||||
for (const auto &g : m_default_guides) {
|
||||
@@ -2792,6 +3126,86 @@ bool QETProject::removeTerminalStrip(TerminalStrip *strip) {
|
||||
return m_terminal_strip_vector.removeOne(strip);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief QETProject::cables
|
||||
* @return every cable of this project
|
||||
*/
|
||||
QVector<Cable *> 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.
|
||||
|
||||
@@ -56,6 +56,7 @@ class QUndoStack;
|
||||
class XmlElementCollection;
|
||||
class QTimer;
|
||||
class TerminalStrip;
|
||||
class Cable;
|
||||
|
||||
|
||||
#include <QColor>
|
||||
@@ -182,6 +183,49 @@ 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 <QString, NumerotationContext> 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);
|
||||
/**
|
||||
Whether the cable tool asks this project whether a
|
||||
numbering rule should be defined, when he is picked up
|
||||
and the project has none. Saying no in that question is
|
||||
remembered with the project rather than in the program
|
||||
settings, so one project saying no does not silence
|
||||
every other one -- each project asks again until it
|
||||
has answered for itself, and taking the rule away
|
||||
brings the question back (see the cable tab of the
|
||||
project properties and the numbering window).
|
||||
*/
|
||||
bool cableAskNumbering() const;
|
||||
void setCableAskNumbering(bool ask);
|
||||
/**
|
||||
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();
|
||||
//Identity of the element numbering schemes. The title is the
|
||||
//name shown to the user and the lookup key of the API; the uuid
|
||||
//is what an element's ELMT_FORMULA_ID refers to, so a scheme can
|
||||
@@ -262,6 +306,12 @@ class QETProject : public QObject
|
||||
bool addTerminalStrip(TerminalStrip *strip);
|
||||
bool removeTerminalStrip(TerminalStrip *strip);
|
||||
|
||||
QVector<Cable *> 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 *);
|
||||
@@ -295,6 +345,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();
|
||||
@@ -329,6 +385,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);
|
||||
void readSymbolTextsXml(QDomDocument &xml_project);
|
||||
@@ -398,6 +455,16 @@ class QETProject : public QObject
|
||||
/// Title -> uuid of each element numbering scheme
|
||||
QHash <QString, QUuid> m_element_autonum_id;
|
||||
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 <QString, NumerotationContext> m_cable_autonum;
|
||||
QString m_current_cable_autonum;
|
||||
///< which axis the cables are numbered along (see cableXAxisFirst)
|
||||
bool m_cable_axis_x_first = true;
|
||||
///< whether the cable tool still asks about a numbering rule
|
||||
///< for this project (see cableAskNumbering)
|
||||
bool m_cable_ask_numbering = true;
|
||||
/// True when the loaded file had element numbering schemes
|
||||
/// saved without an id (written before ids existed)
|
||||
bool m_legacy_element_autonums = false;
|
||||
@@ -423,6 +490,7 @@ class QETProject : public QObject
|
||||
QSet<QUuid> m_saved_item_uuids; //symbol and wire uuids the file carries, see derivedItemUuid()
|
||||
projectDataBase m_data_base;
|
||||
QVector<TerminalStrip *> m_terminal_strip_vector;
|
||||
QVector<Cable *> m_cables;
|
||||
|
||||
ProjectPropertiesHandler m_project_properties_handler;
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
@@ -336,6 +339,31 @@ bool QetScriptApi::exportBom(const QString &output, bool noSlaves,
|
||||
return runFlag(QStringLiteral("--export-bom"), args);
|
||||
}
|
||||
|
||||
/**
|
||||
@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});
|
||||
@@ -2962,12 +2990,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
|
||||
@@ -2991,7 +3023,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
|
||||
@@ -3000,6 +3032,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));
|
||||
@@ -3015,8 +3050,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -386,6 +391,7 @@ class QetScriptApi : public QObject
|
||||
Q_INVOKABLE bool exportWires(const QString &output);
|
||||
Q_INVOKABLE bool exportBom(const QString &output, bool noSlaves = false,
|
||||
bool noJunctions = false);
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "cablenumberingdialog.h"
|
||||
|
||||
#include "../autoNum/ui/selectautonumw.h"
|
||||
#include "../cable/cablerenumber.h"
|
||||
#include "../cable/editcablecommand.h"
|
||||
#include "../qetproject.h"
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QRadioButton>
|
||||
#include <QUndoStack>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
/**
|
||||
@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("Cable numbering"));
|
||||
//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(
|
||||
"This window shows the automatic numbering rule for the cables "
|
||||
"of this project and edits it. A rule changed here is the one "
|
||||
"the project keeps: it is written into the project when you "
|
||||
"apply it."), 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("Axis priority"), this);
|
||||
auto *axis_layout = new QHBoxLayout(axis_group);
|
||||
m_axis_x = new QRadioButton(tr("Priority to the X axis (horizontal)"), axis_group);
|
||||
m_axis_y = new QRadioButton(tr("Priority to the Y axis (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(
|
||||
"The chosen axis decides the order in which the cables are "
|
||||
"renumbered: X numbers them from left to right, Y numbers them "
|
||||
"from top to bottom."), this);
|
||||
axis_note->setWordWrap(true);
|
||||
layout->addWidget(axis_note);
|
||||
|
||||
auto *actions = new QHBoxLayout();
|
||||
m_renumber_pb = new QPushButton(tr("Renumber all cables"), this);
|
||||
m_renumber_pb->setToolTip(tr(
|
||||
"Sets the number of each cable of the project again from the "
|
||||
"rule, starting at the beginning: a number left missing after "
|
||||
"a cable is deleted is filled in this way."));
|
||||
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("Cancel"));
|
||||
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("Cable numbering"),
|
||||
tr("The rule is not complete yet: "
|
||||
"there is still an empty field in it."));
|
||||
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 in this project any more.
|
||||
m_rule->setRuleRemovable(false);
|
||||
m_project->setCableAskNumbering(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("Cable numbering"),
|
||||
tr("%n cable(s) of this project were entered by "
|
||||
"hand.\n\n"
|
||||
"Do you want to renumber them too?", "", count),
|
||||
QMessageBox::NoButton, this);
|
||||
QPushButton *take = box.addButton(
|
||||
tr("Yes", "Les noms saisis à la main sont remplacés eux aussi."),
|
||||
QMessageBox::YesRole);
|
||||
QPushButton *leave = box.addButton(
|
||||
tr("No", "Les noms saisis à la main sont laissés tels quels."),
|
||||
QMessageBox::NoRole);
|
||||
QPushButton *cancel = box.addButton(
|
||||
tr("Cancel", "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("Cable numbering"),
|
||||
tr("The rule is not complete yet."));
|
||||
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("Cable numbering"), 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("Cable numbering"), plan.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (plan.cables.isEmpty())
|
||||
{
|
||||
QMessageBox::information(this, tr("Cable numbering"),
|
||||
tr("No cable to renumber in this project."));
|
||||
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("Cable numbering"),
|
||||
tr("All the cables already carry the number "
|
||||
"that the rule gives them."));
|
||||
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();
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CABLENUMBERINGDIALOG_H
|
||||
#define CABLENUMBERINGDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class QDialogButtonBox;
|
||||
class QETProject;
|
||||
class QPushButton;
|
||||
class QRadioButton;
|
||||
class SelectAutonumW;
|
||||
|
||||
/**
|
||||
@brief The window behind the "Cable numbering" 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
|
||||
@@ -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);
|
||||
@@ -169,6 +176,7 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
|
||||
autonum_inner_tab->addTab(m_autonum_conductor.widget, tr("Conductors"));
|
||||
autonum_inner_tab->addTab(m_autonum_element.widget, tr("Elements"));
|
||||
autonum_inner_tab->addTab(m_autonum_folio.widget, tr("Sheets"));
|
||||
autonum_inner_tab->addTab(m_autonum_cable.widget, tr("Cables"));
|
||||
autonum_layout->addWidget(autonum_inner_tab);
|
||||
tab_widget -> addTab (autonum_widget, tr("Auto Numbering"));
|
||||
}
|
||||
@@ -280,6 +288,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();
|
||||
}
|
||||
}
|
||||
@@ -393,6 +415,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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -402,15 +430,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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -419,17 +457,35 @@ 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: every project of its own asks once more whether
|
||||
//one should be defined -- that answer is kept per project, so
|
||||
//there is nothing to take back here.
|
||||
if (!tab.fixed_name.isEmpty()) {
|
||||
tab.widget->setRuleRemovable(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -439,12 +495,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);
|
||||
@@ -462,7 +527,7 @@ bool NewDiagramPage::eventFilter(QObject *obj, QEvent *event)
|
||||
auto *ke = static_cast<QKeyEvent *>(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
|
||||
|
||||
@@ -82,10 +82,16 @@ public slots:
|
||||
SelectAutonumW *widget = nullptr;
|
||||
QHash<QString, NumerotationContext> 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);
|
||||
|
||||
@@ -23,6 +23,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"
|
||||
@@ -33,6 +34,7 @@
|
||||
#include "../../ElementsCollection/qetlabelsfile.h"
|
||||
#include "../prefixconfigurationdialog.h"
|
||||
#include "../nokde/kcolorbutton.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
@@ -40,6 +42,35 @@
|
||||
#include <QMessageBox>
|
||||
#include <QSettings>
|
||||
|
||||
#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",
|
||||
"Bold italic")
|
||||
: QCoreApplication::translate("GeneralConfigurationPage", "Bold");
|
||||
} else if (font.italic()) {
|
||||
style = QCoreApplication::translate("GeneralConfigurationPage", "Italic");
|
||||
} 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
|
||||
@@ -192,6 +223,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());
|
||||
@@ -271,6 +313,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("Not set (default: %1)",
|
||||
"hint shown in the cable type file field when no file is configured yet")
|
||||
.arg(CableTypeList::defaultPath()));
|
||||
}
|
||||
|
||||
fillLang();
|
||||
}
|
||||
|
||||
@@ -483,6 +536,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -601,6 +678,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("Cable text font"));
|
||||
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("Core colors font"));
|
||||
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()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -838,6 +954,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("Select the cable types file"),
|
||||
start_dir,
|
||||
tr("CSV files (*.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("Create the cable types file"),
|
||||
path,
|
||||
tr("CSV files (*.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("Cannot be created"),
|
||||
tr("Cannot create the file:\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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -600,6 +600,47 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<widget class="QLabel" name="m_cable_type_list_label">
|
||||
<property name="text">
|
||||
<string>Cable type list file</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="1">
|
||||
<layout class="QHBoxLayout" name="m_cable_type_list_layout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="m_cable_type_list_path_le">
|
||||
<property name="toolTip">
|
||||
<string extracomment="tooltip of the cable type file path field">Directory of the cable types used to define a cable (number of cores, colors, cross-section)</string>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string extracomment="hint shown when no cable type file is configured yet">Not set</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="m_cable_type_list_browse_pb">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="m_cable_type_list_create_pb">
|
||||
<property name="toolTip">
|
||||
<string extracomment="tooltip of the button creating the cable type file with its header line">Create the cable type list file with its header line</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Create...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -864,6 +905,113 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_cable_textes">
|
||||
<property name="title">
|
||||
<string>Cable texts</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_cable_textes">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_cable_textes">
|
||||
<property name="text">
|
||||
<string>Designation, type, plant, location, length :</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<spacer name="horizontalSpacer_cable_textes">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QPushButton" name="m_cable_text_font_pb">
|
||||
<property name="toolTip">
|
||||
<string>Font of the texts written along the cable line; a cable which has its own keeps it.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_cable_core_textes">
|
||||
<property name="text">
|
||||
<string>Core colors (all of them) :</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<spacer name="horizontalSpacer_cable_core_textes">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="m_cable_core_font_pb">
|
||||
<property name="toolTip">
|
||||
<string>Font of the core colors written along the line; a cable which has its own keeps it.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_cable_align">
|
||||
<property name="text">
|
||||
<string>Text alignment :</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1" colspan="2">
|
||||
<widget class="QComboBox" name="m_cable_align_cb">
|
||||
<property name="toolTip">
|
||||
<string>Where the texts of a cable stop relative to the start of its line; a cable which has its own keeps it.</string>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Right aligned</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Centred</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Left aligned</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_4">
|
||||
<property name="orientation">
|
||||
|
||||