mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-06 19:54:13 +02:00
Add corner handles to change the width of a text box
TextResizeHandles puts four handles at the corners of a selected text (any QGraphicsTextItem with a qreal "textWidth" property, -1 = automatic width). Dragging one changes the width: the opposite corner stays in place in scene coordinates, for any rotation and rotation point, the text wraps and the height follows it. Vertical mouse movements are ignored, the width never goes below the longest word and is rounded to whole pixels, and a click without a move changes nothing. Double-clicking a handle goes back to the automatic width. The handles are a child of the text, so they follow it when it moves, rotates or is zoomed (as for the symbol text handles, #1002). The width is changed live; the owner makes it undoable with TextResizeCommand, which sets the width then the position on both undo and redo -- a text may move itself when its width changes, so two property commands would restore them in the wrong order. The geometry is header-only (textresize.h). Nothing uses the handles yet: the follow-up changes add them to free texts and to text fields in the element editor. Test: tst_textresizehandles, on a plain QGraphicsTextItem. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "textresizehandles.h"
|
||||
|
||||
#include "qetgraphicshandleritem.h"
|
||||
|
||||
#include <QAbstractTextDocumentLayout>
|
||||
#include <QCursor>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QGraphicsTextItem>
|
||||
#include <QTextDocument>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
/**
|
||||
@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<QetGraphicsHandlerItem *>(watched));
|
||||
if (index < 0)
|
||||
return false;
|
||||
|
||||
switch (event->type())
|
||||
{
|
||||
case QEvent::GraphicsSceneMousePress:
|
||||
{
|
||||
auto *mouse_event = static_cast<QGraphicsSceneMouseEvent *>(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<QGraphicsSceneMouseEvent *>(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);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef TEXTRESIZEHANDLES_H
|
||||
#define TEXTRESIZEHANDLES_H
|
||||
|
||||
#include "../textresize.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QGraphicsObject>
|
||||
#include <QPointer>
|
||||
#include <QTransform>
|
||||
#include <QUndoCommand>
|
||||
#include <QVector>
|
||||
|
||||
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<QetGraphicsHandlerItem *> 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<QGraphicsObject> m_text;
|
||||
qreal m_old_width,
|
||||
m_new_width;
|
||||
QPointF m_old_pos,
|
||||
m_new_pos;
|
||||
};
|
||||
|
||||
#endif // TEXTRESIZEHANDLES_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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef TEXTRESIZE_H
|
||||
#define TEXTRESIZE_H
|
||||
|
||||
#include <QGraphicsItem>
|
||||
#include <QScopedPointer>
|
||||
#include <QTextDocument>
|
||||
#include <QTextOption>
|
||||
#include <QTransform>
|
||||
|
||||
/**
|
||||
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<QTextDocument> 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <QtTest>
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QGraphicsTextItem>
|
||||
#include <QSignalSpy>
|
||||
#include <QTextBlock>
|
||||
#include <QTextLayout>
|
||||
#include <QTextOption>
|
||||
#include <QUndoStack>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#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<QVariant> 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"
|
||||
Reference in New Issue
Block a user