mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-21 13:54:13 +02:00
Merge pull request #1559 from DaTweety/feature/folio-crosshair
Folio-wide crosshair under the mouse (Display menu)
This commit is contained in:
@@ -103,6 +103,15 @@ if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja")
|
||||
--source ${CMAKE_CURRENT_SOURCE_DIR}/cable_undo_probe.cpp
|
||||
--fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet)
|
||||
set_tests_properties(tst_cableundointegration PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE)
|
||||
# The folio-wide crosshair follows the mouse on the grid, on screen
|
||||
# only, and goes when the mouse leaves or the option is off.
|
||||
add_test(NAME tst_crosshairintegration
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_crosshair_probe.py
|
||||
--build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech>
|
||||
--source ${CMAKE_CURRENT_SOURCE_DIR}/crosshair_probe.cpp
|
||||
--fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet)
|
||||
set_tests_properties(tst_crosshairintegration PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE
|
||||
ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
endif()
|
||||
if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja")
|
||||
add_test(NAME tst_darkimageintegration
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// The folio-wide crosshair, linked against the application's objects.
|
||||
//
|
||||
// With the Display option on, two lines cross the whole folio through the
|
||||
// grid point under the mouse, to see at a glance whether parts are in
|
||||
// line. They are drawn on screen only: no item is added to the folio, so
|
||||
// nothing of them is saved, exported or printed. They go when the mouse
|
||||
// leaves the view and when the option is off, and the option is kept in
|
||||
// the settings.
|
||||
#include <QApplication>
|
||||
#include <QFontDatabase>
|
||||
#include <QImage>
|
||||
#include <QMouseEvent>
|
||||
#include <QSettings>
|
||||
#include <QTemporaryDir>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "../../sources/diagram.h"
|
||||
#include "../../sources/diagramview.h"
|
||||
#include "../../sources/qetproject.h"
|
||||
#include "../../sources/qetmessagebox.h"
|
||||
|
||||
static void check(bool value, const char *message)
|
||||
{
|
||||
if (!value) throw std::runtime_error(message);
|
||||
std::printf("ok: %s\n", message);
|
||||
}
|
||||
|
||||
static void moveTo(DiagramView &view, const QPoint &p)
|
||||
{
|
||||
QMouseEvent event(QEvent::MouseMove, QPointF(p), view.viewport()->mapToGlobal(QPointF(p)),
|
||||
Qt::NoButton, Qt::NoButton, Qt::NoModifier);
|
||||
QApplication::sendEvent(view.viewport(), &event);
|
||||
}
|
||||
|
||||
/// How many pixels of the crosshair colour lie along one row of the view
|
||||
static int bluePixels(const QImage &image, int row)
|
||||
{
|
||||
int n = 0;
|
||||
for (int x = 0; x < image.width(); ++x) {
|
||||
const QColor c = image.pixelColor(x, row);
|
||||
if (c.blue() > 150 && c.red() < 80) ++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
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("QETCrosshairRegression");
|
||||
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 *folio = project.diagrams().first();
|
||||
|
||||
DiagramView view(folio);
|
||||
view.resize(800, 600);
|
||||
view.centerOn(folio->border_and_titleblock.insideBorderRect().center());
|
||||
QApplication::processEvents();
|
||||
const int items = folio->items().size();
|
||||
|
||||
check(!DiagramView::crosshairShown(), "off by default");
|
||||
const QPoint mouse(403, 297);
|
||||
moveTo(view, mouse);
|
||||
check(!view.crosshairVisible(), "no crosshair while the option is off");
|
||||
|
||||
DiagramView::setCrosshairShown(true);
|
||||
moveTo(view, mouse);
|
||||
check(view.crosshairVisible(), "the crosshair follows the mouse once on");
|
||||
const QPointF at = view.crosshairPos();
|
||||
check(at == Diagram::snapToGrid(view.mapToScene(mouse)), "it stands on the grid point under the mouse");
|
||||
check(folio->items().size() == items, "nothing is added to the folio");
|
||||
|
||||
const QImage image = view.viewport()->grab().toImage();
|
||||
const int row = view.mapFromScene(at).y();
|
||||
check(bluePixels(image, row) > image.width() / 2, "a line crosses the view through that point");
|
||||
check(bluePixels(image, row + 20) < image.width() / 4, "and not elsewhere");
|
||||
|
||||
QEvent leave(QEvent::Leave);
|
||||
QApplication::sendEvent(view.viewport(), &leave);
|
||||
check(!view.crosshairVisible(), "it goes when the mouse leaves the view");
|
||||
|
||||
DiagramView::setCrosshairShown(false);
|
||||
moveTo(view, mouse);
|
||||
check(!view.crosshairVisible(), "and when the option is switched off");
|
||||
check(!QSettings().value("diagrameditor/crosshair").toBool(), "the option is kept in the settings");
|
||||
std::puts("PASS");
|
||||
}
|
||||
catch (const std::exception &error)
|
||||
{
|
||||
std::printf("FAIL: %s\n", error.what());
|
||||
std::fflush(stdout);
|
||||
return 1;
|
||||
}
|
||||
std::fflush(stdout);
|
||||
return 0;
|
||||
}
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Link the crosshair 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.', 'crosshair_probe.cpp.')
|
||||
probe_binary = binary.with_name('crosshair_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 / 'crosshair-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 / 'crosshair'
|
||||
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