QGIS API Documentation 3.41.0-Master (45a0abf3bec)
Loading...
Searching...
No Matches
qgstextdocument.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgstextdocument.cpp
3 -----------------
4 begin : May 2020
5 copyright : (C) Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16#include "qgstextdocument.h"
17#include "qgis.h"
18#include "qgsstringutils.h"
19#include "qgstextblock.h"
20#include "qgstextfragment.h"
21#include "qgstextformat.h"
22
23#include <QTextDocument>
24#include <QTextBlock>
25
26
28
30
32{
33 mBlocks.append( block );
34}
35
37{
38 mBlocks.append( QgsTextBlock( fragment ) );
39}
40
42{
43 QgsTextDocument document;
44 document.reserve( lines.size() );
45 for ( const QString &line : lines )
46 {
47 document.append( QgsTextBlock::fromPlainText( line ) );
48 }
49 return document;
50}
51
52// Note -- must start and end with spaces, so that a tab character within
53// a html or css tag doesn't mess things up. Instead, Qt will just silently
54// ignore html attributes it doesn't know about, like this replacement string
55#define TAB_REPLACEMENT_MARKER " ignore_me_i_am_a_tab "
56
57QgsTextDocument QgsTextDocument::fromHtml( const QStringList &lines )
58{
59 QgsTextDocument document;
60
61 document.reserve( lines.size() );
62
63 for ( const QString &l : std::as_const( lines ) )
64 {
65 QString line = l;
66 // QTextDocument is a very heavy way of parsing HTML + css (it's heavily geared toward an editable text document,
67 // and includes a LOT of calculations we don't need, when all we're after is a HTML + CSS style parser).
68 // TODO - try to find an alternative library we can use here
69
70 QTextDocument sourceDoc;
71
72 // QTextDocument will replace tab characters with a space. We need to hack around this
73 // by first replacing it with a string which QTextDocument won't mess with, and then
74 // handle these markers as tab characters in the parsed HTML document.
75 line.replace( QString( '\t' ), QStringLiteral( TAB_REPLACEMENT_MARKER ) );
76
77 // cheat a little. Qt css requires some properties to have the "px" suffix. But we don't treat these properties
78 // as pixels, because that doesn't scale well with different dpi render targets! So let's instead use just instead treat the suffix as
79 // optional, and ignore ANY unit suffix the user has put, and then replace it with "px" so that Qt's css parsing engine can process it
80 // correctly...
81 const thread_local QRegularExpression sRxPixelsToPtFix( QStringLiteral( "(word-spacing|line-height|margin-top|margin-bottom|margin-left|margin-right):\\s*(-?\\d+(?:\\.\\d+)?)(?![%\\d])([a-zA-Z]*)" ) );
82 line.replace( sRxPixelsToPtFix, QStringLiteral( "\\1: \\2px" ) );
83 const thread_local QRegularExpression sRxMarginPixelsToPtFix( QStringLiteral( "margin:\\s*(-?\\d+(?:\\.\\d+)?)([a-zA-Z]*)\\s*(-?\\d+(?:\\.\\d+)?)([a-zA-Z]*)\\s*(-?\\d+(?:\\.\\d+)?)([a-zA-Z]*)\\s*(-?\\d+(?:\\.\\d+)?)([a-zA-Z]*)" ) );
84 line.replace( sRxMarginPixelsToPtFix, QStringLiteral( "margin: \\1px \\3px \\5px \\7px" ) );
85
86 // undo default margins on p, h1-6 elements. We didn't use to respect these and can't change the rendering
87 // of existing projects to suddenly start showing them...
88 line.prepend( QStringLiteral( "<style>p, h1, h2, h3, h4, h5, h6 { margin: 0pt; }</style>" ) );
89
90 sourceDoc.setHtml( line );
91
92 QTextBlock sourceBlock = sourceDoc.firstBlock();
93
94 while ( true )
95 {
96 const int headingLevel = sourceBlock.blockFormat().headingLevel();
97 QgsTextCharacterFormat blockFormat;
98 if ( headingLevel > 0 )
99 {
100 switch ( headingLevel )
101 {
102 case 1:
103 blockFormat.setFontPercentageSize( 21.0 / 12 );
104 break;
105 case 2:
106 blockFormat.setFontPercentageSize( 16.0 / 12 );
107 break;
108 case 3:
109 blockFormat.setFontPercentageSize( 13.0 / 12 );
110 break;
111 case 4:
112 blockFormat.setFontPercentageSize( 11.0 / 12 );
113 break;
114 case 5:
115 blockFormat.setFontPercentageSize( 8.0 / 12 );
116 break;
117 case 6:
118 blockFormat.setFontPercentageSize( 7.0 / 12 );
119 break;
120 default:
121 break;
122 }
123 }
124
125 auto it = sourceBlock.begin();
126 QgsTextBlock block;
127 block.setBlockFormat( QgsTextBlockFormat( sourceBlock.blockFormat() ) );
128 while ( !it.atEnd() )
129 {
130 const QTextFragment fragment = it.fragment();
131 if ( fragment.isValid() )
132 {
133 // Search for line breaks in the fragment
134 const QString fragmentText = fragment.text();
135 if ( fragmentText.contains( QStringLiteral( "\u2028" ) ) )
136 {
137 // Split fragment text into lines
138 const QStringList splitLines = fragmentText.split( QStringLiteral( "\u2028" ), Qt::SplitBehaviorFlags::SkipEmptyParts );
139
140 for ( const QString &splitLine : std::as_const( splitLines ) )
141 {
142 const QgsTextCharacterFormat *previousFormat = nullptr;
143
144 // If the splitLine is not the first, inherit style from previous fragment
145 if ( splitLine != splitLines.first() && document.size() > 0 )
146 {
147 previousFormat = &document.at( document.size() - 1 ).at( 0 ).characterFormat();
148 }
149
150 if ( splitLine.contains( QStringLiteral( TAB_REPLACEMENT_MARKER ) ) )
151 {
152 // split line by tab characters, each tab should be a
153 // fragment by itself
154 QgsTextFragment splitFragment( fragment );
155 QgsTextCharacterFormat newFormat { splitFragment.characterFormat() };
156 newFormat.overrideWith( blockFormat );
157 if ( previousFormat )
158 {
159 // Apply overrides from previous fragment
160 newFormat.overrideWith( *previousFormat );
161 splitFragment.setCharacterFormat( newFormat );
162 }
163 splitFragment.setCharacterFormat( newFormat );
164
165 const QStringList tabSplit = splitLine.split( QStringLiteral( TAB_REPLACEMENT_MARKER ) );
166 int index = 0;
167 for ( const QString &part : tabSplit )
168 {
169 if ( !part.isEmpty() )
170 {
171 splitFragment.setText( part );
172 block.append( splitFragment );
173 }
174 if ( index != tabSplit.size() - 1 )
175 {
176 block.append( QgsTextFragment( QString( '\t' ) ) );
177 }
178 index++;
179 }
180 }
181 else
182 {
183 QgsTextFragment splitFragment( fragment );
184 splitFragment.setText( splitLine );
185
186 QgsTextCharacterFormat newFormat { splitFragment.characterFormat() };
187 newFormat.overrideWith( blockFormat );
188 if ( previousFormat )
189 {
190 // Apply overrides from previous fragment
191 newFormat.overrideWith( *previousFormat );
192 }
193 splitFragment.setCharacterFormat( newFormat );
194
195 block.append( splitFragment );
196 }
197
198 document.append( block );
199 block = QgsTextBlock();
200 block.setBlockFormat( QgsTextBlockFormat( sourceBlock.blockFormat() ) );
201 }
202 }
203 else if ( fragmentText.contains( QStringLiteral( TAB_REPLACEMENT_MARKER ) ) )
204 {
205 // split line by tab characters, each tab should be a
206 // fragment by itself
207 QgsTextFragment tmpFragment( fragment );
208
209 QgsTextCharacterFormat newFormat { tmpFragment.characterFormat() };
210 newFormat.overrideWith( blockFormat );
211 tmpFragment.setCharacterFormat( newFormat );
212
213 const QStringList tabSplit = fragmentText.split( QStringLiteral( TAB_REPLACEMENT_MARKER ) );
214 int index = 0;
215 for ( const QString &part : tabSplit )
216 {
217 if ( !part.isEmpty() )
218 {
219 tmpFragment.setText( part );
220 block.append( tmpFragment );
221 }
222 if ( index != tabSplit.size() - 1 )
223 {
224 block.append( QgsTextFragment( QString( '\t' ) ) );
225 }
226 index++;
227 }
228 }
229 else
230 {
231 QgsTextFragment tmpFragment( fragment );
232 QgsTextCharacterFormat newFormat { tmpFragment.characterFormat() };
233 newFormat.overrideWith( blockFormat );
234 tmpFragment.setCharacterFormat( newFormat );
235
236 block.append( tmpFragment );
237 }
238 }
239 it++;
240 }
241
242 if ( !block.empty() )
243 document.append( block );
244
245 sourceBlock = sourceBlock.next();
246 if ( !sourceBlock.isValid() )
247 break;
248 }
249 }
250
251 return document;
252}
253
254QgsTextDocument QgsTextDocument::fromTextAndFormat( const QStringList &lines, const QgsTextFormat &format )
255{
256 QgsTextDocument doc;
257 if ( !format.allowHtmlFormatting() || lines.isEmpty() )
258 {
259 doc = QgsTextDocument::fromPlainText( lines );
260 }
261 else
262 {
263 doc = QgsTextDocument::fromHtml( lines );
264 }
265 if ( doc.size() > 0 )
266 doc.applyCapitalization( format.capitalization() );
267 return doc;
268}
269
271{
272 mBlocks.append( block );
273}
274
276{
277 mBlocks.push_back( block );
278}
279
280void QgsTextDocument::insert( int index, const QgsTextBlock &block )
281{
282 mBlocks.insert( index, block );
283}
284
285void QgsTextDocument::insert( int index, QgsTextBlock &&block )
286{
287 mBlocks.insert( index, block );
288}
289
291{
292 mBlocks.reserve( count );
293}
294
296{
297 return mBlocks.at( i );
298}
299
301{
302 return mBlocks[i];
303}
304
306{
307 return mBlocks.size();
308}
309
311{
312 QStringList textLines;
313 textLines.reserve( mBlocks.size() );
314 for ( const QgsTextBlock &block : mBlocks )
315 {
316 QString line;
317 for ( const QgsTextFragment &fragment : block )
318 {
319 line.append( fragment.text() );
320 }
321 textLines << line;
322 }
323 return textLines;
324}
325
326void QgsTextDocument::splitLines( const QString &wrapCharacter, int autoWrapLength, bool useMaxLineLengthWhenAutoWrapping )
327{
328 const QVector< QgsTextBlock > prevBlocks = mBlocks;
329 mBlocks.clear();
330 mBlocks.reserve( prevBlocks.size() );
331 for ( const QgsTextBlock &block : prevBlocks )
332 {
333 QgsTextBlock destinationBlock;
334 destinationBlock.setBlockFormat( block.blockFormat() );
335 for ( const QgsTextFragment &fragment : block )
336 {
337 QStringList thisParts;
338 if ( !wrapCharacter.isEmpty() && wrapCharacter != QLatin1String( "\n" ) )
339 {
340 //wrap on both the wrapchr and new line characters
341 const QStringList lines = fragment.text().split( wrapCharacter );
342 for ( const QString &line : lines )
343 {
344 thisParts.append( line.split( '\n' ) );
345 }
346 }
347 else
348 {
349 thisParts = fragment.text().split( '\n' );
350 }
351
352 // apply auto wrapping to each manually created line
353 if ( autoWrapLength != 0 )
354 {
355 QStringList autoWrappedLines;
356 autoWrappedLines.reserve( thisParts.count() );
357 for ( const QString &line : std::as_const( thisParts ) )
358 {
359 autoWrappedLines.append( QgsStringUtils::wordWrap( line, autoWrapLength, useMaxLineLengthWhenAutoWrapping ).split( '\n' ) );
360 }
361 thisParts = autoWrappedLines;
362 }
363
364 if ( thisParts.empty() )
365 continue;
366 else if ( thisParts.size() == 1 )
367 destinationBlock.append( fragment );
368 else
369 {
370 if ( !thisParts.at( 0 ).isEmpty() )
371 destinationBlock.append( QgsTextFragment( thisParts.at( 0 ), fragment.characterFormat() ) );
372
373 append( destinationBlock );
374 destinationBlock.clear();
375 for ( int i = 1 ; i < thisParts.size() - 1; ++i )
376 {
377 QgsTextBlock partBlock( QgsTextFragment( thisParts.at( i ), fragment.characterFormat() ) );
378 partBlock.setBlockFormat( block.blockFormat() );
379 append( partBlock );
380 }
381 destinationBlock.append( QgsTextFragment( thisParts.at( thisParts.size() - 1 ), fragment.characterFormat() ) );
382 }
383 }
384 append( destinationBlock );
385 }
386}
387
389{
390 for ( QgsTextBlock &block : mBlocks )
391 {
392 block.applyCapitalization( capitalization );
393 }
394}
395
397QVector< QgsTextBlock >::const_iterator QgsTextDocument::begin() const
398{
399 return mBlocks.begin();
400}
401
402QVector< QgsTextBlock >::const_iterator QgsTextDocument::end() const
403{
404 return mBlocks.end();
405}
Capitalization
String capitalization options.
Definition qgis.h:3140
static QString wordWrap(const QString &string, int length, bool useMaxLineLength=true, const QString &customDelimiter=QString())
Automatically wraps a string by inserting new line characters at appropriate locations in the string.
Stores information relating to individual block formatting.
Represents a block of text consisting of one or more QgsTextFragment objects.
void clear()
Clears the block, removing all its contents.
static QgsTextBlock fromPlainText(const QString &text, const QgsTextCharacterFormat &format=QgsTextCharacterFormat())
Constructor for QgsTextBlock consisting of a plain text, and optional character format.
void setBlockFormat(const QgsTextBlockFormat &format)
Sets the block format for the fragment.
void append(const QgsTextFragment &fragment)
Appends a fragment to the block.
bool empty() const
Returns true if the block is empty.
Stores information relating to individual character formatting.
void overrideWith(const QgsTextCharacterFormat &other)
Override all the default/unset properties of the current character format with the settings from anot...
void setFontPercentageSize(double size)
Sets the font percentage size (as fraction of inherited font size).
Represents a document consisting of one or more QgsTextBlock objects.
void splitLines(const QString &wrapCharacter, int autoWrapLength=0, bool useMaxLineLengthWhenAutoWrapping=true)
Splits lines of text in the document to separate lines, using a specified wrap character (wrapCharact...
QgsTextBlock & operator[](int index)
Returns the block at the specified index.
const QgsTextBlock & at(int index) const
Returns the block at the specified index.
void reserve(int count)
Reserves the specified count of blocks for optimised block appending.
QStringList toPlainText() const
Returns a list of plain text lines of text representing the document.
int size() const
Returns the number of blocks in the document.
static QgsTextDocument fromHtml(const QStringList &lines)
Constructor for QgsTextDocument consisting of a set of HTML formatted lines.
static QgsTextDocument fromPlainText(const QStringList &lines)
Constructor for QgsTextDocument consisting of a set of plain text lines.
void append(const QgsTextBlock &block)
Appends a block to the document.
void insert(int index, const QgsTextBlock &block)
Inserts a block into the document, at the specified index.
static QgsTextDocument fromTextAndFormat(const QStringList &lines, const QgsTextFormat &format)
Constructor for QgsTextDocument consisting of a set of lines, respecting settings from a text format.
void applyCapitalization(Qgis::Capitalization capitalization)
Applies a capitalization style to the document's text.
Container for all settings relating to text rendering.
Qgis::Capitalization capitalization() const
Returns the text capitalization style.
bool allowHtmlFormatting() const
Returns true if text should be treated as a HTML document and HTML tags should be used for formatting...
Stores a fragment of document along with formatting overrides to be used when rendering the fragment.
void setText(const QString &text)
Sets the text content of the fragment.
void setCharacterFormat(const QgsTextCharacterFormat &format)
Sets the character format for the fragment.
const QgsTextCharacterFormat & characterFormat() const
Returns the character formatting for the fragment.
#define TAB_REPLACEMENT_MARKER