mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-09 13:34:14 +02:00
Merge pull request #1398 from saschbe/fix/local-small-handles
Add smaller resize handle sizes
This commit is contained in:
@@ -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()
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}')
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user