Files
qelectrotech-source-mirror/sources/QetGraphicsItemModeler/textresizehandles.cpp
T
Beat Hangartner 010e44ab8a 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>
2026-10-05 17:53:38 +02:00

225 lines
6.4 KiB
C++

/*
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);
}