QGIS API Documentation 4.1.0-Master (26185ffb827)
Loading...
Searching...
No Matches
qgslegendrenderer.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgslegendrenderer.cpp
3 --------------------------------------
4 Date : July 2014
5 Copyright : (C) 2014 by Martin Dobias
6 Email : wonder dot sk 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 "qgslegendrenderer.h"
17
18#include <memory>
19
21#include "qgslayertree.h"
23#include "qgslayertreemodel.h"
25#include "qgslegendstyle.h"
26#include "qgsrendercontext.h"
27#include "qgstextdocument.h"
29#include "qgstextrenderer.h"
30
31#include <QJsonObject>
32#include <QPainter>
33#include <QString>
34
35using namespace Qt::StringLiterals;
36
38 : mLegendModel( legendModel )
39 , mProxyModel( std::make_unique< QgsLayerTreeFilterProxyModel >() )
40 , mSettings( settings )
41{
42 mProxyModel->setLayerTreeModel( mLegendModel );
43}
44
46 : mLegendModel( other.mLegendModel )
47 , mProxyModel( std::move( other.mProxyModel ) )
48 , mSettings( std::move( other.mSettings ) )
49 , mLegendSize( other.mLegendSize )
50{
51 mProxyModel->setLayerTreeModel( mLegendModel );
52}
53
55{
56 if ( mProxyModel.get() == model )
57 return;
58
59 mProxyModel.reset( model );
60 mProxyModel->setLayerTreeModel( mLegendModel );
61}
62
64
66{
67 std::unique_ptr< QgsRenderContext > tmpContext;
68
69 if ( !renderContext )
70 {
71 // QGIS 5.0 - make render context mandatory
73 tmpContext = std::make_unique<QgsRenderContext>( QgsRenderContext::fromQPainter( nullptr ) );
74 tmpContext->setRendererScale( mSettings.mapScale() );
75 tmpContext->setMapToPixel( QgsMapToPixel( 1 / ( mSettings.mmPerMapUnit() * tmpContext->scaleFactor() ) ) );
77 renderContext = tmpContext.get();
79 }
80
81 QgsScopedRenderContextPainterSwap nullPainterSwap( *renderContext, nullptr );
82 return paintAndDetermineSize( *renderContext );
83}
84
85void QgsLegendRenderer::drawLegend( QPainter *painter )
86{
89 QgsScopedRenderContextScaleToMm scaleToMm( context );
90
91 context.setRendererScale( mSettings.mapScale() );
92 context.setMapToPixel( QgsMapToPixel( 1 / ( mSettings.mmPerMapUnit() * context.scaleFactor() ) ) );
94
95 paintAndDetermineSize( context );
96}
97
99{
100 QJsonObject json;
101
102 QgsLayerTreeGroup *rootGroup = mLegendModel->rootGroup();
103 if ( !rootGroup )
104 return json;
105
106 json = exportLegendToJson( context, rootGroup );
107 json[u"title"_s] = mSettings.title();
108 return json;
109}
110
111QJsonObject QgsLegendRenderer::exportLegendToJson( const QgsRenderContext &context, QgsLayerTreeGroup *nodeGroup )
112{
113 QJsonObject json;
114 QJsonArray nodes;
115 const QList<QgsLayerTreeNode *> childNodes = nodeGroup->children();
116 for ( QgsLayerTreeNode *node : childNodes )
117 {
118 if ( !mProxyModel->nodeShown( node ) )
119 continue;
120
121 if ( QgsLayerTree::isGroup( node ) )
122 {
123 QgsLayerTreeGroup *nodeGroup = QgsLayerTree::toGroup( node );
124 const QModelIndex idx = mLegendModel->node2index( nodeGroup );
125 const QString text = mLegendModel->data( idx, Qt::DisplayRole ).toString();
126
127 QJsonObject group = exportLegendToJson( context, nodeGroup );
128 group[u"type"_s] = u"group"_s;
129 group[u"title"_s] = text;
130 nodes.append( group );
131 }
132 else if ( QgsLayerTree::isLayer( node ) )
133 {
134 QgsLayerTreeLayer *nodeLayer = QgsLayerTree::toLayer( node );
135
136 QString text;
138 {
139 const QModelIndex idx = mLegendModel->node2index( nodeLayer );
140 text = mLegendModel->data( idx, Qt::DisplayRole ).toString();
141 }
142
143 QList<QgsLayerTreeModelLegendNode *> legendNodes = mLegendModel->layerLegendNodes( nodeLayer );
144
145 if ( legendNodes.isEmpty() && mLegendModel->legendFilterMapSettings() )
146 continue;
147
148 if ( legendNodes.count() == 1 )
149 {
150 QJsonObject group = legendNodes.at( 0 )->exportToJson( mSettings, context );
151 group[u"type"_s] = u"layer"_s;
152 if ( mSettings.jsonRenderFlags().testFlag( Qgis::LegendJsonRenderFlag::ShowRuleDetails ) )
153 {
154 if ( QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( nodeLayer->layer() ) )
155 {
156 if ( vLayer->renderer() )
157 {
158 const QString ruleKey { legendNodes.at( 0 )->data( static_cast< int >( QgsLayerTreeModelLegendNode::CustomRole::RuleKey ) ).toString() };
159 if ( !ruleKey.isEmpty() )
160 {
161 bool ok = false;
162 const QString ruleExp { vLayer->renderer()->legendKeyToExpression( ruleKey, vLayer, ok ) };
163 if ( ok )
164 {
165 group[u"rule"_s] = ruleExp;
166 }
167 }
168 }
169 }
170 }
171 nodes.append( group );
172 }
173 else if ( legendNodes.count() > 1 )
174 {
175 QJsonObject group;
176 group[u"type"_s] = u"layer"_s;
177 group[u"title"_s] = text;
178
179 QJsonArray symbols;
180 for ( int j = 0; j < legendNodes.count(); j++ )
181 {
182 QgsLayerTreeModelLegendNode *legendNode = legendNodes.at( j );
183 QJsonObject symbol = legendNode->exportToJson( mSettings, context );
184 if ( mSettings.jsonRenderFlags().testFlag( Qgis::LegendJsonRenderFlag::ShowRuleDetails ) )
185 {
186 if ( QgsVectorLayer *vLayer = qobject_cast<QgsVectorLayer *>( nodeLayer->layer() ) )
187 {
188 if ( vLayer->renderer() )
189 {
190 const QString ruleKey { legendNode->data( static_cast< int >( QgsLayerTreeModelLegendNode::CustomRole::RuleKey ) ).toString() };
191 if ( !ruleKey.isEmpty() )
192 {
193 bool ok = false;
194 const QString ruleExp { vLayer->renderer()->legendKeyToExpression( ruleKey, vLayer, ok ) };
195 if ( ok )
196 {
197 symbol[u"rule"_s] = ruleExp;
198 }
199 }
200 }
201 }
202 }
203 symbols.append( symbol );
204 }
205 group[u"symbols"_s] = symbols;
206
207 nodes.append( group );
208 }
209 }
210 }
211
212 json[u"nodes"_s] = nodes;
213 return json;
214}
215
216QSizeF QgsLegendRenderer::paintAndDetermineSize( QgsRenderContext &context )
217{
218 QSizeF size( 0, 0 );
219 QgsLayerTreeGroup *rootGroup = mLegendModel->rootGroup();
220 if ( !rootGroup )
221 return size;
222
223 mSettings.updateDataDefinedProperties( context );
224
225 // temporarily remove painter from context -- we don't need to actually draw anything yet. But we DO need
226 // to send the full render context so that an expression context is available during the size calculation
227 QgsScopedRenderContextPainterSwap noPainter( context, nullptr );
228
229 QList<LegendComponentGroup> componentGroups = createComponentGroupList( rootGroup, context );
230
231 const int columnCount = setColumns( componentGroups );
232
233 QMap< int, double > maxColumnWidths;
234 qreal maxEqualColumnWidth = 0;
235 // another iteration -- this one is required to calculate the maximum item width for each
236 // column. Unfortunately, we can't trust the component group widths at this stage, as they are minimal widths
237 // only. When actually rendering a symbol node, the text is aligned according to the WIDEST
238 // symbol in a column. So that means we can't possibly determine the exact size of legend components
239 // until now. BUUUUUUUUUUUUT. Because everything sucks, we can't even start the actual render of items
240 // at the same time we calculate this -- legend items REQUIRE the REAL width of the columns in order to
241 // correctly align right or center-aligned symbols/text. Bah -- A triple iteration it is!
242 for ( const LegendComponentGroup &group : std::as_const( componentGroups ) )
243 {
244 const QSizeF actualSize = drawGroup( group, context, ColumnContext() );
245 maxEqualColumnWidth = std::max( actualSize.width(), maxEqualColumnWidth );
246 maxColumnWidths[group.column] = std::max( actualSize.width(), maxColumnWidths.value( group.column, 0 ) );
247 }
248
249 if ( columnCount == 1 )
250 {
251 // single column - use the full available width
252 maxEqualColumnWidth = std::max( maxEqualColumnWidth, mLegendSize.width() - 2 * mSettings.boxSpace() );
253 maxColumnWidths[0] = maxEqualColumnWidth;
254 }
255
256 //calculate size of title
257 QSizeF titleSize = drawTitle( context, 0 );
258 //add title margin to size of title text
259 titleSize.rwidth() += mSettings.boxSpace() * 2.0;
260 double columnTop = mSettings.boxSpace() + titleSize.height() + mSettings.style( Qgis::LegendComponent::Title ).margin( QgsLegendStyle::Bottom );
261
262 noPainter.reset();
263
264 bool firstInColumn = true;
265 double columnMaxHeight = 0;
266 qreal columnWidth = 0;
267 int column = -1;
268 ColumnContext columnContext;
269 columnContext.left = mSettings.boxSpace();
270 columnContext.right = std::max( mLegendSize.width() - mSettings.boxSpace(), mSettings.boxSpace() );
271 double currentY = columnTop;
272
273 for ( const LegendComponentGroup &group : std::as_const( componentGroups ) )
274 {
275 if ( group.column > column )
276 {
277 // Switch to next column
278 columnContext.left = group.column > 0 ? ( columnContext.right + mSettings.columnSpace() ) : mSettings.boxSpace();
279 columnWidth = mSettings.equalColumnWidth() ? maxEqualColumnWidth : maxColumnWidths.value( group.column );
280 columnContext.right = columnContext.left + columnWidth;
281 currentY = columnTop;
282 column++;
283 firstInColumn = true;
284 }
285 if ( !firstInColumn )
286 {
287 currentY += spaceAboveGroup( group );
288 }
289
290 drawGroup( group, context, columnContext, currentY );
291
292 currentY += group.size.height();
293 columnMaxHeight = std::max( currentY - columnTop, columnMaxHeight );
294
295 firstInColumn = false;
296 }
297 const double totalWidth = columnContext.right + mSettings.boxSpace();
298
299 size.rheight() = columnTop + columnMaxHeight + mSettings.boxSpace();
300 size.rwidth() = totalWidth;
301 if ( !mSettings.title().isEmpty() )
302 {
303 size.rwidth() = std::max( titleSize.width(), size.width() );
304 }
305
306 // override the size if it was set by the user
307 if ( mLegendSize.isValid() )
308 {
309 qreal w = std::max( size.width(), mLegendSize.width() );
310 qreal h = std::max( size.height(), mLegendSize.height() );
311 size = QSizeF( w, h );
312 }
313
314 // Now we have set the correct total item width and can draw the title centered
315 if ( !mSettings.title().isEmpty() )
316 {
317 drawTitle( context, mSettings.boxSpace(), mSettings.titleAlignment(), size.width() );
318 }
319
320 return size;
321}
322
323void QgsLegendRenderer::widthAndOffsetForTitleText( const Qt::AlignmentFlag halignment, const double legendWidth, double &textBoxWidth, double &textBoxLeft ) const
324{
325 switch ( halignment )
326 {
327 default:
328 textBoxLeft = mSettings.boxSpace();
329 textBoxWidth = legendWidth - 2 * mSettings.boxSpace();
330 break;
331
332 case Qt::AlignHCenter:
333 {
334 // not sure on this logic, I just moved it -- don't blame me for it being totally obscure!
335 const double centerX = legendWidth / 2;
336 textBoxWidth = ( std::min( static_cast< double >( centerX ), legendWidth - centerX ) - mSettings.boxSpace() ) * 2.0;
337 textBoxLeft = centerX - textBoxWidth / 2.;
338 break;
339 }
340 }
341}
342
343QList<QgsLegendRenderer::LegendComponentGroup> QgsLegendRenderer::createComponentGroupList( QgsLayerTreeGroup *parentGroup, QgsRenderContext &context, double indent )
344{
345 QList<LegendComponentGroup> componentGroups;
346
347 if ( !parentGroup )
348 return componentGroups;
349
350 const QList<QgsLayerTreeNode *> childNodes = parentGroup->children();
351 for ( QgsLayerTreeNode *node : childNodes )
352 {
353 if ( !mProxyModel->nodeShown( node ) )
354 continue;
355
356 if ( QgsLayerTree::isGroup( node ) )
357 {
358 QgsLayerTreeGroup *nodeGroup = QgsLayerTree::toGroup( node );
359 QString style = node->customProperty( u"legend/title-style"_s ).toString();
360 // Update the required indent for the group/subgroup items, starting from the indent accumulated from parent groups
361 double newIndent = indent;
362 if ( style == "subgroup"_L1 )
363 {
364 newIndent += mSettings.style( Qgis::LegendComponent::Subgroup ).indent();
365 }
366 else
367 {
368 newIndent += mSettings.style( Qgis::LegendComponent::Group ).indent();
369 }
370
371 // Group subitems
372 QList<LegendComponentGroup> subgroups = createComponentGroupList( nodeGroup, context, newIndent );
373
374 bool hasSubItems = !subgroups.empty();
375
377 {
378 LegendComponent component;
379 component.item = node;
380 component.indent = newIndent;
381 component.size = drawGroupTitle( nodeGroup, context );
382
383 if ( !subgroups.isEmpty() )
384 {
385 // Add internal space between this group title and the next component
386 subgroups[0].size.rheight() += spaceAboveGroup( subgroups[0] );
387 // Prepend this group title to the first group
388 subgroups[0].components.prepend( component );
389 subgroups[0].size.rheight() += component.size.height();
390 subgroups[0].size.rwidth() = std::max( component.size.width(), subgroups[0].size.width() );
391 if ( nodeGroup->customProperty( u"legend/column-break"_s ).toInt() )
392 subgroups[0].placeColumnBreakBeforeGroup = true;
393 }
394 else
395 {
396 // no subitems, create new group
397 LegendComponentGroup group;
398 group.placeColumnBreakBeforeGroup = nodeGroup->customProperty( u"legend/column-break"_s ).toInt();
399 group.components.append( component );
400 group.size.rwidth() += component.size.width();
401 group.size.rheight() += component.size.height();
402 group.size.rwidth() = std::max( component.size.width(), group.size.width() );
403 subgroups.append( group );
404 }
405 }
406
407 if ( hasSubItems ) //leave away groups without content
408 {
409 componentGroups.append( subgroups );
410 }
411 }
412 else if ( QgsLayerTree::isLayer( node ) )
413 {
414 QgsLayerTreeLayer *nodeLayer = QgsLayerTree::toLayer( node );
415 Qgis::LegendComponent layerStyle = nodeLegendStyle( nodeLayer );
416 bool allowColumnSplit = false;
417 switch ( nodeLayer->legendSplitBehavior() )
418 {
420 allowColumnSplit = mSettings.splitLayer();
421 break;
423 allowColumnSplit = true;
424 break;
426 allowColumnSplit = false;
427 break;
428 }
429
430 LegendComponentGroup group;
431 group.placeColumnBreakBeforeGroup = nodeLayer->customProperty( u"legend/column-break"_s ).toInt();
432
433 if ( layerStyle != Qgis::LegendComponent::Hidden )
434 {
435 LegendComponent component;
436 component.item = node;
437 component.size = drawLayerTitle( nodeLayer, context );
438 component.indent = indent;
439 group.components.append( component );
440 group.size.rwidth() = component.size.width();
441 group.size.rheight() = component.size.height();
442 }
443
444 QList<QgsLayerTreeModelLegendNode *> legendNodes = mLegendModel->layerLegendNodes( nodeLayer );
445
446 // workaround for the issue that "filtering by map" does not remove layer nodes that have no symbols present
447 // on the map. We explicitly skip such layers here. In future ideally that should be handled directly
448 // in the layer tree model
449 if ( legendNodes.isEmpty() && mLegendModel->legendFilterMapSettings() )
450 continue;
451
452 QList<LegendComponentGroup> layerGroups;
453 layerGroups.reserve( legendNodes.count() );
454
455 bool groupIsLayerGroup = true;
456 double symbolIndent = indent;
457 switch ( layerStyle )
458 {
461 symbolIndent += mSettings.style( layerStyle ).indent();
462 break;
463 default:
464 break;
465 }
466 for ( int j = 0; j < legendNodes.count(); j++ )
467 {
468 QgsLayerTreeModelLegendNode *legendNode = legendNodes.at( j );
469
470 LegendComponent symbolComponent = drawSymbolItem( legendNode, context, ColumnContext(), 0 );
471
472 const bool forceBreak = legendNode->columnBreak();
473
474 if ( !allowColumnSplit || j == 0 )
475 {
476 if ( forceBreak )
477 {
478 if ( groupIsLayerGroup )
479 layerGroups.prepend( group );
480 else
481 layerGroups.append( group );
482
483 group = LegendComponentGroup();
484 group.placeColumnBreakBeforeGroup = true;
485 groupIsLayerGroup = false;
486 }
487
488 // append to layer group
489 // the width is not correct at this moment, we must align all symbol labels
490 group.size.rwidth() = std::max( symbolComponent.size.width(), group.size.width() );
491 // Add symbol space only if there is already title or another item above
492 if ( !group.components.isEmpty() )
493 {
494 // TODO: for now we keep Symbol and SymbolLabel Top margin in sync
495 group.size.rheight() += mSettings.style( Qgis::LegendComponent::Symbol ).margin( QgsLegendStyle::Top );
496 }
497 group.size.rheight() += symbolComponent.size.height();
498 symbolComponent.indent = symbolIndent;
499 group.components.append( symbolComponent );
500 }
501 else
502 {
503 if ( group.size.height() > 0 )
504 {
505 if ( groupIsLayerGroup )
506 layerGroups.prepend( group );
507 else
508 layerGroups.append( group );
509 group = LegendComponentGroup();
510 groupIsLayerGroup = false;
511 }
512 LegendComponentGroup symbolGroup;
513 symbolGroup.placeColumnBreakBeforeGroup = forceBreak;
514 symbolComponent.indent = symbolIndent;
515 symbolGroup.components.append( symbolComponent );
516 symbolGroup.size.rwidth() = symbolComponent.size.width();
517 symbolGroup.size.rheight() = symbolComponent.size.height();
518 layerGroups.append( symbolGroup );
519 }
520 }
521 if ( group.size.height() > 0 )
522 {
523 if ( groupIsLayerGroup )
524 layerGroups.prepend( group );
525 else
526 layerGroups.append( group );
527 }
528 componentGroups.append( layerGroups );
529 }
530 }
531
532 return componentGroups;
533}
534
535
536int QgsLegendRenderer::setColumns( QList<LegendComponentGroup> &componentGroups )
537{
538 // Divide groups to columns
539 double totalHeight = 0;
540 qreal maxGroupHeight = 0;
541 int forcedColumnBreaks = 0;
542 double totalSpaceAboveGroups = 0;
543
544 for ( const LegendComponentGroup &group : std::as_const( componentGroups ) )
545 {
546 const double topMargin = spaceAboveGroup( group );
547 totalHeight += topMargin;
548 totalSpaceAboveGroups += topMargin;
549
550 const double groupHeight = group.size.height();
551 totalHeight += groupHeight;
552 maxGroupHeight = std::max( groupHeight, maxGroupHeight );
553
554 if ( group.placeColumnBreakBeforeGroup )
555 forcedColumnBreaks++;
556 }
557 const double totalGroupHeight = ( totalHeight - totalSpaceAboveGroups );
558 double averageGroupHeight = totalGroupHeight / componentGroups.size();
559
560 if ( mSettings.columnCount() == 0 && forcedColumnBreaks == 0 )
561 return 0;
562
563 // the target number of columns allowed is dictated by the number of forced column
564 // breaks OR the manually set column count (whichever is greater!)
565 const int targetNumberColumns = std::max( forcedColumnBreaks + 1, mSettings.columnCount() );
566 const int numberAutoPlacedBreaks = targetNumberColumns - forcedColumnBreaks - 1;
567
568 // We know height of each group and we have to split them into columns
569 // minimizing max column height. It is sort of bin packing problem, NP-hard.
570 // We are using simple heuristic, brute fore appeared to be to slow,
571 // the number of combinations is N = n!/(k!*(n-k)!) where n = groupCount-1
572 // and k = columnsCount-1
573 double maxColumnHeight = 0;
574 int currentColumn = 0;
575 int currentColumnGroupCount = 0; // number of groups in current column
576 double currentColumnHeight = 0;
577 int autoPlacedBreaks = 0;
578
579 // Calculate the expected average space between items
580 double averageSpaceAboveGroups = 0;
581 if ( componentGroups.size() > targetNumberColumns )
582 averageSpaceAboveGroups = totalSpaceAboveGroups / ( componentGroups.size() );
583
584 double totalRemainingGroupHeight = totalGroupHeight;
585 double totalRemainingSpaceAboveGroups = totalSpaceAboveGroups;
586 for ( int i = 0; i < componentGroups.size(); i++ )
587 {
588 const LegendComponentGroup &group = componentGroups.at( i );
589 const double currentGroupHeight = group.size.height();
590 const double spaceAboveCurrentGroup = spaceAboveGroup( group );
591
592 totalRemainingGroupHeight -= currentGroupHeight;
593 totalRemainingSpaceAboveGroups -= spaceAboveCurrentGroup;
594
595 double currentColumnHeightIfGroupIsIncluded = currentColumnHeight;
596 if ( currentColumnGroupCount > 0 )
597 currentColumnHeightIfGroupIsIncluded += spaceAboveCurrentGroup;
598 currentColumnHeightIfGroupIsIncluded += currentGroupHeight;
599
600 const int numberRemainingGroupsIncludingThisOne = componentGroups.size() - i;
601 const int numberRemainingColumnsIncludingThisOne = numberAutoPlacedBreaks + 1 - autoPlacedBreaks;
602 const int numberRemainingColumnBreaks = numberRemainingColumnsIncludingThisOne - 1;
603
604 const double averageRemainingSpaceAboveGroups = numberRemainingGroupsIncludingThisOne > 1 ? ( totalRemainingSpaceAboveGroups / ( numberRemainingGroupsIncludingThisOne - 1 ) ) : 0;
605 const double estimatedRemainingSpaceAboveGroupsWhichWontBeUsedBecauseGroupsAreFirstInColumn = numberRemainingColumnBreaks * averageRemainingSpaceAboveGroups;
606 const double estimatedRemainingTotalHeightAfterThisGroup = totalRemainingGroupHeight + totalRemainingSpaceAboveGroups - estimatedRemainingSpaceAboveGroupsWhichWontBeUsedBecauseGroupsAreFirstInColumn;
607
608 const double estimatedTotalHeightOfRemainingColumnsIncludingThisOne = currentColumnHeightIfGroupIsIncluded + estimatedRemainingTotalHeightAfterThisGroup;
609
610 // Recalc average height for remaining columns including current
611 double averageRemainingColumnHeightIncludingThisOne = estimatedTotalHeightOfRemainingColumnsIncludingThisOne / numberRemainingColumnsIncludingThisOne;
612
613 // Round up to the next full number of groups to put in one column
614 // This ensures that earlier columns contain more elements than later columns
615 const int averageGroupsPerRemainingColumnsIncludingThisOne = std::ceil( averageRemainingColumnHeightIncludingThisOne / ( averageGroupHeight + averageSpaceAboveGroups ) );
616
617 averageRemainingColumnHeightIncludingThisOne = averageGroupsPerRemainingColumnsIncludingThisOne * ( averageGroupHeight + averageSpaceAboveGroups ) - averageSpaceAboveGroups;
618
619 bool canCreateNewColumn = ( currentColumnGroupCount > 0 ) // do not leave empty column
620 && ( currentColumn < targetNumberColumns - 1 ) // must not exceed max number of columns
621 && ( autoPlacedBreaks < numberAutoPlacedBreaks );
622
623 bool shouldCreateNewColumn = currentColumnHeightIfGroupIsIncluded > averageRemainingColumnHeightIncludingThisOne // current group height is greater than expected group height
624 && currentColumnGroupCount > 0 // do not leave empty column
625 && currentColumnHeightIfGroupIsIncluded > maxGroupHeight // no sense to make smaller columns than max group height
626 && currentColumnHeightIfGroupIsIncluded > maxColumnHeight; // no sense to make smaller columns than max column already created
627
628 shouldCreateNewColumn |= group.placeColumnBreakBeforeGroup;
629 canCreateNewColumn |= group.placeColumnBreakBeforeGroup;
630
631 // also should create a new column if the number of items left < number of columns left
632 // in this case we should spread the remaining items out over the remaining columns
633 shouldCreateNewColumn |= ( componentGroups.size() - i < targetNumberColumns - currentColumn );
634
635 if ( canCreateNewColumn && shouldCreateNewColumn )
636 {
637 // New column
638 currentColumn++;
639 if ( !group.placeColumnBreakBeforeGroup )
640 autoPlacedBreaks++;
641 currentColumnGroupCount = 0;
642 currentColumnHeight = group.size.height();
643 }
644 else
645 {
646 currentColumnHeight = currentColumnHeightIfGroupIsIncluded;
647 }
648 componentGroups[i].column = currentColumn;
649 currentColumnGroupCount++;
650 maxColumnHeight = std::max( currentColumnHeight, maxColumnHeight );
651 }
652
653 auto refineColumns = [&componentGroups, this]() -> bool {
654 QHash< int, double > columnHeights;
655 QHash< int, int > columnGroupCounts;
656 double currentColumnHeight = 0;
657 int currentColumn = -1;
658 int columnCount = 0;
659 int groupCount = 0;
660 double maxCurrentColumnHeight = 0;
661 for ( int i = 0; i < componentGroups.size(); i++ )
662 {
663 const LegendComponentGroup &group = componentGroups.at( i );
664 if ( group.column != currentColumn )
665 {
666 if ( currentColumn >= 0 )
667 {
668 columnHeights.insert( currentColumn, currentColumnHeight );
669 columnGroupCounts.insert( currentColumn, groupCount );
670 }
671
672 currentColumn = group.column;
673 currentColumnHeight = 0;
674 groupCount = 0;
675 columnCount = std::max( columnCount, currentColumn + 1 );
676 }
677
678 const double spaceAbove = spaceAboveGroup( group );
679 currentColumnHeight += spaceAbove + group.size.height();
680 groupCount++;
681 }
682 columnHeights.insert( currentColumn, currentColumnHeight );
683 columnGroupCounts.insert( currentColumn, groupCount );
684
685 double totalColumnHeights = 0;
686 for ( int i = 0; i < columnCount; ++i )
687 {
688 totalColumnHeights += columnHeights[i];
689 maxCurrentColumnHeight = std::max( maxCurrentColumnHeight, columnHeights[i] );
690 }
691
692 const double averageColumnHeight = totalColumnHeights / columnCount;
693
694 bool changed = false;
695 int nextCandidateColumnForShift = 1;
696 for ( int i = 0; i < componentGroups.size(); i++ )
697 {
698 LegendComponentGroup &group = componentGroups[i];
699 if ( group.column < nextCandidateColumnForShift )
700 continue;
701
702 // try shifting item to previous group
703 const bool canShift = !group.placeColumnBreakBeforeGroup && columnGroupCounts[group.column] >= 2;
704
705 if ( canShift && columnHeights[group.column - 1] < averageColumnHeight && ( columnHeights[group.column - 1] + group.size.height() ) * 0.9 < maxCurrentColumnHeight )
706 {
707 group.column -= 1;
708 columnHeights[group.column] += group.size.height() + spaceAboveGroup( group );
709 columnGroupCounts[group.column]++;
710 columnHeights[group.column + 1] -= group.size.height();
711 columnGroupCounts[group.column + 1]--;
712 changed = true;
713 }
714 else
715 {
716 nextCandidateColumnForShift = group.column + 1;
717 }
718 }
719 return changed;
720 };
721
722 bool wasRefined = true;
723 int iterations = 0;
724 while ( wasRefined && iterations < 2 )
725 {
726 wasRefined = refineColumns();
727 iterations++;
728 }
729
730 // Align labels of symbols for each layer/column to the same labelXOffset
731 QMap<QString, qreal> maxSymbolWidth;
732 for ( int i = 0; i < componentGroups.size(); i++ )
733 {
734 LegendComponentGroup &group = componentGroups[i];
735 for ( int j = 0; j < group.components.size(); j++ )
736 {
737 if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast<QgsLayerTreeModelLegendNode *>( group.components.at( j ).item ) )
738 {
739 QString key = u"%1-%2"_s.arg( reinterpret_cast< qulonglong >( legendNode->layerNode() ) ).arg( group.column );
740 maxSymbolWidth[key] = std::max( group.components.at( j ).symbolSize.width(), maxSymbolWidth[key] );
741 }
742 }
743 }
744 for ( int i = 0; i < componentGroups.size(); i++ )
745 {
746 LegendComponentGroup &group = componentGroups[i];
747 for ( int j = 0; j < group.components.size(); j++ )
748 {
749 if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast<QgsLayerTreeModelLegendNode *>( group.components.at( j ).item ) )
750 {
751 QString key = u"%1-%2"_s.arg( reinterpret_cast< qulonglong >( legendNode->layerNode() ) ).arg( group.column );
752 double space = mSettings.style( Qgis::LegendComponent::Symbol ).margin( QgsLegendStyle::Right ) + mSettings.style( Qgis::LegendComponent::SymbolLabel ).margin( QgsLegendStyle::Left );
753 group.components[j].labelXOffset = maxSymbolWidth[key] + space;
754 group.components[j].maxSiblingSymbolWidth = maxSymbolWidth[key];
755 group.components[j].size.rwidth() = maxSymbolWidth[key] + space + group.components.at( j ).labelSize.width();
756 }
757 }
758 }
759 return targetNumberColumns;
760}
761
762QSizeF QgsLegendRenderer::drawTitle( QgsRenderContext &context, double top, Qt::AlignmentFlag halignment, double legendWidth ) const
763{
764 QSizeF size( 0, 0 );
765 if ( mSettings.title().isEmpty() )
766 {
767 return size;
768 }
769 const double dotsPerMM = context.scaleFactor();
770
771 const QgsTextFormat titleFormat = mSettings.style( Qgis::LegendComponent::Title ).textFormat();
772
773 QStringList lines = mSettings.splitStringForWrapping( mSettings.title() );
774 const QgsTextDocument textDocument = QgsTextDocument::fromTextAndFormat( lines, titleFormat );
775
776 QgsTextDocumentRenderContext documentContext;
777 QgsScopedRenderContextScaleToPixels scaleToPx( context );
778 if ( mSettings.autoWrapLinesAfter() > 0 )
779 {
780 documentContext.setMaximumWidth( context.convertToPainterUnits( mSettings.autoWrapLinesAfter(), Qgis::RenderUnit::Millimeters ) );
782 }
783
784 //calculate width and left pos of rectangle to draw text into
785 double textBoxWidth;
786 double textBoxLeft;
787 widthAndOffsetForTitleText( halignment, legendWidth, textBoxWidth, textBoxLeft );
788
789 const double textScaleFactor = QgsTextRenderer::calculateScaleFactorForFormat( context, titleFormat );
790 const QgsTextDocumentMetrics textDocumentMetrics = QgsTextDocumentMetrics::calculateMetrics( textDocument, titleFormat, context, textScaleFactor, documentContext );
791 const double overallTextHeight = textDocumentMetrics.documentSize( Qgis::TextLayoutMode::Rectangle, Qgis::TextOrientation::Horizontal ).height();
792 const double overallTextWidth = textDocumentMetrics.documentSize( Qgis::TextLayoutMode::Rectangle, Qgis::TextOrientation::Horizontal ).width();
793
794 size.rheight() = overallTextHeight / dotsPerMM;
795 size.rwidth() = overallTextWidth / dotsPerMM;
796
797 if ( context.painter() )
798 {
799 const QRectF r( textBoxLeft * dotsPerMM, top * dotsPerMM, textBoxWidth * dotsPerMM, overallTextHeight );
800
801 Qgis::TextHorizontalAlignment halign = halignment == Qt::AlignLeft ? Qgis::TextHorizontalAlignment::Left
802 : halignment == Qt::AlignRight ? Qgis::TextHorizontalAlignment::Right
804
805 QgsTextRenderer::drawDocument( r, titleFormat, textDocumentMetrics.document(), textDocumentMetrics, context, halign, Qgis::TextVerticalAlignment::Top );
806 }
807
808 return size;
809}
810
811
812double QgsLegendRenderer::spaceAboveGroup( const LegendComponentGroup &group )
813{
814 if ( group.components.isEmpty() )
815 return 0;
816
817 LegendComponent component = group.components.first();
818
819 if ( QgsLayerTreeGroup *nodeGroup = qobject_cast<QgsLayerTreeGroup *>( component.item ) )
820 {
821 return mSettings.style( nodeLegendStyle( nodeGroup ) ).margin( QgsLegendStyle::Top );
822 }
823 else if ( QgsLayerTreeLayer *nodeLayer = qobject_cast<QgsLayerTreeLayer *>( component.item ) )
824 {
825 return mSettings.style( nodeLegendStyle( nodeLayer ) ).margin( QgsLegendStyle::Top );
826 }
827 else if ( qobject_cast<QgsLayerTreeModelLegendNode *>( component.item ) )
828 {
829 // TODO: use Symbol or SymbolLabel Top margin
830 return mSettings.style( Qgis::LegendComponent::Symbol ).margin( QgsLegendStyle::Top );
831 }
832
833 return 0;
834}
835
836QSizeF QgsLegendRenderer::drawGroup( const LegendComponentGroup &group, QgsRenderContext &context, ColumnContext columnContext, double top )
837{
838 bool first = true;
839 QSizeF size = QSizeF( group.size );
840 double currentY = top;
841 for ( const LegendComponent &component : std::as_const( group.components ) )
842 {
843 if ( QgsLayerTreeGroup *groupItem = qobject_cast<QgsLayerTreeGroup *>( component.item ) )
844 {
845 Qgis::LegendComponent s = nodeLegendStyle( groupItem );
847 {
848 if ( !first )
849 {
850 currentY += mSettings.style( s ).margin( QgsLegendStyle::Top );
851 }
852 QSizeF groupSize;
853 ColumnContext columnContextForItem = columnContext;
854 double indentWidth = component.indent;
856 {
857 // Remove indent - the subgroup items should be indented, not the subgroup title
858 indentWidth -= mSettings.style( Qgis::LegendComponent::Subgroup ).indent();
859 }
860 else
861 {
862 // Remove indent - the group items should be indented, not the group title
863 indentWidth -= mSettings.style( Qgis::LegendComponent::Group ).indent();
864 }
865 if ( mSettings.style( Qgis::LegendComponent::SymbolLabel ).alignment() == Qt::AlignLeft )
866 {
867 columnContextForItem.left += indentWidth;
868 }
869 if ( mSettings.style( Qgis::LegendComponent::SymbolLabel ).alignment() == Qt::AlignRight )
870 {
871 columnContextForItem.right -= indentWidth;
872 }
873 groupSize = drawGroupTitle( groupItem, context, columnContextForItem, currentY );
874 size.rwidth() = std::max( groupSize.width(), size.width() );
875 }
876 }
877 else if ( QgsLayerTreeLayer *layerItem = qobject_cast<QgsLayerTreeLayer *>( component.item ) )
878 {
879 Qgis::LegendComponent s = nodeLegendStyle( layerItem );
881 {
882 if ( !first )
883 {
884 currentY += mSettings.style( s ).margin( QgsLegendStyle::Top );
885 }
886 QSizeF subGroupSize;
887
888 ColumnContext columnContextForItem = columnContext;
889 double indentWidth = component.indent;
890 columnContextForItem.left += indentWidth;
891 subGroupSize = drawLayerTitle( layerItem, context, columnContextForItem, currentY );
892 size.rwidth() = std::max( subGroupSize.width(), size.width() );
893 }
894 }
895 else if ( QgsLayerTreeModelLegendNode *legendNode = qobject_cast<QgsLayerTreeModelLegendNode *>( component.item ) )
896 {
897 if ( !first )
898 {
899 currentY += mSettings.style( Qgis::LegendComponent::Symbol ).margin( QgsLegendStyle::Top );
900 }
901
902 ColumnContext columnContextForItem = columnContext;
903 double indentWidth = 0;
904 indentWidth = component.indent;
905 if ( mSettings.style( Qgis::LegendComponent::SymbolLabel ).alignment() == Qt::AlignLeft )
906 {
907 columnContextForItem.left += indentWidth;
908 }
909 if ( mSettings.style( Qgis::LegendComponent::SymbolLabel ).alignment() == Qt::AlignRight )
910 {
911 columnContextForItem.right -= indentWidth;
912 }
913
914 LegendComponent symbolComponent = drawSymbolItem( legendNode, context, columnContextForItem, currentY, component.maxSiblingSymbolWidth );
915 // expand width, it may be wider because of label offsets
916 size.rwidth() = std::max( symbolComponent.size.width() + indentWidth, size.width() );
917 }
918 currentY += component.size.height();
919 first = false;
920 }
921 return size;
922}
923
924QgsLegendRenderer::LegendComponent QgsLegendRenderer::drawSymbolItem( QgsLayerTreeModelLegendNode *symbolItem, QgsRenderContext &context, ColumnContext columnContext, double top, double maxSiblingSymbolWidth )
925{
926 QgsLayerTreeModelLegendNode::ItemContext ctx;
927 ctx.context = &context;
928
929 // add a layer expression context scope
930 QgsExpressionContextScope *layerScope = nullptr;
931 if ( symbolItem->layerNode()->layer() )
932 {
933 layerScope = QgsExpressionContextUtils::layerScope( symbolItem->layerNode()->layer() );
934 context.expressionContext().appendScope( layerScope );
935 }
936
937 ctx.painter = context.painter();
939 ctx.point = QPointF( columnContext.left, top );
940 ctx.labelXOffset = maxSiblingSymbolWidth;
942
943 ctx.top = top;
944
945 ctx.columnLeft = columnContext.left;
946 ctx.columnRight = columnContext.right;
947
948 switch ( mSettings.symbolAlignment() )
949 {
950 case Qt::AlignLeft:
951 default:
952 ctx.columnLeft += mSettings.style( Qgis::LegendComponent::Symbol ).margin( QgsLegendStyle::Left );
953 break;
954
955 case Qt::AlignRight:
956 ctx.columnRight -= mSettings.style( Qgis::LegendComponent::Symbol ).margin( QgsLegendStyle::Left );
957 break;
958 }
959
960 ctx.maxSiblingSymbolWidth = maxSiblingSymbolWidth;
961
962 QgsExpressionContextScope *symbolScope = nullptr;
963 if ( const QgsSymbolLegendNode *symbolNode = dynamic_cast< const QgsSymbolLegendNode * >( symbolItem ) )
964 {
965 symbolScope = symbolNode->createSymbolScope();
966 context.expressionContext().appendScope( symbolScope );
967 ctx.patchShape = symbolNode->patchShape();
968 }
969
970 ctx.patchSize = symbolItem->userPatchSize();
971
972 QgsLayerTreeModelLegendNode::ItemMetrics im = symbolItem->draw( mSettings, ctx );
973
974 if ( symbolScope )
975 delete context.expressionContext().popScope();
976
977 if ( layerScope )
978 delete context.expressionContext().popScope();
979
980 LegendComponent component;
981 component.item = symbolItem;
982 component.symbolSize = im.symbolSize;
983 component.labelSize = im.labelSize;
984 //QgsDebugMsgLevel( u"symbol height = %1 label height = %2"_s.arg( symbolSize.height()).arg( labelSize.height() ), 2);
985 // NOTE -- we hard code left/right margins below, because those are the only ones exposed for use currently.
986 // ideally we could (should?) expose all these margins as settings, and then adapt the below to respect the current symbol/text alignment
987 // and consider the correct margin sides...
988 double width = std::max( static_cast< double >( im.symbolSize.width() ), maxSiblingSymbolWidth )
989 + mSettings.style( Qgis::LegendComponent::Symbol ).margin( QgsLegendStyle::Left )
990 + mSettings.style( Qgis::LegendComponent::Symbol ).margin( QgsLegendStyle::Right )
991 + mSettings.style( Qgis::LegendComponent::SymbolLabel ).margin( QgsLegendStyle::Left )
992 + im.labelSize.width();
993
994 double height = std::max( im.symbolSize.height(), im.labelSize.height() );
995 component.size = QSizeF( width, height );
996 return component;
997}
998
999QSizeF QgsLegendRenderer::drawLayerTitle( QgsLayerTreeLayer *nodeLayer, QgsRenderContext &context, ColumnContext columnContext, double top )
1000{
1001 QSizeF size( 0, 0 );
1002 QModelIndex idx = mLegendModel->node2index( nodeLayer );
1003 QString titleString = mLegendModel->data( idx, Qt::DisplayRole ).toString();
1004 //Let the user omit the layer title item by having an empty layer title string
1005 if ( titleString.isEmpty() )
1006 return size;
1007
1008 const double dotsPerMM = context.scaleFactor();
1009
1010 const QgsTextFormat layerFormat = mSettings.style( nodeLegendStyle( nodeLayer ) ).textFormat();
1011
1012 QgsExpressionContextScope *layerScope = nullptr;
1013 if ( nodeLayer->layer() )
1014 {
1015 layerScope = QgsExpressionContextUtils::layerScope( nodeLayer->layer() );
1016 context.expressionContext().appendScope( layerScope );
1017 }
1018
1019 const QStringList lines = mSettings.evaluateItemText( titleString, context.expressionContext() );
1020 const QgsTextDocument textDocument = QgsTextDocument::fromTextAndFormat( lines, layerFormat );
1021
1022 QgsTextDocumentRenderContext documentContext;
1023 QgsScopedRenderContextScaleToPixels scaleToPx( context );
1024 if ( mSettings.autoWrapLinesAfter() > 0 )
1025 {
1026 documentContext.setMaximumWidth( context.convertToPainterUnits( mSettings.autoWrapLinesAfter(), Qgis::RenderUnit::Millimeters ) );
1028 }
1029
1030 const double textScaleFactor = QgsTextRenderer::calculateScaleFactorForFormat( context, layerFormat );
1031 const QgsTextDocumentMetrics textDocumentMetrics = QgsTextDocumentMetrics::calculateMetrics( textDocument, layerFormat, context, textScaleFactor, documentContext );
1032
1033 const double overallTextHeight = textDocumentMetrics.documentSize( Qgis::TextLayoutMode::RectangleAscentBased, Qgis::TextOrientation::Horizontal ).height();
1034 const double overallTextWidth = textDocumentMetrics.documentSize( Qgis::TextLayoutMode::RectangleAscentBased, Qgis::TextOrientation::Horizontal ).width();
1035
1036 const double sideMargin = mSettings.style( nodeLegendStyle( nodeLayer ) ).margin( QgsLegendStyle::Left );
1037
1038 size.rheight() = overallTextHeight / dotsPerMM;
1039 size.rwidth() = overallTextWidth / dotsPerMM + sideMargin * ( mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() == Qt::AlignHCenter ? 2 : 1 );
1040
1041 if ( context.painter() )
1042 {
1043 Qgis::TextHorizontalAlignment halign = mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() == Qt::AlignLeft ? Qgis::TextHorizontalAlignment::Left
1044 : mSettings.style( nodeLegendStyle( nodeLayer ) ).alignment() == Qt::AlignRight ? Qgis::TextHorizontalAlignment::Right
1046 const QRectF
1047 r( ( columnContext.left + ( halign == Qgis::TextHorizontalAlignment::Left ? sideMargin : 0 ) ) * dotsPerMM,
1048 top * dotsPerMM,
1049 ( ( columnContext.right - columnContext.left ) - ( halign == Qgis::TextHorizontalAlignment::Right ? sideMargin : 0 ) ) * dotsPerMM,
1050 overallTextHeight );
1051 QgsTextRenderer::drawDocument( r, layerFormat, textDocumentMetrics.document(), textDocumentMetrics, context, halign, Qgis::TextVerticalAlignment::Top );
1052 }
1053
1054 size.rheight() += mSettings.style( nodeLegendStyle( nodeLayer ) ).margin( QgsLegendStyle::Side::Bottom );
1055
1056 if ( layerScope )
1057 delete context.expressionContext().popScope();
1058
1059 return size;
1060}
1061
1062QSizeF QgsLegendRenderer::drawGroupTitle( QgsLayerTreeGroup *nodeGroup, QgsRenderContext &context, ColumnContext columnContext, double top )
1063{
1064 QSizeF size( 0, 0 );
1065 QModelIndex idx = mLegendModel->node2index( nodeGroup );
1066
1067 const double dotsPerMM = context.scaleFactor();
1068
1069 const QgsTextFormat groupFormat = mSettings.style( nodeLegendStyle( nodeGroup ) ).textFormat();
1070
1071 const QStringList lines = mSettings.evaluateItemText( mLegendModel->data( idx, Qt::DisplayRole ).toString(), context.expressionContext() );
1072 const QgsTextDocument textDocument = QgsTextDocument::fromTextAndFormat( lines, groupFormat );
1073
1074 QgsTextDocumentRenderContext documentContext;
1075 QgsScopedRenderContextScaleToPixels scaleToPx( context );
1076 if ( mSettings.autoWrapLinesAfter() > 0 )
1077 {
1078 documentContext.setMaximumWidth( context.convertToPainterUnits( mSettings.autoWrapLinesAfter(), Qgis::RenderUnit::Millimeters ) );
1080 }
1081
1082 const double textScaleFactor = QgsTextRenderer::calculateScaleFactorForFormat( context, groupFormat );
1083 const QgsTextDocumentMetrics textDocumentMetrics = QgsTextDocumentMetrics::calculateMetrics( textDocument, groupFormat, context, textScaleFactor, documentContext );
1084
1085 const double overallTextHeight = textDocumentMetrics.documentSize( Qgis::TextLayoutMode::RectangleAscentBased, Qgis::TextOrientation::Horizontal ).height();
1086 const double overallTextWidth = textDocumentMetrics.documentSize( Qgis::TextLayoutMode::RectangleAscentBased, Qgis::TextOrientation::Horizontal ).width();
1087
1088 const double sideMargin = mSettings.style( nodeLegendStyle( nodeGroup ) ).margin( QgsLegendStyle::Left );
1089
1090 size.rheight() = overallTextHeight / dotsPerMM;
1091 size.rwidth() = overallTextWidth / dotsPerMM + sideMargin * ( mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() == Qt::AlignHCenter ? 2 : 1 );
1092
1093 if ( context.painter() )
1094 {
1095 Qgis::TextHorizontalAlignment halign = mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() == Qt::AlignLeft ? Qgis::TextHorizontalAlignment::Left
1096 : mSettings.style( nodeLegendStyle( nodeGroup ) ).alignment() == Qt::AlignRight ? Qgis::TextHorizontalAlignment::Right
1098
1099 const QRectF
1100 r( dotsPerMM * ( columnContext.left + ( halign == Qgis::TextHorizontalAlignment::Left ? sideMargin : 0 ) ),
1101 top * dotsPerMM,
1102 dotsPerMM * ( ( columnContext.right - columnContext.left ) - ( halign == Qgis::TextHorizontalAlignment::Right ? sideMargin : 0 ) ),
1103 overallTextHeight );
1104 QgsTextRenderer::drawDocument( r, groupFormat, textDocumentMetrics.document(), textDocumentMetrics, context, halign, Qgis::TextVerticalAlignment::Top );
1105 }
1106
1107 size.rheight() += mSettings.style( nodeLegendStyle( nodeGroup ) ).margin( QgsLegendStyle::Bottom );
1108 return size;
1109}
1110
1112{
1113 QString style = node->customProperty( u"legend/title-style"_s ).toString();
1114 if ( style == "hidden"_L1 )
1116 else if ( style == "group"_L1 )
1118 else if ( style == "subgroup"_L1 )
1120
1121 // use a default otherwise
1122 if ( QgsLayerTree::isGroup( node ) )
1124 else if ( QgsLayerTree::isLayer( node ) )
1125 {
1126 if ( model->legendNodeEmbeddedInParent( QgsLayerTree::toLayer( node ) ) )
1129 }
1130
1131 return Qgis::LegendComponent::Undefined; // should not happen, only if corrupted project file
1132}
1133
1135{
1136 return nodeLegendStyle( node, mLegendModel );
1137}
1138
1140{
1141 return mProxyModel.get();
1142}
1143
1145{
1146 QString str;
1147 switch ( style )
1148 {
1150 str = u"hidden"_s;
1151 break;
1153 str = u"group"_s;
1154 break;
1156 str = u"subgroup"_s;
1157 break;
1158 default:
1159 break; // nothing
1160 }
1161
1162 if ( !str.isEmpty() )
1163 node->setCustomProperty( u"legend/title-style"_s, str );
1164 else
1165 node->removeCustomProperty( u"legend/title-style"_s );
1166}
1167
1169{
1170 paintAndDetermineSize( context );
1171}
LegendComponent
Component of legends which can be styled.
Definition qgis.h:4965
@ Symbol
Symbol icon (excluding label).
Definition qgis.h:4971
@ Group
Legend group title.
Definition qgis.h:4969
@ Hidden
Special style, item is hidden including margins around.
Definition qgis.h:4967
@ Subgroup
Legend subgroup title.
Definition qgis.h:4970
@ Title
Legend title.
Definition qgis.h:4968
@ SymbolLabel
Symbol label (excluding icon).
Definition qgis.h:4972
@ Undefined
Should not happen, only if corrupted project file.
Definition qgis.h:4966
@ RectangleAscentBased
Similar to Rectangle mode, but uses ascents only when calculating font and line heights.
Definition qgis.h:3074
@ Rectangle
Text within rectangle layout mode.
Definition qgis.h:3070
@ ShowRuleDetails
If set, the rule expression of a rule based renderer legend item will be added to the JSON.
Definition qgis.h:4999
@ Horizontal
Horizontally oriented text.
Definition qgis.h:3055
@ Millimeters
Millimeters.
Definition qgis.h:5587
@ ApplyScalingWorkaroundForTextRendering
Whether a scaling workaround designed to stablise the rendering of small font sizes (or for painters ...
Definition qgis.h:2926
@ Top
Align to top.
Definition qgis.h:3130
TextHorizontalAlignment
Text horizontal alignment.
Definition qgis.h:3110
@ Center
Center align.
Definition qgis.h:3112
@ WrapLines
Automatically wrap long lines of text.
Definition qgis.h:3589
static QgsExpressionContextScope * layerScope(const QgsMapLayer *layer)
Creates a new scope which contains variables and functions relating to a QgsMapLayer.
QgsExpressionContextScope * popScope()
Removes the last scope from the expression context and return it.
void appendScope(QgsExpressionContextScope *scope)
Appends a scope to the end of the context.
A sort filter proxy model to easily reproduce the legend/layer tree in a tree view.
Layer tree group node serves as a container for layers and further groups.
Layer tree node points to a map layer.
@ AllowSplittingLegendNodesOverMultipleColumns
Allow splitting node's legend nodes across multiple columns.
@ PreventSplittingLegendNodesOverMultipleColumns
Prevent splitting node's legend nodes across multiple columns.
@ UseDefaultLegendSetting
Inherit default legend column splitting setting.
LegendNodesSplitBehavior legendSplitBehavior() const
Returns the column split behavior for the node.
QgsMapLayer * layer() const
Returns the map layer associated with this node.
An abstract interface for legend items returned from QgsMapLayerLegend implementation.
virtual QVariant data(int role) const =0
Returns data associated with the item. Must be implemented in derived class.
QJsonObject exportToJson(const QgsLegendSettings &settings, const QgsRenderContext &context)
Entry point called from QgsLegendRenderer to do the rendering in a JSON object.
virtual bool columnBreak() const
Returns whether a forced column break should occur before the node.
virtual ItemMetrics draw(const QgsLegendSettings &settings, ItemContext &ctx)
Entry point called from QgsLegendRenderer to do the rendering.
virtual QSizeF userPatchSize() const
Returns the user (overridden) size for the legend node.
QgsLayerTreeLayer * layerNode() const
Returns pointer to the parent layer node.
A model representing the layer tree, including layers and groups of layers.
QModelIndex node2index(QgsLayerTreeNode *node) const
Returns index for a given node. If the node does not belong to the layer tree, the result is undefine...
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
QgsLayerTreeModelLegendNode * legendNodeEmbeddedInParent(QgsLayerTreeLayer *nodeLayer) const
Returns legend node that may be embedded in parent (i.e.
Base class for nodes in a layer tree.
void setCustomProperty(const QString &key, const QVariant &value)
Sets a custom property for the node. Properties are stored in a map and saved in project file.
QList< QgsLayerTreeNode * > children()
Gets list of children of the node. Children are owned by the parent.
void removeCustomProperty(const QString &key)
Remove a custom property from layer. Properties are stored in a map and saved in project file.
QVariant customProperty(const QString &key, const QVariant &defaultValue=QVariant()) const
Read a custom property from layer. Properties are stored in a map and saved in project file.
static QgsLayerTreeLayer * toLayer(QgsLayerTreeNode *node)
Cast node to a layer.
static bool isLayer(const QgsLayerTreeNode *node)
Check whether the node is a valid layer node.
static bool isGroup(QgsLayerTreeNode *node)
Check whether the node is a valid group node.
static QgsLayerTreeGroup * toGroup(QgsLayerTreeNode *node)
Cast node to a group.
QSizeF minimumSize(QgsRenderContext *renderContext=nullptr)
Runs the layout algorithm and returns the minimum size required for the legend.
QgsLayerTreeFilterProxyModel * proxyModel()
Returns the filter proxy model used for filtering the legend model content during rendering.
QJsonObject exportLegendToJson(const QgsRenderContext &context)
Renders the legend in a json object.
QgsLegendRenderer(QgsLayerTreeModel *legendModel, const QgsLegendSettings &settings)
Constructor for QgsLegendRenderer.
Q_DECL_DEPRECATED void drawLegend(QPainter *painter)
Draws the legend with given painter.
static void setNodeLegendStyle(QgsLayerTreeNode *node, Qgis::LegendComponent style)
Sets the style of a node.
static Qgis::LegendComponent nodeLegendStyle(QgsLayerTreeNode *node, QgsLayerTreeModel *model)
Returns the style for the given node, within the specified model.
void setProxyModel(QgsLayerTreeFilterProxyModel *model)
Sets the filter proxy model to use for filtering the legend model content during rendering.
Stores the appearance and layout settings for legend drawing with QgsLegendRenderer.
@ Right
Right side.
@ Left
Left side.
@ Bottom
Bottom side.
Perform transforms between map coordinates and device coordinates.
Contains information about the context of a rendering operation.
double scaleFactor() const
Returns the scaling factor for the render to convert painter units to physical sizes.
double convertToPainterUnits(double size, Qgis::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale(), Qgis::RenderSubcomponentProperty property=Qgis::RenderSubcomponentProperty::Generic) const
Converts a size from the specified units to painter units (pixels).
QPainter * painter()
Returns the destination QPainter for the render operation.
QgsExpressionContext & expressionContext()
Gets the expression context.
static QgsRenderContext fromQPainter(QPainter *painter)
Creates a default render context given a pixel based QPainter destination.
Scoped object for temporary replacement of a QgsRenderContext destination painter.
Scoped object for temporary scaling of a QgsRenderContext for millimeter based rendering.
QSizeF documentSize(Qgis::TextLayoutMode mode, Qgis::TextOrientation orientation) const
Returns the overall size of the document.
static QgsTextDocumentMetrics calculateMetrics(const QgsTextDocument &document, const QgsTextFormat &format, const QgsRenderContext &context, double scaleFactor=1.0, const QgsTextDocumentRenderContext &documentContext=QgsTextDocumentRenderContext())
Returns precalculated text metrics for a text document, when rendered using the given base format and...
const QgsTextDocument & document() const
Returns the document associated with the calculated metrics.
void setFlags(Qgis::TextRendererFlags flags)
Sets associated text renderer flags.
void setMaximumWidth(double width)
Sets the maximum width (in painter units) for rendered text.
static QgsTextDocument fromTextAndFormat(const QStringList &lines, const QgsTextFormat &format)
Constructor for QgsTextDocument consisting of a set of lines, respecting settings from a text format.
static void drawDocument(const QRectF &rect, const QgsTextFormat &format, const QgsTextDocument &document, const QgsTextDocumentMetrics &metrics, QgsRenderContext &context, Qgis::TextHorizontalAlignment horizontalAlignment=Qgis::TextHorizontalAlignment::Left, Qgis::TextVerticalAlignment verticalAlignment=Qgis::TextVerticalAlignment::Top, double rotation=0, Qgis::TextLayoutMode mode=Qgis::TextLayoutMode::Rectangle, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags())
Draws a text document within a rectangle using the specified settings.
static double calculateScaleFactorForFormat(const QgsRenderContext &context, const QgsTextFormat &format)
Returns the scale factor used for upscaling font sizes and downscaling destination painter devices.
QgsLayerTreeModelLegendNode * legendNode(const QString &rule, QgsLayerTreeModel &model)
#define Q_NOWARN_DEPRECATED_POP
Definition qgis.h:7801
#define Q_NOWARN_DEPRECATED_PUSH
Definition qgis.h:7800
Q_DECL_DEPRECATED double labelXOffset
Offset from the left side where label should start.
QgsLegendPatchShape patchShape
The patch shape to render for the node.
double maxSiblingSymbolWidth
Largest symbol width, considering all other sibling legend components associated with the current com...
QSizeF patchSize
Symbol patch size to render for the node.
double columnLeft
Left side of current legend column.
double columnRight
Right side of current legend column.
Q_DECL_DEPRECATED QPointF point
Top-left corner of the legend item.
Q_NOWARN_DEPRECATED_POP QgsRenderContext * context
Render context, if available.