From 74f908aa1e6d9e7a8bb4c39f8f68c26324885fed Mon Sep 17 00:00:00 2001 From: DaTweety <129504390+DaTweety@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:55:54 +0200 Subject: [PATCH 1/5] Symbol text width handles: the opposite edge stays in place Dragging the left width handle of a symbol's text changed its width but kept its position, which is its left edge: the right edge moved with the drag and the text jumped away from where the user held it. Now the edge opposite the dragged handle stays in place, as in a drawing program and as the corner handles of a free text already do (TextResizeHandles). It is found along the text's own baseline, so a rotated text behaves the same. Undo restores width and position together, with TextResizeCommand. Co-Authored-By: Claude Opus 5.5 --- .../dynamicelementtextitem.cpp | 35 ++++- .../qetgraphicsitem/dynamicelementtextitem.h | 2 + tests/qttest/CMakeLists.txt | 9 ++ tests/qttest/element_text_resize_probe.cpp | 148 ++++++++++++++++++ tests/qttest/run_element_text_resize_probe.py | 60 +++++++ 5 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 tests/qttest/element_text_resize_probe.cpp create mode 100755 tests/qttest/run_element_text_resize_probe.py diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.cpp b/sources/qetgraphicsitem/dynamicelementtextitem.cpp index 7fc536a45..0a04a63b9 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.cpp +++ b/sources/qetgraphicsitem/dynamicelementtextitem.cpp @@ -16,6 +16,7 @@ along with QElectroTech. If not, see . */ #include "dynamicelementtextitem.h" +#include "../QetGraphicsItemModeler/textresizehandles.h" #include "../shownkinds.h" #include "../qetproject.h" #include "../QPropertyUndoCommand/qpropertyundocommand.h" @@ -1040,8 +1041,6 @@ void DynamicElementTextItem::updateResizeHandlesPos() */ void DynamicElementTextItem::handlerMousePressEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event) { - Q_UNUSED(handle) - //The actual property value, kept as-is (possibly -1, meaning "auto") //so a later undo restores the exact original state rather than a //synthesized fixed width. @@ -1050,6 +1049,14 @@ void DynamicElementTextItem::handlerMousePressEvent(QetGraphicsHandlerItem *hand //start from -1. m_resize_baseline_width = (m_resize_original_width < 0) ? frameRect().width() : m_resize_original_width; m_resize_start_local_x = mapFromScene(event->scenePos()).x(); + + //The edge opposite the dragged handle stays where it is, as in a + //drawing program: remember it in the parent's coordinates, which + //take the text's rotation into account + m_resize_start_pos = pos(); + const QRectF br = boundingRect(); + m_resize_fixed_parent = mapToParent(QPointF(handle == m_left_resize_handle ? br.right() : br.left(), + br.center().y())); } /** @@ -1071,14 +1078,23 @@ void DynamicElementTextItem::handlerMouseMoveEvent(QetGraphicsHandlerItem *handl qreal new_width = qMax(m_resize_baseline_width + delta, qreal(10)); setTextWidth(new_width); + + //The text grows and shrinks on its right side: dragging the left + //handle moves the text so that its right edge stays in place + if (handle == m_left_resize_handle) + { + const QRectF br = boundingRect(); + setPos(pos() + m_resize_fixed_parent - mapToParent(QPointF(br.right(), br.center().y()))); + } updateResizeHandlesPos(); } /** @brief DynamicElementTextItem::handlerMouseReleaseEvent - Push the same QPropertyUndoCommand the properties-panel width spinbox - already pushes (sources/ui/dynamicelementtextmodel.cpp) -- the value is - already applied live from the drag, so this only makes it undoable. + Make the drag undoable with a TextResizeCommand, as the corner handles + of a free text do: the width and, after a drag on the left handle, the + position that kept the right edge in place. Both are already applied, + live from the drag. @param handle @param event */ @@ -1088,10 +1104,13 @@ void DynamicElementTextItem::handlerMouseReleaseEvent(QetGraphicsHandlerItem *ha Q_UNUSED(event) qreal new_width = textWidth(); - if (!qFuzzyCompare(m_resize_original_width, new_width) && m_parent_element && m_parent_element->diagram()) + if ((!qFuzzyCompare(m_resize_original_width, new_width) || pos() != m_resize_start_pos) + && m_parent_element && m_parent_element->diagram()) { - auto *undo = new QPropertyUndoCommand(this, "textWidth", QVariant(m_resize_original_width), QVariant(new_width)); - undo->setAnimated(true, false); + //Width and position together: undoing a drag on the left + //handle puts the text back where it was + auto *undo = new TextResizeCommand(this, m_resize_original_width, new_width, + m_resize_start_pos, pos()); undo->setText(tr("Resize an element text")); m_parent_element->diagram()->undoStack().push(undo); } diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.h b/sources/qetgraphicsitem/dynamicelementtextitem.h index eb7cd8f09..4fc22b5a8 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.h +++ b/sources/qetgraphicsitem/dynamicelementtextitem.h @@ -213,6 +213,8 @@ class DynamicElementTextItem : public DiagramTextItem qreal m_resize_original_width = -1; qreal m_resize_baseline_width = -1; qreal m_resize_start_local_x = 0; + QPointF m_resize_start_pos, + m_resize_fixed_parent; QMetaObject::Connection m_resize_handles_con; }; diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 83249d7a1..becbecceb 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -130,6 +130,15 @@ if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja") --source ${CMAKE_CURRENT_SOURCE_DIR}/image_dark_integration_probe.cpp --fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet) set_tests_properties(tst_darkimageintegration PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE) + # Dragging the left width handle of a symbol's text keeps its right + # edge in place, the right handle its left edge; undo restores both. + add_test(NAME tst_elementtextresizeintegration + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_element_text_resize_probe.py + --build ${CMAKE_BINARY_DIR} --binary $ + --source ${CMAKE_CURRENT_SOURCE_DIR}/element_text_resize_probe.cpp + --fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet) + set_tests_properties(tst_elementtextresizeintegration PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE + ENVIRONMENT "QT_QPA_PLATFORM=offscreen") endif() add_executable(${PROJECT_NAME} tst_mytest.cpp) diff --git a/tests/qttest/element_text_resize_probe.cpp b/tests/qttest/element_text_resize_probe.cpp new file mode 100644 index 000000000..199c66e8f --- /dev/null +++ b/tests/qttest/element_text_resize_probe.cpp @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Width handles of a symbol's text, linked against the application's objects. +// +// Dragging the left handle of a symbol's text changes its width and keeps +// its right edge in place; dragging the right handle keeps its left edge, +// as in a drawing program. Before, the text always kept its left edge, so +// a drag on the left handle moved the right edge and the text jumped. +// Undo puts back the width and the position together. Checked for an +// upright and a rotated text. +#include +#include +#include +#include +#include +#include +#include + +#include "../../sources/diagram.h" +#include "../../sources/qetproject.h" +#include "../../sources/qetmessagebox.h" +#include "../../sources/qetgraphicsitem/element.h" +#include "../../sources/qetgraphicsitem/dynamicelementtextitem.h" +#include "../../sources/QetGraphicsItemModeler/qetgraphicshandleritem.h" + +static void check(bool value, const char *message) +{ + if (!value) throw std::runtime_error(message); +} + +static bool near(const QPointF &a, const QPointF &b) +{ + return qAbs(a.x() - b.x()) < 0.01 && qAbs(a.y() - b.y()) < 0.01; +} + +static void mouse(Diagram *scene, QGraphicsItem *item, QEvent::Type type, const QPointF &point) +{ + QGraphicsSceneMouseEvent event(type); + event.setButton(Qt::LeftButton); + event.setButtons(type == QEvent::GraphicsSceneMouseRelease ? Qt::NoButton : Qt::LeftButton); + event.setScenePos(point); + event.setPos(item->mapFromScene(point)); + scene->sendEvent(item, &event); +} + +/// The left or right width handle of text, by where it sits on the text. +static QetGraphicsHandlerItem *handle(DynamicElementTextItem *text, bool left) +{ + QetGraphicsHandlerItem *found = nullptr; + for (QGraphicsItem *child : text->childItems()) { + if (child->type() != QetGraphicsHandlerItem::Type) continue; + const qreal x = child->pos().x(); + if (!found || (left ? x < found->pos().x() : x > found->pos().x())) + found = static_cast(child); + } + check(found, "the selected symbol's text shows its width handles"); + return found; +} + +static QPointF edge(DynamicElementTextItem *text, bool left) +{ + const QRectF br = text->boundingRect(); + return text->mapToScene(QPointF(left ? br.left() : br.right(), br.center().y())); +} + +/// Drag one handle by dx along the text's own baseline. +static void drag(Diagram *scene, DynamicElementTextItem *text, bool left, qreal dx) +{ + QetGraphicsHandlerItem *h = handle(text, left); + const QPointF start = h->scenePos(); + const QPointF end = start + text->mapToScene(QPointF(dx, 0)) - text->mapToScene(QPointF(0, 0)); + mouse(scene, h, QEvent::GraphicsSceneMousePress, start); + mouse(scene, h, QEvent::GraphicsSceneMouseMove, (start + end) / 2); + mouse(scene, h, QEvent::GraphicsSceneMouseMove, end); + mouse(scene, h, QEvent::GraphicsSceneMouseRelease, end); +} + +static void checkText(Diagram *scene, Element *element, DynamicElementTextItem *text) +{ + element->setSelected(true); + const QPointF pos = text->pos(); + const qreal width = text->boundingRect().width(); + const QPointF right = edge(text, false); + + // Left handle 30 to the left: wider, right edge where it was. + drag(scene, text, true, -30); + check(text->boundingRect().width() > width + 10, "the left handle widens the text"); + check(near(edge(text, false), right), "dragging the left handle keeps the right edge in place"); + + scene->undoStack().undo(); + check(near(text->pos(), pos), "undo puts the text back where it was"); + check(qAbs(text->boundingRect().width() - width) < 0.01, "undo puts the width back"); + scene->undoStack().redo(); + check(near(edge(text, false), right), "redo keeps the right edge in place"); + scene->undoStack().undo(); + + // Right handle 30 to the right: wider, left edge where it was. + element->setSelected(true); + const QPointF left = edge(text, true); + drag(scene, text, false, 30); + check(text->boundingRect().width() > width + 10, "the right handle widens the text"); + check(near(edge(text, true), left), "dragging the right handle keeps the left edge in place"); + scene->undoStack().undo(); + check(near(text->pos(), pos), "undo of the right handle leaves the position alone"); + element->setSelected(false); +} + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + std::freopen(argv[2], "w", stdout); + + QTemporaryDir settings; + QSettings::setDefaultFormat(QSettings::IniFormat); + QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, settings.path()); + QCoreApplication::setOrganizationName("QETElementTextResizeRegression"); + QETProject::setBackupEnabled(false); + QET::QetMessageBox::setNonInteractive(true); + QFontDatabase::addApplicationFont(":/fonts/LiberationSans-Regular.ttf"); + + try + { + QETProject project(QString::fromLocal8Bit(argv[1])); + check(project.state() == QETProject::Ok, "fixture opens"); + project.buildFolios(project.diagrams()); + Diagram *scene = project.diagrams().first(); + + DynamicElementTextItem *text = nullptr; + Element *element = nullptr; + for (Element *e : scene->elements()) + for (DynamicElementTextItem *t : e->dynamicTextItems()) + if (!text) { text = t; element = e; } + check(text, "the fixture has a symbol with a text"); + + checkText(scene, element, text); + // The same on a text turned by 90 degrees: the fixed edge is + // found along the text's own baseline, not the folio's x axis. + text->setRotation(90); + checkText(scene, element, text); + + std::puts("PASS: a symbol text's width handles keep the opposite edge in place, undo restores both"); + } + catch (const std::exception &error) + { + std::printf("FAIL: %s\n", error.what()); + return 1; + } + return 0; +} diff --git a/tests/qttest/run_element_text_resize_probe.py b/tests/qttest/run_element_text_resize_probe.py new file mode 100755 index 000000000..e4920c28d --- /dev/null +++ b/tests/qttest/run_element_text_resize_probe.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Link the element text resize integration probe using an existing Ninja app build. + +Reuses app objects without duplicating the app or introducing a core-library +refactor. No application source or existing executable is replaced. The +generated manifest, probe binary, reports and export fixtures stay in build/. +""" +import argparse +import pathlib +import re +import shutil +import subprocess +import sys + +parser = argparse.ArgumentParser() +parser.add_argument('--build', required=True) +parser.add_argument('--binary', required=True) +parser.add_argument('--source', required=True) +parser.add_argument('--fixture', required=True) +args = parser.parse_args() +build = pathlib.Path(args.build).resolve() +binary = pathlib.Path(args.binary).resolve() +source = pathlib.Path(args.source).resolve() +fixture = pathlib.Path(args.fixture).resolve() +manifest = (build / 'build.ninja').read_text(encoding='utf-8') +match = re.search(r'^build (\S*?/sources/main\.cpp\.(?:obj|o)):', manifest, re.MULTILINE) +if not match: + sys.exit('Cannot find the application main object in the Ninja build.') +main_object = match.group(1) +probe_object = main_object.replace('main.cpp.', 'element_text_resize_probe.cpp.') +probe_binary = binary.with_name('element_text_resize_probe' + binary.suffix) +def escape(path): + return pathlib.Path(path).as_posix().replace('$', '$$').replace(':', '$:').replace(' ', '$ ') +main_source = source.parents[2] / 'sources' / 'main.cpp' +manifest = manifest.replace(main_object, probe_object) +manifest = manifest.replace(escape(main_source), escape(source)) +if binary.suffix: + manifest = manifest.replace(binary.name, probe_binary.name) +else: + old_target = escape(binary.name if binary.parent == build else binary) + new_target = escape(probe_binary.name if probe_binary.parent == build else probe_binary) + manifest = manifest.replace('build ' + old_target + ':', 'build ' + new_target + ':') + old_file = binary.name if binary.parent == build else binary.as_posix() + new_file = probe_binary.name if probe_binary.parent == build else probe_binary.as_posix() + manifest = re.sub(r'^ TARGET_FILE = ' + re.escape(old_file) + r'$', + ' TARGET_FILE = ' + new_file, manifest, flags=re.MULTILINE) +probe_manifest = build / 'element-text-resize-probe.ninja' +probe_manifest.write_text(manifest, encoding='utf-8') +ninja = shutil.which('ninja') +if not ninja: + sys.exit('Ninja is required for the integration probe.') +target = probe_binary.name if probe_binary.parent == build else str(probe_binary) +subprocess.run([ninja, '-C', str(build), '-f', probe_manifest.name, target], check=True) +output = build / 'element-text-resize' +output.mkdir(exist_ok=True) +report = output / 'results.txt' +result = subprocess.run([str(probe_binary), str(fixture), str(report), str(output)], timeout=90) +if report.exists(): + print(report.read_text(encoding='utf-8', errors='replace')) +sys.exit(result.returncode) From 52adc8b3f806f47b0c317b4df661334bb1469d00 Mon Sep 17 00:00:00 2001 From: DaTweety <129504390+DaTweety@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:19:30 +0200 Subject: [PATCH 2/5] Width handles: a text that wraps keeps its top corner in place Narrowing a symbol's text with the left handle until it wraps onto more lines moved it up: the point kept in place was the middle of the opposite edge, and the middle moves down as the text grows. Keep the top of the opposite edge instead, or the middle or the bottom for a text aligned that way, for both handles. Co-Authored-By: Claude Opus 5.5 --- .../dynamicelementtextitem.cpp | 32 +++++++++++++------ .../qetgraphicsitem/dynamicelementtextitem.h | 1 + tests/qttest/element_text_resize_probe.cpp | 19 +++++++++-- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.cpp b/sources/qetgraphicsitem/dynamicelementtextitem.cpp index 0a04a63b9..78db79049 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.cpp +++ b/sources/qetgraphicsitem/dynamicelementtextitem.cpp @@ -1054,9 +1054,26 @@ void DynamicElementTextItem::handlerMousePressEvent(QetGraphicsHandlerItem *hand //drawing program: remember it in the parent's coordinates, which //take the text's rotation into account m_resize_start_pos = pos(); + m_resize_fixed_parent = mapToParent(resizeAnchor(handle == m_left_resize_handle)); +} + +/** + @brief DynamicElementTextItem::resizeAnchor + @param left_handle : the left width handle is dragged + @return the point of the text, in its own coordinates, that a drag on + a width handle keeps in place: on the opposite edge, and at the top, + the middle or the bottom as the text is aligned. A text that wraps onto + more lines while it narrows then grows away from that point, down for + a text aligned at the top, instead of moving into its neighbours. +*/ +QPointF DynamicElementTextItem::resizeAnchor(bool left_handle) const +{ const QRectF br = boundingRect(); - m_resize_fixed_parent = mapToParent(QPointF(handle == m_left_resize_handle ? br.right() : br.left(), - br.center().y())); + const Qt::Alignment v = alignment() & Qt::AlignVertical_Mask; + const qreal y = (v & Qt::AlignBottom) ? br.bottom() + : (v & Qt::AlignVCenter) ? br.center().y() + : br.top(); + return QPointF(left_handle ? br.right() : br.left(), y); } /** @@ -1079,13 +1096,10 @@ void DynamicElementTextItem::handlerMouseMoveEvent(QetGraphicsHandlerItem *handl qreal new_width = qMax(m_resize_baseline_width + delta, qreal(10)); setTextWidth(new_width); - //The text grows and shrinks on its right side: dragging the left - //handle moves the text so that its right edge stays in place - if (handle == m_left_resize_handle) - { - const QRectF br = boundingRect(); - setPos(pos() + m_resize_fixed_parent - mapToParent(QPointF(br.right(), br.center().y()))); - } + //Keep the opposite edge in place, and the top, middle or bottom + //as the text is aligned, whatever the new number of lines + setPos(pos() + m_resize_fixed_parent + - mapToParent(resizeAnchor(handle == m_left_resize_handle))); updateResizeHandlesPos(); } diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.h b/sources/qetgraphicsitem/dynamicelementtextitem.h index 4fc22b5a8..3077282c7 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.h +++ b/sources/qetgraphicsitem/dynamicelementtextitem.h @@ -170,6 +170,7 @@ class DynamicElementTextItem : public DiagramTextItem void addResizeHandles(); void removeResizeHandles(); void updateResizeHandlesPos(); + QPointF resizeAnchor(bool left_handle) const; void fitAutoTextWidth(); void handlerMousePressEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event); void handlerMouseMoveEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event); diff --git a/tests/qttest/element_text_resize_probe.cpp b/tests/qttest/element_text_resize_probe.cpp index 199c66e8f..ee7d02896 100644 --- a/tests/qttest/element_text_resize_probe.cpp +++ b/tests/qttest/element_text_resize_probe.cpp @@ -5,7 +5,8 @@ // its right edge in place; dragging the right handle keeps its left edge, // as in a drawing program. Before, the text always kept its left edge, so // a drag on the left handle moved the right edge and the text jumped. -// Undo puts back the width and the position together. Checked for an +// Undo puts back the width and the position together. A text narrowed +// until it wraps grows downwards from its top corner. Checked for an // upright and a rotated text. #include #include @@ -56,10 +57,11 @@ static QetGraphicsHandlerItem *handle(DynamicElementTextItem *text, bool left) return found; } +/// The top corner of the text on the left or right, in the folio. static QPointF edge(DynamicElementTextItem *text, bool left) { const QRectF br = text->boundingRect(); - return text->mapToScene(QPointF(left ? br.left() : br.right(), br.center().y())); + return text->mapToScene(QPointF(left ? br.left() : br.right(), br.top())); } /// Drag one handle by dx along the text's own baseline. @@ -101,6 +103,16 @@ static void checkText(Diagram *scene, Element *element, DynamicElementTextItem * check(near(edge(text, true), left), "dragging the right handle keeps the left edge in place"); scene->undoStack().undo(); check(near(text->pos(), pos), "undo of the right handle leaves the position alone"); + + // Narrowed until it wraps onto more lines with the left handle: the + // text grows downwards from its top right corner, it does not move up + // into what stands above it. + const qreal height = text->boundingRect().height(); + drag(scene, text, true, width - 25); + check(text->boundingRect().height() > height + 5, "narrowing the text wraps it onto more lines"); + check(near(edge(text, false), right), "a text that wraps keeps its top right corner in place"); + scene->undoStack().undo(); + check(near(text->pos(), pos), "undo puts the wrapped text back"); element->setSelected(false); } @@ -130,6 +142,9 @@ int main(int argc, char **argv) for (DynamicElementTextItem *t : e->dynamicTextItems()) if (!text) { text = t; element = e; } check(text, "the fixture has a symbol with a text"); + // Several words, so that narrowing it makes it wrap + text->setTextFrom(DynamicElementTextItem::UserText); + text->setText(QStringLiteral("Sockets bathroom first floor, 5")); checkText(scene, element, text); // The same on a text turned by 90 degrees: the fixed edge is From 214b0e3b7d137fa83ca3910169d2b503fdde8567 Mon Sep 17 00:00:00 2001 From: DaTweety <129504390+DaTweety@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:16:27 +0200 Subject: [PATCH 3/5] Width handles keep the point the text is aligned on Which point of a symbol's text stays in place while its width is dragged now follows the text's alignment, the nine points of its alignment dialog, rather than the handle: a text aligned top right keeps its top right corner whichever handle is dragged, one centred keeps its middle, and a text that wraps grows away from that point. Co-Authored-By: Claude Opus 5.5 --- .../dynamicelementtextitem.cpp | 40 ++++++----- .../qetgraphicsitem/dynamicelementtextitem.h | 2 +- tests/qttest/element_text_resize_probe.cpp | 71 +++++++++++-------- 3 files changed, 65 insertions(+), 48 deletions(-) diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.cpp b/sources/qetgraphicsitem/dynamicelementtextitem.cpp index 78db79049..1c056a2ad 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.cpp +++ b/sources/qetgraphicsitem/dynamicelementtextitem.cpp @@ -1041,6 +1041,8 @@ void DynamicElementTextItem::updateResizeHandlesPos() */ void DynamicElementTextItem::handlerMousePressEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event) { + Q_UNUSED(handle) + //The actual property value, kept as-is (possibly -1, meaning "auto") //so a later undo restores the exact original state rather than a //synthesized fixed width. @@ -1050,30 +1052,33 @@ void DynamicElementTextItem::handlerMousePressEvent(QetGraphicsHandlerItem *hand m_resize_baseline_width = (m_resize_original_width < 0) ? frameRect().width() : m_resize_original_width; m_resize_start_local_x = mapFromScene(event->scenePos()).x(); - //The edge opposite the dragged handle stays where it is, as in a - //drawing program: remember it in the parent's coordinates, which - //take the text's rotation into account + //The point the text is aligned on stays where it is: remember it + //in the parent's coordinates, which take the text's rotation + //into account m_resize_start_pos = pos(); - m_resize_fixed_parent = mapToParent(resizeAnchor(handle == m_left_resize_handle)); + m_resize_fixed_parent = mapToParent(resizeAnchor()); } /** @brief DynamicElementTextItem::resizeAnchor - @param left_handle : the left width handle is dragged @return the point of the text, in its own coordinates, that a drag on - a width handle keeps in place: on the opposite edge, and at the top, - the middle or the bottom as the text is aligned. A text that wraps onto - more lines while it narrows then grows away from that point, down for - a text aligned at the top, instead of moving into its neighbours. + a width handle keeps in place: the point it is aligned on, one of the + nine of its alignment (left, centre or right; top, middle or bottom). + A text aligned top right keeps its top right corner whichever handle + is dragged, and a text that wraps onto more lines while it narrows + grows away from that point instead of moving into its neighbours. */ -QPointF DynamicElementTextItem::resizeAnchor(bool left_handle) const +QPointF DynamicElementTextItem::resizeAnchor() const { const QRectF br = boundingRect(); - const Qt::Alignment v = alignment() & Qt::AlignVertical_Mask; - const qreal y = (v & Qt::AlignBottom) ? br.bottom() - : (v & Qt::AlignVCenter) ? br.center().y() + const Qt::Alignment a = alignment(); + const qreal x = (a & Qt::AlignRight) ? br.right() + : (a & Qt::AlignHCenter) ? br.center().x() + : br.left(); + const qreal y = (a & Qt::AlignBottom) ? br.bottom() + : (a & Qt::AlignVCenter) ? br.center().y() : br.top(); - return QPointF(left_handle ? br.right() : br.left(), y); + return QPointF(x, y); } /** @@ -1096,10 +1101,9 @@ void DynamicElementTextItem::handlerMouseMoveEvent(QetGraphicsHandlerItem *handl qreal new_width = qMax(m_resize_baseline_width + delta, qreal(10)); setTextWidth(new_width); - //Keep the opposite edge in place, and the top, middle or bottom - //as the text is aligned, whatever the new number of lines - setPos(pos() + m_resize_fixed_parent - - mapToParent(resizeAnchor(handle == m_left_resize_handle))); + //Keep the point the text is aligned on in place, whatever the new + //width and number of lines + setPos(pos() + m_resize_fixed_parent - mapToParent(resizeAnchor())); updateResizeHandlesPos(); } diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.h b/sources/qetgraphicsitem/dynamicelementtextitem.h index 3077282c7..c009cc537 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.h +++ b/sources/qetgraphicsitem/dynamicelementtextitem.h @@ -170,7 +170,7 @@ class DynamicElementTextItem : public DiagramTextItem void addResizeHandles(); void removeResizeHandles(); void updateResizeHandlesPos(); - QPointF resizeAnchor(bool left_handle) const; + QPointF resizeAnchor() const; void fitAutoTextWidth(); void handlerMousePressEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event); void handlerMouseMoveEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event); diff --git a/tests/qttest/element_text_resize_probe.cpp b/tests/qttest/element_text_resize_probe.cpp index ee7d02896..4f17bbbc0 100644 --- a/tests/qttest/element_text_resize_probe.cpp +++ b/tests/qttest/element_text_resize_probe.cpp @@ -1,13 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later // Width handles of a symbol's text, linked against the application's objects. // -// Dragging the left handle of a symbol's text changes its width and keeps -// its right edge in place; dragging the right handle keeps its left edge, -// as in a drawing program. Before, the text always kept its left edge, so -// a drag on the left handle moved the right edge and the text jumped. -// Undo puts back the width and the position together. A text narrowed -// until it wraps grows downwards from its top corner. Checked for an -// upright and a rotated text. +// Dragging a width handle of a symbol's text changes its width and keeps +// the point the text is aligned on in place, one of the nine of its +// alignment: a text aligned top right keeps its top right corner, one +// centred its middle. Before, the text always kept its left edge, so a +// drag on the left handle moved the right edge and the text jumped. A +// text narrowed until it wraps grows away from that point. Undo puts back +// the width and the position together. Checked for four alignments, on +// an upright and on a rotated text. #include #include #include @@ -57,11 +58,14 @@ static QetGraphicsHandlerItem *handle(DynamicElementTextItem *text, bool left) return found; } -/// The top corner of the text on the left or right, in the folio. -static QPointF edge(DynamicElementTextItem *text, bool left) +/// The point the text is aligned on, one of its nine, in the folio. +static QPointF anchor(DynamicElementTextItem *text) { const QRectF br = text->boundingRect(); - return text->mapToScene(QPointF(left ? br.left() : br.right(), br.top())); + const Qt::Alignment a = text->alignment(); + const qreal x = (a & Qt::AlignRight) ? br.right() : (a & Qt::AlignHCenter) ? br.center().x() : br.left(); + const qreal y = (a & Qt::AlignBottom) ? br.bottom() : (a & Qt::AlignVCenter) ? br.center().y() : br.top(); + return text->mapToScene(QPointF(x, y)); } /// Drag one handle by dx along the text's own baseline. @@ -76,46 +80,55 @@ static void drag(Diagram *scene, DynamicElementTextItem *text, bool left, qreal mouse(scene, h, QEvent::GraphicsSceneMouseRelease, end); } -static void checkText(Diagram *scene, Element *element, DynamicElementTextItem *text) +static void checkText(Diagram *scene, Element *element, DynamicElementTextItem *text, Qt::Alignment alignment) { + text->setTextWidth(-1); + text->setAlignment(alignment); element->setSelected(true); const QPointF pos = text->pos(); const qreal width = text->boundingRect().width(); - const QPointF right = edge(text, false); + const QPointF fixed = anchor(text); - // Left handle 30 to the left: wider, right edge where it was. + // Left handle 30 outwards: wider, the alignment point where it was. drag(scene, text, true, -30); check(text->boundingRect().width() > width + 10, "the left handle widens the text"); - check(near(edge(text, false), right), "dragging the left handle keeps the right edge in place"); - + check(near(anchor(text), fixed), "dragging the left handle keeps the alignment point in place"); scene->undoStack().undo(); check(near(text->pos(), pos), "undo puts the text back where it was"); check(qAbs(text->boundingRect().width() - width) < 0.01, "undo puts the width back"); scene->undoStack().redo(); - check(near(edge(text, false), right), "redo keeps the right edge in place"); + check(near(anchor(text), fixed), "redo keeps the alignment point in place"); scene->undoStack().undo(); - // Right handle 30 to the right: wider, left edge where it was. + // Right handle 30 outwards: the same point stays. element->setSelected(true); - const QPointF left = edge(text, true); drag(scene, text, false, 30); check(text->boundingRect().width() > width + 10, "the right handle widens the text"); - check(near(edge(text, true), left), "dragging the right handle keeps the left edge in place"); + check(near(anchor(text), fixed), "dragging the right handle keeps the alignment point in place"); scene->undoStack().undo(); - check(near(text->pos(), pos), "undo of the right handle leaves the position alone"); + check(near(text->pos(), pos), "undo of the right handle puts the text back"); - // Narrowed until it wraps onto more lines with the left handle: the - // text grows downwards from its top right corner, it does not move up - // into what stands above it. + // Narrowed until it wraps onto more lines: it grows away from the + // alignment point, a text aligned at the top downwards, so that it + // does not move into what stands above it. + element->setSelected(true); const qreal height = text->boundingRect().height(); - drag(scene, text, true, width - 25); + drag(scene, text, false, 25 - width); check(text->boundingRect().height() > height + 5, "narrowing the text wraps it onto more lines"); - check(near(edge(text, false), right), "a text that wraps keeps its top right corner in place"); + check(near(anchor(text), fixed), "a text that wraps keeps its alignment point in place"); scene->undoStack().undo(); check(near(text->pos(), pos), "undo puts the wrapped text back"); element->setSelected(false); } +static void checkAll(Diagram *scene, Element *element, DynamicElementTextItem *text) +{ + checkText(scene, element, text, Qt::AlignLeft | Qt::AlignTop); + checkText(scene, element, text, Qt::AlignRight | Qt::AlignTop); + checkText(scene, element, text, Qt::AlignHCenter | Qt::AlignVCenter); + checkText(scene, element, text, Qt::AlignRight | Qt::AlignBottom); +} + int main(int argc, char **argv) { QApplication app(argc, argv); @@ -146,13 +159,13 @@ int main(int argc, char **argv) text->setTextFrom(DynamicElementTextItem::UserText); text->setText(QStringLiteral("Sockets bathroom first floor, 5")); - checkText(scene, element, text); - // The same on a text turned by 90 degrees: the fixed edge is + checkAll(scene, element, text); + // The same on a text turned by 90 degrees: the fixed point is // found along the text's own baseline, not the folio's x axis. text->setRotation(90); - checkText(scene, element, text); + checkAll(scene, element, text); - std::puts("PASS: a symbol text's width handles keep the opposite edge in place, undo restores both"); + std::puts("PASS: a symbol text's width handles keep its alignment point in place, undo restores both"); } catch (const std::exception &error) { From 8f2cd9f039eb24ee9506a0e40edbb394d8e75a0d Mon Sep 17 00:00:00 2001 From: DaTweety <129504390+DaTweety@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:45:07 +0200 Subject: [PATCH 4/5] Width handles: read the drag in the text's coordinates as they were at the press The text moves on every step of a drag to keep its alignment point in place. Mapping the mouse with its current transform made the delta grow with every step, so a right or centre aligned text ran away: 90 px of mouse gave +1220 px of width. The probe now drags in 30 steps and checks the width grows by exactly as much as the mouse moved. Reported by ispyisail in the review of #1555. Co-Authored-By: Claude Opus 5.5 --- .../dynamicelementtextitem.cpp | 8 ++++++-- .../qetgraphicsitem/dynamicelementtextitem.h | 1 + tests/qttest/element_text_resize_probe.cpp | 19 ++++++++++++++----- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.cpp b/sources/qetgraphicsitem/dynamicelementtextitem.cpp index 1c056a2ad..0b8dbf982 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.cpp +++ b/sources/qetgraphicsitem/dynamicelementtextitem.cpp @@ -1050,7 +1050,11 @@ void DynamicElementTextItem::handlerMousePressEvent(QetGraphicsHandlerItem *hand //A concrete baseline for the live drag's delta math, which can't //start from -1. m_resize_baseline_width = (m_resize_original_width < 0) ? frameRect().width() : m_resize_original_width; - m_resize_start_local_x = mapFromScene(event->scenePos()).x(); + //The text moves while it is dragged, to keep its alignment point + //in place: read the mouse in the text's coordinates as they were + //at the press, otherwise the delta grows with every step + m_resize_scene_to_local = sceneTransform().inverted(); + m_resize_start_local_x = m_resize_scene_to_local.map(event->scenePos()).x(); //The point the text is aligned on stays where it is: remember it //in the parent's coordinates, which take the text's rotation @@ -1093,7 +1097,7 @@ QPointF DynamicElementTextItem::resizeAnchor() const */ void DynamicElementTextItem::handlerMouseMoveEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event) { - qreal local_x = mapFromScene(event->scenePos()).x(); + qreal local_x = m_resize_scene_to_local.map(event->scenePos()).x(); qreal delta = local_x - m_resize_start_local_x; if (handle == m_left_resize_handle) delta = -delta; diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.h b/sources/qetgraphicsitem/dynamicelementtextitem.h index c009cc537..275420f36 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.h +++ b/sources/qetgraphicsitem/dynamicelementtextitem.h @@ -216,6 +216,7 @@ class DynamicElementTextItem : public DiagramTextItem qreal m_resize_start_local_x = 0; QPointF m_resize_start_pos, m_resize_fixed_parent; + QTransform m_resize_scene_to_local; QMetaObject::Connection m_resize_handles_con; }; diff --git a/tests/qttest/element_text_resize_probe.cpp b/tests/qttest/element_text_resize_probe.cpp index 4f17bbbc0..ba144873a 100644 --- a/tests/qttest/element_text_resize_probe.cpp +++ b/tests/qttest/element_text_resize_probe.cpp @@ -68,18 +68,26 @@ static QPointF anchor(DynamicElementTextItem *text) return text->mapToScene(QPointF(x, y)); } -/// Drag one handle by dx along the text's own baseline. +/// Drag one handle by dx along the text's own baseline, in 30 steps as a +/// mouse does: the text moves on every step to keep its alignment point, +/// so each step must be read the same way as the first. static void drag(Diagram *scene, DynamicElementTextItem *text, bool left, qreal dx) { QetGraphicsHandlerItem *h = handle(text, left); const QPointF start = h->scenePos(); const QPointF end = start + text->mapToScene(QPointF(dx, 0)) - text->mapToScene(QPointF(0, 0)); mouse(scene, h, QEvent::GraphicsSceneMousePress, start); - mouse(scene, h, QEvent::GraphicsSceneMouseMove, (start + end) / 2); - mouse(scene, h, QEvent::GraphicsSceneMouseMove, end); + for (int i = 1; i <= 30; ++i) + mouse(scene, h, QEvent::GraphicsSceneMouseMove, start + (end - start) * i / 30); mouse(scene, h, QEvent::GraphicsSceneMouseRelease, end); } +/// The width changed by exactly as much as the mouse moved. +static void checkGrowth(DynamicElementTextItem *text, qreal before, qreal moved, const char *message) +{ + check(qAbs(text->textWidth() - before - moved) < 0.5, message); +} + static void checkText(Diagram *scene, Element *element, DynamicElementTextItem *text, Qt::Alignment alignment) { text->setTextWidth(-1); @@ -88,10 +96,11 @@ static void checkText(Diagram *scene, Element *element, DynamicElementTextItem * const QPointF pos = text->pos(); const qreal width = text->boundingRect().width(); const QPointF fixed = anchor(text); + const qreal frame = text->frameRect().width(); // Left handle 30 outwards: wider, the alignment point where it was. drag(scene, text, true, -30); - check(text->boundingRect().width() > width + 10, "the left handle widens the text"); + checkGrowth(text, frame, 30, "the left handle widens the text by as much as the mouse moved"); check(near(anchor(text), fixed), "dragging the left handle keeps the alignment point in place"); scene->undoStack().undo(); check(near(text->pos(), pos), "undo puts the text back where it was"); @@ -103,7 +112,7 @@ static void checkText(Diagram *scene, Element *element, DynamicElementTextItem * // Right handle 30 outwards: the same point stays. element->setSelected(true); drag(scene, text, false, 30); - check(text->boundingRect().width() > width + 10, "the right handle widens the text"); + checkGrowth(text, frame, 30, "the right handle widens the text by as much as the mouse moved"); check(near(anchor(text), fixed), "dragging the right handle keeps the alignment point in place"); scene->undoStack().undo(); check(near(text->pos(), pos), "undo of the right handle puts the text back"); From 1bf076eca8c065c8515b065c629016b908446fd2 Mon Sep 17 00:00:00 2001 From: DaTweety <129504390+DaTweety@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:53:52 +0200 Subject: [PATCH 5/5] Width handles: a text is not dragged narrower than its widest word Narrower, a long word ran out of the text's box while the short lines were still aligned inside the too narrow width: on a right aligned or centred text a lone "1" stood in the middle under its words, and moved on every step. Found on a real plan (-XS20, "Steckdosen Windfang, 1"). The probe checks that no line runs out of the text after narrowing, and also for a text aligned bottom left. Co-Authored-By: Claude Opus 5.5 --- .../dynamicelementtextitem.cpp | 18 +++++++++++++++++- .../qetgraphicsitem/dynamicelementtextitem.h | 1 + tests/qttest/element_text_resize_probe.cpp | 19 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.cpp b/sources/qetgraphicsitem/dynamicelementtextitem.cpp index 0b8dbf982..6f178f8dd 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.cpp +++ b/sources/qetgraphicsitem/dynamicelementtextitem.cpp @@ -16,6 +16,8 @@ along with QElectroTech. If not, see . */ #include "dynamicelementtextitem.h" +#include +#include #include "../QetGraphicsItemModeler/textresizehandles.h" #include "../shownkinds.h" #include "../qetproject.h" @@ -1063,6 +1065,20 @@ void DynamicElementTextItem::handlerMousePressEvent(QetGraphicsHandlerItem *hand m_resize_fixed_parent = mapToParent(resizeAnchor()); } +/** + @brief DynamicElementTextItem::minimumTextWidth + @return the narrowest width the text can be dragged to: that of its + widest word. Narrower, a long word runs out of the text's box while + the short lines are still aligned inside it, and a centred or right + aligned text falls apart (a lone "1" in the middle under its words). +*/ +qreal DynamicElementTextItem::minimumTextWidth() const +{ + QScopedPointer narrowest(document()->clone()); + narrowest->setTextWidth(0); + return qMax(narrowest->idealWidth(), qreal(10)); +} + /** @brief DynamicElementTextItem::resizeAnchor @return the point of the text, in its own coordinates, that a drag on @@ -1102,7 +1118,7 @@ void DynamicElementTextItem::handlerMouseMoveEvent(QetGraphicsHandlerItem *handl if (handle == m_left_resize_handle) delta = -delta; - qreal new_width = qMax(m_resize_baseline_width + delta, qreal(10)); + qreal new_width = qMax(m_resize_baseline_width + delta, minimumTextWidth()); setTextWidth(new_width); //Keep the point the text is aligned on in place, whatever the new diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.h b/sources/qetgraphicsitem/dynamicelementtextitem.h index 275420f36..3bc2a0afc 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.h +++ b/sources/qetgraphicsitem/dynamicelementtextitem.h @@ -171,6 +171,7 @@ class DynamicElementTextItem : public DiagramTextItem void removeResizeHandles(); void updateResizeHandlesPos(); QPointF resizeAnchor() const; + qreal minimumTextWidth() const; void fitAutoTextWidth(); void handlerMousePressEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event); void handlerMouseMoveEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event); diff --git a/tests/qttest/element_text_resize_probe.cpp b/tests/qttest/element_text_resize_probe.cpp index ba144873a..965efd5ad 100644 --- a/tests/qttest/element_text_resize_probe.cpp +++ b/tests/qttest/element_text_resize_probe.cpp @@ -14,6 +14,9 @@ #include #include #include +#include +#include +#include #include #include @@ -82,6 +85,20 @@ static void drag(Diagram *scene, DynamicElementTextItem *text, bool left, qreal mouse(scene, h, QEvent::GraphicsSceneMouseRelease, end); } +/// Every line lies inside the text's width: none runs out of it. +static bool linesInside(DynamicElementTextItem *text) +{ + QTextDocument *doc = text->document(); + const qreal inner = doc->textWidth() - 2 * doc->documentMargin(); + for (QTextBlock b = doc->begin(); b.isValid(); b = b.next()) + for (int i = 0; i < b.layout()->lineCount(); ++i) { + const QTextLine line = b.layout()->lineAt(i); + if (line.x() < -0.01 || line.x() + line.naturalTextWidth() > inner + 0.01) + return false; + } + return true; +} + /// The width changed by exactly as much as the mouse moved. static void checkGrowth(DynamicElementTextItem *text, qreal before, qreal moved, const char *message) { @@ -124,6 +141,7 @@ static void checkText(Diagram *scene, Element *element, DynamicElementTextItem * const qreal height = text->boundingRect().height(); drag(scene, text, false, 25 - width); check(text->boundingRect().height() > height + 5, "narrowing the text wraps it onto more lines"); + check(linesInside(text), "a text is not narrower than its widest word: no line runs out of it"); check(near(anchor(text), fixed), "a text that wraps keeps its alignment point in place"); scene->undoStack().undo(); check(near(text->pos(), pos), "undo puts the wrapped text back"); @@ -136,6 +154,7 @@ static void checkAll(Diagram *scene, Element *element, DynamicElementTextItem *t checkText(scene, element, text, Qt::AlignRight | Qt::AlignTop); checkText(scene, element, text, Qt::AlignHCenter | Qt::AlignVCenter); checkText(scene, element, text, Qt::AlignRight | Qt::AlignBottom); + checkText(scene, element, text, Qt::AlignLeft | Qt::AlignBottom); } int main(int argc, char **argv)