Merge pull request #1398 from saschbe/fix/local-small-handles

Add smaller resize handle sizes
This commit is contained in:
Laurent Trinques
2026-10-08 16:29:07 +02:00
committed by GitHub
12 changed files with 561 additions and 26 deletions
+13
View File
@@ -0,0 +1,13 @@
cmake_minimum_required(VERSION 3.16)
project(handle_refresh_probe LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets)
add_executable(handle_refresh_probe main.cpp
${QET_SOURCE}/sources/QetGraphicsItemModeler/qetgraphicshandleritem.cpp
${QET_SOURCE}/sources/QetGraphicsItemModeler/qetgraphicshandleritem.h)
target_include_directories(handle_refresh_probe PRIVATE ${QET_SOURCE}/sources)
target_link_libraries(handle_refresh_probe PRIVATE Qt6::Widgets)
if(WIN32)
target_link_libraries(handle_refresh_probe PRIVATE psapi)
endif()
+69
View File
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Synthetic raster viewport workload, not a full editor latency benchmark.
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QElapsedTimer>
#include <QPainter>
#include <cstdio>
#include <ctime>
#ifdef Q_OS_WIN
#include <windows.h>
#include <psapi.h>
#endif
#include "QetGraphicsItemModeler/qetgraphicshandleritem.h"
static double cpuMilliseconds() {
#ifdef Q_OS_WIN
FILETIME created,exited,kernel,user;
if(GetProcessTimes(GetCurrentProcess(),&created,&exited,&kernel,&user)) {
ULARGE_INTEGER k,u; k.LowPart=kernel.dwLowDateTime;k.HighPart=kernel.dwHighDateTime;
u.LowPart=user.dwLowDateTime;u.HighPart=user.dwHighDateTime;
return double(k.QuadPart+u.QuadPart)/10000;
}
return 0;
#else
return double(std::clock())*1000/CLOCKS_PER_SEC;
#endif
}
int main(int argc,char **argv) {
QApplication app(argc,argv);
const qreal size=QString::fromLocal8Bit(argv[1]).toDouble();
const int count=QString::fromLocal8Bit(argv[2]).toInt();
const QString operation=QString::fromLocal8Bit(argv[3]);
QGraphicsScene scene; scene.setSceneRect(-2000,-2000,4000,4000);
QList<QGraphicsRectItem *> objects;
for(int i=0;i<count/4;++i) {
auto *object=scene.addRect(0,0,32,24,QPen(Qt::black),QBrush(Qt::lightGray));
object->setPos((i%32)*36-576,(i/32)*28-224); objects<<object;
for(auto *handle:QetGraphicsHandlerItem::handlerForPoint({{0,0},{32,0},{32,24},{0,24}},size)) {
handle->setParentItem(object); handle->setColor(Qt::darkGreen);
}
}
QGraphicsView view(&scene); view.resize(1280,800); view.setSceneRect(scene.sceneRect());
view.show(); QApplication::processEvents();
QImage image(1280,800,QImage::Format_ARGB32_Premultiplied);
auto frame=[&](int i) {
if(operation=="zoom") {view.resetTransform();view.scale(0.75+0.005*(i%60),0.75+0.005*(i%60));}
if(operation=="pan") view.centerOn((i%60)*2,0);
if(operation=="manipulate") for(auto *object:objects) {
object->setRotation(i%30); object->setRect(0,0,32+(i%8),24); object->moveBy(i%2?1:-1,0);
}
image.fill(Qt::white); QPainter painter(&image); view.render(&painter);
};
for(int i=0;i<20;++i) frame(i);
std::puts("size,handles,operation,repeat,ms_per_frame,cpu_ms_per_frame,working_set_bytes");
for(int repeat=0;repeat<7;++repeat) {
const auto cpu=cpuMilliseconds(); QElapsedTimer timer; timer.start();
for(int i=0;i<120;++i) frame(i);
const double elapsed=timer.nsecsElapsed()/1e6/120;
const double cpuElapsed=(cpuMilliseconds()-cpu)/120;
unsigned long long memory=0;
#ifdef Q_OS_WIN
PROCESS_MEMORY_COUNTERS counters{}; counters.cb=sizeof(counters);
if(GetProcessMemoryInfo(GetCurrentProcess(),&counters,sizeof(counters))) memory=counters.WorkingSetSize;
#endif
std::printf("%.2f,%d,%s,%d,%.6f,%.6f,%llu\n",size,count,qPrintable(operation),repeat,elapsed,cpuElapsed,memory);
}
}
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Run identical Release component workloads; preserve samples and variability."""
import argparse
import csv
import io
import os
import pathlib
import statistics
import subprocess
p = argparse.ArgumentParser()
p.add_argument('--reference', required=True)
p.add_argument('--modified', required=True)
p.add_argument('--output', required=True)
a = p.parse_args()
env = os.environ.copy()
env['QT_QPA_PLATFORM'] = 'offscreen'
rows = []
# Compare x1 first, then exercise every remaining size on the modified code.
for size in [10, 2.5, 5, 7.5, 20, 30]:
for count in [16, 2048]:
for operation in ['zoom', 'pan', 'manipulate']:
variants = [('reference', a.reference), ('modified', a.modified)] if size == 10 else [('modified', a.modified)]
for variant, binary in variants:
result = subprocess.run([binary, str(size), str(count), operation], env=env,
capture_output=True, text=True, check=True, timeout=120)
samples = list(csv.DictReader(io.StringIO(result.stdout)))
assert len(samples) == 7
rows.extend(dict(variant=variant, **sample) for sample in samples)
output = pathlib.Path(a.output)
output.mkdir(parents=True, exist_ok=True)
with (output / 'samples.csv').open('w', newline='', encoding='utf-8') as stream:
writer = csv.DictWriter(stream, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
groups = {}
for row in rows:
key = (row['variant'], row['size'], row['handles'], row['operation'])
groups.setdefault(key, []).append(row)
lines = ['# Release handle refresh measurements', '',
'Synthetic QGraphicsView raster rendering at 1280×800, offscreen, on the same PC.',
'Each sample comprises 120 frames; 20 warm-up frames precede seven samples.',
'16 or 2048 handles; same scenes and workload code for both builds.',
'Manipulate changes position, rectangle size and rotation of all scene objects.',
'This is not full-editor mouse latency, GPU frame presentation or human usability.', '',
'| Code | Factor | Handles | Workload | Mean ms/frame ± SD | Min–max | CPU ms/frame | Working set MiB |',
'|---|---:|---:|---|---:|---:|---:|---:|']
for (variant, size, count, operation), samples in groups.items():
wall = [float(s['ms_per_frame']) for s in samples]
cpu = [float(s['cpu_ms_per_frame']) for s in samples]
memory = [int(s['working_set_bytes']) / 1048576 for s in samples]
lines.append(f'| {variant} | {float(size)/10:g} | {count} | {operation} | '
f'{statistics.mean(wall):.3f} ± {statistics.stdev(wall):.3f} | '
f'{min(wall):.3f}–{max(wall):.3f} | {statistics.mean(cpu):.3f} | '
f'{statistics.median(memory):.1f} |')
(output / 'measurements.md').write_text('\n'.join(lines)+'\n', encoding='utf-8')
print(f'{len(rows)} samples saved in {output}')
+15
View File
@@ -74,6 +74,13 @@ target_include_directories(tst_darkimagelayers PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_darkimagelayers PRIVATE Qt::Test Qt::Widgets)
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja")
add_test(NAME tst_handlersizeintegration
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_handle_size_probe.py
--build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech>
--source ${CMAKE_CURRENT_SOURCE_DIR}/handle_size_probe.cpp
--fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet)
set_tests_properties(tst_handlersizeintegration PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE
ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
add_test(NAME tst_attachedplainpaste
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_attached_plain_paste_probe.py
--build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech>
@@ -208,6 +215,14 @@ 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)
# Shared handle geometry and picking, including fractional sizes.
add_executable(tst_handlersize tst_handlersize.cpp
${QET_DIR}/sources/QetGraphicsItemModeler/qetgraphicshandleritem.cpp)
add_test(NAME tst_handlersize COMMAND tst_handlersize)
set_tests_properties(tst_handlersize PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_handlersize PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_handlersize 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(
+229
View File
@@ -0,0 +1,229 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QApplication>
#include <QComboBox>
#include <QTemporaryDir>
#include <QSettings>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsSceneHoverEvent>
#include <QStandardItemModel>
#include <cstdio>
#include <stdexcept>
#include <algorithm>
#include "../../sources/qetapp.h"
#include "../../sources/qetdiagrameditor.h"
#include "../../sources/qetproject.h"
#include "../../sources/qetresult.h"
#include "../../sources/qetmessagebox.h"
#include "../../sources/projectview.h"
#include "../../sources/diagram.h"
#include "../../sources/utils/qetutils.h"
#include "../../sources/ui/diagrameditorhandlersizewidget.h"
#include "../../sources/QetGraphicsItemModeler/qetgraphicshandleritem.h"
#include "../../sources/qetgraphicsitem/independenttextitem.h"
#include "../../sources/qetgraphicsitem/dynamicelementtextitem.h"
#include "../../sources/qetgraphicsitem/diagramimageitem.h"
#include "../../sources/qetgraphicsitem/qetshapeitem.h"
#include "../../sources/editor/ui/qetelementeditor.h"
#include "../../sources/editor/elementscene.h"
#include "../../sources/editor/graphicspart/partrectangle.h"
#include "../../sources/editor/graphicspart/partdynamictextfield.h"
#include "../../sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
#include "../../sources/qetgraphicsitem/conductor.h"
static void check(bool value,const char *why){if(!value)throw std::runtime_error(why);}
static QString canonicalProjectXml(QDomDocument document) {
// Scene index updates can reorder equal-z conductors. Compare their
// complete serialized content in UUID order, rather than scene order.
const auto groups=document.elementsByTagName("conductors");
for(int i=0;i<groups.size();++i) {
auto group=groups.at(i).toElement();QList<QDomElement> wires;
for(auto wire=group.firstChildElement("conductor");!wire.isNull();wire=wire.nextSiblingElement("conductor"))wires<<wire;
std::sort(wires.begin(),wires.end(),[](const QDomElement &a,const QDomElement &b){return a.attribute("uuid")<b.attribute("uuid");});
for(auto wire:wires)group.appendChild(wire);
}
return document.toString();
}
static void mouse(Diagram *scene,QGraphicsItem *item,QEvent::Type type,const QPointF &point,const QPointF *press=nullptr) {
QGraphicsSceneMouseEvent event(type);event.setButton(Qt::LeftButton);
event.setButtons(type==QEvent::GraphicsSceneMouseRelease?Qt::NoButton:Qt::LeftButton);
event.setScenePos(point);event.setPos(item->mapFromScene(point));
event.setButtonDownScenePos(Qt::LeftButton,press?*press:point);scene->sendEvent(item,&event);
}
static QetGraphicsHandlerItem *bottomRightHandle(Diagram *scene) {
QetGraphicsHandlerItem *found=nullptr;
for(auto *item:scene->items())if(item->type()==QetGraphicsHandlerItem::Type&&item->isVisible()) {
auto *handle=qgraphicsitem_cast<QetGraphicsHandlerItem *>(item);
if(!found||handle->scenePos().x()+handle->scenePos().y()>found->scenePos().x()+found->scenePos().y())found=handle;
}
check(found,"mouse gesture handle present");return found;
}
static void drag(Diagram *scene,QGraphicsItem *item,const QPointF &end) {
const auto start=item->scenePos();
mouse(scene,item,QEvent::GraphicsSceneMousePress,start);
mouse(scene,item,QEvent::GraphicsSceneMouseMove,end,&start);
mouse(scene,item,QEvent::GraphicsSceneMouseRelease,end,&start);
}
int main(int argc,char **argv) {
const QString fixture=QString::fromLocal8Bit(argv[1]);
const QString report=QString::fromLocal8Bit(argv[2]);
QTemporaryDir isolated;
QByteArray config="--config-dir="+isolated.path().toUtf8();
QByteArray data="--data-dir="+isolated.path().toUtf8();
QByteArray collection="--common-elements-dir="+isolated.path().toUtf8();
char *args[]={argv[0],config.data(),data.data(),collection.data()};int count=4;
QApplication app(count,args);
std::freopen(report.toLocal8Bit().constData(),"w",stdout);
std::setbuf(stdout,nullptr);
QSettings::setDefaultFormat(QSettings::IniFormat);
QSettings::setPath(QSettings::IniFormat,QSettings::UserScope,isolated.path());
QETProject::setBackupEnabled(false);QET::QetMessageBox::setNonInteractive(true);
try {
auto *qet=new QETApp; Q_UNUSED(qet);
auto editors=QETApp::diagramEditors();check(!editors.isEmpty(),"editor created");
auto *editor=editors.first();
auto *project=new QETProject(fixture);check(project->state()==QETProject::Ok,"fixture loaded");
editor->addProjectView(new ProjectView(project));
auto *scene=project->diagrams().first();
auto widgets=editor->findChildren<DiagramEditorHandlerSizeWidget *>();check(!widgets.isEmpty(),"toolbar widget present");
auto *combo=widgets.first()->findChild<QComboBox *>();check(combo&&combo->count()==6,"six choices");
check(combo->currentIndex()==3&&editor->property("graphics_handler_size").toReal()==10,"default x1");
auto *free=new IndependentTextItem;free->setPlainText("Mouse placement");scene->addItem(free);free->setSelected(true);
QPixmap pixmap(30,20);pixmap.fill(Qt::red);auto *image=new DiagramImageItem(pixmap);scene->addItem(image);image->setSelected(true);
auto *shape=new QetShapeItem({100,100},{150,140},QetShapeItem::Rectangle);scene->addItem(shape);shape->setSelected(true);
auto *attached=scene->elements().first()->dynamicTextItems().first();attached->setSelected(true);
auto *background=project->addNewDiagram();
auto *backgroundHandle=new QetGraphicsHandlerItem;
background->addItem(backgroundHandle);
QApplication::processEvents();
const QString xml=project->toXml().toString();
const int undoCount=scene->undoStack().count();const int undoIndex=scene->undoStack().index();
const bool dirty=project->projectWasModified();
const qreal sizes[]={2.5,5,7.5,10,20,30};
for(int i=0;i<6;++i) {
combo->setCurrentIndex(i);QApplication::processEvents();
check(editor->property("graphics_handler_size").toReal()==sizes[i],"fractional editor property");
check(QETUtils::graphicsHandlerSize(image)==sizes[i],"new handle size lookup");
check(backgroundHandle->property("currentSize").toReal()==sizes[i],"inactive folio handles updated");
int handles=0;for(auto *item:scene->items())if(item->type()==QetGraphicsHandlerItem::Type){
++handles;auto *handle=qgraphicsitem_cast<QetGraphicsHandlerItem *>(item);
check(handle->property("currentSize").toReal()==sizes[i],"existing handles updated immediately");
}
check(handles>=8,"multiple selected object handles tested");
image->setSelected(false);image->setSelected(true);
for(auto *item:scene->items())if(item->type()==QetGraphicsHandlerItem::Type)
check(qgraphicsitem_cast<QetGraphicsHandlerItem *>(item)->property("currentSize").toReal()==sizes[i],"recreated handles retain fractions");
DiagramEditorHandlerSizeWidget recreated(editor);
check(recreated.findChild<QComboBox *>()->currentIndex()==i,"toolbar recreation preserves chosen size");
check(project->toXml().toString()==xml,"handle size leaves project XML unchanged");
check(scene->undoStack().count()==undoCount&&scene->undoStack().index()==undoIndex,"handle size leaves undo stack unchanged");
check(project->projectWasModified()==dirty,"handle size leaves dirty state unchanged");
}
for(int i=0;i<6;++i) {
combo->setCurrentIndex(i);
scene->clearSelection();
auto *picture=new DiagramImageItem(pixmap);scene->addItem(picture);picture->setPos(400,400);picture->setSelected(true);
auto *handle=bottomRightHandle(scene);
check(handle->property("currentSize").toReal()==sizes[i],"image mouse handle size");
drag(scene,handle,handle->scenePos()+QPointF(20,20));
check(picture->scaleFactorX()!=1,"image resized by mouse events");
const qreal changed=picture->scaleFactorX();scene->undoStack().undo();
check(picture->scaleFactorX()==1,"image resize undo");scene->undoStack().redo();
check(picture->scaleFactorX()==changed,"image resize redo");scene->undoStack().undo();
const auto centre=picture->mapToScene(picture->imageRect().center());
mouse(scene,picture,QEvent::GraphicsSceneMousePress,centre);
mouse(scene,picture,QEvent::GraphicsSceneMouseRelease,centre);
handle=bottomRightHandle(scene);
const auto pivot=picture->mapToScene(picture->pivot());QTransform turn;turn.rotate(30);
drag(scene,handle,pivot+turn.map(handle->scenePos()-pivot));
check(qAbs(picture->rotationAngle())>1,"image rotated by mouse events");
const qreal angle=picture->rotationAngle();scene->undoStack().undo();
check(qAbs(picture->rotationAngle())<0.001,"image rotation undo");scene->undoStack().redo();
check(qAbs(picture->rotationAngle()-angle)<0.001,"image rotation redo");scene->undoStack().undo();
scene->undoStack().clear();delete picture;
auto *rectangle=new QetShapeItem({0,0},{50,40},QetShapeItem::Rectangle);scene->addItem(rectangle);rectangle->setPos(400,400);rectangle->setSelected(true);
const auto rect=rectangle->rect();handle=bottomRightHandle(scene);
drag(scene,handle,handle->scenePos()+QPointF(20,20));check(rectangle->rect()!=rect,"shape resized by mouse events");
const auto resized=rectangle->rect();scene->undoStack().undo();check(rectangle->rect()==rect,"shape resize undo");
scene->undoStack().redo();check(rectangle->rect()==resized,"shape resize redo");scene->undoStack().undo();
for(int click=0;click<2;++click) {
const auto middle=rectangle->mapToScene(rect.center());
mouse(scene,rectangle,QEvent::GraphicsSceneMousePress,middle);mouse(scene,rectangle,QEvent::GraphicsSceneMouseRelease,middle);
}
handle=bottomRightHandle(scene);const auto shapePivot=rectangle->mapToScene(rectangle->pivot());
drag(scene,handle,shapePivot+turn.map(handle->scenePos()-shapePivot));
check(qAbs(rectangle->rotation())>1,"shape rotated by mouse events");
const qreal shapeAngle=rectangle->rotation();scene->undoStack().undo();check(qAbs(rectangle->rotation())<0.001,"shape rotation undo");
scene->undoStack().redo();check(qAbs(rectangle->rotation()-shapeAngle)<0.001,"shape rotation redo");scene->undoStack().undo();
scene->undoStack().clear();
auto *companion=new DiagramImageItem(pixmap);scene->addItem(companion);companion->setPos(600,400);companion->setSelected(true);
rectangle->setSelected(true);
const auto rectanglePos=rectangle->pos();const auto companionPos=companion->pos();
const auto start=companion->mapToScene(companion->imageRect().center());const auto end=start+QPointF(20,20);
mouse(scene,companion,QEvent::GraphicsSceneMousePress,start);
mouse(scene,companion,QEvent::GraphicsSceneMouseMove,end,&start);
mouse(scene,companion,QEvent::GraphicsSceneMouseRelease,end,&start);
check(companion->pos()==companionPos+QPointF(20,20)&&rectangle->pos()==rectanglePos+QPointF(20,20),"multiple objects moved by mouse events");
scene->undoStack().undo();check(companion->pos()==companionPos&&rectangle->pos()==rectanglePos,"multiple object movement undo");
scene->undoStack().redo();check(companion->pos()==companionPos+QPointF(20,20)&&rectangle->pos()==rectanglePos+QPointF(20,20),"multiple object movement redo");
scene->undoStack().undo();scene->undoStack().clear();delete companion;delete rectangle;
}
std::puts("PASS: image and rectangle mouse resize/rotation, multiple object movement, and undo/redo at all six sizes");
project->setFilePath(QString::fromLocal8Bit(argv[3])+"/handles.qet");
check(project->write().isOk(),"save after handle size changes");
QETProject reopened(project->filePath());check(reopened.state()==QETProject::Ok,"reopen after handle size changes");
bool imageFound=false,textFound=false;
for(auto *item:reopened.diagrams().first()->items()) {
if(auto *loaded=qgraphicsitem_cast<DiagramImageItem *>(item))imageFound|=loaded->pixmap().size()==pixmap.size();
if(auto *loaded=qgraphicsitem_cast<IndependentTextItem *>(item))textFound|=loaded->toPlainText()==free->toPlainText();
}
check(imageFound&&textFound,"object content survives reopen");
auto *elementEditor=new QETElementEditor;elementEditor->show();
auto *elementScene=elementEditor->elementScene();
auto *part=new PartRectangle(elementEditor);part->setRect({0,0,50,40});elementScene->addItems({part});part->setSelected(true);
QApplication::processEvents();
auto checkElementHandles=[&] {
int count=0;
for(auto *item:elementScene->items())if(item->type()==QetGraphicsHandlerItem::Type&&item->isVisible()) {
++count;check(qgraphicsitem_cast<QetGraphicsHandlerItem *>(item)->property("currentSize").toReal()==10,"element editor retains default handle size");
}
check(count>=4,"element editor shared handles present");
};
for(int i=0;i<6;++i){combo->setCurrentIndex(i);checkElementHandles();check(part->rect()==QRectF(0,0,50,40),"element primitive geometry unchanged");}
elementScene->clearSelection();
auto *field=new PartDynamicTextField(elementEditor);field->setPlainText("Element field");elementScene->addItems({field});field->setSelected(true);
QApplication::processEvents();checkElementHandles();
std::puts("PASS: element editor rectangle and dynamic field keep their default shared handles");
QStandardItemModel model(2,2);model.setData(model.index(0,0),"Table");
auto *table=new QetGraphicsTableItem;table->setModel(&model);scene->addItem(table);
const auto tableSize=table->size();
for(int i=0;i<6;++i) {
combo->setCurrentIndex(i);
QGraphicsSceneHoverEvent enter(QEvent::GraphicsSceneHoverEnter);scene->sendEvent(table,&enter);
check(bottomRightHandle(scene)->property("currentSize").toReal()==sizes[i],"table hover handle retains fraction");
check(table->size()==tableSize,"table dimensions unchanged");
QGraphicsSceneHoverEvent leave(QEvent::GraphicsSceneHoverLeave);scene->sendEvent(table,&leave);
}
delete table;std::puts("PASS: table hover handles retain all six sizes without changing the table");
const auto wireFixture=QFileInfo(fixture).dir().absoluteFilePath("../../../examples/tremie_vibrante.qet");
auto *wireProject=new QETProject(wireFixture);check(wireProject->state()==QETProject::Ok,"conductor fixture opens");
editor->addProjectView(new ProjectView(wireProject));
Diagram *wireScene=nullptr;for(auto *diagram:wireProject->diagrams())if(!diagram->conductors().isEmpty()){wireScene=diagram;break;}
check(wireScene,"conductor scene present");auto *wire=wireScene->conductors().first();
wire->setSelected(true);QApplication::processEvents();
const auto wireXml=canonicalProjectXml(wireProject->toXml());
for(int i=0;i<6;++i) {
combo->setCurrentIndex(i);wire->setSelected(false);wire->setSelected(true);
int count=0;for(auto *item:wireScene->items())if(item->type()==QetGraphicsHandlerItem::Type) {
++count;check(qgraphicsitem_cast<QetGraphicsHandlerItem *>(item)->property("currentSize").toReal()==sizes[i],"conductor handles retain fractions");
}
check(count>0,"conductor handles present");
const auto after=canonicalProjectXml(wireProject->toXml());
check(after==wireXml,"conductor project content unchanged");
}
std::puts("PASS: conductor handles retain all six sizes without changing their project");
std::puts("PASS: six toolbar choices, default, fractions, existing/recreated handles, multi-selection, toolbar recreation, unchanged XML/undo/dirty state");
}catch(const std::exception &error){std::printf("FAIL: %s\n",error.what());std::fflush(stdout);std::_Exit(1);}
std::fflush(stdout);std::_Exit(0);
}
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Link the handle-size 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.', 'handle_size_probe.cpp.')
probe_binary = binary.with_name('handle_size_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 / 'handle-size-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 / 'handle-size'
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)
+53
View File
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QtTest>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QGraphicsSceneHoverEvent>
#include "QetGraphicsItemModeler/qetgraphicshandleritem.h"
class tst_handlersize : public QObject
{
Q_OBJECT
private slots:
void sizeChangeCancelsOldHoverAnimation() {
QGraphicsScene scene;
auto *handle=new QetGraphicsHandlerItem(30);scene.addItem(handle);
QGraphicsSceneHoverEvent event(QEvent::GraphicsSceneHoverEnter);
scene.sendEvent(handle,&event);
handle->setSize(2.5);
QTest::qWait(250);
QCOMPARE(handle->property("currentSize").toReal(),qreal(2.5));
}
void fractionalGeometryAndPicking() {
for (qreal size : {2.5, 5.0, 7.5, 10.0, 20.0, 30.0}) {
QGraphicsScene scene;
auto *object = scene.addRect(QRectF(0,0,100,60));
object->setPos(80,40); object->setRotation(37); object->setSelected(true);
const QRectF geometry=object->rect(); const auto transform=object->sceneTransform();
auto handles=QetGraphicsHandlerItem::handlerForPoint({QPointF(120,80)},size);
auto *handle=handles.first(); scene.addItem(handle); handle->setColor(Qt::darkGreen);
QCOMPARE(handle->property("currentSize").toReal(),size);
QCOMPARE(handle->boundingRect().width(),size+2);
const auto position=handle->pos();
for (qreal next : {2.5,5.0,7.5,10.0,20.0,30.0}) {
handle->setSize(next);
QCOMPARE(handle->property("currentSize").toReal(),next);
QCOMPARE(handle->pos(),position); QVERIFY(handle->isVisible());
QCOMPARE(object->rect(),geometry); QCOMPARE(object->sceneTransform(),transform);
for (qreal zoom : {0.25,1.0,4.0}) {
QGraphicsView view(&scene); view.scale(zoom,zoom);
const auto device=handle->deviceTransform(view.viewportTransform());
QCOMPARE(device.mapRect(handle->boundingRect()).width(),next+2);
const QPointF centre=handle->scenePos();
QVERIFY(scene.items(centre,Qt::IntersectsItemShape,Qt::DescendingOrder,view.viewportTransform()).contains(handle));
const QPointF outside=centre+QPointF((next/2+2)/zoom,0);
QVERIFY(!scene.items(outside,Qt::IntersectsItemShape,Qt::DescendingOrder,view.viewportTransform()).contains(handle));
}
}
handle->hide(); QVERIFY(!scene.items(position).contains(handle));
}
}
};
QTEST_MAIN(tst_handlersize)
#include "tst_handlersize.moc"
+2 -1
View File
@@ -176,6 +176,7 @@ private slots:
// and reports the change once, on release.
void dragKeepsTheOppositeCorner()
{
for (qreal size : {2.5, 5.0, 7.5, 10.0, 20.0, 30.0})
for (int c = TextResize::TopLeft ; c <= TextResize::BottomLeft ; ++c)
for (qreal angle : {0.0, 90.0, 37.0}) {
QGraphicsScene scene;
@@ -183,7 +184,7 @@ private slots:
scene.addItem(text);
text->setPos(100, 100);
text->setRotation(angle);
auto *handles = new TextResizeHandles(text);
auto *handles = new TextResizeHandles(text, size);
QSignalSpy finished(handles, &TextResizeHandles::resizeFinished);
const Corner corner = Corner(c);