From 81449faffd3d708db6be6c8a450525f27d1051e2 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sat, 1 Aug 2026 17:51:26 +1200 Subject: [PATCH] Add drag-to-resize for dynamic element text width (#577 phase 1) Adds two QetGraphicsHandlerItem grip handles at the left/right edges of a selected DynamicElementTextItem's frameRect(), reusing the exact same handle class, scene-event-filter wiring, and live-drag-then-undo-on-release pattern QetShapeItem already uses for its own diagram-level resize handles (sources/qetgraphicsitem/qetshapeitem.cpp). - Handles are created/destroyed on ItemSelectedHasChanged, matching QetShapeItem's convention (and ElementPrimitiveDecorator's, for the element editor's own primitives). - Position is recomputed in paint() rather than hooked to specific mutators, since textWidth/font/text/rotation can all move frameRect() and there's no single itemChange notification that covers all of them. - The drag delta is resolved through mapFromScene() into the item's own local coordinates, so a rotated text box still resizes along its own baseline rather than along the scene's x-axis. - setTextWidth() is called live during the drag for immediate visual feedback (matching how QetShapeItem's handlerMouseMoveEvent live-updates geometry); only on release is a QPropertyUndoCommand pushed -- the exact same command the properties-panel width spinbox already uses (sources/ui/dynamicelementtextmodel.cpp), so no new undo-command class or XML was needed. - The original textWidth() value is preserved as-is (including -1, the "auto" sentinel) for the undo command's old_value, separately from the concrete baseline used for the live drag's delta math -- otherwise an undo would replace "auto width" with a synthesized fixed width instead of actually restoring the auto-sizing state. Scoped to DynamicElementTextItem per the discussion's phase 1 (the buildable, no-new-XML piece); IndependentTextItem and the element editor's PartText/PartDynamicTextField have no serialized width property to resize yet and are left as explicitly out-of-scope follow-ups. Verified headlessly (Xvfb + xdotool + scrot): selecting an element's label text shows the two handles, dragging one live-resizes the text (confirmed via the properties panel's width field updating in real time), and undo/redo correctly restores the exact original width including the auto-width (-1) case. --- .../dynamicelementtextitem.cpp | 164 +++++++++++++++++- .../qetgraphicsitem/dynamicelementtextitem.h | 12 ++ 2 files changed, 171 insertions(+), 5 deletions(-) diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.cpp b/sources/qetgraphicsitem/dynamicelementtextitem.cpp index 8f6359e97..70ae4934d 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.cpp +++ b/sources/qetgraphicsitem/dynamicelementtextitem.cpp @@ -24,6 +24,7 @@ #include "../qetgraphicsitem/terminal.h" #include "../qetinformation.h" #include "../utils/qetutils.h" +#include "../QetGraphicsItemModeler/qetgraphicshandleritem.h" #include "crossrefitem.h" #include "element.h" #include "elementtextitemgroup.h" @@ -66,7 +67,9 @@ DynamicElementTextItem::DynamicElementTextItem(Element *parent_element) : } DynamicElementTextItem::~DynamicElementTextItem() -{} +{ + removeResizeHandles(); +} /** @brief DynamicElementTextItem::textFromMetaEnum @@ -688,7 +691,10 @@ void DynamicElementTextItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) void DynamicElementTextItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { DiagramTextItem::paint(painter, option, widget); - + + if (m_left_resize_handle || m_right_resize_handle) + updateResizeHandlesPos(); + if (m_frame) { painter->save(); @@ -776,15 +782,44 @@ QVariant DynamicElementTextItem::itemChange(QGraphicsItem::GraphicsItemChange ch updateXref(); updateXref(); } - + else if (change == QGraphicsItem::ItemSelectedHasChanged) + { + if (value.toBool()) + addResizeHandles(); + else + removeResizeHandles(); + } + else if (change == QGraphicsItem::ItemSceneHasChanged && !scene()) + { + removeResizeHandles(); + } + return QGraphicsObject::itemChange(change, value); } bool DynamicElementTextItem::sceneEventFilter(QGraphicsItem *watched, QEvent *event) { + if (watched == m_left_resize_handle || watched == m_right_resize_handle) + { + auto *handle = static_cast(watched); + if (event->type() == QEvent::GraphicsSceneMousePress) { + handlerMousePressEvent(handle, static_cast(event)); + return true; + } + else if (event->type() == QEvent::GraphicsSceneMouseMove) { + handlerMouseMoveEvent(handle, static_cast(event)); + return true; + } + else if (event->type() == QEvent::GraphicsSceneMouseRelease) { + handlerMouseReleaseEvent(handle, static_cast(event)); + return true; + } + return false; + } + if(watched != m_slave_Xref_item) return false; - + if(event->type() == QEvent::GraphicsSceneHoverEnter) { m_slave_Xref_item->setDefaultTextColor(Qt::blue); return true; @@ -797,10 +832,129 @@ bool DynamicElementTextItem::sceneEventFilter(QGraphicsItem *watched, QEvent *ev zoomToLinkedElement(); return true; } - + return false; } +/** + @brief DynamicElementTextItem::addResizeHandles + Create and show the two width-resize handles (left/right edge of + frameRect()), reusing QetGraphicsHandlerItem the same way QetShapeItem + does for its own resize handles. +*/ +void DynamicElementTextItem::addResizeHandles() +{ + if (m_left_resize_handle || !scene()) + return; + + qreal size = QETUtils::graphicsHandlerSize(this); + m_left_resize_handle = new QetGraphicsHandlerItem(size); + m_right_resize_handle = new QetGraphicsHandlerItem(size); + + for (QetGraphicsHandlerItem *handle : {m_left_resize_handle, m_right_resize_handle}) + { + scene()->addItem(handle); + handle->setColor(Qt::darkGreen); + handle->setZValue(zValue() + 1); + handle->installSceneEventFilter(this); + } + + updateResizeHandlesPos(); +} + +/** + @brief DynamicElementTextItem::removeResizeHandles +*/ +void DynamicElementTextItem::removeResizeHandles() +{ + delete m_left_resize_handle; + delete m_right_resize_handle; + m_left_resize_handle = nullptr; + m_right_resize_handle = nullptr; +} + +/** + @brief DynamicElementTextItem::updateResizeHandlesPos + Keep the two resize handles at the vertical middle of frameRect()'s left + and right edges, in scene coordinates -- called on every paint() so it + stays correct across every kind of change that can move this item or + change its size (position, rotation, font, text, textWidth...) without + needing a dedicated hook for each one. +*/ +void DynamicElementTextItem::updateResizeHandlesPos() +{ + if (!m_left_resize_handle || !m_right_resize_handle) + return; + + QRectF fr = frameRect(); + m_left_resize_handle->setPos(mapToScene(QPointF(fr.left(), fr.center().y()))); + m_right_resize_handle->setPos(mapToScene(QPointF(fr.right(), fr.center().y()))); +} + +/** + @brief DynamicElementTextItem::handlerMousePressEvent + @param handle + @param event +*/ +void DynamicElementTextItem::handlerMousePressEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(handle) + + //The actual property value, kept as-is (possibly -1, meaning "auto") + //so a later undo restores the exact original state rather than a + //synthesized fixed width. + m_resize_original_width = textWidth(); + //A concrete baseline for the live drag's delta math, which can't + //start from -1. + m_resize_baseline_width = (m_resize_original_width < 0) ? frameRect().width() : m_resize_original_width; + m_resize_start_local_x = mapFromScene(event->scenePos()).x(); +} + +/** + @brief DynamicElementTextItem::handlerMouseMoveEvent + Live-resize the text while dragging, exactly like the element editor's + resize handles live-update geometry during a drag (undo is only pushed + on release). The drag delta is resolved in this item's own local + coordinates (not scene coordinates) so a rotated text box still resizes + along its own baseline. + @param handle + @param event +*/ +void DynamicElementTextItem::handlerMouseMoveEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event) +{ + qreal local_x = mapFromScene(event->scenePos()).x(); + qreal delta = local_x - m_resize_start_local_x; + if (handle == m_left_resize_handle) + delta = -delta; + + qreal new_width = qMax(m_resize_baseline_width + delta, qreal(10)); + setTextWidth(new_width); + updateResizeHandlesPos(); +} + +/** + @brief DynamicElementTextItem::handlerMouseReleaseEvent + Push the same QPropertyUndoCommand the properties-panel width spinbox + already pushes (sources/ui/dynamicelementtextmodel.cpp) -- the value is + already applied live from the drag, so this only makes it undoable. + @param handle + @param event +*/ +void DynamicElementTextItem::handlerMouseReleaseEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(handle) + Q_UNUSED(event) + + qreal new_width = textWidth(); + if (!qFuzzyCompare(m_resize_original_width, new_width) && m_parent_element && m_parent_element->diagram()) + { + auto *undo = new QPropertyUndoCommand(this, "textWidth", QVariant(m_resize_original_width), QVariant(new_width)); + undo->setAnimated(true, false); + undo->setText(tr("Redimensionner un texte d'élément")); + m_parent_element->diagram()->undoStack().push(undo); + } +} + void DynamicElementTextItem::elementInfoChanged() { DiagramContext dc; diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.h b/sources/qetgraphicsitem/dynamicelementtextitem.h index f31096b53..bcf5689f5 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.h +++ b/sources/qetgraphicsitem/dynamicelementtextitem.h @@ -29,6 +29,7 @@ class Element; class Conductor; class ElementTextItemGroup; class CrossRefItem; +class QetGraphicsHandlerItem; /** @brief The DynamicElementTextItem class @@ -144,6 +145,12 @@ class DynamicElementTextItem : public DiagramTextItem void zoomToLinkedElement(); void parentElementRotationChanged(); void thisRotationChanged(); + void addResizeHandles(); + void removeResizeHandles(); + void updateResizeHandlesPos(); + void handlerMousePressEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event); + void handlerMouseMoveEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event); + void handlerMouseReleaseEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event); private: QPointer @@ -174,6 +181,11 @@ class DynamicElementTextItem : public DiagramTextItem bool m_keep_visual_rotation = true; qreal m_visual_rotation_ref = 0; bool m_move_parent = true; + QetGraphicsHandlerItem *m_left_resize_handle = nullptr; + QetGraphicsHandlerItem *m_right_resize_handle = nullptr; + qreal m_resize_original_width = -1; + qreal m_resize_baseline_width = -1; + qreal m_resize_start_local_x = 0; }; #endif // DYNAMICELEMENTTEXTITEM_H