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