QGIS API Documentation 3.41.0-Master (af5edcb665c)
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
397{
398 return std::any_of( mBlocks.begin(), mBlocks.end(), []( const QgsTextBlock & block ) { return block.hasBackgrounds(); } );
399}
400
402QVector< QgsTextBlock >::const_iterator QgsTextDocument::begin() const
403{
404 return mBlocks.begin();
405}
406
407QVector< QgsTextBlock >::const_iterator QgsTextDocument::end() const
408{
409 return mBlocks.end();
410}
Capitalization
String capitalization options.
Definition qgis.h:3203
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