Files
qelectrotech-source-mirror/sources/elementsmover.cpp
T
ispyisail 2dbaa69186 Fix a group splitting apart when one of its items is locked (#1146)
Grouping two symbols and then locking one of them (Lock position in its
properties) left the group in a broken state: dragging the unlocked symbol
pulled it away while the locked one stayed, and dragging the locked one
did nothing. The move simply dropped locked items, so the rest of the
group went without them.

A group with a locked member now does not move at all, whichever member
is dragged, and the status bar says why. This is the rule the item-groups
proposal (discussion #1070) set out for this case. The same rule applies
to the arrow keys and to the Align commands, which share
DiagramContent::removeNonMovableItems().

Also fixed on the way, for a plain selection with a locked symbol: a wire
between the locked symbol and one being dragged kept its user-placed text
moving with the dragged end. Such a wire is now redrawn only, as a wire to
an unselected symbol already is.

Checked in the GUI on two symbols joined by a wire (grafcet example),
master against this branch, positions read from the saved file:
- drag the unlocked member: master moves it 190 px, this branch moves
  nothing and shows the message
- arrow keys on the selected group (3 runs each): master moves the
  unlocked member, this branch nothing
- the same two symbols ungrouped: both move the unlocked one, as before
- user-placed wire text: master shifts it 190 px, this branch keeps it
ctest: 34/34.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:23:32 +13:00

288 lines
9.0 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 "elementsmover.h"
#include "qetproject.h"
#include "autobreakconductor.h"
#include "conductorautonumerotation.h"
#include "diagram.h"
#include "itemgroups.h"
#include "qetgraphicsitem/conductor.h"
#include "qetgraphicsitem/conductortextitem.h"
#include "qetgraphicsitem/diagramimageitem.h"
#include "qetgraphicsitem/dynamicelementtextitem.h"
#include "qetgraphicsitem/element.h"
#include "qetgraphicsitem/elementtextitemgroup.h"
#include "qetgraphicsitem/independenttextitem.h"
#include "undocommand/addgraphicsobjectcommand.h"
#include "qetapp.h"
#include "qetdiagrameditor.h"
#include "undocommand/movegraphicsitemcommand.h"
/**
@brief ElementsMover::ElementsMover Constructor
*/
ElementsMover::ElementsMover(){}
/**
@brief ElementsMover::~ElementsMover Destructor
*/
ElementsMover::~ElementsMover(){}
/**
@brief ElementsMover::isReady
@return True if this element mover is ready to be used.
A element mover is ready when the previous managed movement is finish.
*/
bool ElementsMover::isReady() const
{
return(!m_movement_running);
}
/**
@brief ElementsMover::beginMovement
Start a new movement
@param diagram diagram where the movement is applied
@param driver_item item moved by mouse and don't be moved by Element mover
@return the numbers of items to be moved or -1 if movement can't be init.
*/
int ElementsMover::beginMovement(Diagram *diagram, QGraphicsItem *driver_item)
{
m_driver_held = false;
// They must be no movement in progress
if (m_movement_running) return(-1);
// Be sure we have diagram to work
if (!diagram) return(-1);
m_diagram = diagram;
if (!diagram->views().isEmpty()) {
const auto qde = QETApp::diagramEditorAncestorOf(diagram->views().at(0));
if (qde) {
m_status_bar = qde->statusBar();
}
} else {
m_status_bar.clear();
}
// Take count of driver item
m_movement_driver = driver_item;
// At the beginning of movement, move is NULL
m_current_movement -= m_current_movement;
m_moved_content = DiagramContent(diagram);
m_moved_content.removeNonMovableItems();
//A grouped driver left out of the move belongs to a group that one
//locked member holds in place: it stays with the group
m_driver_held = driver_item
&& !ItemGroups::groupOf(driver_item).isNull()
&& !m_moved_content.items().contains(driver_item);
if (m_driver_held && m_status_bar) {
m_status_bar->showMessage(QObject::tr(
"Ce groupe ne peut pas être déplacé : "
"la position d'un de ses éléments est verrouillée."));
}
//Remove element text and text group, if the parent element is selected.
const auto element_text{m_moved_content.m_element_texts};
for(const auto &deti : element_text) {
if(m_moved_content.m_elements.contains(deti->parentElement())) {
m_moved_content.m_element_texts.remove(deti);
}
}
const auto element_text_group{m_moved_content.m_texts_groups};
for(const auto &etig : element_text_group) {
if (m_moved_content.m_elements.contains(etig->parentElement())) {
m_moved_content.m_texts_groups.remove(etig);
}
}
if (!m_moved_content.count()) return(-1);
/* At this point, we've got all info to manage movement.
* There is now a move in progress */
m_movement_running = true;
return(m_moved_content.count());
}
/**
@brief ElementsMover::holds
@return true if @a item is the item the user drags and it must not move:
it is in a group that a locked member keeps in place (#1146). Each item
that drives a movement asks before moving itself.
*/
bool ElementsMover::holds(const QGraphicsItem *item) const
{
return m_driver_held && item && item == m_movement_driver;
}
/**
@brief ElementsMover::continueMovement
Add a move to the current movement.
@param movement movement to applied
*/
void ElementsMover::continueMovement(const QPointF &movement)
{
if (!m_movement_running || movement.isNull()) return;
m_current_movement += movement;
//Move every movable item, except conductor
typedef DiagramContent dc;
for (auto &qgi : m_moved_content.items(dc::Elements
| dc::TextFields
| dc::Images
| dc::Shapes
| dc::ElementTextFields
| dc::TextGroup))
{
if (qgi == m_movement_driver)
continue;
qgi->setPos(qgi->pos() + movement);
}
QVector<Conductor *>list_conductors;
for(auto *con : m_moved_content.m_conductors_to_move){
list_conductors << con;
}
// update conductors 'conductors_to_move'
for(auto *cond : list_conductors){
cond->updatePath();
if(cond->textItem()->wasMovedByUser() == true)
cond->textItem()->setPos(cond->textItem()->pos()+movement);
}
// update conductors 'conductors_to_update'
for (auto &conductor : m_moved_content.m_conductors_to_update)
{
conductor->updatePath();
}
if (m_status_bar && m_movement_driver)
{
const auto point_{m_movement_driver->scenePos()};
m_status_bar->showMessage(QString("x %1 : y %2").arg(QString::number(point_.x()), QString::number(point_.y())));
}
}
/**
@brief ElementsMover::endMovement
Ended the current movement by creating an undo added to the undostack of the diagram.
If there is only one element moved, we try to auto-connect new conductor from this element
and other possible element.
*/
void ElementsMover::endMovement()
{
// A movement must be inited
if (!m_movement_running) return;
//empty command to be used has parent of commands below
QUndoCommand *undo_object{new QUndoCommand()};
//Create undo move if there is a movement
if (!m_current_movement.isNull()) {
QUndoCommand *quc{new MoveGraphicsItemCommand(m_diagram, m_moved_content, m_current_movement, undo_object)};
undo_object->setText(quc->text());
}
//Auto-break conductors: for each moved element, break any conductor
//whose path passes through a terminal dock point, and reconnect through
//the element. The element is already at its final position on screen.
if (m_diagram->project()->autoBreakConductor())
{
QList<Conductor *> conductors_handled;
QSet<Terminal *> used_terminals;
for (Element *e : m_moved_content.m_elements) {
autoBreakConductors(m_diagram, e, undo_object,
conductors_handled, used_terminals);
}
}
//There is only one element moved, and project authorize auto conductor,
//we try auto connection of conductor;
typedef DiagramContent dc;
if (m_moved_content.items(dc::TextFields
| dc::Images
| dc::Shapes
| dc::TerminalStrip).isEmpty()
&& m_moved_content.items(dc::Elements).size() == 1
&& m_diagram->project()->autoConductor())
{
const Element *elmt{m_moved_content.m_elements.first()};
const auto aligned_free_terminals{elmt->AlignedFreeTerminals()};
if (const int acc = aligned_free_terminals.size())
{
for (const auto &pair : aligned_free_terminals)
{
Conductor *conductor{new Conductor(pair.first, pair.second)};
//Create an undo object for each new auto conductor, with undo_object for parent
new AddGraphicsObjectCommand(conductor, m_diagram, QPointF(), undo_object);
if (undo_object->text().isEmpty())
undo_object->setText(QObject::tr("Ajouter %n conducteur(s)", "add a numbers of conductor one or more", acc));
//Get all conductors at the same potential of conductor
const auto conductors_list{conductor->relatedPotentialConductors()};
//Compare the properties of every conductors stored in conductors_list,
//if every conductors properties is equal, we use this properties for conductor.
ConductorProperties others_properties;
bool use_properties = false;
if (!conductors_list.isEmpty())
{
use_properties = true;
others_properties = (*conductors_list.cbegin())->properties();
for (const auto &cond : conductors_list)
if (cond->properties() != others_properties)
use_properties = false;
}
if (use_properties)
conductor->setProperties(others_properties);
else
{
conductor -> setProperties(m_diagram -> defaultConductorProperties);
//Autonum the new conductor, the undo command associated for this, have for parent undo_object
ConductorAutoNumerotation can (conductor, m_diagram, undo_object);
can.numerate();
}
}
}
}
//Add undo_object if have child
if (undo_object->childCount())
m_diagram->undoStack().push(undo_object);
else
delete undo_object;
// There is no movement in progress now
m_movement_running = false;
m_moved_content.clear();
//Keep saying why a held group did not move
if (m_status_bar && !m_driver_held) {
m_status_bar->clearMessage();
}
}