diff --git a/sources/dxfexport.cpp b/sources/dxfexport.cpp index 5f3c266ff..6db86c5b4 100644 --- a/sources/dxfexport.cpp +++ b/sources/dxfexport.cpp @@ -35,6 +35,7 @@ #include "qetgraphicsitem/independenttextitem.h" #include "qetgraphicsitem/qetshapeitem.h" #include "qetgraphicsitem/terminal.h" +#include "textlines.h" #include #include @@ -358,7 +359,8 @@ void DxfExport::write(Diagram *diagram, int width, int height, qreal y = dti->scenePos().y() - ydir * fontSize * 1.8 - xdir * fontSize * 0.9; - QStringList lines = dti -> toPlainText().split('\n'); + //As drawn: a text with a width is wrapped + QStringList lines = TextLines::layoutLines(dti -> document()); qreal offset = fontSize * 1.6; foreach (QString line, lines) { if (line.size() > 0 && line != "_" ) diff --git a/sources/textlines.h b/sources/textlines.h new file mode 100644 index 000000000..8d3382382 --- /dev/null +++ b/sources/textlines.h @@ -0,0 +1,79 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#ifndef TEXTLINES_H +#define TEXTLINES_H + +#include +#include +#include +#include + +namespace TextLines +{ + /** + @return the lines of document as they are laid out: a paragraph + wrapped to the width of the text gives several lines. For the + exports that write a text line by line (DXF). + A paragraph that is not wrapped gives exactly its line of + toPlainText().split('\n'), as these exports wrote before: trailing + spaces are kept, and non-breaking spaces become plain spaces. In a + wrapped paragraph, the lines lose their trailing spaces (the one a + line was broken at, and a line made only of spaces becomes empty). + */ + inline QStringList layoutLines(const QTextDocument *document) + { + document->size(); //Lay the document out + + QStringList lines; + for (QTextBlock block = document->begin() ; block.isValid() ; block = block.next()) + { + const QString text = block.text(); + const QTextLayout *layout = block.layout(); + const int count = layout ? layout->lineCount() : 0; + if (count == 0) { + lines << QString(text).replace(QChar::Nbsp, QLatin1Char(' ')); + continue; + } + + //Wrapped: a line ends without a line break typed by the user + bool wrapped = false; + for (int i = 0 ; i < count - 1 ; ++i) { + const QTextLine line = layout->lineAt(i); + const int end = line.textStart() + line.textLength(); + if (end == 0 || text.at(end - 1) != QChar::LineSeparator) + wrapped = true; + } + + for (int i = 0 ; i < count ; ++i) + { + const QTextLine line = layout->lineAt(i); + QString part = text.mid(line.textStart(), line.textLength()); + if (part.endsWith(QChar::LineSeparator)) + part.chop(1); //A line break typed by the user (Shift+Enter) + if (wrapped) { + while (!part.isEmpty() && part.back().isSpace()) + part.chop(1); + } + lines << part.replace(QChar::Nbsp, QLatin1Char(' ')); + } + } + return lines; + } +} + +#endif // TEXTLINES_H diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index b2ef2ec72..85261f41a 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -104,6 +104,14 @@ add_test(NAME tst_textgrid COMMAND tst_textgrid) target_include_directories(tst_textgrid PRIVATE ${QET_DIR}/sources) target_link_libraries(tst_textgrid PRIVATE Qt::Test) +# textlines.h is header-only: the lines of a text as they are laid out, +# for the exports that write a text line by line. +add_executable(tst_textlines tst_textlines.cpp) +add_test(NAME tst_textlines COMMAND tst_textlines) +set_tests_properties(tst_textlines PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen") +target_include_directories(tst_textlines PRIVATE ${QET_DIR}/sources) +target_link_libraries(tst_textlines PRIVATE Qt::Test Qt::Gui) + # elementviewgrid.h is header-only: the symbol editor's snap step at each # zoom (bugtracker #112), tested without an editor window. add_executable(tst_elementviewgrid tst_elementviewgrid.cpp) @@ -427,6 +435,16 @@ target_compile_definitions(tst_wiringlistexport PRIVATE "QET_TEST_BINARY_PATH=\"$\"" "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") +# --export-dxf writes a wrapped symbol text line by line, as it is drawn. +add_executable( + tst_dxfwrappedtext + tst_dxfwrappedtext.cpp) +add_test(NAME tst_dxfwrappedtext COMMAND tst_dxfwrappedtext) +add_dependencies(tst_dxfwrappedtext qelectrotech) +target_link_libraries(tst_dxfwrappedtext PRIVATE Qt::Test) +target_compile_definitions(tst_dxfwrappedtext PRIVATE + "QET_TEST_BINARY_PATH=\"$\"") + # --export-pdf puts an A3 folio on an A3 page whichever way it is turned: # QPageSize matches standard sheets upright only, so a wide folio is matched # upright and turned. fixtures/pdf_page_a3.qet has one folio each way. diff --git a/tests/qttest/fixtures/dxf_wrapped_text.qet b/tests/qttest/fixtures/dxf_wrapped_text.qet new file mode 100644 index 000000000..25fdaf656 --- /dev/null +++ b/tests/qttest/fixtures/dxf_wrapped_text.qet @@ -0,0 +1,60 @@ + + + 10/2/26 + 02-10-2026 + 2026-10-02 + dxf_wrapped_text + 18:39 + + + + + + + + + + + + + + + + + + + + + + + + + + WrapAlpha WrapBeta WrapGamma WrapDelta WrapEpsilon + LineAlpha LineBeta LineGamma LineDelta LineEpsilon + + + + + + + + + Imported elements + + + + + + Wrapped symbol text + + + + WrapAlpha WrapBeta WrapGamma WrapDelta WrapEpsilon + LineAlpha LineBeta LineGamma LineDelta LineEpsilon + + + + + + diff --git a/tests/qttest/tst_dxfwrappedtext.cpp b/tests/qttest/tst_dxfwrappedtext.cpp new file mode 100644 index 000000000..857c32084 --- /dev/null +++ b/tests/qttest/tst_dxfwrappedtext.cpp @@ -0,0 +1,60 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include +#include +#include + +/** + --export-dxf writes a symbol text wrapped to its width line by line, + as it is drawn, rather than as one long line. A text without a width + is still written on one line. +*/ +class tst_dxfwrappedtext : public QObject +{ + Q_OBJECT + +private slots: + void wrappedLines() + { + const QString project = QFINDTESTDATA("fixtures/dxf_wrapped_text.qet"); + QVERIFY2(!project.isEmpty(), "fixture project not found"); + QTemporaryDir dir; + QVERIFY(dir.isValid()); + + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen")); + QProcess proc; + proc.setProcessEnvironment(env); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--export-dxf"), project, dir.path()}); + QVERIFY2(proc.waitForFinished(60000), "--export-dxf timed out"); + QCOMPARE(proc.exitCode(), 0); + + QFile file(dir.filePath(QStringLiteral("01_diagram.dxf"))); + QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text)); + const QStringList values = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n')); + + //One text entity per word of the text 70 px wide... + for (const QString &word : {QStringLiteral("WrapAlpha"), QStringLiteral("WrapGamma"), QStringLiteral("WrapEpsilon")}) + QVERIFY2(values.contains(word), qPrintable(word)); + //...one for the whole text without a width + QVERIFY(values.contains(QStringLiteral("LineAlpha LineBeta LineGamma LineDelta LineEpsilon"))); + } +}; + +QTEST_APPLESS_MAIN(tst_dxfwrappedtext) +#include "tst_dxfwrappedtext.moc" diff --git a/tests/qttest/tst_textlines.cpp b/tests/qttest/tst_textlines.cpp new file mode 100644 index 000000000..e9bd25ec3 --- /dev/null +++ b/tests/qttest/tst_textlines.cpp @@ -0,0 +1,115 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include +#include +#include + +#include "textlines.h" + +class tst_textlines : public QObject +{ + Q_OBJECT + +private slots: + // A text without a width gives its own lines. + void unwrappedTextGivesItsLines() + { + QTextDocument document(QStringLiteral("Motor protection\nswitch Q12")); + QCOMPARE(TextLines::layoutLines(&document), + QStringList({QStringLiteral("Motor protection"), QStringLiteral("switch Q12")})); + } + + // A text without a width gives exactly the lines the DXF export wrote + // before, toPlainText().split('\n'): trailing spaces and tabs kept, + // non-breaking spaces as plain spaces,
as a line break. + void unwrappedTextIsUnchanged_data() + { + QTest::addColumn("html"); + QTest::newRow("trailing spaces") << QStringLiteral("K1 \nswitch\t"); + QTest::newRow("non-breaking spaces") << QStringLiteral("

K1  24V DC

"); + QTest::newRow("line break") << QStringLiteral("

Wiper to 6
contacts

"); + QTest::newRow("paragraphs") << QStringLiteral("

relais voyant

preventa

"); + QTest::newRow("empty") << QString(); + } + + void unwrappedTextIsUnchanged() + { + QFETCH(QString, html); + QTextDocument document; + if (Qt::mightBeRichText(html)) + document.setHtml(html); + else + document.setPlainText(html); + + QCOMPARE(TextLines::layoutLines(&document), + document.toPlainText().split(QLatin1Char('\n'))); + } + + // A text wrapped to its width gives one line per drawn line, without + // the space the line was broken at. + void wrappedTextGivesTheDrawnLines() + { + QTextDocument document(QStringLiteral("Motor protection switch Q12")); + QTextOption option = document.defaultTextOption(); + option.setWrapMode(QTextOption::WordWrap); + document.setDefaultTextOption(option); + document.setTextWidth(1); + + QCOMPARE(TextLines::layoutLines(&document), + QStringList({QStringLiteral("Motor"), QStringLiteral("protection"), + QStringLiteral("switch"), QStringLiteral("Q12")})); + } + + // A wrapped paragraph has no trailing spaces left, even on its last + // line: tabs and spaces that wrap to a line of their own (as in the + // affuteuse_250h example) give an empty line, which is not written but + // still keeps the line spacing. + void wrappedTextHasNoTrailingSpaces() + { + QTextDocument document(QStringLiteral("\tMotor\t\t ")); + QTextOption option = document.defaultTextOption(); + option.setWrapMode(QTextOption::WordWrap); + document.setDefaultTextOption(option); + document.setTextWidth(80); + + const QStringList lines = TextLines::layoutLines(&document); + QVERIFY(lines.size() > 1); + QVERIFY(lines.contains(QStringLiteral("Motor"))); + for (const QString &line : lines) + QVERIFY2(line.isEmpty() || !line.back().isSpace(), qPrintable(line)); + } + + // Non-breaking spaces also become plain spaces in a wrapped text. + void wrappedTextHasPlainSpaces() + { + QTextDocument document; + document.setHtml(QStringLiteral("

K1 24V and more words

")); + QTextOption option = document.defaultTextOption(); + option.setWrapMode(QTextOption::WordWrap); + document.setDefaultTextOption(option); + document.setTextWidth(1); + + const QStringList lines = TextLines::layoutLines(&document); + QCOMPARE(lines.first(), QStringLiteral("K1 24V")); + for (const QString &line : lines) + QVERIFY(!line.contains(QChar::Nbsp)); + } +}; + +QTEST_MAIN(tst_textlines) +#include "tst_textlines.moc"