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