mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-07 20:34:14 +02:00
Merge pull request #1321 from bhangart/feature/text-resize-handles
Add corner handles to change the width of a text box
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,252 @@
|
||||
/*
|
||||
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>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
@brief TextResizeHandles::TextResizeHandles
|
||||
@param text : the text to resize, becomes the parent of the handles.
|
||||
The text may be in a scene or not yet.
|
||||
@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);
|
||||
m_handles << handle;
|
||||
}
|
||||
installHandleFilters();
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief TextResizeHandles::itemChange
|
||||
A scene drops the event filters of an item that leaves it, so the
|
||||
handles stop reacting to the mouse when the text is removed from its
|
||||
scene and added back (undoing a delete does that), or when they were
|
||||
created before the text was in a scene. Install them again each time
|
||||
these handles enter a scene.
|
||||
*/
|
||||
QVariant TextResizeHandles::itemChange(GraphicsItemChange change, const QVariant &value)
|
||||
{
|
||||
if (change == QGraphicsItem::ItemSceneHasChanged)
|
||||
installHandleFilters();
|
||||
return QGraphicsObject::itemChange(change, value);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief TextResizeHandles::installHandleFilters
|
||||
Filter the mouse events of the handles, when they are in a scene.
|
||||
*/
|
||||
void TextResizeHandles::installHandleFilters()
|
||||
{
|
||||
if (!scene())
|
||||
return;
|
||||
for (QetGraphicsHandlerItem *handle : std::as_const(m_handles))
|
||||
if (handle->scene() == scene())
|
||||
handle->installSceneEventFilter(this);
|
||||
}
|
||||
|
||||
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,124 @@
|
||||
/*
|
||||
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;
|
||||
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
|
||||
|
||||
private:
|
||||
void installHandleFilters();
|
||||
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,329 @@
|
||||
/*
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
// The handles still work after the text left its scene and came back,
|
||||
// as undoing a delete does: Qt drops scene event filters on removal.
|
||||
void dragAfterLeavingTheScene()
|
||||
{
|
||||
QGraphicsScene scene;
|
||||
auto *text = new WidthText(QStringLiteral("Motor protection switch Q12"));
|
||||
scene.addItem(text);
|
||||
auto *handles = new TextResizeHandles(text);
|
||||
|
||||
scene.removeItem(text);
|
||||
scene.addItem(text);
|
||||
|
||||
const QPointF start_pos = text->pos();
|
||||
drag(scene, *handles, *text, TextResize::BottomRight, QPointF(-40, 0));
|
||||
QVERIFY(text->textWidth() > 0);
|
||||
QCOMPARE(text->pos(), start_pos);
|
||||
delete text;
|
||||
}
|
||||
|
||||
// The handles also work when they are created before the text is in a
|
||||
// scene.
|
||||
void dragWhenCreatedOutsideAScene()
|
||||
{
|
||||
QGraphicsScene scene;
|
||||
auto *text = new WidthText(QStringLiteral("Motor protection switch Q12"));
|
||||
auto *handles = new TextResizeHandles(text);
|
||||
scene.addItem(text);
|
||||
|
||||
drag(scene, *handles, *text, TextResize::BottomRight, QPointF(-40, 0));
|
||||
QVERIFY(text->textWidth() > 0);
|
||||
}
|
||||
|
||||
// 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