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"