mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-26 08:04:13 +01:00
Texts are outlined and picked where they are written
A text kept at a fixed width was outlined, hovered and picked over its whole box. A short label such as "-XS6", aligned to the right in a box 120 wide, took the clicks meant for whatever lay in the empty part of the box, and its selection frame was six times wider than the text. The selection outline, the hover outline and the click area (shape() and contains()) are now the lines actually written, placed as the text is aligned. While editing, the whole box still takes the clicks; a text with nothing written keeps its box, so it can still be picked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -131,6 +131,15 @@ if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja")
|
||||
--fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet)
|
||||
set_tests_properties(tst_symboltextplacementintegration PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE
|
||||
ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
# The outline and the click area of a fixed-width text are the lines
|
||||
# written, not the whole box.
|
||||
add_test(NAME tst_textoutlineintegration
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_text_outline_probe.py
|
||||
--build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech>
|
||||
--source ${CMAKE_CURRENT_SOURCE_DIR}/text_outline_probe.cpp
|
||||
--fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet)
|
||||
set_tests_properties(tst_textoutlineintegration 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
|
||||
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Link the text outline 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.', 'text_outline_probe.cpp.')
|
||||
probe_binary = binary.with_name('text_outline_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 / 'text-outline-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 / 'text-outline'
|
||||
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,89 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// The outline and the click area of a text hug the lines written,
|
||||
// linked against the application's objects.
|
||||
//
|
||||
// A symbol's text kept at a fixed width and aligned to the right, "-XS6"
|
||||
// in a box 120 wide, used to be outlined, hovered and picked over the
|
||||
// whole box: a double click on the empty left part of it hit this text
|
||||
// instead of whatever lay beneath. Now the outline and the click area
|
||||
// are the written line, at the right of the box; while editing, the
|
||||
// whole box still takes the clicks.
|
||||
#include <QApplication>
|
||||
#include <QFontDatabase>
|
||||
#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"
|
||||
|
||||
static void check(bool value, const char *message)
|
||||
{
|
||||
if (!value) throw std::runtime_error(message);
|
||||
std::printf("ok: %s\n", message);
|
||||
}
|
||||
|
||||
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("QETTextOutlineRegression");
|
||||
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;
|
||||
for (Element *e : scene->elements())
|
||||
for (DynamicElementTextItem *t : e->dynamicTextItems())
|
||||
if (!text) text = t;
|
||||
check(text, "the fixture has a symbol with a text");
|
||||
|
||||
text->setTextFrom(DynamicElementTextItem::UserText);
|
||||
text->setText(QStringLiteral("-XS6"));
|
||||
text->setTextWidth(120);
|
||||
text->setAlignment(Qt::AlignRight | Qt::AlignTop);
|
||||
|
||||
const QRectF box = text->boundingRect();
|
||||
const QRectF written = text->writtenRect();
|
||||
check(box.width() >= 120, "the box keeps its fixed width");
|
||||
check(written.width() < 40, "the outline is as wide as the text written");
|
||||
check(qAbs(written.right() - box.right()) < 8, "and sits at the right, as the text is aligned");
|
||||
|
||||
const QPointF empty(box.left() + 10, written.center().y());
|
||||
check(!text->shape().contains(empty), "the empty part of the box is not this text's to click");
|
||||
check(text->shape().contains(written.center()), "the written line is");
|
||||
check(text->contains(written.center()) && !text->contains(empty), "hover and click follow the shape");
|
||||
|
||||
text->setText(QStringLiteral("Sockets bathroom first floor, 5"));
|
||||
text->setTextWidth(50);
|
||||
check(text->writtenRect().height() > written.height() + 5, "a text on several lines is outlined whole");
|
||||
|
||||
text->setText(QString());
|
||||
check(text->writtenRect() == text->boundingRect(), "a text with nothing written can still be picked");
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user