diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake
index 3e7b6f573..8afd0f6e1 100644
--- a/cmake/qet_compilation_vars.cmake
+++ b/cmake/qet_compilation_vars.cmake
@@ -614,6 +614,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/QetGraphicsItemModeler/qetgraphicshandleritem.h
${QET_DIR}/sources/QetGraphicsItemModeler/qetgraphicshandlerutility.cpp
${QET_DIR}/sources/QetGraphicsItemModeler/qetgraphicshandlerutility.h
+ ${QET_DIR}/sources/QetGraphicsItemModeler/textresizehandles.cpp
+ ${QET_DIR}/sources/QetGraphicsItemModeler/textresizehandles.h
${QET_DIR}/sources/QPropertyUndoCommand/qpropertyundocommand.cpp
${QET_DIR}/sources/QPropertyUndoCommand/qpropertyundocommand.h
diff --git a/sources/QetGraphicsItemModeler/textresizehandles.cpp b/sources/QetGraphicsItemModeler/textresizehandles.cpp
new file mode 100644
index 000000000..c92a02b2c
--- /dev/null
+++ b/sources/QetGraphicsItemModeler/textresizehandles.cpp
@@ -0,0 +1,224 @@
+/*
+ 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 .
+*/
+#include "textresizehandles.h"
+
+#include "qetgraphicshandleritem.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+/**
+ @brief TextResizeHandles::TextResizeHandles
+ @param text : the text to resize, becomes the parent of the handles.
+ It must be in a scene, the handles filter their own events.
+ @param handle_size : see QETUtils::graphicsHandlerSize()
+*/
+TextResizeHandles::TextResizeHandles(QGraphicsTextItem *text, qreal handle_size) :
+ m_text(text)
+{
+ setFlag(QGraphicsItem::ItemHasNoContents);
+ setParentItem(text);
+
+ for (int i = TextResize::TopLeft ; i <= TextResize::BottomLeft ; ++i)
+ {
+ auto *handle = new QetGraphicsHandlerItem(handle_size);
+ handle->setParentItem(this);
+ handle->setColor(Qt::darkGreen);
+ handle->setToolTip(tr("Glisser pour changer la largeur du texte, "
+ "double-cliquer pour une largeur automatique"));
+ handle->setCursor(i == TextResize::TopLeft || i == TextResize::BottomRight
+ ? Qt::SizeFDiagCursor : Qt::SizeBDiagCursor);
+ if (scene())
+ handle->installSceneEventFilter(this);
+ m_handles << handle;
+ }
+
+ m_size_connection = connect(text->document()->documentLayout(),
+ &QAbstractTextDocumentLayout::documentSizeChanged,
+ this, &TextResizeHandles::updateHandlesPos);
+ updateHandlesPos();
+}
+
+TextResizeHandles::~TextResizeHandles()
+{
+ disconnect(m_size_connection);
+}
+
+QRectF TextResizeHandles::boundingRect() const {
+ return QRectF();
+}
+
+/**
+ @return the handle at corner
+*/
+QetGraphicsHandlerItem *TextResizeHandles::handle(TextResize::Corner corner) const {
+ return m_handles.at(corner);
+}
+
+/**
+ @brief TextResizeHandles::updateHandlesPos
+ Put the handles at the corners of the box of the text. Called when the
+ size of the text changes; the owner calls it when the box changes in an
+ other way.
+ The box is boundingRect(), the box a user sees selected and resizes,
+ not the tighter frame of a framed text (qelectrotech#591).
+*/
+void TextResizeHandles::updateHandlesPos()
+{
+ const QRectF rect = m_text->boundingRect();
+ for (int i = TextResize::TopLeft ; i <= TextResize::BottomLeft ; ++i)
+ m_handles.at(i)->setPos(TextResize::cornerOf(rect, TextResize::Corner(i)));
+}
+
+bool TextResizeHandles::sceneEventFilter(QGraphicsItem *watched, QEvent *event)
+{
+ const int index = m_handles.indexOf(static_cast(watched));
+ if (index < 0)
+ return false;
+
+ switch (event->type())
+ {
+ case QEvent::GraphicsSceneMousePress:
+ {
+ auto *mouse_event = static_cast(event);
+ if (mouse_event->button() != Qt::LeftButton)
+ return false;
+ pressed(TextResize::Corner(index), mouse_event->scenePos());
+ return true;
+ }
+ case QEvent::GraphicsSceneMouseMove:
+ if (m_dragging)
+ moved(static_cast(event)->scenePos());
+ return true;
+ case QEvent::GraphicsSceneMouseRelease:
+ released();
+ return true;
+ case QEvent::GraphicsSceneMouseDoubleClick:
+ resetToAutomaticWidth();
+ return true;
+ default:
+ return false;
+ }
+}
+
+void TextResizeHandles::pressed(TextResize::Corner corner, const QPointF &scene_pos)
+{
+ m_dragging = true;
+ m_press_scene = scene_pos;
+ m_dragged = corner;
+ m_old_width = currentWidth();
+ m_old_pos = m_text->pos();
+ m_press_inverse = m_text->sceneTransform().inverted();
+ m_fixed_local = TextResize::cornerOf(m_text->boundingRect(), TextResize::opposite(corner));
+ m_fixed_scene = m_text->mapToScene(m_fixed_local);
+ m_min_width = TextResize::minimumWidth(m_text->document());
+}
+
+void TextResizeHandles::moved(const QPointF &scene_pos)
+{
+ //A click without a move keeps an automatic width automatic
+ if (scene_pos == m_press_scene)
+ return;
+
+ //Whole pixels, as in the width spin boxes
+ const qreal width = qMax(qreal(qRound(TextResize::widthForDrag(m_press_inverse, m_fixed_local,
+ scene_pos, m_dragged, 0))),
+ std::ceil(m_min_width));
+ m_text->setProperty("textWidth", width);
+ TextResize::pinCorner(m_text, TextResize::opposite(m_dragged), m_fixed_scene);
+ updateHandlesPos();
+}
+
+void TextResizeHandles::released()
+{
+ if (!m_dragging)
+ return;
+ m_dragging = false;
+
+ const qreal new_width = currentWidth();
+ if (!qFuzzyCompare(m_old_width, new_width) || m_old_pos != m_text->pos())
+ emit resizeFinished(m_old_width, new_width, m_old_pos, m_text->pos());
+}
+
+/**
+ @brief TextResizeHandles::resetToAutomaticWidth
+ Back to the automatic width (-1): the text keeps the point chosen by its
+ alignment in place, as it does for any other change of its width.
+*/
+void TextResizeHandles::resetToAutomaticWidth()
+{
+ m_dragging = false;
+
+ const qreal old_width = currentWidth();
+ if (old_width < 0)
+ return;
+
+ const QPointF old_pos = m_text->pos();
+ m_text->setProperty("textWidth", qreal(-1));
+ updateHandlesPos();
+ emit resizeFinished(old_width, currentWidth(), old_pos, m_text->pos());
+}
+
+qreal TextResizeHandles::currentWidth() const {
+ return m_text->property("textWidth").toReal();
+}
+
+
+/**
+ @brief TextResizeCommand::TextResizeCommand
+ @param text : a text with a "textWidth" property
+ @param old_width
+ @param new_width
+ @param old_pos
+ @param new_pos
+ @param parent
+*/
+TextResizeCommand::TextResizeCommand(QGraphicsObject *text,
+ qreal old_width, qreal new_width,
+ const QPointF &old_pos, const QPointF &new_pos,
+ QUndoCommand *parent) :
+ QUndoCommand(parent),
+ m_text(text),
+ m_old_width(old_width),
+ m_new_width(new_width),
+ m_old_pos(old_pos),
+ m_new_pos(new_pos)
+{
+ setText(tr("Redimensionner un texte"));
+}
+
+void TextResizeCommand::undo() {
+ apply(m_old_width, m_old_pos);
+}
+
+void TextResizeCommand::redo() {
+ apply(m_new_width, m_new_pos);
+}
+
+void TextResizeCommand::apply(qreal width, const QPointF &pos)
+{
+ if (!m_text)
+ return;
+ m_text->setProperty("textWidth", width);
+ m_text->setPos(pos);
+}
diff --git a/sources/QetGraphicsItemModeler/textresizehandles.h b/sources/QetGraphicsItemModeler/textresizehandles.h
new file mode 100644
index 000000000..e098b54d4
--- /dev/null
+++ b/sources/QetGraphicsItemModeler/textresizehandles.h
@@ -0,0 +1,122 @@
+/*
+ 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 .
+*/
+#ifndef TEXTRESIZEHANDLES_H
+#define TEXTRESIZEHANDLES_H
+
+#include "../textresize.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+class QetGraphicsHandlerItem;
+class QGraphicsTextItem;
+
+/**
+ @brief The TextResizeHandles class
+ Four corner handles to change the width of a text box with the mouse.
+ The opposite corner of the dragged one stays in place, the text wraps
+ to the new width and the height of the box follows the text.
+ Double-clicking a handle goes back to the automatic width.
+
+ The text must have a qreal "textWidth" property (-1 = automatic width).
+ The handles are a child of the text, so they follow it when it, or its
+ symbol, is moved, rotated or zoomed (qelectrotech#1002).
+
+ The width is changed live while dragging; the owner of the text makes
+ the change undoable when resizeFinished() is emitted, by pushing a
+ TextResizeCommand on its own undo stack.
+*/
+class TextResizeHandles : public QGraphicsObject
+{
+ Q_OBJECT
+
+ public:
+ TextResizeHandles(QGraphicsTextItem *text, qreal handle_size = 10);
+ ~TextResizeHandles() override;
+
+ QRectF boundingRect() const override;
+ void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override {}
+
+ QetGraphicsHandlerItem *handle(TextResize::Corner corner) const;
+ void updateHandlesPos();
+
+ signals:
+ void resizeFinished(qreal old_width, qreal new_width,
+ QPointF old_pos, QPointF new_pos);
+
+ protected:
+ bool sceneEventFilter(QGraphicsItem *watched, QEvent *event) override;
+
+ private:
+ void pressed(TextResize::Corner corner, const QPointF &scene_pos);
+ void moved(const QPointF &scene_pos);
+ void released();
+ void resetToAutomaticWidth();
+ qreal currentWidth() const;
+
+ QGraphicsTextItem *m_text;
+ QVector m_handles;
+ QMetaObject::Connection m_size_connection;
+
+ //The drag in progress
+ bool m_dragging = false;
+ TextResize::Corner m_dragged = TextResize::BottomRight;
+ QTransform m_press_inverse;
+ QPointF m_press_scene,
+ m_fixed_local,
+ m_fixed_scene,
+ m_old_pos;
+ qreal m_old_width = -1,
+ m_min_width = 0;
+};
+
+/**
+ @brief The TextResizeCommand class
+ Undo a width change made with TextResizeHandles: the width, then the
+ position, which keeps the opposite corner of the dragged one in place.
+ Both are set in the same order on undo and redo, because a text may
+ move itself when its width changes (to keep its alignment point).
+*/
+class TextResizeCommand : public QUndoCommand
+{
+ Q_DECLARE_TR_FUNCTIONS(TextResizeCommand)
+
+ public:
+ TextResizeCommand(QGraphicsObject *text,
+ qreal old_width, qreal new_width,
+ const QPointF &old_pos, const QPointF &new_pos,
+ QUndoCommand *parent = nullptr);
+
+ void undo() override;
+ void redo() override;
+
+ private:
+ void apply(qreal width, const QPointF &pos);
+
+ QPointer m_text;
+ qreal m_old_width,
+ m_new_width;
+ QPointF m_old_pos,
+ m_new_pos;
+};
+
+#endif // TEXTRESIZEHANDLES_H
diff --git a/sources/textresize.h b/sources/textresize.h
new file mode 100644
index 000000000..626a3e4d0
--- /dev/null
+++ b/sources/textresize.h
@@ -0,0 +1,120 @@
+/*
+ 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 .
+*/
+#ifndef TEXTRESIZE_H
+#define TEXTRESIZE_H
+
+#include
+#include
+#include
+#include
+#include
+
+/**
+ The geometry of changing the width of a text box by dragging one of
+ its corners: the text wraps to the new width, the opposite corner
+ stays where it is drawn.
+*/
+namespace TextResize
+{
+ /**
+ A corner of the box of a text, in the order of the resize handles.
+ */
+ enum Corner {
+ TopLeft,
+ TopRight,
+ BottomRight,
+ BottomLeft
+ };
+
+ inline Corner opposite(Corner corner) {
+ return Corner((int(corner) + 2) % 4);
+ }
+
+ inline bool isLeft(Corner corner) {
+ return corner == TopLeft || corner == BottomLeft;
+ }
+
+ /**
+ @return the corner of rect, in the coordinates of rect.
+ */
+ inline QPointF cornerOf(const QRectF &rect, Corner corner)
+ {
+ switch (corner) {
+ case TopLeft: return rect.topLeft();
+ case TopRight: return rect.topRight();
+ case BottomRight: return rect.bottomRight();
+ case BottomLeft: return rect.bottomLeft();
+ }
+ return rect.topLeft();
+ }
+
+ /**
+ @return the smallest width that document can be given: the width
+ of its longest word, which is never broken, plus the margins.
+ */
+ inline qreal minimumWidth(const QTextDocument *document)
+ {
+ QScopedPointer narrow(document->clone());
+ QTextOption option = narrow->defaultTextOption();
+ option.setWrapMode(QTextOption::WordWrap);
+ narrow->setDefaultTextOption(option);
+ narrow->setTextWidth(0);
+ return narrow->size().width();
+ }
+
+ /**
+ @return the width given by dragging the corner dragged of a text
+ box to mouse_scene, while the opposite corner stays at fixed_local.
+ @param press_inverse : the inverse of the scene transform of the
+ text when the drag started. Moving the text to keep the opposite
+ corner in place changes its transform, using the transform of the
+ press keeps that move from feeding back into the width.
+ @param fixed_local : the opposite corner, in the coordinates of the
+ text when the drag started.
+ @param min_width : see minimumWidth().
+ Vertical movements of the mouse are ignored: the height of the box
+ follows the wrapped text.
+ */
+ inline qreal widthForDrag(const QTransform &press_inverse,
+ const QPointF &fixed_local,
+ const QPointF &mouse_scene,
+ Corner dragged,
+ qreal min_width)
+ {
+ const QPointF mouse = press_inverse.map(mouse_scene);
+ const qreal width = isLeft(dragged) ? fixed_local.x() - mouse.x()
+ : mouse.x() - fixed_local.x();
+ return qMax(width, min_width);
+ }
+
+ /**
+ Move item so that its corner corner is at scene_point, in scene
+ coordinates. Right for any rotation, rotation point and alignment.
+ */
+ inline void pinCorner(QGraphicsItem *item, Corner corner, const QPointF &scene_point)
+ {
+ const QPointF now = item->mapToScene(cornerOf(item->boundingRect(), corner));
+ QPointF delta = scene_point - now;
+ if (const QGraphicsItem *parent = item->parentItem())
+ delta = parent->mapFromScene(scene_point) - parent->mapFromScene(now);
+ item->setPos(item->pos() + delta);
+ }
+
+}
+
+#endif // TEXTRESIZE_H
diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt
index 3b7e8c2b5..1b98dbb68 100644
--- a/tests/qttest/CMakeLists.txt
+++ b/tests/qttest/CMakeLists.txt
@@ -155,6 +155,18 @@ set_tests_properties(tst_textanchor PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offs
target_include_directories(tst_textanchor PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_textanchor PRIVATE Qt::Test Qt::Widgets)
+# TextResizeHandles: the corner handles that change the width of a text box,
+# on a plain QGraphicsTextItem with the "textWidth" property QET texts have.
+add_executable(
+ tst_textresizehandles
+ tst_textresizehandles.cpp
+ ${QET_DIR}/sources/QetGraphicsItemModeler/qetgraphicshandleritem.cpp
+ ${QET_DIR}/sources/QetGraphicsItemModeler/textresizehandles.cpp)
+add_test(NAME tst_textresizehandles COMMAND tst_textresizehandles)
+set_tests_properties(tst_textresizehandles PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
+target_include_directories(tst_textresizehandles PRIVATE ${QET_DIR}/sources)
+target_link_libraries(tst_textresizehandles PRIVATE Qt::Test Qt::Widgets)
+
# legacyelementtypes.h is header-only: the rewrite of element type names in
# nomenclature queries saved before June 2022.
add_executable(tst_legacyelementtypes tst_legacyelementtypes.cpp)
diff --git a/tests/qttest/tst_textresizehandles.cpp b/tests/qttest/tst_textresizehandles.cpp
new file mode 100644
index 000000000..d4e7f34e7
--- /dev/null
+++ b/tests/qttest/tst_textresizehandles.cpp
@@ -0,0 +1,297 @@
+/*
+ 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 .
+*/
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include "QetGraphicsItemModeler/qetgraphicshandleritem.h"
+#include "QetGraphicsItemModeler/textresizehandles.h"
+
+using TextResize::Corner;
+
+/**
+ A text with the "textWidth" property the texts of QElectroTech have.
+*/
+class WidthText : public QGraphicsTextItem
+{
+ Q_OBJECT
+ Q_PROPERTY(qreal textWidth READ textWidth WRITE setWidth)
+
+ public:
+ WidthText(const QString &text) : QGraphicsTextItem(text)
+ {
+ QTextOption option = document()->defaultTextOption();
+ option.setWrapMode(QTextOption::WordWrap);
+ document()->setDefaultTextOption(option);
+ }
+ void setWidth(qreal width) {setTextWidth(width);}
+};
+
+/// The number of lines the text is laid out on.
+static int lineCount(const QGraphicsTextItem &text)
+{
+ text.document()->size();
+ int lines = 0;
+ for (QTextBlock block = text.document()->begin() ; block.isValid() ; block = block.next())
+ lines += block.layout()->lineCount();
+ return lines;
+}
+
+static bool samePoint(const QPointF &a, const QPointF &b)
+{
+ return qAbs(a.x() - b.x()) < 1e-6 && qAbs(a.y() - b.y()) < 1e-6;
+}
+
+static QPointF sceneCorner(const QGraphicsTextItem &text, Corner corner)
+{
+ return text.mapToScene(TextResize::cornerOf(text.boundingRect(), corner));
+}
+
+/// Send a mouse event of type to the handle at corner, as the scene would.
+static void sendMouse(QGraphicsScene &scene, TextResizeHandles &handles,
+ Corner corner, QEvent::Type type, const QPointF &scene_pos)
+{
+ QGraphicsSceneMouseEvent event(type);
+ event.setButton(Qt::LeftButton);
+ event.setButtons(type == QEvent::GraphicsSceneMouseRelease ? Qt::NoButton : Qt::LeftButton);
+ event.setScenePos(scene_pos);
+ scene.sendEvent(handles.handle(corner), &event);
+}
+
+/// Drag the handle at corner from where it is by offset, in scene coordinates.
+static void drag(QGraphicsScene &scene, TextResizeHandles &handles,
+ WidthText &text, Corner corner, const QPointF &offset)
+{
+ const QPointF start = sceneCorner(text, corner);
+ sendMouse(scene, handles, corner, QEvent::GraphicsSceneMousePress, start);
+ sendMouse(scene, handles, corner, QEvent::GraphicsSceneMouseMove, start + offset / 2);
+ sendMouse(scene, handles, corner, QEvent::GraphicsSceneMouseMove, start + offset);
+ sendMouse(scene, handles, corner, QEvent::GraphicsSceneMouseRelease, start + offset);
+}
+
+class tst_textresizehandles : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ // Pure geometry: the width given by a drag does not change while the
+ // text moves to keep its opposite corner in place.
+ void widthForDragUsesThePressTransform()
+ {
+ WidthText text(QStringLiteral("Motor protection switch"));
+ text.setRotation(37);
+ text.setPos(10, 10);
+ const QTransform press_inverse = text.sceneTransform().inverted();
+ const QPointF fixed = TextResize::cornerOf(text.boundingRect(), TextResize::TopLeft);
+ const QPointF mouse = text.mapToScene(QPointF(80, 3));
+
+ const qreal width = TextResize::widthForDrag(press_inverse, fixed, mouse, TextResize::BottomRight, 0);
+ QCOMPARE(qRound(width * 1000), 80000);
+
+ text.setPos(500, -300);
+ QCOMPARE(TextResize::widthForDrag(press_inverse, fixed, mouse, TextResize::BottomRight, 0), width);
+ }
+
+ // The minimum width is the longest word: a text that narrow does not
+ // break any word, one narrower would.
+ void minimumWidthIsTheLongestWord()
+ {
+ WidthText text(QStringLiteral("a Motorschutzschalter b"));
+ const qreal minimum = TextResize::minimumWidth(text.document());
+
+ WidthText word(QStringLiteral("Motorschutzschalter"));
+ QVERIFY(qAbs(minimum - word.boundingRect().width()) < 0.5);
+
+ text.setWidth(minimum);
+ QVERIFY(qAbs(text.boundingRect().width() - minimum) < 0.5);
+ QCOMPARE(lineCount(text), 3);
+ }
+
+ // The minimum width never cuts a word, even for a document that would
+ // otherwise break a word anywhere.
+ void minimumWidthKeepsWords()
+ {
+ QGraphicsTextItem text(QStringLiteral("a Motorschutzschalter"));
+ QTextOption option = text.document()->defaultTextOption();
+ option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
+ text.document()->setDefaultTextOption(option);
+
+ QGraphicsTextItem word(QStringLiteral("Motorschutzschalter"));
+ QVERIFY(qAbs(TextResize::minimumWidth(text.document()) - word.boundingRect().width()) < 0.5);
+ }
+
+ // pinCorner() puts a corner where it was, for each corner, rotation,
+ // rotation point and inside a rotated parent.
+ void pinCornerKeepsTheCorner()
+ {
+ QGraphicsRectItem parent(0, 0, 10, 10);
+ parent.setRotation(30);
+ for (bool with_parent : {false, true})
+ for (int c = TextResize::TopLeft ; c <= TextResize::BottomLeft ; ++c)
+ for (qreal angle : {0.0, 90.0, 37.0})
+ for (bool centre : {false, true}) {
+ WidthText text(QStringLiteral("Motor protection switch"));
+ if (with_parent)
+ text.setParentItem(&parent);
+ text.setRotation(angle);
+ text.setTransformOriginPoint(centre ? text.boundingRect().center() : QPointF());
+ const Corner corner = Corner(c);
+ const QPointF before = sceneCorner(text, corner);
+
+ text.setWidth(70);
+ if (centre)
+ text.setTransformOriginPoint(text.boundingRect().center());
+ TextResize::pinCorner(&text, corner, before);
+ QVERIFY2(samePoint(sceneCorner(text, corner), before),
+ qPrintable(QStringLiteral("corner %1 angle %2 centre %3 parent %4")
+ .arg(c).arg(angle).arg(centre).arg(with_parent)));
+ text.setParentItem(nullptr);
+ }
+ }
+
+ // Dragging a handle keeps the opposite corner in place, wraps the text
+ // and reports the change once, on release.
+ void dragKeepsTheOppositeCorner()
+ {
+ for (int c = TextResize::TopLeft ; c <= TextResize::BottomLeft ; ++c)
+ for (qreal angle : {0.0, 90.0, 37.0}) {
+ QGraphicsScene scene;
+ auto *text = new WidthText(QStringLiteral("Motor protection switch Q12"));
+ scene.addItem(text);
+ text->setPos(100, 100);
+ text->setRotation(angle);
+ auto *handles = new TextResizeHandles(text);
+ QSignalSpy finished(handles, &TextResizeHandles::resizeFinished);
+
+ const Corner corner = Corner(c);
+ const QPointF fixed = sceneCorner(*text, TextResize::opposite(corner));
+ const qreal natural = text->boundingRect().width();
+ const QPointF start_pos = text->pos();
+
+ //Narrower by 40 along the text, whatever its rotation;
+ //a vertical move of the mouse is ignored.
+ QTransform turn;
+ turn.rotate(angle);
+ const qreal sign = TextResize::isLeft(corner) ? 1 : -1;
+ drag(scene, *handles, *text, corner, turn.map(QPointF(sign * 40, 25)));
+
+ const QString what = QStringLiteral("corner %1 angle %2").arg(c).arg(angle);
+ QCOMPARE(text->textWidth(), qreal(qRound(natural - 40)));
+ QVERIFY2(lineCount(*text) > 1, qPrintable(what));
+ QVERIFY2(samePoint(sceneCorner(*text, TextResize::opposite(corner)), fixed), qPrintable(what));
+
+ QCOMPARE(finished.count(), 1);
+ const QList args = finished.first();
+ QCOMPARE(args.at(0).toReal(), qreal(-1));
+ QCOMPARE(args.at(1).toReal(), text->textWidth());
+ QCOMPARE(args.at(2).toPointF(), start_pos);
+ QCOMPARE(args.at(3).toPointF(), text->pos());
+
+ //The handles follow the new box
+ QVERIFY(samePoint(handles->handle(corner)->scenePos(), sceneCorner(*text, corner)));
+ }
+ }
+
+ // A text is never made narrower than its longest word.
+ void dragStopsAtTheLongestWord()
+ {
+ QGraphicsScene scene;
+ auto *text = new WidthText(QStringLiteral("a Motorschutzschalter b"));
+ scene.addItem(text);
+ auto *handles = new TextResizeHandles(text);
+ drag(scene, *handles, *text, TextResize::BottomRight, QPointF(-1000, 0));
+ QCOMPARE(text->textWidth(), std::ceil(TextResize::minimumWidth(text->document())));
+ }
+
+ // A press and release without moving changes nothing.
+ void clickChangesNothing()
+ {
+ QGraphicsScene scene;
+ auto *text = new WidthText(QStringLiteral("Motor protection switch"));
+ scene.addItem(text);
+ auto *handles = new TextResizeHandles(text);
+ QSignalSpy finished(handles, &TextResizeHandles::resizeFinished);
+ drag(scene, *handles, *text, TextResize::TopRight, QPointF());
+ QCOMPARE(finished.count(), 0);
+ QCOMPARE(text->textWidth(), qreal(-1));
+ }
+
+ // Double-clicking a handle goes back to the automatic width.
+ void doubleClickResetsToAutomaticWidth()
+ {
+ QGraphicsScene scene;
+ auto *text = new WidthText(QStringLiteral("Motor protection switch"));
+ scene.addItem(text);
+ text->setWidth(60);
+ auto *handles = new TextResizeHandles(text);
+ QSignalSpy finished(handles, &TextResizeHandles::resizeFinished);
+
+ sendMouse(scene, *handles, TextResize::BottomLeft, QEvent::GraphicsSceneMouseDoubleClick,
+ sceneCorner(*text, TextResize::BottomLeft));
+ QCOMPARE(text->textWidth(), qreal(-1));
+ QCOMPARE(finished.count(), 1);
+ QCOMPARE(finished.first().at(0).toReal(), qreal(60));
+ QCOMPARE(finished.first().at(1).toReal(), qreal(-1));
+
+ //Already automatic: nothing to undo
+ sendMouse(scene, *handles, TextResize::BottomLeft, QEvent::GraphicsSceneMouseDoubleClick,
+ sceneCorner(*text, TextResize::BottomLeft));
+ QCOMPARE(finished.count(), 1);
+ }
+
+ // One undo step gives back the width and the position, one redo
+ // applies them again.
+ void undoRestoresWidthAndPosition()
+ {
+ QGraphicsScene scene;
+ auto *text = new WidthText(QStringLiteral("Motor protection switch Q12"));
+ scene.addItem(text);
+ text->setPos(100, 100);
+ text->setRotation(90);
+ auto *handles = new TextResizeHandles(text);
+ QUndoStack stack;
+ connect(handles, &TextResizeHandles::resizeFinished, &stack,
+ [&stack, text](qreal ow, qreal nw, QPointF op, QPointF np) {
+ stack.push(new TextResizeCommand(text, ow, nw, op, np));
+ });
+
+ drag(scene, *handles, *text, TextResize::TopLeft, QPointF(0, 40));
+ QCOMPARE(stack.count(), 1);
+ const qreal new_width = text->textWidth();
+ const QPointF new_pos = text->pos();
+ QVERIFY(new_pos != QPointF(100, 100));
+
+ stack.undo();
+ QCOMPARE(text->textWidth(), qreal(-1));
+ QCOMPARE(text->pos(), QPointF(100, 100));
+ stack.redo();
+ QCOMPARE(text->textWidth(), new_width);
+ QCOMPARE(text->pos(), new_pos);
+ }
+};
+
+QTEST_MAIN(tst_textresizehandles)
+#include "tst_textresizehandles.moc"