mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-26 08:04:13 +01:00
Symbol texts: width and height, each fixed or not
In a symbol text's properties, "Width (fixed)" and the new "Height (fixed)" each carry a tick box. Not ticked, the field follows the text and shows the size it has now, ready to be fixed; ticked, it keeps the size beside the box. A fixed height is new: the field keeps it whatever is written, and the lines that do not fit are cut off, neither drawn nor picked, to shorten a text where room is short. It is saved as text_height on the text, only when set, and changed through an undoable property command like the width. Translations for all languages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -140,6 +140,15 @@ if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja")
|
||||
--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")
|
||||
# Width and height of a symbol's text, each fixed or not: a fixed
|
||||
# height cuts off the lines that do not fit; saved and undone.
|
||||
add_test(NAME tst_textfixedsizeintegration
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_text_fixed_size_probe.py
|
||||
--build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech>
|
||||
--source ${CMAKE_CURRENT_SOURCE_DIR}/text_fixed_size_probe.cpp
|
||||
--fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet)
|
||||
set_tests_properties(tst_textfixedsizeintegration 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 fixed 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.', 'text_fixed_size_probe.cpp.')
|
||||
probe_binary = binary.with_name('text_fixed_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 / 'text-fixed-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 / 'text-fixed-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,129 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Width and height of a symbol's text, each fixed or not, linked against
|
||||
// the application's objects.
|
||||
//
|
||||
// In the text's properties, "Width (fixed)" and "Height (fixed)" each
|
||||
// carry a tick box. Not ticked, the field follows the text and shows the
|
||||
// size it has now; ticked, it keeps the size beside the box. A fixed
|
||||
// height cuts off the lines that do not fit, to shorten a text where
|
||||
// room is short: they are neither drawn nor picked. Both are saved with
|
||||
// the text and undone as any other change.
|
||||
#include <QApplication>
|
||||
#include <QDomDocument>
|
||||
#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"
|
||||
#include "../../sources/ui/dynamicelementtextmodel.h"
|
||||
|
||||
static void check(bool value, const char *message)
|
||||
{
|
||||
if (!value) throw std::runtime_error(message);
|
||||
std::printf("ok: %s\n", message);
|
||||
}
|
||||
|
||||
/// The value cell of a text's row with the given type
|
||||
static QStandardItem *cell(DynamicElementTextModel &model, DynamicElementTextItem *text, int type)
|
||||
{
|
||||
QStandardItem *row = model.itemFromIndex(model.indexFromText(text));
|
||||
for (int r = 0; r < row->rowCount(); ++r)
|
||||
if (QStandardItem *c = row->child(r, 1))
|
||||
if (c->data(Qt::UserRole + 1).toInt() == type) return c;
|
||||
throw std::runtime_error("row missing");
|
||||
}
|
||||
|
||||
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("QETTextFixedSizeRegression");
|
||||
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");
|
||||
text->setTextFrom(DynamicElementTextItem::UserText);
|
||||
text->setText(QStringLiteral("Sockets bathroom first floor, 5"));
|
||||
text->setTextWidth(-1);
|
||||
text->setTextHeight(-1);
|
||||
|
||||
// The properties: not ticked, the size the field has now.
|
||||
DynamicElementTextModel model(element);
|
||||
QStandardItem *width = cell(model, text, DynamicElementTextModel::textWidth);
|
||||
QStandardItem *height = cell(model, text, DynamicElementTextModel::textHeight);
|
||||
check(width->isCheckable() && width->checkState() == Qt::Unchecked, "width not fixed: not ticked");
|
||||
check(height->isCheckable() && height->checkState() == Qt::Unchecked, "height not fixed: not ticked");
|
||||
check(qAbs(width->data(Qt::EditRole).toDouble() - text->boundingRect().width()) <= 1,
|
||||
"it shows the width the field has now");
|
||||
|
||||
// Fix the width at 50: the text wraps onto more lines.
|
||||
const qreal one_line = text->boundingRect().height();
|
||||
width->setData(50, Qt::EditRole);
|
||||
width->setCheckState(Qt::Checked);
|
||||
scene->undoStack().push(model.undoForEditedText(text));
|
||||
check(qFuzzyCompare(text->textWidth(), 50), "ticked, the width is fixed at the value beside it");
|
||||
const qreal wrapped = text->boundingRect().height();
|
||||
check(wrapped > one_line * 2, "the text wraps inside it");
|
||||
|
||||
// Fix the height at one line: the other lines are cut off.
|
||||
height->setData(qRound(one_line), Qt::EditRole);
|
||||
height->setCheckState(Qt::Checked);
|
||||
scene->undoStack().push(model.undoForEditedText(text));
|
||||
check(qFuzzyCompare(text->textHeight(), qRound(one_line)), "ticked, the height is fixed");
|
||||
check(qAbs(text->boundingRect().height() - qRound(one_line)) < 0.01, "the field keeps that height");
|
||||
const QPointF below(text->boundingRect().center().x(), wrapped - 2);
|
||||
check(!text->contains(below) && !text->shape().contains(below), "the lines cut off are not picked");
|
||||
|
||||
// Kept in the file.
|
||||
QDomDocument doc;
|
||||
const QDomElement xml = text->toXml(doc);
|
||||
check(xml.attribute("text_height").toDouble() == qRound(one_line), "the height is saved with the text");
|
||||
text->setTextHeight(-1);
|
||||
QDomElement copy = xml;
|
||||
text->fromXml(copy);
|
||||
check(qFuzzyCompare(text->textHeight(), qRound(one_line)), "and read back");
|
||||
|
||||
// Undo, untick.
|
||||
scene->undoStack().undo();
|
||||
check(text->textHeight() < 0 && qAbs(text->boundingRect().height() - wrapped) < 0.01,
|
||||
"undo gives the text its full height back");
|
||||
check(cell(model, text, DynamicElementTextModel::textHeight)->checkState() == Qt::Unchecked,
|
||||
"and the properties show it not fixed");
|
||||
width->setCheckState(Qt::Unchecked);
|
||||
scene->undoStack().push(model.undoForEditedText(text));
|
||||
check(text->textWidth() < 0, "not ticked, the width follows the text again");
|
||||
check(!text->toXml(doc).hasAttribute("text_height"), "a height that follows the text is not written");
|
||||
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