QGIS API Documentation 3.99.0-Master (21b3aa880ba)
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
18#include "qgis.h"
19#include "qgsstringutils.h"
20#include "qgstextblock.h"
21#include "qgstextformat.h"
22#include "qgstextfragment.h"
23
24#include <QTextBlock>
25#include <QTextDocument>
26
28
30
32 : mBlocks( other.mBlocks )
33{
34
35}
36
38 : mBlocks( std::move( other.mBlocks ) )
39{
40
41}
42
44{
45 if ( &other == this )
46 return *this;
47
48 mBlocks = other.mBlocks;
49 return *this;
50}
51
53{
54 if ( &other == this )
55 return *this;
56
57 mBlocks = std::move( other.mBlocks );
58 return *this;
59}
60
62{
63 mBlocks.append( block );
64}
65
67{
68 mBlocks.append( QgsTextBlock( fragment ) );
69}
70
72{
73 QgsTextDocument document;
74 document.reserve( lines.size() );
75 for ( const QString &line : lines )
76 {
77 document.append( QgsTextBlock::fromPlainText( line ) );
78 }
79 return document;
80}
81
82// Note -- must start and end with spaces, so that a tab character within
83// a html or css tag doesn't mess things up. Instead, Qt will just silently
84// ignore html attributes it doesn't know about, like this replacement string
85#define TAB_REPLACEMENT_MARKER " ignore_me_i_am_a_tab "
86// when splitting by the tab replacement marker we need to be tolerant to the
87// spaces surrounding REPLACEMENT_MARKER being swallowed when multiple consecutive
88// tab characters exist
89#define TAB_REPLACEMENT_MARKER_RX " ?ignore_me_i_am_a_tab ?"
90
91QgsTextDocument QgsTextDocument::fromHtml( const QStringList &lines )
92{
93 QgsTextDocument document;
94
95 document.reserve( lines.size() );
96
97 for ( const QString &l : std::as_const( lines ) )
98 {
99 QString line = l;
100 // QTextDocument is a very heavy way of parsing HTML + css (it's heavily geared toward an editable text document,
101 // and includes a LOT of calculations we don't need, when all we're after is a HTML + CSS style parser).
102 // TODO - try to find an alternative library we can use here
103
104 QTextDocument sourceDoc;
105
106 // QTextDocument will replace tab characters with a space. We need to hack around this
107 // by first replacing it with a string which QTextDocument won't mess with, and then
108 // handle these markers as tab characters in the parsed HTML document.
109 line.replace( QString( '\t' ), QStringLiteral( TAB_REPLACEMENT_MARKER ) );
110 const thread_local QRegularExpression sTabReplacementMarkerRx( QStringLiteral( TAB_REPLACEMENT_MARKER_RX ) );
111
112 // cheat a little. Qt css requires some properties to have the "px" suffix. But we don't treat these properties
113 // as pixels, because that doesn't scale well with different dpi render targets! So let's instead use just instead treat the suffix as
114 // 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
115 // correctly...
116 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]*)" ) );
117 line.replace( sRxPixelsToPtFix, QStringLiteral( "\\1: \\2px" ) );
118 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]*)" ) );
119 line.replace( sRxMarginPixelsToPtFix, QStringLiteral( "margin: \\1px \\3px \\5px \\7px" ) );
120
121 // undo default margins on p, h1-6 elements. We didn't use to respect these and can't change the rendering
122 // of existing projects to suddenly start showing them...
123 line.prepend( QStringLiteral( "<style>p, h1, h2, h3, h4, h5, h6 { margin: 0pt; }</style>" ) );
124
125 sourceDoc.setHtml( line );
126
127 QTextBlock sourceBlock = sourceDoc.firstBlock();
128
129 while ( true )
130 {
131 const int headingLevel = sourceBlock.blockFormat().headingLevel();
132 QgsTextCharacterFormat blockFormat;
133 if ( headingLevel > 0 )
134 {
135 switch ( headingLevel )
136 {
137 case 1:
138 blockFormat.setFontPercentageSize( 21.0 / 12 );
139 break;
140 case 2:
141 blockFormat.setFontPercentageSize( 16.0 / 12 );
142 break;
143 case 3:
144 blockFormat.setFontPercentageSize( 13.0 / 12 );
145 break;
146 case 4:
147 blockFormat.setFontPercentageSize( 11.0 / 12 );
148 break;
149 case 5:
150 blockFormat.setFontPercentageSize( 8.0 / 12 );
151 break;
152 case 6:
153 blockFormat.setFontPercentageSize( 7.0 / 12 );
154 break;
155 default:
156 break;
157 }
158 }
159
160 auto it = sourceBlock.begin();
161 QgsTextBlock block;
162 block.setBlockFormat( QgsTextBlockFormat( sourceBlock.blockFormat() ) );
163 while ( !it.atEnd() )
164 {
165 const QTextFragment fragment = it.fragment();
166 if ( fragment.isValid() )
167 {
168 // Search for line breaks in the fragment
169 const QString fragmentText = fragment.text();
170 if ( fragmentText.contains( QStringLiteral( "\u2028" ) ) )
171 {
172 // Split fragment text into lines
173 const QStringList splitLines = fragmentText.split( QStringLiteral( "\u2028" ), Qt::SplitBehaviorFlags::SkipEmptyParts );
174
175 for ( const QString &splitLine : std::as_const( splitLines ) )
176 {
177 const QgsTextCharacterFormat *previousFormat = nullptr;
178
179 // If the splitLine is not the first, inherit style from previous fragment
180 if ( splitLine != splitLines.first() && document.size() > 0 )
181 {
182 previousFormat = &document.at( document.size() - 1 ).at( 0 ).characterFormat();
183 }
184
185 if ( splitLine.contains( QStringLiteral( TAB_REPLACEMENT_MARKER ) ) )
186 {
187 // split line by tab characters, each tab should be a
188 // fragment by itself
189 QgsTextFragment splitFragment( fragment );
190 QgsTextCharacterFormat newFormat { splitFragment.characterFormat() };
191 newFormat.overrideWith( blockFormat );
192 if ( previousFormat )
193 {
194 // Apply overrides from previous fragment
195 newFormat.overrideWith( *previousFormat );
196 splitFragment.setCharacterFormat( newFormat );
197 }
198 splitFragment.setCharacterFormat( newFormat );
199
200 const QStringList tabSplit = splitLine.split( sTabReplacementMarkerRx );
201 int index = 0;
202 for ( const QString &part : tabSplit )
203 {
204 if ( !part.isEmpty() )
205 {
206 splitFragment.setText( part );
207 block.append( splitFragment );
208 }
209 if ( index != tabSplit.size() - 1 )
210 {
211 block.append( QgsTextFragment( QString( '\t' ) ) );
212 }
213 index++;
214 }
215 }
216 else
217 {
218 QgsTextFragment splitFragment( fragment );
219 splitFragment.setText( splitLine );
220
221 QgsTextCharacterFormat newFormat { splitFragment.characterFormat() };
222 newFormat.overrideWith( blockFormat );
223 if ( previousFormat )
224 {
225 // Apply overrides from previous fragment
226 newFormat.overrideWith( *previousFormat );
227 }
228 splitFragment.setCharacterFormat( newFormat );
229
230 block.append( splitFragment );
231 }
232
233 document.append( block );
234 block = QgsTextBlock();
235 block.setBlockFormat( QgsTextBlockFormat( sourceBlock.blockFormat() ) );
236 }
237 }
238 else if ( fragmentText.contains( QStringLiteral( TAB_REPLACEMENT_MARKER ) ) )
239 {
240 // split line by tab characters, each tab should be a
241 // fragment by itself
242 QgsTextFragment tmpFragment( fragment );
243
244 QgsTextCharacterFormat newFormat { tmpFragment.characterFormat() };
245 newFormat.overrideWith( blockFormat );
246 tmpFragment.setCharacterFormat( newFormat );
247
248 const QStringList tabSplit = fragmentText.split( sTabReplacementMarkerRx );
249 int index = 0;
250 for ( const QString &part : tabSplit )
251 {
252 if ( !part.isEmpty() )
253 {
254 tmpFragment.setText( part );
255 block.append( tmpFragment );
256 }
257 if ( index != tabSplit.size() - 1 )
258 {
259 block.append( QgsTextFragment( QString( '\t' ) ) );
260 }
261 index++;
262 }
263 }
264 else
265 {
266 QgsTextFragment tmpFragment( fragment );
267 QgsTextCharacterFormat newFormat { tmpFragment.characterFormat() };
268 newFormat.overrideWith( blockFormat );
269 tmpFragment.setCharacterFormat( newFormat );
270
271 block.append( tmpFragment );
272 }
273 }
274 it++;
275 }
276
277 if ( !block.empty() )
278 document.append( block );
279
280 sourceBlock = sourceBlock.next();
281 if ( !sourceBlock.isValid() )
282 break;
283 }
284 }
285
286 return document;
287}
288
289QgsTextDocument QgsTextDocument::fromTextAndFormat( const QStringList &lines, const QgsTextFormat &format )
290{
291 QgsTextDocument doc;
292 if ( !format.allowHtmlFormatting() || lines.isEmpty() )
293 {
294 doc = QgsTextDocument::fromPlainText( lines );
295 }
296 else
297 {
298 doc = QgsTextDocument::fromHtml( lines );
299 }
300 if ( doc.size() > 0 )
301 doc.applyCapitalization( format.capitalization() );
302 return doc;
303}
304
306{
307 mBlocks.append( block );
308}
309
311{
312 mBlocks.push_back( block );
313}
314
315void QgsTextDocument::insert( int index, const QgsTextBlock &block )
316{
317 mBlocks.insert( index, block );
318}
319
320void QgsTextDocument::insert( int index, QgsTextBlock &&block )
321{
322 mBlocks.insert( index, block );
323}
324
326{
327 mBlocks.reserve( count );
328}
329
331{
332 return mBlocks.at( i );
333}
334
336{
337 return mBlocks[i];
338}
339
341{
342 return mBlocks.size();
343}
344
346{
347 QStringList textLines;
348 textLines.reserve( mBlocks.size() );
349 for ( const QgsTextBlock &block : mBlocks )
350 {
351 QString line;
352 for ( const QgsTextFragment &fragment : block )
353 {
354 line.append( fragment.text() );
355 }
356 textLines << line;
357 }
358 return textLines;
359}
360
361void QgsTextDocument::splitLines( const QString &wrapCharacter, int autoWrapLength, bool useMaxLineLengthWhenAutoWrapping )
362{
363 const QVector< QgsTextBlock > prevBlocks = mBlocks;
364 mBlocks.clear();
365 mBlocks.reserve( prevBlocks.size() );
366 for ( const QgsTextBlock &block : prevBlocks )
367 {
368 QgsTextBlock destinationBlock;
369 destinationBlock.setBlockFormat( block.blockFormat() );
370 for ( const QgsTextFragment &fragment : block )
371 {
372 QStringList thisParts;
373 if ( !wrapCharacter.isEmpty() && wrapCharacter != QLatin1String( "\n" ) )
374 {
375 //wrap on both the wrapchr and new line characters
376 const QStringList lines = fragment.text().split( wrapCharacter );
377 for ( const QString &line : lines )
378 {
379 thisParts.append( line.split( '\n' ) );
380 }
381 }
382 else
383 {
384 thisParts = fragment.text().split( '\n' );
385 }
386
387 // apply auto wrapping to each manually created line
388 if ( autoWrapLength != 0 )
389 {
390 QStringList autoWrappedLines;
391 autoWrappedLines.reserve( thisParts.count() );
392 for ( const QString &line : std::as_const( thisParts ) )
393 {
394 autoWrappedLines.append( QgsStringUtils::wordWrap( line, autoWrapLength, useMaxLineLengthWhenAutoWrapping ).split( '\n' ) );
395 }
396 thisParts = autoWrappedLines;
397 }
398
399 if ( thisParts.empty() )
400 continue;
401 else if ( thisParts.size() == 1 )
402 destinationBlock.append( fragment );
403 else
404 {
405 if ( !thisParts.at( 0 ).isEmpty() )
406 destinationBlock.append( QgsTextFragment( thisParts.at( 0 ), fragment.characterFormat() ) );
407
408 append( destinationBlock );
409 destinationBlock.clear();
410 for ( int i = 1 ; i < thisParts.size() - 1; ++i )
411 {
412 QgsTextBlock partBlock( QgsTextFragment( thisParts.at( i ), fragment.characterFormat() ) );
413 partBlock.setBlockFormat( block.blockFormat() );
414 append( partBlock );
415 }
416 destinationBlock.append( QgsTextFragment( thisParts.at( thisParts.size() - 1 ), fragment.characterFormat() ) );
417 }
418 }
419 append( destinationBlock );
420 }
421}
422
424{
425 for ( QgsTextBlock &block : mBlocks )
426 {
427 block.applyCapitalization( capitalization );
428 }
429}
430
432{
433 return std::any_of( mBlocks.begin(), mBlocks.end(), []( const QgsTextBlock & block ) { return block.hasBackgrounds(); } );
434}
435
437QVector< QgsTextBlock >::const_iterator QgsTextDocument::begin() const
438{
439 return mBlocks.begin();
440}
441
442QVector< QgsTextBlock >::const_iterator QgsTextDocument::end() const
443{
444 return mBlocks.end();
445}
Capitalization
String capitalization options.
Definition qgis.h:3389
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.
int size() const
Returns the number of fragments in the block.
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.
const QgsTextFragment & at(int index) const
Returns the fragment at the specified index.
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).
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.
QgsTextDocument & operator=(const QgsTextDocument &other)
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.
bool hasBackgrounds() const
Returns true if any blocks or fragments in the document have background brushes set.
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_RX
#define TAB_REPLACEMENT_MARKER