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