mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-24 18:54:20 +02:00
de9b3eae06
Reviving #659, closed 2026-09-10 purely to clear a review backlog (#630), not on merit. Rebuilt fresh against current master rather than merged from the old branch (elementspanelwidget.cpp had drifted enough that a textual merge risked silently losing content, as it did earlier in this same session for a different revival). Builds discussion #607. Cutting/copying a linked group of elements -- a relay coil with its contacts, a PLC master with its slave I/O elements -- dropped the master/slave link entirely. Traced end to end: Element::toXml() writes each partner's uuid into <link_uuid>, Element::fromXml() reads it back into a deferred, unresolved buffer (tmp_uuids_link), and the only code that ever resolves that buffer is initLink(QETProject *) -- called only from Diagram::refreshContents(), itself only called from full project load and macro-block insertion. Neither DiagramView::paste() nor ElementsPanelWidget::duplicateDiagram() ever call it, so tmp_uuids_link is populated correctly and never resolved: the link is silently dropped. duplicateDiagram() already knew this and worked around it by calling clearPendingLinks() -- correct to not link back to a stale source, but it meant folio duplication never preserved a link either. Added Element::initLink(const QList<Element *> &candidates) -- resolves against a caller-supplied list instead of a project-wide search. The scoping is the subtle part: right after the XML round-trip and before uuids are renewed, a pasted/duplicated element's tmp_uuids_link still holds its source's original partner uuid, which at that exact moment still equals the not-yet-renewed uuid of that partner's own copy, if it was carried along in the same batch. Resolving only within the batch is what stops a linked pair pasted together from matching an original element left elsewhere that happens to still carry that same soon-to-be-replaced uuid. If only one half of a linked group is in the batch, its entry finds no match and is dropped -- the same "leave it unlinked" outcome as before. Wired into PasteDiagramCommand::redo(), before the existing newUuid() loop and gated by the same first_redo flag. Wired into duplicateDiagram() the same way, replacing its clearPendingLinks() call (initLink() clears tmp_uuids_link internally, matched or not). Verified live -- the original PR's own test plan left both of these unchecked, so this closes that gap rather than repeating it. Built a project with a linked PLC master/slave pair (qet-mcp's link_elements), then drove the real interaction under Xvfb: Ctrl+A, Ctrl+C, Ctrl+V: originals 95ad58fc <-> e728632c (unchanged) pasted 513e6bf8 <-> 29aa60b4 (linked to each other) Right-click folio > "Copier et coller": originals 95ad58fc <-> e728632c (unchanged) duplicated 0a33ccb4 <-> 3264fe66 (linked to each other) Neither copy links back to an original or comes in unlinked. Qt 6.10.2, ctest 13/13. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
825 lines
32 KiB
C++
825 lines
32 KiB
C++
/*
|
|
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 "elementspanelwidget.h"
|
|
#include "diagram.h"
|
|
#include "qetgraphicsitem/conductor.h"
|
|
#include "editor/ui/qetelementeditor.h"
|
|
#include "elementscategoryeditor.h"
|
|
#include "qetapp.h"
|
|
#include "qeticons.h"
|
|
#include "qetproject.h"
|
|
#include "shortcutmanager.h"
|
|
#include "titleblock/templatedeleter.h"
|
|
#include <QFileInfo>
|
|
#include <QMessageBox>
|
|
#include "qetgraphicsitem/element.h"
|
|
#include "qetgraphicsitem/dynamicelementtextitem.h"
|
|
#include "qetinformation.h"
|
|
|
|
/*
|
|
When the ENABLE_PANEL_WIDGET_DND_CHECKS flag is set, the panel
|
|
performs checks during drag'n drop of items and categories.
|
|
For example, it checks that a target category is writable
|
|
before authorizing the drop of an element.
|
|
Removing this flag allows you to test the behavior of management functions
|
|
items (copy, move, etc.).
|
|
|
|
Lorsque le flag ENABLE_PANEL_WIDGET_DND_CHECKS est defini, le panel
|
|
effectue des verifications lors des drag'n drop d'elements et categories.
|
|
Par exemple, il verifie qu'une categorie cible est accessible en ecriture
|
|
avant d'y autoriser le drop d'un element.
|
|
Supprimer ce flag permet de tester le comportement des fonctions de gestion
|
|
des items (copy, move, etc.).
|
|
*/
|
|
#define ENABLE_PANEL_WIDGET_DND_CHECKS
|
|
|
|
/**
|
|
Constructeur
|
|
@param parent Le QWidget parent de ce widget
|
|
*/
|
|
ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
|
|
// initalise le panel d'elements
|
|
elements_panel = new ElementsPanel(this);
|
|
|
|
// initialise les actions
|
|
open_directory = new QAction(QET::Icons::FolderOpen, tr("Ouvrir le dossier correspondant"), this);
|
|
copy_path = new QAction(QET::Icons::IC_CopyFile, tr("Copier le chemin"), this);
|
|
prj_activate = new QAction(QET::Icons::ProjectFile, tr("Basculer vers ce projet"), this);
|
|
prj_close = new QAction(QET::Icons::DocumentClose, tr("Fermer ce projet"), this);
|
|
prj_edit_prop = new QAction(QET::Icons::DialogInformation, tr("Propriétés du projet"), this);
|
|
prj_prop_diagram = new QAction(QET::Icons::DialogInformation, tr("Propriétés du folio"), this);
|
|
prj_add_diagram = new QAction(QET::Icons::DiagramAdd, tr("Ajouter un folio"), this);
|
|
prj_insert_diagram_above = new QAction(QET::Icons::DiagramAdd, tr("Insérer un folio au-dessus"), this);
|
|
prj_insert_diagram_below = new QAction(QET::Icons::DiagramAdd, tr("Insérer un folio en dessous"), this);
|
|
prj_duplicate_diagram = new QAction(QET::Icons::IC_CopyFile, tr("Copier et coller"), this);
|
|
prj_del_diagram = new QAction(QET::Icons::DiagramDelete, tr("Supprimer ce folio"), this);
|
|
prj_move_diagram_up = new QAction(QET::Icons::GoUp, tr("Remonter ce folio"), this);
|
|
prj_move_diagram_down = new QAction(QET::Icons::GoDown, tr("Abaisser ce folio"), this);
|
|
prj_move_diagram_upx10 = new QAction(QET::Icons::GoUpDouble, tr("Remonter ce folio x10"), this);
|
|
prj_move_diagram_upx100 = new QAction(QET::Icons::GoUpDouble, tr("Remonter ce folio x100"), this);
|
|
prj_move_diagram_top = new QAction(QET::Icons::GoTop, tr("Remonter ce folio au debut"), this);
|
|
prj_move_diagram_downx10 = new QAction(QET::Icons::GoDownDouble, tr("Abaisser ce folio x10"), this);
|
|
prj_move_diagram_downx100 = new QAction(QET::Icons::GoDownDouble, tr("Abaisser ce folio x100"), this);
|
|
tbt_add = new QAction(QET::Icons::TitleBlock, tr("Nouveau modèle"), this);
|
|
tbt_edit = new QAction(QET::Icons::TitleBlock, tr("Éditer ce modèle"), this);
|
|
tbt_remove = new QAction(QET::Icons::TitleBlock, tr("Supprimer ce modèle"), this);
|
|
|
|
|
|
ShortcutManager::instance().registerAction(prj_del_diagram, "panel.delete_diagram", tr("Panneau des éléments"), QKeySequence(Qt::Key_Delete));
|
|
ShortcutManager::instance().registerAction(prj_move_diagram_up, "panel.move_diagram_up", tr("Panneau des éléments"), QKeySequence(Qt::Key_F3));
|
|
ShortcutManager::instance().registerAction(prj_move_diagram_down, "panel.move_diagram_down", tr("Panneau des éléments"), QKeySequence(Qt::Key_F4));
|
|
ShortcutManager::instance().registerAction(prj_move_diagram_top, "panel.move_diagram_top", tr("Panneau des éléments"), QKeySequence(Qt::Key_F5));
|
|
ShortcutManager::instance().registerAction(prj_move_diagram_downx10, "panel.move_diagram_downx10", tr("Panneau des éléments"), QKeySequence(Qt::Key_F6));
|
|
ShortcutManager::instance().registerAction(prj_move_diagram_downx100, "panel.move_diagram_downx100", tr("Panneau des éléments"), QKeySequence(Qt::Key_F7));
|
|
ShortcutManager::instance().registerAction(prj_move_diagram_upx10, "panel.move_diagram_upx10", tr("Panneau des éléments"), QKeySequence(Qt::Key_F8));
|
|
ShortcutManager::instance().registerAction(prj_move_diagram_upx100, "panel.move_diagram_upx100", tr("Panneau des éléments"), QKeySequence(Qt::Key_F9));
|
|
|
|
|
|
|
|
// initialise le champ de texte pour filtrer avec une disposition horizontale
|
|
filter_textfield = new QLineEdit(this);
|
|
filter_textfield -> setClearButtonEnabled(true);
|
|
filter_textfield -> setPlaceholderText(tr("Filtrer"));
|
|
|
|
|
|
context_menu = new QMenu(this);
|
|
|
|
connect(open_directory, &QAction::triggered, this, &ElementsPanelWidget::openDirectoryForSelectedItem);
|
|
connect(copy_path, &QAction::triggered, this, &ElementsPanelWidget::copyPathForSelectedItem);
|
|
connect(prj_activate, &QAction::triggered, this, &ElementsPanelWidget::activateProject);
|
|
connect(prj_close, &QAction::triggered, this, &ElementsPanelWidget::closeProject);
|
|
connect(prj_edit_prop, &QAction::triggered, this, &ElementsPanelWidget::editProjectProperties);
|
|
connect(prj_prop_diagram, &QAction::triggered, this, &ElementsPanelWidget::editDiagramProperties);
|
|
connect(prj_add_diagram, &QAction::triggered, this, &ElementsPanelWidget::newDiagram);
|
|
connect(prj_insert_diagram_above, &QAction::triggered, this, &ElementsPanelWidget::insertDiagramAbove);
|
|
connect(prj_insert_diagram_below, &QAction::triggered, this, &ElementsPanelWidget::insertDiagramBelow);
|
|
connect(prj_del_diagram, &QAction::triggered, this, &ElementsPanelWidget::deleteDiagram);
|
|
connect(prj_duplicate_diagram, &QAction::triggered, this, &ElementsPanelWidget::duplicateDiagram);
|
|
connect(prj_move_diagram_up, &QAction::triggered, this, &ElementsPanelWidget::moveDiagramUp);
|
|
connect(prj_move_diagram_down, &QAction::triggered, this, &ElementsPanelWidget::moveDiagramDown);
|
|
connect(prj_move_diagram_top, &QAction::triggered, this, &ElementsPanelWidget::moveDiagramUpTop);
|
|
connect(prj_move_diagram_upx10, &QAction::triggered, this, &ElementsPanelWidget::moveDiagramUpx10);
|
|
connect(prj_move_diagram_upx100, &QAction::triggered, this, &ElementsPanelWidget::moveDiagramUpx100);
|
|
connect(prj_move_diagram_downx10, &QAction::triggered, this, &ElementsPanelWidget::moveDiagramDownx10);
|
|
connect(prj_move_diagram_downx100, &QAction::triggered, this, &ElementsPanelWidget::moveDiagramDownx100);
|
|
connect(tbt_add, &QAction::triggered, this, &ElementsPanelWidget::addTitleBlockTemplate);
|
|
connect(tbt_edit, &QAction::triggered, this, &ElementsPanelWidget::editTitleBlockTemplate);
|
|
connect(tbt_remove, &QAction::triggered, this, &ElementsPanelWidget::removeTitleBlockTemplate);
|
|
connect(filter_textfield, &QLineEdit::textChanged, this, &ElementsPanelWidget::filterEdited);
|
|
connect(elements_panel, &ElementsPanel::currentItemChanged, this, &ElementsPanelWidget::updateButtons);
|
|
connect(elements_panel, &ElementsPanel::customContextMenuRequested, this, &ElementsPanelWidget::handleContextMenu);
|
|
connect(
|
|
elements_panel,
|
|
&ElementsPanel::requestForTitleBlockTemplate,
|
|
QETApp::instance(),
|
|
[app = QETApp::instance()](const TitleBlockTemplateLocation &location) { app->openTitleBlockTemplate(location); }
|
|
);
|
|
|
|
// manage double click on TreeWidgetItem
|
|
connect(elements_panel, &ElementsPanel::requestForProjectPropertiesEdition, this, &ElementsPanelWidget::editProjectProperties);
|
|
connect(elements_panel, &ElementsPanel::requestForDiagramPropertiesEdition, this, &ElementsPanelWidget::editDiagramProperties);
|
|
// manage project activation
|
|
connect(elements_panel, &ElementsPanel::requestForProject, this, &ElementsPanelWidget::requestForProject);
|
|
|
|
// disposition verticale
|
|
QVBoxLayout *vlayout = new QVBoxLayout(this);
|
|
vlayout -> setContentsMargins(0,0,0,0);
|
|
vlayout -> setSpacing(0);
|
|
vlayout -> addWidget(filter_textfield);
|
|
vlayout -> addWidget(elements_panel);
|
|
vlayout -> setStretchFactor(elements_panel, 75000);
|
|
setLayout(vlayout);
|
|
}
|
|
|
|
/**
|
|
Destructeur
|
|
*/
|
|
ElementsPanelWidget::~ElementsPanelWidget()
|
|
{
|
|
}
|
|
|
|
/**
|
|
Require the desktop environment to open the directory containing the file
|
|
represented by the selected item, if any.
|
|
*/
|
|
void ElementsPanelWidget::openDirectoryForSelectedItem()
|
|
{
|
|
if (QTreeWidgetItem *qtwi = elements_panel -> currentItem()) {
|
|
QString dir_path = elements_panel -> dirPathForItem(qtwi);
|
|
if (!dir_path.isEmpty()) {
|
|
QFileInfo fileInfo(dir_path);
|
|
// Wenn der Pfad auf eine Datei (z.B. Makro) zeigt, isoliere den Ordnerpfad
|
|
if (fileInfo.isFile()) {
|
|
dir_path = fileInfo.absolutePath();
|
|
}
|
|
QDesktopServices::openUrl(QUrl::fromLocalFile(dir_path));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Copy the full path to the file represented by the selected item to the
|
|
clipboard.
|
|
*/
|
|
void ElementsPanelWidget::copyPathForSelectedItem()
|
|
{
|
|
if (QTreeWidgetItem *qtwi = elements_panel -> currentItem()) {
|
|
QString file_path = elements_panel -> filePathForItem(qtwi);
|
|
file_path = QDir::toNativeSeparators(file_path);
|
|
if (!file_path.isEmpty()) {
|
|
QApplication::clipboard() -> setText(file_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Recharge le panel d'elements
|
|
*/
|
|
void ElementsPanelWidget::reloadAndFilter()
|
|
{
|
|
// recharge tous les elements
|
|
elements_panel -> reload();
|
|
// reapplique le filtre
|
|
if (!filter_textfield -> text().isEmpty()) {
|
|
elements_panel -> filter(filter_textfield -> text());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emit the requestForProject signal with the selected project
|
|
*/
|
|
void ElementsPanelWidget::activateProject()
|
|
{
|
|
if (QETProject *selected_project = elements_panel -> selectedProject()) {
|
|
emit(requestForProject(selected_project));
|
|
}
|
|
}
|
|
|
|
/**
|
|
Emet le signal requestForProjectClosing avec le projet selectionne
|
|
*/
|
|
void ElementsPanelWidget::closeProject()
|
|
{
|
|
if (QETProject *selected_project = elements_panel -> selectedProject()) {
|
|
emit(requestForProjectClosing(selected_project));
|
|
}
|
|
}
|
|
|
|
/**
|
|
Emet le signal requestForProjectPropertiesEdition avec le projet selectionne
|
|
*/
|
|
void ElementsPanelWidget::editProjectProperties()
|
|
{
|
|
if (QETProject *selected_project = elements_panel -> selectedProject()) {
|
|
emit(requestForProjectPropertiesEdition(selected_project));
|
|
}
|
|
}
|
|
|
|
/**
|
|
Emet le signal requestForDiagramPropertiesEdition avec le schema selectionne
|
|
*/
|
|
void ElementsPanelWidget::editDiagramProperties()
|
|
{
|
|
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
|
|
emit(requestForDiagramPropertiesEdition(selected_diagram));
|
|
}
|
|
}
|
|
|
|
/**
|
|
Emet le signal requestForNewDiagram avec le projet selectionne
|
|
*/
|
|
void ElementsPanelWidget::newDiagram()
|
|
{
|
|
if (QETProject *selected_project = elements_panel -> selectedProject()) {
|
|
emit(requestForNewDiagram(selected_project));
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief ElementsPanelWidget::insertDiagramAbove
|
|
Emit requestForNewDiagramAt with the position of the currently
|
|
selected diagram, inserting the new folio right before it.
|
|
*/
|
|
void ElementsPanelWidget::insertDiagramAbove()
|
|
{
|
|
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
|
|
QETProject *project = selected_diagram->project();
|
|
emit(requestForNewDiagramAt(project, project->folioIndex(selected_diagram)));
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief ElementsPanelWidget::insertDiagramBelow
|
|
Emit requestForNewDiagramAt with the position right after the
|
|
currently selected diagram, inserting the new folio right after it.
|
|
*/
|
|
void ElementsPanelWidget::insertDiagramBelow()
|
|
{
|
|
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
|
|
QETProject *project = selected_diagram->project();
|
|
emit(requestForNewDiagramAt(project, project->folioIndex(selected_diagram) + 1));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emet le signal requestForDiagramsDeletion avec les schemas selectionnes
|
|
*/
|
|
void ElementsPanelWidget::deleteDiagram()
|
|
{
|
|
QList<Diagram *> diagrams_to_delete = elements_panel->selectedDiagrams();
|
|
|
|
if (diagrams_to_delete.isEmpty()) return;
|
|
|
|
emit(requestForDiagramsDeletion(diagrams_to_delete));
|
|
|
|
elements_panel->reload();
|
|
}
|
|
/**
|
|
* Emits the requestForDiagramMoveUpTop signal with all selected diagrams.
|
|
*/
|
|
void ElementsPanelWidget::moveDiagramUpTop() {
|
|
QList<Diagram *> diagrams_to_move = elements_panel->selectedDiagrams();
|
|
if (diagrams_to_move.isEmpty()) return;
|
|
|
|
// Emit the entire list at once
|
|
emit requestForDiagramMoveUpTop(diagrams_to_move);
|
|
|
|
// Clear messy tree selection caused by moving items, then restore clean selection
|
|
elements_panel->clearSelection();
|
|
for (Diagram *d : diagrams_to_move) {
|
|
if (auto item = elements_panel->getItemForDiagram(d)) item->setSelected(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emits the requestForDiagramMoveUp signal with all selected diagrams.
|
|
*/
|
|
void ElementsPanelWidget::moveDiagramUp() {
|
|
QList<Diagram *> diagrams_to_move = elements_panel->selectedDiagrams();
|
|
if (diagrams_to_move.isEmpty()) return;
|
|
|
|
// Emit the entire list at once
|
|
emit requestForDiagramMoveUp(diagrams_to_move);
|
|
|
|
// Clear messy tree selection caused by moving items, then restore clean selection
|
|
elements_panel->clearSelection();
|
|
for (Diagram *d : diagrams_to_move) {
|
|
if (auto item = elements_panel->getItemForDiagram(d)) item->setSelected(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emits the requestForDiagramMoveDown signal with all selected diagrams.
|
|
*/
|
|
void ElementsPanelWidget::moveDiagramDown() {
|
|
QList<Diagram *> diagrams_to_move = elements_panel->selectedDiagrams();
|
|
if (diagrams_to_move.isEmpty()) return;
|
|
|
|
// Emit the entire list at once
|
|
emit requestForDiagramMoveDown(diagrams_to_move);
|
|
|
|
// Clear messy tree selection caused by moving items, then restore clean selection
|
|
elements_panel->clearSelection();
|
|
for (Diagram *d : diagrams_to_move) {
|
|
if (auto item = elements_panel->getItemForDiagram(d)) item->setSelected(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emits the requestForDiagramMoveUpx10 signal with all selected diagrams.
|
|
*/
|
|
void ElementsPanelWidget::moveDiagramUpx10() {
|
|
QList<Diagram *> diagrams_to_move = elements_panel->selectedDiagrams();
|
|
if (diagrams_to_move.isEmpty()) return;
|
|
|
|
// Emit the entire list at once
|
|
emit requestForDiagramMoveUpx10(diagrams_to_move);
|
|
|
|
// Clear messy tree selection caused by moving items, then restore clean selection
|
|
elements_panel->clearSelection();
|
|
for (Diagram *d : diagrams_to_move) {
|
|
if (auto item = elements_panel->getItemForDiagram(d)) item->setSelected(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emits the requestForDiagramMoveUpx100 signal with all selected diagrams.
|
|
*/
|
|
void ElementsPanelWidget::moveDiagramUpx100() {
|
|
QList<Diagram *> diagrams_to_move = elements_panel->selectedDiagrams();
|
|
if (diagrams_to_move.isEmpty()) return;
|
|
|
|
// Emit the entire list at once
|
|
emit requestForDiagramMoveUpx100(diagrams_to_move);
|
|
|
|
// Clear messy tree selection caused by moving items, then restore clean selection
|
|
elements_panel->clearSelection();
|
|
for (Diagram *d : diagrams_to_move) {
|
|
if (auto item = elements_panel->getItemForDiagram(d)) item->setSelected(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emits the requestForDiagramMoveDownx10 signal with all selected diagrams.
|
|
*/
|
|
void ElementsPanelWidget::moveDiagramDownx10() {
|
|
QList<Diagram *> diagrams_to_move = elements_panel->selectedDiagrams();
|
|
if (diagrams_to_move.isEmpty()) return;
|
|
|
|
// Emit the entire list at once
|
|
emit requestForDiagramMoveDownx10(diagrams_to_move);
|
|
|
|
// Clear messy tree selection caused by moving items, then restore clean selection
|
|
elements_panel->clearSelection();
|
|
for (Diagram *d : diagrams_to_move) {
|
|
if (auto item = elements_panel->getItemForDiagram(d)) item->setSelected(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emits the requestForDiagramMoveDownx100 signal with all selected diagrams.
|
|
*/
|
|
void ElementsPanelWidget::moveDiagramDownx100() {
|
|
QList<Diagram *> diagrams_to_move = elements_panel->selectedDiagrams();
|
|
if (diagrams_to_move.isEmpty()) return;
|
|
|
|
// Emit the entire list at once
|
|
emit requestForDiagramMoveDownx100(diagrams_to_move);
|
|
|
|
// Clear messy tree selection caused by moving items, then restore clean selection
|
|
elements_panel->clearSelection();
|
|
for (Diagram *d : diagrams_to_move) {
|
|
if (auto item = elements_panel->getItemForDiagram(d)) item->setSelected(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
Opens a template editor to create a new title block template.
|
|
*/
|
|
void ElementsPanelWidget::addTitleBlockTemplate()
|
|
{
|
|
QTreeWidgetItem *current_item = elements_panel -> currentItem();
|
|
if (!current_item) return;
|
|
|
|
if (current_item -> type() == QET::TitleBlockTemplatesCollection) {
|
|
QETApp::instance() -> openTitleBlockTemplate(
|
|
elements_panel -> templateLocationForItem(current_item)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
Opens an editor to edit the currently selected title block template, if any.
|
|
*/
|
|
void ElementsPanelWidget::editTitleBlockTemplate()
|
|
{
|
|
QTreeWidgetItem *current_item = elements_panel -> currentItem();
|
|
if (current_item && current_item -> type() == QET::TitleBlockTemplate) {
|
|
QETApp::instance() -> openTitleBlockTemplate(
|
|
elements_panel -> templateLocationForItem(current_item)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
Delete the currently selected title block template, if any.
|
|
*/
|
|
void ElementsPanelWidget::removeTitleBlockTemplate()
|
|
{
|
|
QTreeWidgetItem *current_item = elements_panel -> currentItem();
|
|
if (current_item && current_item -> type() == QET::TitleBlockTemplate) {
|
|
TitleBlockTemplateDeleter(
|
|
elements_panel -> templateLocationForItem(current_item),
|
|
this
|
|
).exec();
|
|
}
|
|
}
|
|
|
|
/**
|
|
Met a jour les boutons afin d'assurer la coherence de l'interface
|
|
*/
|
|
void ElementsPanelWidget::updateButtons()
|
|
{
|
|
QTreeWidgetItem *current_item = elements_panel -> currentItem();
|
|
int current_type = elements_panel -> currentItemType();
|
|
|
|
if (current_type == QET::Project) {
|
|
bool is_writable = !(elements_panel -> selectedProject() -> isReadOnly());
|
|
prj_add_diagram -> setEnabled(is_writable);
|
|
} else if (current_type == QET::Diagram) {
|
|
// Fetch ALL selected diagrams instead of just one
|
|
QList<Diagram *> selected_diagrams = elements_panel -> selectedDiagrams();
|
|
|
|
if (!selected_diagrams.isEmpty()) {
|
|
QETProject *selected_diagram_project = selected_diagrams.first() -> project();
|
|
bool is_writable = !(selected_diagram_project -> isReadOnly());
|
|
int project_diagrams_count = selected_diagram_project -> diagrams().count();
|
|
|
|
// Find the highest (min) and lowest (max) index among the selection
|
|
int min_position = project_diagrams_count;
|
|
int max_position = -1;
|
|
|
|
for (Diagram *diagram : selected_diagrams) {
|
|
int pos = selected_diagram_project -> diagrams().indexOf(diagram);
|
|
if (pos < min_position) min_position = pos;
|
|
if (pos > max_position) max_position = pos;
|
|
}
|
|
|
|
prj_del_diagram -> setEnabled(is_writable);
|
|
prj_duplicate_diagram -> setEnabled(is_writable);
|
|
prj_insert_diagram_above -> setEnabled(is_writable);
|
|
prj_insert_diagram_below -> setEnabled(is_writable);
|
|
prj_move_diagram_up -> setEnabled(is_writable && min_position > 0);
|
|
prj_move_diagram_down -> setEnabled(is_writable && max_position < project_diagrams_count - 1);
|
|
prj_move_diagram_top -> setEnabled(is_writable && min_position > 0);
|
|
|
|
// Adjusted to >= to allow exactly 10 or 100 steps if space permits
|
|
prj_move_diagram_upx10 -> setEnabled(is_writable && min_position > 10);
|
|
prj_move_diagram_upx100 -> setEnabled(is_writable && min_position > 100);
|
|
prj_move_diagram_downx10 -> setEnabled(is_writable && max_position < project_diagrams_count - 10);
|
|
prj_move_diagram_downx100 -> setEnabled(is_writable && max_position < project_diagrams_count - 100);
|
|
}
|
|
} else if (current_type == QET::TitleBlockTemplatesCollection) {
|
|
TitleBlockTemplateLocation location = elements_panel -> templateLocationForItem(current_item);
|
|
tbt_add -> setEnabled(!location.isReadOnly());
|
|
tbt_edit -> setEnabled(false); // would not make sense
|
|
tbt_remove -> setEnabled(false); // would not make sense
|
|
} else if (current_type == QET::TitleBlockTemplate) {
|
|
QTreeWidgetItem *item = elements_panel -> currentItem();
|
|
TitleBlockTemplateLocation location = elements_panel -> templateLocationForItem(item);
|
|
tbt_add -> setEnabled(false); // would not make sense
|
|
tbt_edit -> setEnabled(true); // the tbt editor has a read-only mode
|
|
// deleting a tbt requires its parent collection to be writable
|
|
tbt_remove -> setEnabled(location.parentCollection() && !(location.parentCollection() -> isReadOnly()));
|
|
}
|
|
}
|
|
|
|
/**
|
|
Gere le menu contextuel du panel d'elements
|
|
@param pos Position ou le menu contextuel a ete demande
|
|
*/
|
|
void ElementsPanelWidget::handleContextMenu(const QPoint &pos) {
|
|
// recupere l'item concerne par l'evenement ainsi que son chemin
|
|
QTreeWidgetItem *item = elements_panel -> itemAt(pos);
|
|
if (!item) return;
|
|
|
|
updateButtons();
|
|
context_menu -> clear();
|
|
|
|
QString dir_path = elements_panel -> dirPathForItem(item);
|
|
if (!dir_path.isEmpty()) {
|
|
context_menu -> addAction(open_directory);
|
|
context_menu -> addAction(copy_path);
|
|
context_menu -> addSeparator();
|
|
}
|
|
|
|
switch(item -> type()) {
|
|
case QET::Project:
|
|
context_menu -> addAction(prj_activate);
|
|
context_menu -> addAction(prj_edit_prop);
|
|
context_menu -> addAction(prj_add_diagram);
|
|
context_menu -> addAction(prj_close);
|
|
break;
|
|
case QET::Diagram:
|
|
context_menu -> addAction(prj_prop_diagram);
|
|
context_menu -> addAction(prj_insert_diagram_above);
|
|
context_menu -> addAction(prj_insert_diagram_below);
|
|
context_menu -> addAction(prj_del_diagram);
|
|
context_menu -> addAction(prj_duplicate_diagram);
|
|
context_menu -> addAction(prj_move_diagram_top);
|
|
context_menu -> addAction(prj_move_diagram_upx10);
|
|
context_menu -> addAction(prj_move_diagram_upx100);
|
|
context_menu -> addAction(prj_move_diagram_up);
|
|
context_menu -> addAction(prj_move_diagram_down);
|
|
context_menu -> addAction(prj_move_diagram_downx10);
|
|
context_menu -> addAction(prj_move_diagram_downx100);
|
|
break;
|
|
case QET::TitleBlockTemplatesCollection:
|
|
context_menu -> addAction(tbt_add);
|
|
break;
|
|
case QET::TitleBlockTemplate:
|
|
context_menu -> addAction(tbt_edit);
|
|
context_menu -> addAction(tbt_remove);
|
|
break;
|
|
}
|
|
|
|
// affiche le menu
|
|
if (!context_menu -> isEmpty()) {
|
|
context_menu -> popup(mapToGlobal(elements_panel -> mapTo(this, pos + QPoint(2, 2))));
|
|
}
|
|
}
|
|
|
|
void ElementsPanelWidget::filterEdited(const QString &next_text) {
|
|
if (previous_filter_.isEmpty() && next_text.length() == 1) {
|
|
// the field is not empty anymore: begin filtering
|
|
elements_panel -> filter(next_text, QET::BeginFilter);
|
|
} else if (!previous_filter_.isEmpty() && next_text.isEmpty()) {
|
|
// the field is now empty again: end of filtering
|
|
elements_panel -> filter(QString(), QET::EndFilter);
|
|
} else {
|
|
// regular filtering
|
|
elements_panel -> filter(next_text, QET::RegularFilter);
|
|
}
|
|
previous_filter_ = next_text;
|
|
}
|
|
|
|
/**
|
|
* Treat key press event inside elements panel widget
|
|
*/
|
|
/**
|
|
* Treat key press event inside elements panel widget.
|
|
* Respects the enabled/disabled state of the corresponding QActions.
|
|
*/
|
|
void ElementsPanelWidget::keyPressEvent(QKeyEvent *e) {
|
|
switch(e->key()) {
|
|
case Qt::Key_Delete:
|
|
if (prj_del_diagram && prj_del_diagram->isEnabled()) {
|
|
deleteDiagram();
|
|
}
|
|
break;
|
|
case Qt::Key_F3:
|
|
if (prj_move_diagram_up && prj_move_diagram_up->isEnabled()) {
|
|
moveDiagramUp();
|
|
}
|
|
break;
|
|
case Qt::Key_F4:
|
|
if (prj_move_diagram_down && prj_move_diagram_down->isEnabled()) {
|
|
moveDiagramDown();
|
|
}
|
|
break;
|
|
case Qt::Key_F5:
|
|
if (prj_move_diagram_top && prj_move_diagram_top->isEnabled()) {
|
|
moveDiagramUpTop();
|
|
}
|
|
break;
|
|
case Qt::Key_F6:
|
|
if (prj_move_diagram_downx10 && prj_move_diagram_downx10->isEnabled()) {
|
|
moveDiagramDownx10();
|
|
}
|
|
break;
|
|
case Qt::Key_F7:
|
|
if (prj_move_diagram_downx100 && prj_move_diagram_downx100->isEnabled()) {
|
|
moveDiagramDownx100();
|
|
}
|
|
break;
|
|
case Qt::Key_F8:
|
|
if (prj_move_diagram_upx10 && prj_move_diagram_upx10->isEnabled()) {
|
|
moveDiagramUpx10();
|
|
}
|
|
break;
|
|
case Qt::Key_F9:
|
|
if (prj_move_diagram_upx100 && prj_move_diagram_upx100->isEnabled()) {
|
|
moveDiagramUpx100();
|
|
}
|
|
break;
|
|
default:
|
|
// Pass unhandled key events to the base class
|
|
QWidget::keyPressEvent(e);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Duplicates the selected folios (pages) along with their content
|
|
* and properties, and cleanly resolves cross-references.
|
|
*/
|
|
void ElementsPanelWidget::duplicateDiagram()
|
|
{
|
|
QList<Diagram *> diagrams_to_duplicate = elements_panel->selectedDiagrams();
|
|
if (diagrams_to_duplicate.isEmpty()) return;
|
|
|
|
QETProject *project = diagrams_to_duplicate.first()->project();
|
|
if (!project || project->isReadOnly()) return;
|
|
|
|
for (Diagram *source_diagram : diagrams_to_duplicate) {
|
|
|
|
Diagram *new_diagram = project->addNewDiagram();
|
|
if (!new_diagram) continue;
|
|
|
|
QString template_name = source_diagram->border_and_titleblock.titleBlockTemplateName();
|
|
new_diagram->setTitleBlockTemplate(template_name);
|
|
|
|
TitleBlockProperties tbp = source_diagram->border_and_titleblock.exportTitleBlock();
|
|
new_diagram->border_and_titleblock.importTitleBlock(tbp);
|
|
|
|
BorderProperties bp = source_diagram->border_and_titleblock.exportBorder();
|
|
new_diagram->border_and_titleblock.importBorder(bp);
|
|
|
|
// Serialize the whole diagram with is_copy_command=true.
|
|
// This is the same mechanism as Ctrl+C: toXml(true, true)
|
|
// internally calls correctTextPos/restoreText for Slave and
|
|
// Report elements only — producing correct text positions
|
|
// in the XML. No manual correctTextPos/restoreText needed.
|
|
QDomDocument doc = source_diagram->toXml(true, true);
|
|
QDomElement diagram_elmt = doc.documentElement();
|
|
|
|
new_diagram->fromXml(diagram_elmt, QPointF(0, 0), false, nullptr);
|
|
|
|
QSettings settings;
|
|
bool erase_labels = settings.value(
|
|
"diagramcommands/erase-label-on-copy", true).toBool();
|
|
|
|
// Resolve a linked pair duplicated together against each other
|
|
// (bugtracker #607) before the loop below renews their uuids or
|
|
// clears their pending links: at this exact moment a copy's
|
|
// tmp_uuids_link still holds its source's original partner
|
|
// uuid, which still equals the not-yet-renewed uuid of that
|
|
// partner's own copy if both were duplicated together. Scoped
|
|
// to this diagram's own copies, not a project-wide search, so
|
|
// this never links back to the source elements the copies were
|
|
// made from -- if only one half of a linked pair is here, its
|
|
// link entry simply finds no match and is dropped, same as
|
|
// clearPendingLinks() used to do unconditionally for every copy.
|
|
QList<Element *> new_elements;
|
|
for (QGraphicsItem *item : new_diagram->items()) {
|
|
if (Element *elmt = dynamic_cast<Element *>(item)) {
|
|
new_elements << elmt;
|
|
}
|
|
}
|
|
for (Element *elmt : new_elements) {
|
|
elmt->initLink(new_elements);
|
|
}
|
|
|
|
for (QGraphicsItem *item : new_diagram->items()) {
|
|
if (Element *elmt = dynamic_cast<Element *>(item)) {
|
|
// The XML round-trip kept the source elements' uuids. Give the
|
|
// copies their own identity (as PasteDiagramCommand::redo()
|
|
// does for on-diagram paste): element.uuid is the PRIMARY KEY
|
|
// of the project database, so duplicates fail to insert and
|
|
// silently vanish from nomenclature/summary tables.
|
|
elmt->newUuid();
|
|
|
|
// toXml(true, true) applied correctTextPos to Slave and
|
|
// Report elements, which shifted their text positions to
|
|
// match the stripped composite text. restoreText()
|
|
// recalculates the position for the actual resolved text.
|
|
// Only Slave and Report need this — other element types
|
|
// were not affected by correctTextPos.
|
|
if (elmt->linkType() == Element::Slave ||
|
|
elmt->linkType() & Element::AllReport)
|
|
{
|
|
new_diagram->restoreText(elmt);
|
|
}
|
|
|
|
// initLink() above already cleared tmp_uuids_link for
|
|
// every copy, matched or not -- nothing left here that
|
|
// could link back to a stale source uuid.
|
|
|
|
// Clean up copied element data:
|
|
// 1. Slaves always lose label/formula/comment/location
|
|
// and PLC master data — their text comes from a
|
|
// master element not available on the copy.
|
|
// 2. Non-slaves: honour "erase-label-on-copy".
|
|
|
|
DiagramContext dc = elmt->elementInformations();
|
|
bool changed = false;
|
|
|
|
// Slaves always lose their label/BMK and PLC data —
|
|
// their text comes from the master, which is not
|
|
// available on the copied element.
|
|
if (elmt->linkType() == Element::Slave) {
|
|
dc.addValue("formula", "");
|
|
dc.addValue("label", "");
|
|
dc.addValue("comment", "");
|
|
dc.addValue("location", "");
|
|
changed = true;
|
|
|
|
for (const QString &key : {
|
|
QETInformation::ELMT_PLC_TYPE,
|
|
QETInformation::ELMT_PLC_ADDRESS,
|
|
QETInformation::ELMT_PLC_FUNCTION,
|
|
QETInformation::ELMT_PLC_COMMENT,
|
|
QETInformation::ELMT_PLC_CROSSREF,
|
|
QETInformation::ELMT_PLC_TC,
|
|
QETInformation::ELMT_PLC_T1,
|
|
QETInformation::ELMT_PLC_T2,
|
|
QETInformation::ELMT_PLC_T3,
|
|
QETInformation::ELMT_PLC_T4,
|
|
QStringLiteral("xref")}) {
|
|
if (dc.contains(key)) {
|
|
dc.remove(key);
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
// Non-slaves: honour the "erase-label-on-copy" preference.
|
|
else if (erase_labels) {
|
|
dc.addValue("formula", "");
|
|
dc.addValue("label", "");
|
|
dc.addValue("comment", "");
|
|
dc.addValue("location", "");
|
|
changed = true;
|
|
}
|
|
|
|
if (changed) {
|
|
const bool is_slave = (elmt->linkType() == Element::Slave);
|
|
|
|
// Block alignment during setElementInformations
|
|
// for non-slaves, same as Element::fromXml() (line 890-896).
|
|
// For PLC slaves, don't block — elementInfoChanged()
|
|
// fires (elementUseForInfo returns self) and needs
|
|
// finishAlignment() to adjust positions for the
|
|
// new (empty) text.
|
|
if (!is_slave) {
|
|
for (auto deti : elmt->dynamicTextItems())
|
|
deti->m_block_alignment = true;
|
|
}
|
|
elmt->setElementInformations(dc);
|
|
for (auto deti : elmt->dynamicTextItems())
|
|
deti->m_block_alignment = false;
|
|
|
|
// For non-PLC slaves, elementInfoChanged() doesn't
|
|
// fire (no linked master), so the text items keep
|
|
// their original text. Clear them directly.
|
|
// For PLC slaves, elementInfoChanged() already
|
|
// cleared the text above. Skip UserText items
|
|
// (free text typed by the user) in both cases.
|
|
if (is_slave) {
|
|
for (auto deti : elmt->dynamicTextItems()) {
|
|
if (deti->textFrom() != DynamicElementTextItem::UserText) {
|
|
deti->m_block_alignment = true;
|
|
deti->setPlainText(QString());
|
|
deti->m_block_alignment = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (Conductor *cond = dynamic_cast<Conductor *>(item)) {
|
|
// Same reasoning for conductors: conductor.uuid is the PRIMARY
|
|
// KEY of the conductor table, and its insert is a plain INSERT,
|
|
// so a duplicated uuid fails and the wire silently disappears
|
|
// from the wiring list and the per-element wire count.
|
|
cond->newUuid();
|
|
|
|
// Reset conductor labels when "erase-label-on-copy" is
|
|
// active, matching PasteDiagramCommand::redo() —
|
|
// "erase on copy" means erase, not "replace with the
|
|
// project's default new-conductor text" (see issue #413).
|
|
if (erase_labels) {
|
|
ConductorProperties cp = cond->properties();
|
|
cp.text = "";
|
|
cond->setProperties(cp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
elements_panel->reload();
|
|
}
|