mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-25 07:24:21 +01:00
Merge pull request #1558 from DaTweety/feature/tight-text-outline
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m45s
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m45s
Texts are outlined and picked where they are written
This commit is contained in:
@@ -25,6 +25,11 @@
|
||||
#include "../utils/qetutils.h"
|
||||
#include "../QetGraphicsItemModeler/textresizehandles.h"
|
||||
|
||||
#include <QAbstractTextDocumentLayout>
|
||||
#include <QStyleOptionGraphicsItem>
|
||||
#include <QTextBlock>
|
||||
#include <QTextLayout>
|
||||
|
||||
/**
|
||||
@brief DiagramTextItem::DiagramTextItem
|
||||
@param parent : parent item
|
||||
@@ -255,6 +260,59 @@ QRectF DiagramTextItem::frameRect() const
|
||||
return QRectF(pos, size);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramTextItem::writtenRect
|
||||
@return the rect around the lines actually written, in this item's
|
||||
coordinates: narrower than boundingRect() for a text kept at a fixed
|
||||
width, and placed as the text is aligned. A text with nothing written
|
||||
gives its bounding rect, so that it can still be picked.
|
||||
*/
|
||||
QRectF DiagramTextItem::writtenRect() const
|
||||
{
|
||||
QRectF written;
|
||||
QAbstractTextDocumentLayout *layout = document()->documentLayout();
|
||||
for (QTextBlock block = document()->begin(); block.isValid(); block = block.next())
|
||||
{
|
||||
const QTextLayout *lines = block.layout();
|
||||
if (!lines)
|
||||
continue;
|
||||
const QPointF origin = layout->blockBoundingRect(block).topLeft();
|
||||
for (int i = 0; i < lines->lineCount(); ++i)
|
||||
{
|
||||
const QRectF line = lines->lineAt(i).naturalTextRect().translated(origin);
|
||||
if (line.width() > 0)
|
||||
written |= line;
|
||||
}
|
||||
}
|
||||
if (written.isEmpty())
|
||||
return boundingRect();
|
||||
//A little room around the letters, as the frame keeps
|
||||
return written.adjusted(-1, -1, 1, 1) & boundingRect();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramTextItem::shape
|
||||
@return the lines written as the place to click: the empty part of a
|
||||
fixed-width text belongs to whatever lies beneath it. While editing,
|
||||
the whole box, so that a click anywhere in it moves the cursor.
|
||||
*/
|
||||
QPainterPath DiagramTextItem::shape() const
|
||||
{
|
||||
QPainterPath path;
|
||||
path.addRect(isEditing() ? boundingRect() : writtenRect());
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramTextItem::contains
|
||||
QGraphicsTextItem answers from its whole box: answer from shape(), so
|
||||
that hovering and picking at a point follow it too.
|
||||
*/
|
||||
bool DiagramTextItem::contains(const QPointF &point) const
|
||||
{
|
||||
return shape().contains(point);
|
||||
}
|
||||
|
||||
void DiagramTextItem::setHtml(const QString &text)
|
||||
{
|
||||
QGraphicsTextItem::setHtml(text);
|
||||
@@ -301,7 +359,25 @@ void DiagramTextItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *o
|
||||
{
|
||||
painter -> setRenderHint(QPainter::Antialiasing, false);
|
||||
|
||||
QGraphicsTextItem::paint(painter, option, widget);
|
||||
//Outside editing, the selection is drawn around the lines written,
|
||||
//not around the whole width a fixed-width text keeps free
|
||||
const bool outline = option && (option->state & QStyle::State_Selected) && !isEditing();
|
||||
if (outline)
|
||||
{
|
||||
QStyleOptionGraphicsItem plain(*option);
|
||||
plain.state &= ~(QStyle::State_Selected | QStyle::State_HasFocus);
|
||||
QGraphicsTextItem::paint(painter, &plain, widget);
|
||||
|
||||
painter -> save();
|
||||
painter -> setBrush(Qt::NoBrush);
|
||||
QPen pen(Qt::black, 0, Qt::DashLine);
|
||||
pen.setCosmetic(true);
|
||||
painter -> setPen(pen);
|
||||
painter -> drawRect(writtenRect());
|
||||
painter -> restore();
|
||||
}
|
||||
else
|
||||
QGraphicsTextItem::paint(painter, option, widget);
|
||||
|
||||
if (m_mouse_hover && !isSelected())
|
||||
{
|
||||
@@ -318,7 +394,7 @@ void DiagramTextItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *o
|
||||
t.setStyle(Qt::DashDotLine);
|
||||
t.setCosmetic(true);
|
||||
painter -> setPen(t);
|
||||
painter -> drawRoundedRect(boundingRect(), 10, 10);
|
||||
painter -> drawRoundedRect(writtenRect(), 2, 2);
|
||||
|
||||
painter -> restore();
|
||||
}
|
||||
|
||||
@@ -86,6 +86,9 @@ class DiagramTextItem : public QGraphicsTextItem
|
||||
void setAlignmentAtAnchor(const Qt::Alignment &alignment);
|
||||
|
||||
QRectF frameRect() const;
|
||||
QRectF writtenRect() const;
|
||||
QPainterPath shape() const override;
|
||||
bool contains(const QPointF &point) const override;
|
||||
|
||||
void setHtml(const QString &text);
|
||||
void setPlainText(const QString &text);
|
||||
|
||||
@@ -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