QGIS API Documentation 3.39.0-Master (d85f3c2a281)
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
22#include <QTextDocument>
23#include <QTextBlock>
24
25
27
29
31{
32 mBlocks.append( block );
33}
34
36{
37 mBlocks.append( QgsTextBlock( fragment ) );
38}
39
41{
42 QgsTextDocument document;
43 document.reserve( lines.size() );
44 for ( const QString &line : lines )
45 {
46 document.append( QgsTextBlock::fromPlainText( line ) );
47 }
48 return document;
49}
50
51// Note -- must start and end with spaces, so that a tab character within
52// a html or css tag doesn't mess things up. Instead, Qt will just silently
53// ignore html attributes it doesn't know about, like this replacement string
54#define TAB_REPLACEMENT_MARKER " ignore_me_i_am_a_tab "
55
56QgsTextDocument QgsTextDocument::fromHtml( const QStringList &lines )
57{
58 QgsTextDocument document;
59
60 document.reserve( lines.size() );
61
62 for ( const QString &l : std::as_const( lines ) )
63 {
64 QString line = l;
65 // QTextDocument is a very heavy way of parsing HTML + css (it's heavily geared toward an editable text document,
66 // and includes a LOT of calculations we don't need, when all we're after is a HTML + CSS style parser).
67 // TODO - try to find an alternative library we can use here
68
69 QTextDocument sourceDoc;
70
71 // QTextDocument will replace tab characters with a space. We need to hack around this
72 // by first replacing it with a string which QTextDocument won't mess with, and then
73 // handle these markers as tab characters in the parsed HTML document.
74 line.replace( QString( '\t' ), QStringLiteral( TAB_REPLACEMENT_MARKER ) );
75
76 // cheat a little. Qt css requires word-spacing to have the "px" suffix. But we don't treat word spacing
77 // as pixels, because that doesn't scale well with different dpi render targets! So let's instead use just instead treat the suffix as
78 // 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
79 // correctly...
80 const thread_local QRegularExpression sRxWordSpacingFix( QStringLiteral( "word-spacing:\\s*(-?\\d+(?:\\.\\d+)?)([a-zA-Z]*)" ) );
81 line.replace( sRxWordSpacingFix, QStringLiteral( "word-spacing: \\1px" ) );
82
83 sourceDoc.setHtml( line );
84
85 QTextBlock sourceBlock = sourceDoc.firstBlock();
86
87 while ( true )
88 {
89 const int headingLevel = sourceBlock.blockFormat().headingLevel();
90 QgsTextCharacterFormat blockFormat;
91 if ( headingLevel > 0 )
92 {
93 switch ( headingLevel )
94 {
95 case 1:
96 blockFormat.setFontPercentageSize( 21.0 / 12 );
97 break;
98 case 2:
99 blockFormat.setFontPercentageSize( 16.0 / 12 );
100 break;
101 case 3:
102 blockFormat.setFontPercentageSize( 13.0 / 12 );
103 break;
104 case 4:
105 blockFormat.setFontPercentageSize( 11.0 / 12 );
106 break;
107 case 5:
108 blockFormat.setFontPercentageSize( 8.0 / 12 );
109 break;
110 case 6:
111 blockFormat.setFontPercentageSize( 7.0 / 12 );
112 break;
113 default:
114 break;
115 }
116 }
117
118 auto it = sourceBlock.begin();
119 QgsTextBlock block;
120 block.setBlockFormat( QgsTextBlockFormat( sourceBlock.blockFormat() ) );
121 while ( !it.atEnd() )
122 {
123 const QTextFragment fragment = it.fragment();
124 if ( fragment.isValid() )
125 {
126 // Search for line breaks in the fragment
127 const QString fragmentText = fragment.text();
128 if ( fragmentText.contains( QStringLiteral( "\u2028" ) ) )
129 {
130 // Split fragment text into lines
131 const QStringList splitLines = fragmentText.split( QStringLiteral( "\u2028" ), Qt::SplitBehaviorFlags::SkipEmptyParts );
132
133 for ( const QString &splitLine : std::as_const( splitLines ) )
134 {
135 const QgsTextCharacterFormat *previousFormat = nullptr;
136
137 // If the splitLine is not the first, inherit style from previous fragment
138 if ( splitLine != splitLines.first() && document.size() > 0 )
139 {
140 previousFormat = &document.at( document.size() - 1 ).at( 0 ).characterFormat();
141 }
142
143 if ( splitLine.contains( QStringLiteral( TAB_REPLACEMENT_MARKER ) ) )
144 {
145 // split line by tab characters, each tab should be a
146 // fragment by itself
147 QgsTextFragment splitFragment( fragment );
148 QgsTextCharacterFormat newFormat { splitFragment.characterFormat() };
149 newFormat.overrideWith( blockFormat );
150 if ( previousFormat )
151 {
152 // Apply overrides from previous fragment
153 newFormat.overrideWith( *previousFormat );
154 splitFragment.setCharacterFormat( newFormat );
155 }
156 splitFragment.setCharacterFormat( newFormat );
157
158 const QStringList tabSplit = splitLine.split( QStringLiteral( TAB_REPLACEMENT_MARKER ) );
159 int index = 0;
160 for ( const QString &part : tabSplit )
161 {
162 if ( !part.isEmpty() )
163 {
164 splitFragment.setText( part );
165 block.append( splitFragment );
166 }
167 if ( index != tabSplit.size() - 1 )
168 {
169 block.append( QgsTextFragment( QString( '\t' ) ) );
170 }
171 index++;
172 }
173 }
174 else
175 {
176 QgsTextFragment splitFragment( fragment );
177 splitFragment.setText( splitLine );
178
179 QgsTextCharacterFormat newFormat { splitFragment.characterFormat() };
180 newFormat.overrideWith( blockFormat );
181 if ( previousFormat )
182 {
183 // Apply overrides from previous fragment
184 newFormat.overrideWith( *previousFormat );
185 }
186 splitFragment.setCharacterFormat( newFormat );
187
188 block.append( splitFragment );
189 }
190
191 document.append( block );
192 block = QgsTextBlock();
193 block.setBlockFormat( QgsTextBlockFormat( sourceBlock.blockFormat() ) );
194 }
195 }
196 else if ( fragmentText.contains( QStringLiteral( TAB_REPLACEMENT_MARKER ) ) )
197 {
198 // split line by tab characters, each tab should be a
199 // fragment by itself
200 QgsTextFragment tmpFragment( fragment );
201
202 QgsTextCharacterFormat newFormat { tmpFragment.characterFormat() };
203 newFormat.overrideWith( blockFormat );
204 tmpFragment.setCharacterFormat( newFormat );
205
206 const QStringList tabSplit = fragmentText.split( QStringLiteral( TAB_REPLACEMENT_MARKER ) );
207 int index = 0;
208 for ( const QString &part : tabSplit )
209 {
210 if ( !part.isEmpty() )
211 {
212 tmpFragment.setText( part );
213 block.append( tmpFragment );
214 }
215 if ( index != tabSplit.size() - 1 )
216 {
217 block.append( QgsTextFragment( QString( '\t' ) ) );
218 }
219 index++;
220 }
221 }
222 else
223 {
224 QgsTextFragment tmpFragment( fragment );
225 QgsTextCharacterFormat newFormat { tmpFragment.characterFormat() };
226 newFormat.overrideWith( blockFormat );
227 tmpFragment.setCharacterFormat( newFormat );
228
229 block.append( tmpFragment );
230 }
231 }
232 it++;
233 }
234
235 if ( !block.empty() )
236 document.append( block );
237
238 sourceBlock = sourceBlock.next();
239 if ( !sourceBlock.isValid() )
240 break;
241 }
242 }
243
244 return document;
245}
246
248{
249 mBlocks.append( block );
250}
251
253{
254 mBlocks.push_back( block );
255}
256
258{
259 mBlocks.reserve( count );
260}
261
263{
264 return mBlocks.at( i );
265}
266
268{
269 return mBlocks[i];
270}
271
273{
274 return mBlocks.size();
275}
276
278{
279 QStringList textLines;
280 textLines.reserve( mBlocks.size() );
281 for ( const QgsTextBlock &block : mBlocks )
282 {
283 QString line;
284 for ( const QgsTextFragment &fragment : block )
285 {
286 line.append( fragment.text() );
287 }
288 textLines << line;
289 }
290 return textLines;
291}
292
293void QgsTextDocument::splitLines( const QString &wrapCharacter, int autoWrapLength, bool useMaxLineLengthWhenAutoWrapping )
294{
295 const QVector< QgsTextBlock > prevBlocks = mBlocks;
296 mBlocks.clear();
297 mBlocks.reserve( prevBlocks.size() );
298 for ( const QgsTextBlock &block : prevBlocks )
299 {
300 QgsTextBlock destinationBlock;
301 destinationBlock.setBlockFormat( block.blockFormat() );
302 for ( const QgsTextFragment &fragment : block )
303 {
304 QStringList thisParts;
305 if ( !wrapCharacter.isEmpty() && wrapCharacter != QLatin1String( "\n" ) )
306 {
307 //wrap on both the wrapchr and new line characters
308 const QStringList lines = fragment.text().split( wrapCharacter );
309 for ( const QString &line : lines )
310 {
311 thisParts.append( line.split( '\n' ) );
312 }
313 }
314 else
315 {
316 thisParts = fragment.text().split( '\n' );
317 }
318
319 // apply auto wrapping to each manually created line
320 if ( autoWrapLength != 0 )
321 {
322 QStringList autoWrappedLines;
323 autoWrappedLines.reserve( thisParts.count() );
324 for ( const QString &line : std::as_const( thisParts ) )
325 {
326 autoWrappedLines.append( QgsStringUtils::wordWrap( line, autoWrapLength, useMaxLineLengthWhenAutoWrapping ).split( '\n' ) );
327 }
328 thisParts = autoWrappedLines;
329 }
330
331 if ( thisParts.empty() )
332 continue;
333 else if ( thisParts.size() == 1 )
334 destinationBlock.append( fragment );
335 else
336 {
337 if ( !thisParts.at( 0 ).isEmpty() )
338 destinationBlock.append( QgsTextFragment( thisParts.at( 0 ), fragment.characterFormat() ) );
339
340 append( destinationBlock );
341 destinationBlock.clear();
342 for ( int i = 1 ; i < thisParts.size() - 1; ++i )
343 {
344 QgsTextBlock partBlock( QgsTextFragment( thisParts.at( i ), fragment.characterFormat() ) );
345 partBlock.setBlockFormat( block.blockFormat() );
346 append( partBlock );
347 }
348 destinationBlock.append( QgsTextFragment( thisParts.at( thisParts.size() - 1 ), fragment.characterFormat() ) );
349 }
350 }
351 append( destinationBlock );
352 }
353}
354
356{
357 for ( QgsTextBlock &block : mBlocks )
358 {
359 block.applyCapitalization( capitalization );
360 }
361}
362
364QVector< QgsTextBlock >::const_iterator QgsTextDocument::begin() const
365{
366 return mBlocks.begin();
367}
368
369QVector< QgsTextBlock >::const_iterator QgsTextDocument::end() const
370{
371 return mBlocks.end();
372}
Capitalization
String capitalization options.
Definition qgis.h:3130
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 applyCapitalization(Qgis::Capitalization capitalization)
Applies a capitalization style to the document's text.
Stores a fragment of text 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