From 32dd686144f2af7abcd49e80853ad1576176a0e5 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Tue, 4 Aug 2026 18:51:22 +1200 Subject: [PATCH] Add "rotate group" to actually rotate a selection as a whole RotateSelectionCommand's existing "Pivoter" action (Space) only ever bumps each selected item's own rotation property -- QGraphicsItem's setRotation() spins an item around its own local origin and never touches pos(). Select three elements arranged in a row and rotate: each spins 90 degrees individually, but the row stays a row. That's "rotate each item," not "rotate the group." Add a rotate_as_group parameter to RotateSelectionCommand (default false, so the existing action and its one call site are unchanged). When set, it computes a shared pivot once -- the bounding-box center of the whole selection -- and queues a second, parallel "pos" QPropertyUndoCommand alongside the existing "rotation" one, rotating each item's position around that pivot by the same angle. Scoped the position change to Element/IndependentTextItem/ DiagramImageItem only: these are the only selectable types with scene-space pos(). ConductorTextItem, DynamicElementTextItem and ElementTextItemGroup are all parent-relative children (confirmed by reading their constructors), so when their owning Element is also selected and gets its own pos() rotated, they're carried along for free by Qt's normal parent/child transform propagation -- exactly what the existing "skip rotation if parent is also selected" guard already assumes for those three cases. Exposed as a new, separate action ("Pivoter le groupe", Shift+Space) next to the existing one rather than changing Space's behavior, since some workflows may rely on the current per-item rotation. --- sources/qetdiagrameditor.cpp | 12 ++++ sources/qetdiagrameditor.h | 1 + .../undocommand/rotateselectioncommand.cpp | 61 +++++++++++++++++-- sources/undocommand/rotateselectioncommand.h | 6 +- 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index 5b2ba6779..199e923a2 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -628,6 +628,7 @@ void QETDiagramEditor::setUpActions() //Selections Actions (related to a selected item) m_delete_selection = m_selection_actions_group.addAction( QET::Icons::EditDelete, tr("Supprimer") ); m_rotate_selection = m_selection_actions_group.addAction( QET::Icons::TransformRotate, tr("Pivoter") ); + m_rotate_group_selection = m_selection_actions_group.addAction( QET::Icons::TransformRotate, tr("Pivoter le groupe") ); m_rotate_texts = m_selection_actions_group.addAction( QET::Icons::ObjectRotateRight, tr("Orienter les textes") ); m_find_element = m_selection_actions_group.addAction( QET::Icons::ZoomDraw, tr("Retrouver dans le panel") ); m_edit_selection = m_selection_actions_group.addAction( QET::Icons::ElementEdit, tr("Éditer l'item sélectionné") ); @@ -635,16 +636,19 @@ void QETDiagramEditor::setUpActions() ShortcutManager::instance().registerAction(m_delete_selection, "diagrameditor.delete_selection", tr("Éditeur de schémas"), Qt::Key_Delete); ShortcutManager::instance().registerAction(m_rotate_selection, "diagrameditor.rotate_selection", tr("Éditeur de schémas"), Qt::Key_Space); + ShortcutManager::instance().registerAction(m_rotate_group_selection, "diagrameditor.rotate_group_selection", tr("Éditeur de schémas"), Qt::SHIFT | Qt::Key_Space); ShortcutManager::instance().registerAction(m_rotate_texts, "diagrameditor.rotate_texts", tr("Éditeur de schémas"), Qt::CTRL | Qt::Key_Space); ShortcutManager::instance().registerAction(m_edit_selection, "diagrameditor.edit_selection", tr("Éditeur de schémas"), Qt::CTRL | Qt::Key_E); m_delete_selection->setStatusTip( tr("Enlève les éléments sélectionnés du folio", "status bar tip")); m_rotate_selection->setStatusTip( tr("Pivote les éléments et textes sélectionnés", "status bar tip")); + m_rotate_group_selection->setStatusTip( tr("Pivote la sélection comme un groupe autour de son centre, au lieu de chaque élément sur place", "status bar tip")); m_rotate_texts ->setStatusTip( tr("Pivote les textes sélectionnés à un angle précis", "status bar tip")); m_find_element ->setStatusTip( tr("Retrouve l'élément sélectionné dans le panel", "status bar tip")); m_delete_selection ->setData("delete_selection"); m_rotate_selection ->setData("rotate_selection"); + m_rotate_group_selection->setData("rotate_group_selection"); m_rotate_texts ->setData("rotate_selected_text"); m_find_element ->setData("find_selected_element"); m_edit_selection ->setData("edit_selected_element"); @@ -1601,6 +1605,12 @@ void QETDiagramEditor::selectionGroupTriggered(QAction *action) if(c->isValid()) diagram->undoStack().push(c); } + else if (value == "rotate_group_selection") + { + RotateSelectionCommand *c = new RotateSelectionCommand(diagram, 90, nullptr, true); + if(c->isValid()) + diagram->undoStack().push(c); + } else if (value == "rotate_selected_text") diagram->undoStack().push(new RotateTextsCommand(diagram)); else if (value == "find_selected_element" && currentElement()) @@ -1735,6 +1745,7 @@ void QETDiagramEditor::slot_updateComplexActions() << m_copy << m_delete_selection << m_rotate_selection + << m_rotate_group_selection << m_edit_selection << m_group_selected_texts; for(QAction *action : action_list) @@ -1763,6 +1774,7 @@ void QETDiagramEditor::slot_updateComplexActions() m_copy -> setEnabled(copiable_items); m_delete_selection -> setEnabled(!ro && deletable_items); m_rotate_selection -> setEnabled(!ro && diagram_->canRotateSelection()); + m_rotate_group_selection -> setEnabled(!ro && diagram_->canRotateSelection()); //Action that need selected texts or texts group QList texts = DiagramContent(diagram_).selectedTexts(); diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h index 0114a186e..ecc3b5164 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -216,6 +216,7 @@ class QETDiagramEditor : public QETMainWindow *m_edit_selection, ///< To edit selected item *m_delete_selection, ///< Delete selection *m_rotate_selection, ///< Rotate selected elements and text items by 90 degrees + *m_rotate_group_selection = nullptr, ///< Rotate the selection as a whole around its shared center, instead of each item in place *m_rotate_texts, ///< Direct selected text items to a specific angle *m_find_element, ///< Find the selected element in the panel *m_group_selected_texts = nullptr, diff --git a/sources/undocommand/rotateselectioncommand.cpp b/sources/undocommand/rotateselectioncommand.cpp index a3d3cc821..f893c5510 100644 --- a/sources/undocommand/rotateselectioncommand.cpp +++ b/sources/undocommand/rotateselectioncommand.cpp @@ -29,21 +29,37 @@ #include "../qetgraphicsitem/independenttextitem.h" #include +#include -RotateSelectionCommand::RotateSelectionCommand(Diagram *diagram, qreal angle, QUndoCommand *parent) : +RotateSelectionCommand::RotateSelectionCommand(Diagram *diagram, qreal angle, QUndoCommand *parent, bool rotate_as_group) : QUndoCommand(parent), m_diagram(diagram) { - setText(QObject::tr("Pivoter la selection")); - + setText(rotate_as_group ? QObject::tr("Pivoter le groupe") : QObject::tr("Pivoter la selection")); + if(!m_diagram->isReadOnly()) { + //Shared pivot for group rotation: the bounding-box center of + //everything selected, computed once up front from the + //selection as a whole (not just the items that end up being + //individually repositioned below). + QPointF pivot; + if (rotate_as_group) + { + QRectF bounding_rect; + for (QGraphicsItem *item : m_diagram->selectedItems()) + bounding_rect |= item->sceneBoundingRect(); + pivot = bounding_rect.center(); + } + for (QGraphicsItem *item : m_diagram->selectedItems()) { switch (item->type()) { case Element::Type: m_undo << new QPropertyUndoCommand(item->toGraphicsObject(), "rotation", QVariant(item->rotation()), QVariant(item->rotation()+angle), this); + if (rotate_as_group) + addGroupPositionUndo(item, pivot, angle); break; case ConductorTextItem::Type: { @@ -53,9 +69,19 @@ m_diagram(diagram) break; case IndependentTextItem::Type: m_undo << new QPropertyUndoCommand(item->toGraphicsObject(), "rotation", QVariant(item->rotation()), QVariant(item->rotation()+angle), this); + if (rotate_as_group) + addGroupPositionUndo(item, pivot, angle); break; case DynamicElementTextItem::Type: { + //No pos() undo here even in group mode: this item is + //only rotated in place when its parent Element isn't + //also selected (guard below), and its pos() is + //parent-local, not scene coordinates -- when the + //parent Element *is* selected and gets its own pos() + //rotated around the shared pivot above, this child + //text item is carried along for free by Qt's normal + //parent/child transform propagation. if(item->parentItem() && !item->parentItem()->isSelected()) m_undo << new QPropertyUndoCommand(item->toGraphicsObject(), "rotation", QVariant(item->rotation()), QVariant(item->rotation()+angle), this); } @@ -69,17 +95,44 @@ m_diagram(diagram) break; case DiagramImageItem::Type: m_undo << new QPropertyUndoCommand(item->toGraphicsObject(), "rotation", QVariant(item->rotation()), QVariant(item->rotation()+angle), this); + if (rotate_as_group) + addGroupPositionUndo(item, pivot, angle); break; default: break; } } - + for (QPropertyUndoCommand *undo : m_undo) undo->setAnimated(true, false); } } +/** + @brief RotateSelectionCommand::addGroupPositionUndo + Queue a "pos" QPropertyUndoCommand that rotates @a item's position + around @a pivot by @a angle degrees (Qt's clockwise-positive + convention, matching QGraphicsItem::setRotation() so a group + rotation turns the same direction as each item's own spin). + Only meaningful for items whose pos() is in scene coordinates + (Element, IndependentTextItem, DiagramImageItem) -- never call this + for a child item positioned relative to its own parent. + @param item : item to reposition, its own rotation undo already queued + @param pivot : shared pivot point, in scene coordinates + @param angle : rotation angle in degrees +*/ +void RotateSelectionCommand::addGroupPositionUndo(QGraphicsItem *item, const QPointF &pivot, qreal angle) +{ + const QPointF old_pos = item->pos(); + const qreal radians = qDegreesToRadians(angle); + const QPointF delta = old_pos - pivot; + const QPointF new_pos( + pivot.x() + delta.x() * qCos(radians) - delta.y() * qSin(radians), + pivot.y() + delta.x() * qSin(radians) + delta.y() * qCos(radians) + ); + m_undo << new QPropertyUndoCommand(item->toGraphicsObject(), "pos", QVariant(old_pos), QVariant(new_pos), this); +} + /** @brief RotateSelectionCommand::undo */ diff --git a/sources/undocommand/rotateselectioncommand.h b/sources/undocommand/rotateselectioncommand.h index abc75bcc9..df96fba96 100644 --- a/sources/undocommand/rotateselectioncommand.h +++ b/sources/undocommand/rotateselectioncommand.h @@ -21,10 +21,12 @@ #include #include #include +#include class Diagram; class ConductorTextItem; class QPropertyUndoCommand; +class QGraphicsItem; /** @brief The RotateSelectionCommand class @@ -33,13 +35,15 @@ class QPropertyUndoCommand; class RotateSelectionCommand : public QUndoCommand { public: - RotateSelectionCommand(Diagram *diagram, qreal angle=90, QUndoCommand *parent=nullptr); + RotateSelectionCommand(Diagram *diagram, qreal angle=90, QUndoCommand *parent=nullptr, bool rotate_as_group=false); void undo() override; void redo() override; bool isValid(); private: + void addGroupPositionUndo(QGraphicsItem *item, const QPointF &pivot, qreal angle); + Diagram *m_diagram =nullptr; QList> m_cond_text;