mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-21 22:04:13 +02:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 $<TARGET_FILE:qelectrotech>
|
||||
--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)
|
||||
|
||||
@@ -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 <QApplication>
|
||||
#include <QFontDatabase>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QSettings>
|
||||
#include <QTemporaryDir>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
|
||||
#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<QetGraphicsHandlerItem *>(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;
|
||||
}
|
||||
+60
@@ -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)
|
||||
Reference in New Issue
Block a user