Merge master (cce75f430) into feat/qetz-container
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGxcrnd2rTYfHjwTdP79zo
@@ -352,6 +352,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)
|
||||
|
||||
@@ -395,6 +395,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
|
||||
@@ -589,6 +591,7 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/qetgraphicsitem/crossrefitem.h
|
||||
${QET_DIR}/sources/qetgraphicsitem/diagramimageitem.cpp
|
||||
${QET_DIR}/sources/qetgraphicsitem/diagramimageitem.h
|
||||
${QET_DIR}/sources/qetgraphicsitem/cropgeometry.h
|
||||
${QET_DIR}/sources/qetgraphicsitem/diagramtextitem.cpp
|
||||
${QET_DIR}/sources/qetgraphicsitem/diagramtextitem.h
|
||||
${QET_DIR}/sources/qetgraphicsitem/dynamicelementtextitem.cpp
|
||||
@@ -819,6 +822,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
|
||||
@@ -944,6 +949,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 |
@@ -25,6 +25,8 @@
|
||||
#include "../terminalstrip.h"
|
||||
#include "../UndoCommand/addterminaltostripcommand.h"
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
/**
|
||||
* @brief FreeTerminalEditor::FreeTerminalEditor
|
||||
* @param project
|
||||
@@ -39,8 +41,23 @@ FreeTerminalEditor::FreeTerminalEditor(QETProject *project, QWidget *parent) :
|
||||
ui->m_table_view->setItemDelegate(new FreeTerminalModelDelegate(ui->m_table_view));
|
||||
|
||||
m_model = new FreeTerminalModel(m_project, this);
|
||||
ui->m_table_view->setModel(m_model);
|
||||
ui->m_table_view->setCurrentIndex(m_model->index(0,0));
|
||||
|
||||
//The view works on a filtered proxy of the model. The filter is a plain,
|
||||
//case insensitive "contains" on the label, so typing Z1 shows Z1:1, Z1:2...
|
||||
//Not dynamic : an edited label must not vanish from the table while it is typed.
|
||||
m_proxy_model = new QSortFilterProxyModel(this);
|
||||
m_proxy_model->setSourceModel(m_model);
|
||||
m_proxy_model->setFilterKeyColumn(FreeTerminalModel::Label);
|
||||
m_proxy_model->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
m_proxy_model->setDynamicSortFilter(false);
|
||||
ui->m_table_view->setModel(m_proxy_model);
|
||||
ui->m_table_view->setCurrentIndex(m_proxy_model->index(0,0));
|
||||
|
||||
connect(ui->m_filter_le, &QLineEdit::textChanged, this, [this](const QString &text)
|
||||
{
|
||||
m_proxy_model->setFilterFixedString(text);
|
||||
selectionChanged();
|
||||
});
|
||||
//Selected rows can be dragged onto a strip of the terminal strip tree
|
||||
ui->m_table_view->setDragEnabled(true);
|
||||
ui->m_table_view->setDragDropMode(QAbstractItemView::DragOnly);
|
||||
@@ -55,8 +72,9 @@ FreeTerminalEditor::FreeTerminalEditor(QETProject *project, QWidget *parent) :
|
||||
connect(ui->m_table_view->selectionModel(), &QItemSelectionModel::selectionChanged,
|
||||
this, &FreeTerminalEditor::selectionChanged);
|
||||
|
||||
connect(ui->m_table_view, &QAbstractItemView::doubleClicked, this, [this](const QModelIndex &index)
|
||||
connect(ui->m_table_view, &QAbstractItemView::doubleClicked, this, [this](const QModelIndex &proxy_index)
|
||||
{
|
||||
const auto index = m_proxy_model->mapToSource(proxy_index);
|
||||
if (m_model->columnTypeForIndex(index) == FreeTerminalModel::XRef)
|
||||
{
|
||||
auto mrtd = m_model->dataAtRow(index.row());
|
||||
@@ -176,7 +194,7 @@ void FreeTerminalEditor::on_m_type_cb_activated(int index)
|
||||
|
||||
for (auto model_index : index_list)
|
||||
{
|
||||
auto type_index = m_model->index(model_index.row(), FreeTerminalModel::Type, model_index.parent());
|
||||
auto type_index = m_model->index(m_proxy_model->mapToSource(model_index).row(), FreeTerminalModel::Type);
|
||||
if (type_index.isValid())
|
||||
{
|
||||
ElementData::TerminalType override_type;
|
||||
@@ -219,7 +237,7 @@ void FreeTerminalEditor::on_m_function_cb_activated(int index)
|
||||
|
||||
for (auto model_index : index_list)
|
||||
{
|
||||
auto function_index = m_model->index(model_index.row(), FreeTerminalModel::Function, model_index.parent());
|
||||
auto function_index = m_model->index(m_proxy_model->mapToSource(model_index).row(), FreeTerminalModel::Function);
|
||||
if (function_index.isValid())
|
||||
{
|
||||
ElementData::TerminalFunction override_function;
|
||||
@@ -248,7 +266,7 @@ void FreeTerminalEditor::on_m_led_cb_activated(int index)
|
||||
|
||||
for (auto model_index : index_list)
|
||||
{
|
||||
auto led_index = m_model->index(model_index.row(), FreeTerminalModel::Led, model_index.parent());
|
||||
auto led_index = m_model->index(m_proxy_model->mapToSource(model_index).row(), FreeTerminalModel::Led);
|
||||
|
||||
if (led_index.isValid()) {
|
||||
m_model->setData(led_index,
|
||||
@@ -262,7 +280,10 @@ void FreeTerminalEditor::on_m_led_cb_activated(int index)
|
||||
void FreeTerminalEditor::on_m_move_pb_clicked()
|
||||
{
|
||||
//Get the selected real terminal
|
||||
const auto index_list = ui->m_table_view->selectionModel()->selectedIndexes();
|
||||
QModelIndexList index_list;
|
||||
for (const auto &proxy_index : ui->m_table_view->selectionModel()->selectedIndexes()) {
|
||||
index_list.append(m_proxy_model->mapToSource(proxy_index));
|
||||
}
|
||||
const auto real_t_vector = m_model->realTerminalForIndex(index_list);
|
||||
if (real_t_vector.isEmpty()) {
|
||||
return;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
class RealTerminal;
|
||||
class FreeTerminalModel;
|
||||
class QTableView;
|
||||
class QSortFilterProxyModel;
|
||||
|
||||
namespace Ui {
|
||||
class FreeTerminalEditor;
|
||||
@@ -57,5 +58,6 @@ class FreeTerminalEditor : public QWidget
|
||||
Ui::FreeTerminalEditor *ui;
|
||||
QPointer <QETProject> m_project;
|
||||
FreeTerminalModel *m_model {nullptr};
|
||||
QSortFilterProxyModel *m_proxy_model {nullptr};
|
||||
};
|
||||
#endif // FREETERMINALEDITOR_H
|
||||
|
||||
@@ -14,14 +14,24 @@
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLineEdit" name="m_filter_le">
|
||||
<property name="placeholderText">
|
||||
<string>Filter by label (e.g. Z1)</string>
|
||||
</property>
|
||||
<property name="clearButtonEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLabel" name="m_move_label">
|
||||
<property name="text">
|
||||
<string>Move to :</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<item row="1" column="2">
|
||||
<widget class="QComboBox" name="m_move_in_cb">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
@@ -34,7 +44,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="2">
|
||||
<item row="6" column="2">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@@ -47,35 +57,35 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="0" column="0" rowspan="6">
|
||||
<item row="1" column="0" rowspan="6">
|
||||
<widget class="QTableView" name="m_table_view">
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="3" column="1">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Type :</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<item row="4" column="1">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Function :</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<item row="5" column="1">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>LED :</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<item row="1" column="3">
|
||||
<widget class="QPushButton" name="m_move_pb">
|
||||
<property name="toolTip">
|
||||
<string>Apply displacement</string>
|
||||
@@ -88,14 +98,14 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1" colspan="3">
|
||||
<item row="2" column="1" colspan="3">
|
||||
<widget class="Line" name="line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2" colspan="2">
|
||||
<item row="3" column="2" colspan="2">
|
||||
<widget class="QComboBox" name="m_type_cb">
|
||||
<item>
|
||||
<property name="text">
|
||||
@@ -149,7 +159,7 @@
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2" colspan="2">
|
||||
<item row="4" column="2" colspan="2">
|
||||
<widget class="QComboBox" name="m_function_cb">
|
||||
<item>
|
||||
<property name="text">
|
||||
@@ -168,7 +178,7 @@
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2" colspan="2">
|
||||
<item row="5" column="2" colspan="2">
|
||||
<widget class="QComboBox" name="m_led_cb">
|
||||
<item>
|
||||
<property name="text">
|
||||
|
||||
@@ -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;
|
||||
|
||||