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