mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-28 04:54:13 +02:00
f7591e0c9d
Symbols, free texts, shapes and pictures can be grouped from the Edit menu, the selection's context menu or the command search. Clicking one member selects the group, so moving, copying and deleting act on all of it; Ctrl+click on a member deselects the group; a rubber band touching part of a group selects all of it when released. No default shortcut: Ctrl+G is "jump to element". A group is not an object in the scene. Each member keeps its place and carries the group's uuid (QGraphicsItem::data()), saved as a "group" attribute written only when set: a project without groups saves exactly as before, and older versions open a grouped one and ignore the groups. Re-parenting under a QGraphicsItemGroup would have made every member's position group-relative; ElementTextItemGroup already needs nine special cases for that. - Selection is completed on clicks and at the end of a rubber band, not on every selectionChanged(): export, search and Tab select items themselves and must not have groups pulled back in. - Project database: group_uuid on element, shape, independent_text and image, kept in step by projectDataBase::itemGroupChanged(). - Paste and folio duplication give each source group one new uuid. - Undo of Ungroup restores each item's exact group. Builds on #1065 (uuids and database rows for texts, shapes and images). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
855 lines
33 KiB
C++
855 lines
33 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 "qetgraphicsitem/diagramimageitem.h"
|
|
#include "qetgraphicsitem/independenttextitem.h"
|
|
#include "qetgraphicsitem/qetshapeitem.h"
|
|
#include "itemgroups.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 (auto text = dynamic_cast<IndependentTextItem *>(item)) {
|
|
// Not a database key (yet), but a script or the MCP server
|
|
// addresses a text, image or shape by it: a copy must not
|
|
// answer to its source's name.
|
|
text->newUuid();
|
|
}
|
|
else if (auto image = dynamic_cast<DiagramImageItem *>(item)) {
|
|
image->newUuid();
|
|
}
|
|
else if (auto shape = dynamic_cast<QetShapeItem *>(item)) {
|
|
shape->newUuid();
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Groups too: a group of the copy is its own, so selecting it
|
|
// is the same on both folios but the database tells them apart.
|
|
QHash<QUuid, QUuid> renewed_groups;
|
|
for (QGraphicsItem *item : new_diagram->items()) {
|
|
const QUuid source_group = ItemGroups::groupOf(item);
|
|
if (source_group.isNull()) {
|
|
continue;
|
|
}
|
|
if (!renewed_groups.contains(source_group)) {
|
|
renewed_groups.insert(source_group, QUuid::createUuid());
|
|
}
|
|
new_diagram->setItemGroup(item, renewed_groups.value(source_group));
|
|
}
|
|
}
|
|
|
|
elements_panel->reload();
|
|
}
|