QGIS API Documentation  3.25.0-Master (dec16ba68b)
qgslayouttable.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgslayouttable.cpp
3  ------------------
4  begin : November 2017
5  copyright : (C) 2017 by Nyall Dawson
6  email : nyall dot dawson at gmail dot com
7  ***************************************************************************/
8 
9 /***************************************************************************
10  * *
11  * This program is free software; you can redistribute it and/or modify *
12  * it under the terms of the GNU General Public License as published by *
13  * the Free Software Foundation; either version 2 of the License, or *
14  * (at your option) any later version. *
15  * *
16  ***************************************************************************/
17 
19 #include "qgsexpressionutils.h"
20 #include "qgslayouttable.h"
21 #include "qgslayout.h"
22 #include "qgslayoututils.h"
23 #include "qgslayouttablecolumn.h"
24 #include "qgssymbollayerutils.h"
25 #include "qgslayoutframe.h"
26 #include "qgsfontutils.h"
27 #include "qgssettings.h"
29 #include "qgstextrenderer.h"
30 
31 //
32 // QgsLayoutTableStyle
33 //
34 
35 bool QgsLayoutTableStyle::writeXml( QDomElement &styleElem, QDomDocument &doc ) const
36 {
37  Q_UNUSED( doc )
38  styleElem.setAttribute( QStringLiteral( "cellBackgroundColor" ), QgsSymbolLayerUtils::encodeColor( cellBackgroundColor ) );
39  styleElem.setAttribute( QStringLiteral( "enabled" ), enabled );
40  return true;
41 }
42 
43 bool QgsLayoutTableStyle::readXml( const QDomElement &styleElem )
44 {
45  cellBackgroundColor = QgsSymbolLayerUtils::decodeColor( styleElem.attribute( QStringLiteral( "cellBackgroundColor" ), QStringLiteral( "255,255,255,255" ) ) );
46  enabled = ( styleElem.attribute( QStringLiteral( "enabled" ), QStringLiteral( "0" ) ) != QLatin1String( "0" ) );
47  return true;
48 }
49 
50 
51 //
52 // QgsLayoutTable
53 //
54 
56  : QgsLayoutMultiFrame( layout )
57 {
58  initStyles();
59 }
60 
62 {
63  mColumns.clear();
64  mSortColumns.clear();
65 
66  qDeleteAll( mCellStyles );
67  mCellStyles.clear();
68 }
69 
70 bool QgsLayoutTable::writePropertiesToElement( QDomElement &elem, QDomDocument &doc, const QgsReadWriteContext &context ) const
71 {
72  elem.setAttribute( QStringLiteral( "cellMargin" ), QString::number( mCellMargin ) );
73  elem.setAttribute( QStringLiteral( "emptyTableMode" ), QString::number( static_cast< int >( mEmptyTableMode ) ) );
74  elem.setAttribute( QStringLiteral( "emptyTableMessage" ), mEmptyTableMessage );
75  elem.setAttribute( QStringLiteral( "showEmptyRows" ), mShowEmptyRows );
76 
77  QDomElement headerElem = doc.createElement( QStringLiteral( "headerTextFormat" ) );
78  const QDomElement headerTextElem = mHeaderTextFormat.writeXml( doc, context );
79  headerElem.appendChild( headerTextElem );
80  elem.appendChild( headerElem );
81  elem.setAttribute( QStringLiteral( "headerHAlignment" ), QString::number( static_cast< int >( mHeaderHAlignment ) ) );
82  elem.setAttribute( QStringLiteral( "headerMode" ), QString::number( static_cast< int >( mHeaderMode ) ) );
83 
84  QDomElement contentElem = doc.createElement( QStringLiteral( "contentTextFormat" ) );
85  const QDomElement contentTextElem = mContentTextFormat.writeXml( doc, context );
86  contentElem.appendChild( contentTextElem );
87  elem.appendChild( contentElem );
88  elem.setAttribute( QStringLiteral( "gridStrokeWidth" ), QString::number( mGridStrokeWidth ) );
89  elem.setAttribute( QStringLiteral( "gridColor" ), QgsSymbolLayerUtils::encodeColor( mGridColor ) );
90  elem.setAttribute( QStringLiteral( "horizontalGrid" ), mHorizontalGrid );
91  elem.setAttribute( QStringLiteral( "verticalGrid" ), mVerticalGrid );
92  elem.setAttribute( QStringLiteral( "showGrid" ), mShowGrid );
93  elem.setAttribute( QStringLiteral( "backgroundColor" ), QgsSymbolLayerUtils::encodeColor( mBackgroundColor ) );
94  elem.setAttribute( QStringLiteral( "wrapBehavior" ), QString::number( static_cast< int >( mWrapBehavior ) ) );
95 
96  // display columns
97  QDomElement displayColumnsElem = doc.createElement( QStringLiteral( "displayColumns" ) );
98  for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
99  {
100  QDomElement columnElem = doc.createElement( QStringLiteral( "column" ) );
101  column.writeXml( columnElem, doc );
102  displayColumnsElem.appendChild( columnElem );
103  }
104  elem.appendChild( displayColumnsElem );
105  // sort columns
106  QDomElement sortColumnsElem = doc.createElement( QStringLiteral( "sortColumns" ) );
107  for ( const QgsLayoutTableColumn &column : std::as_const( mSortColumns ) )
108  {
109  QDomElement columnElem = doc.createElement( QStringLiteral( "column" ) );
110  column.writeXml( columnElem, doc );
111  sortColumnsElem.appendChild( columnElem );
112  }
113  elem.appendChild( sortColumnsElem );
114 
115 
116  //cell styles
117  QDomElement stylesElem = doc.createElement( QStringLiteral( "cellStyles" ) );
118  QMap< CellStyleGroup, QString >::const_iterator it = mCellStyleNames.constBegin();
119  for ( ; it != mCellStyleNames.constEnd(); ++it )
120  {
121  QString styleName = it.value();
122  QDomElement styleElem = doc.createElement( styleName );
123  QgsLayoutTableStyle *style = mCellStyles.value( it.key() );
124  if ( style )
125  {
126  style->writeXml( styleElem, doc );
127  stylesElem.appendChild( styleElem );
128  }
129  }
130  elem.appendChild( stylesElem );
131  return true;
132 }
133 
134 bool QgsLayoutTable::readPropertiesFromElement( const QDomElement &itemElem, const QDomDocument &, const QgsReadWriteContext &context )
135 {
136  mEmptyTableMode = QgsLayoutTable::EmptyTableMode( itemElem.attribute( QStringLiteral( "emptyTableMode" ), QStringLiteral( "0" ) ).toInt() );
137  mEmptyTableMessage = itemElem.attribute( QStringLiteral( "emptyTableMessage" ), tr( "No matching records" ) );
138  mShowEmptyRows = itemElem.attribute( QStringLiteral( "showEmptyRows" ), QStringLiteral( "0" ) ).toInt();
139 
140  const QDomElement headerTextFormat = itemElem.firstChildElement( QStringLiteral( "headerTextFormat" ) );
141  if ( !headerTextFormat.isNull() )
142  {
143  QDomNodeList textFormatNodeList = headerTextFormat.elementsByTagName( QStringLiteral( "text-style" ) );
144  QDomElement textFormatElem = textFormatNodeList.at( 0 ).toElement();
145  mHeaderTextFormat.readXml( textFormatElem, context );
146  }
147  else
148  {
149  QFont headerFont;
150  if ( !QgsFontUtils::setFromXmlChildNode( headerFont, itemElem, QStringLiteral( "headerFontProperties" ) ) )
151  {
152  headerFont.fromString( itemElem.attribute( QStringLiteral( "headerFont" ), QString() ) );
153  }
154  QColor headerFontColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "headerFontColor" ), QStringLiteral( "0,0,0,255" ) ) );
156  if ( headerFont.pointSizeF() > 0 )
157  {
158  mHeaderTextFormat.setSize( headerFont.pointSizeF() );
160  }
161  else if ( headerFont.pixelSize() > 0 )
162  {
163  mHeaderTextFormat.setSize( headerFont.pixelSize() );
165  }
167  }
168 
169  mHeaderHAlignment = QgsLayoutTable::HeaderHAlignment( itemElem.attribute( QStringLiteral( "headerHAlignment" ), QStringLiteral( "0" ) ).toInt() );
170  mHeaderMode = QgsLayoutTable::HeaderMode( itemElem.attribute( QStringLiteral( "headerMode" ), QStringLiteral( "0" ) ).toInt() );
171 
172  const QDomElement contentTextFormat = itemElem.firstChildElement( QStringLiteral( "contentTextFormat" ) );
173  if ( !contentTextFormat.isNull() )
174  {
175  QDomNodeList textFormatNodeList = contentTextFormat.elementsByTagName( QStringLiteral( "text-style" ) );
176  QDomElement textFormatElem = textFormatNodeList.at( 0 ).toElement();
177  mContentTextFormat.readXml( textFormatElem, context );
178  }
179  else
180  {
181  QFont contentFont;
182  if ( !QgsFontUtils::setFromXmlChildNode( contentFont, itemElem, QStringLiteral( "contentFontProperties" ) ) )
183  {
184  contentFont.fromString( itemElem.attribute( QStringLiteral( "contentFont" ), QString() ) );
185  }
186  QColor contentFontColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "contentFontColor" ), QStringLiteral( "0,0,0,255" ) ) );
188  if ( contentFont.pointSizeF() > 0 )
189  {
190  mContentTextFormat.setSize( contentFont.pointSizeF() );
192  }
193  else if ( contentFont.pixelSize() > 0 )
194  {
195  mContentTextFormat.setSize( contentFont.pixelSize() );
197  }
199  }
200 
201  mCellMargin = itemElem.attribute( QStringLiteral( "cellMargin" ), QStringLiteral( "1.0" ) ).toDouble();
202  mGridStrokeWidth = itemElem.attribute( QStringLiteral( "gridStrokeWidth" ), QStringLiteral( "0.5" ) ).toDouble();
203  mHorizontalGrid = itemElem.attribute( QStringLiteral( "horizontalGrid" ), QStringLiteral( "1" ) ).toInt();
204  mVerticalGrid = itemElem.attribute( QStringLiteral( "verticalGrid" ), QStringLiteral( "1" ) ).toInt();
205  mShowGrid = itemElem.attribute( QStringLiteral( "showGrid" ), QStringLiteral( "1" ) ).toInt();
206  mGridColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "gridColor" ), QStringLiteral( "0,0,0,255" ) ) );
207  mBackgroundColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "backgroundColor" ), QStringLiteral( "255,255,255,0" ) ) );
208  mWrapBehavior = QgsLayoutTable::WrapBehavior( itemElem.attribute( QStringLiteral( "wrapBehavior" ), QStringLiteral( "0" ) ).toInt() );
209 
210  //restore display column specifications
211  mColumns.clear();
212  QDomNodeList columnsList = itemElem.elementsByTagName( QStringLiteral( "displayColumns" ) );
213  if ( !columnsList.isEmpty() )
214  {
215  QDomElement columnsElem = columnsList.at( 0 ).toElement();
216  QDomNodeList columnEntryList = columnsElem.elementsByTagName( QStringLiteral( "column" ) );
217  for ( int i = 0; i < columnEntryList.size(); ++i )
218  {
219  QDomElement columnElem = columnEntryList.at( i ).toElement();
220  QgsLayoutTableColumn column;
221  column.readXml( columnElem );
222  mColumns.append( column );
223  }
224  }
225  // sort columns
226  mSortColumns.clear();
227  QDomNodeList sortColumnsList = itemElem.elementsByTagName( QStringLiteral( "sortColumns" ) );
228  if ( !sortColumnsList.isEmpty() )
229  {
230  QDomElement columnsElem = sortColumnsList.at( 0 ).toElement();
231  QDomNodeList columnEntryList = columnsElem.elementsByTagName( QStringLiteral( "column" ) );
232  for ( int i = 0; i < columnEntryList.size(); ++i )
233  {
234  QDomElement columnElem = columnEntryList.at( i ).toElement();
235  QgsLayoutTableColumn column;
236  column.readXml( columnElem );
237  mSortColumns.append( column );
238  }
239  }
240  else
241  {
242  // backward compatibility for QGIS < 3.14
243  // copy the display columns if sortByRank > 0 and then, sort them by rank
245  std::copy_if( mColumns.begin(), mColumns.end(), std::back_inserter( mSortColumns ), []( const QgsLayoutTableColumn & col ) {return col.sortByRank() > 0;} );
246  std::sort( mSortColumns.begin(), mSortColumns.end(), []( const QgsLayoutTableColumn & a, const QgsLayoutTableColumn & b ) {return a.sortByRank() < b.sortByRank();} );
248  }
249 
250  //restore cell styles
251  QDomNodeList stylesList = itemElem.elementsByTagName( QStringLiteral( "cellStyles" ) );
252  if ( !stylesList.isEmpty() )
253  {
254  QDomElement stylesElem = stylesList.at( 0 ).toElement();
255 
256  QMap< CellStyleGroup, QString >::const_iterator it = mCellStyleNames.constBegin();
257  for ( ; it != mCellStyleNames.constEnd(); ++it )
258  {
259  QString styleName = it.value();
260  QDomNodeList styleList = stylesElem.elementsByTagName( styleName );
261  if ( !styleList.isEmpty() )
262  {
263  QDomElement styleElem = styleList.at( 0 ).toElement();
264  QgsLayoutTableStyle *style = mCellStyles.value( it.key() );
265  if ( style )
266  style->readXml( styleElem );
267  }
268  }
269  }
270 
271  emit changed();
272  return true;
273 }
274 
276 {
277  return mTableSize;
278 }
279 
281 {
284 }
285 
286 int QgsLayoutTable::rowsVisible( QgsRenderContext &context, double frameHeight, int firstRow, bool includeHeader, bool includeEmptyRows ) const
287 {
288  //calculate header height
289  double headerHeight = 0;
290  if ( includeHeader )
291  {
292  headerHeight = mMaxRowHeightMap.value( 0 ) + 2 * ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) + 2 * mCellMargin;
293  }
294  else
295  {
296  //frame has no header text, just the stroke
297  headerHeight = ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
298  }
299 
300  //remaining height available for content rows
301  double contentHeight = frameHeight - headerHeight;
302 
303  double gridHeight = ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
304 
305  int currentRow = firstRow;
306  while ( contentHeight > 0 && currentRow <= mTableContents.count() )
307  {
308  double currentRowHeight = mMaxRowHeightMap.value( currentRow + 1 ) + gridHeight + 2 * mCellMargin;
309  contentHeight -= currentRowHeight;
310  currentRow++;
311  }
312 
313  if ( includeEmptyRows && contentHeight > 0 )
314  {
315  const QFontMetricsF emptyRowContentFontMetrics = QgsTextRenderer::fontMetrics( context, mContentTextFormat, QgsTextRenderer::FONT_WORKAROUND_SCALE );
316  double rowHeight = ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) + 2 * mCellMargin + emptyRowContentFontMetrics.ascent() / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters ) / QgsTextRenderer::FONT_WORKAROUND_SCALE;
317  currentRow += std::max( std::floor( contentHeight / rowHeight ), 0.0 );
318  }
319 
320  return currentRow - firstRow - 1;
321 }
322 
323 int QgsLayoutTable::rowsVisible( QgsRenderContext &context, int frameIndex, int firstRow, bool includeEmptyRows ) const
324 {
325  //get frame extent
326  if ( frameIndex >= frameCount() )
327  {
328  return 0;
329  }
330  QRectF frameExtent = frame( frameIndex )->extent();
331 
332  bool includeHeader = false;
335  {
336  includeHeader = true;
337  }
338  return rowsVisible( context, frameExtent.height(), firstRow, includeHeader, includeEmptyRows );
339 }
340 
341 QPair<int, int> QgsLayoutTable::rowRange( QgsRenderContext &context, const int frameIndex ) const
342 {
343  //calculate row height
344  if ( frameIndex >= frameCount() )
345  {
346  //bad frame index
347  return qMakePair( 0, 0 );
348  }
349 
350  //loop through all previous frames to calculate how many rows are visible in each
351  //as the entire height of a frame may not be utilized for content rows
352  int rowsAlreadyShown = 0;
353  for ( int idx = 0; idx < frameIndex; ++idx )
354  {
355  rowsAlreadyShown += rowsVisible( context, idx, rowsAlreadyShown, false );
356  }
357 
358  //using zero based indexes
359  int firstVisible = std::min( rowsAlreadyShown, static_cast<int>( mTableContents.length() ) );
360  int possibleRowsVisible = rowsVisible( context, frameIndex, rowsAlreadyShown, false );
361  int lastVisible = std::min( firstVisible + possibleRowsVisible, static_cast<int>( mTableContents.length() ) );
362 
363  return qMakePair( firstVisible, lastVisible );
364 }
365 
366 void QgsLayoutTable::render( QgsLayoutItemRenderContext &context, const QRectF &, const int frameIndex )
367 {
368  bool emptyTable = mTableContents.length() == 0;
369  if ( emptyTable && mEmptyTableMode == QgsLayoutTable::HideTable )
370  {
371  //empty table set to hide table mode, so don't draw anything
372  return;
373  }
374 
375  if ( !mLayout->renderContext().isPreviewRender() )
376  {
377  //exporting composition, so force an attribute refresh
378  //we do this in case vector layer has changed via an external source (e.g., another database user)
380  }
381 
382  const bool prevTextFormatScaleFlag = context.renderContext().testFlag( Qgis::RenderContextFlag::ApplyScalingWorkaroundForTextRendering );
384 
385  //calculate which rows to show in this frame
386  QPair< int, int > rowsToShow = rowRange( context.renderContext(), frameIndex );
387 
388  double gridSizeX = mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0;
389  double gridSizeY = mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0;
390  double cellHeaderHeight = mMaxRowHeightMap[0] + 2 * mCellMargin;
392  QRectF cell;
393 
394  //calculate whether a header is required
395  bool drawHeader = ( ( mHeaderMode == QgsLayoutTable::FirstFrame && frameIndex < 1 )
397  //calculate whether drawing table contents is required
398  bool drawContents = !( emptyTable && mEmptyTableMode == QgsLayoutTable::ShowMessage );
399 
400  int numberRowsToDraw = rowsToShow.second - rowsToShow.first;
401  int numberEmptyRows = 0;
402  if ( drawContents && mShowEmptyRows )
403  {
404  numberRowsToDraw = rowsVisible( context.renderContext(), frameIndex, rowsToShow.first, true );
405  numberEmptyRows = numberRowsToDraw - rowsToShow.second + rowsToShow.first;
406  }
407  bool mergeCells = false;
408  if ( emptyTable && mEmptyTableMode == QgsLayoutTable::ShowMessage )
409  {
410  //draw a merged row for the empty table message
411  numberRowsToDraw++;
412  rowsToShow.second++;
413  mergeCells = true;
414  }
415 
416  QPainter *p = context.renderContext().painter();
417  QgsScopedQPainterState painterState( p );
418  // painter is scaled to dots, so scale back to layout units
419  p->scale( context.renderContext().scaleFactor(), context.renderContext().scaleFactor() );
420 
421  //draw the text
422  p->setPen( Qt::SolidLine );
423 
424  double currentX = gridSizeX;
425  double currentY = gridSizeY;
426  if ( drawHeader )
427  {
428  //draw the headers
429  int col = 0;
430  for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
431  {
432  std::unique_ptr< QgsExpressionContextScope > headerCellScope = std::make_unique< QgsExpressionContextScope >();
433  headerCellScope->setVariable( QStringLiteral( "column_number" ), col + 1, true );
434  QgsExpressionContextScopePopper popper( context.renderContext().expressionContext(), headerCellScope.release() );
435 
436  const QgsTextFormat headerFormat = textFormatForHeader( col );
437  //draw background
438  p->save();
439  p->setPen( Qt::NoPen );
440  p->setBrush( backgroundColor( -1, col ) );
441  p->drawRect( QRectF( currentX, currentY, mMaxColumnWidthMap[col] + 2 * mCellMargin, cellHeaderHeight ) );
442  p->restore();
443 
444  currentX += mCellMargin;
445 
446  cell = QRectF( currentX, currentY, mMaxColumnWidthMap[col], cellHeaderHeight );
447 
448  //calculate alignment of header
450  switch ( mHeaderHAlignment )
451  {
452  case FollowColumn:
453  headerAlign = QgsTextRenderer::convertQtHAlignment( column.hAlignment() );
454  break;
455  case HeaderLeft:
456  headerAlign = QgsTextRenderer::AlignLeft;
457  break;
458  case HeaderCenter:
459  headerAlign = QgsTextRenderer::AlignCenter;
460  break;
461  case HeaderRight:
462  headerAlign = QgsTextRenderer::AlignRight;
463  break;
464  }
465 
466  const QRectF textCell = QRectF( currentX, currentY + mCellMargin, mMaxColumnWidthMap[col], cellHeaderHeight - 2 * mCellMargin );
467 
468  const QStringList str = column.heading().split( '\n' );
469 
470  // scale to dots
471  {
473  QgsTextRenderer::drawText( QRectF( textCell.left() * context.renderContext().scaleFactor(),
474  textCell.top() * context.renderContext().scaleFactor(),
475  textCell.width() * context.renderContext().scaleFactor(),
476  textCell.height() * context.renderContext().scaleFactor() ), 0,
477  headerAlign, str, context.renderContext(), headerFormat, true, QgsTextRenderer::AlignVCenter,
478  mWrapBehavior == WrapText ? Qgis::TextRendererFlag::WrapLines : Qgis::TextRendererFlags()
479  );
480  }
481 
482  currentX += mMaxColumnWidthMap[ col ];
483  currentX += mCellMargin;
484  currentX += gridSizeX;
485  col++;
486  }
487 
488  currentY += cellHeaderHeight;
489  currentY += gridSizeY;
490  }
491 
492  //now draw the body cells
493  int rowsDrawn = 0;
494  if ( drawContents )
495  {
496  //draw the attribute values
497  for ( int row = rowsToShow.first; row < rowsToShow.second; ++row )
498  {
499  rowsDrawn++;
500  currentX = gridSizeX;
501  int col = 0;
502 
503  //calculate row height
504  double rowHeight = mMaxRowHeightMap[row + 1] + 2 * mCellMargin;
505 
506  for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
507  {
508  ( void )column;
509  const QRectF fullCell( currentX, currentY, mMaxColumnWidthMap[col] + 2 * mCellMargin, rowHeight );
510  //draw background
511  p->save();
512  p->setPen( Qt::NoPen );
513  p->setBrush( backgroundColor( row, col ) );
514  p->drawRect( fullCell );
515  p->restore();
516 
517  // currentY = gridSize;
518  currentX += mCellMargin;
519 
520  QVariant cellContents = mTableContents.at( row ).at( col );
521  const QString localizedString { QgsExpressionUtils::toLocalizedString( cellContents ) };
522  const QStringList str = localizedString.split( '\n' );
523 
524  QgsTextFormat cellFormat = textFormatForCell( row, col );
526  cellFormat.updateDataDefinedProperties( context.renderContext() );
527 
528  p->save();
529  p->setClipRect( fullCell );
530  const QRectF textCell = QRectF( currentX, currentY + mCellMargin, mMaxColumnWidthMap[col], rowHeight - 2 * mCellMargin );
531 
532  const QgsConditionalStyle style = conditionalCellStyle( row, col );
533  QColor foreColor = cellFormat.color();
534  if ( style.textColor().isValid() )
535  foreColor = style.textColor();
536 
537  cellFormat.setColor( foreColor );
538 
539  // scale to dots
540  {
542  QgsTextRenderer::drawText( QRectF( textCell.left() * context.renderContext().scaleFactor(),
543  textCell.top() * context.renderContext().scaleFactor(),
544  textCell.width() * context.renderContext().scaleFactor(),
545  textCell.height() * context.renderContext().scaleFactor() ), 0,
546  QgsTextRenderer::convertQtHAlignment( horizontalAlignmentForCell( row, col ) ), str, context.renderContext(), cellFormat, true,
548  mWrapBehavior == WrapText ? Qgis::TextRendererFlag::WrapLines : Qgis::TextRendererFlags() );
549  }
550  p->restore();
551 
552  currentX += mMaxColumnWidthMap[ col ];
553  currentX += mCellMargin;
554  currentX += gridSizeX;
555  col++;
556  }
557  currentY += rowHeight;
558  currentY += gridSizeY;
559  }
560  }
561 
562  if ( numberRowsToDraw > rowsDrawn )
563  {
564  p->save();
565  p->setPen( Qt::NoPen );
566 
567  //draw background of empty rows
568  for ( int row = rowsDrawn; row < numberRowsToDraw; ++row )
569  {
570  currentX = gridSizeX;
571  int col = 0;
572 
573  if ( mergeCells )
574  {
575  p->setBrush( backgroundColor( row + 10000, 0 ) );
576  p->drawRect( QRectF( gridSizeX, currentY, mTableSize.width() - 2 * gridSizeX, cellBodyHeightForEmptyRows ) );
577  }
578  else
579  {
580  for ( const QgsLayoutTableColumn &column : std::as_const( mColumns ) )
581  {
582  Q_UNUSED( column )
583 
584  //draw background
585 
586  //we use a bit of a hack here - since we don't want these extra blank rows to match the firstrow/lastrow rule, add 10000 to row number
587  p->setBrush( backgroundColor( row + 10000, col ) );
588  p->drawRect( QRectF( currentX, currentY, mMaxColumnWidthMap[col] + 2 * mCellMargin, cellBodyHeightForEmptyRows ) );
589 
590  // currentY = gridSize;
591  currentX += mMaxColumnWidthMap[ col ] + 2 * mCellMargin;
592  currentX += gridSizeX;
593  col++;
594  }
595  }
596  currentY += cellBodyHeightForEmptyRows + gridSizeY;
597  }
598  p->restore();
599  }
600 
601  //and the borders
602  if ( mShowGrid )
603  {
604  QPen gridPen;
605  gridPen.setWidthF( mGridStrokeWidth );
606  gridPen.setColor( mGridColor );
607  gridPen.setJoinStyle( Qt::MiterJoin );
608  p->setPen( gridPen );
609  if ( mHorizontalGrid )
610  {
611  drawHorizontalGridLines( context, rowsToShow.first, rowsToShow.second + numberEmptyRows, drawHeader );
612  }
613  if ( mVerticalGrid )
614  {
615  drawVerticalGridLines( context, mMaxColumnWidthMap, rowsToShow.first, rowsToShow.second + numberEmptyRows, drawHeader, mergeCells );
616  }
617  }
618 
619  //special case - no records and table is set to ShowMessage mode
620  if ( emptyTable && mEmptyTableMode == QgsLayoutTable::ShowMessage )
621  {
622  double messageX = gridSizeX + mCellMargin;
623  double messageY = gridSizeY + ( drawHeader ? cellHeaderHeight + gridSizeY : 0 );
624  cell = QRectF( messageX, messageY, mTableSize.width() - messageX, cellBodyHeightForEmptyRows );
625 
626  // scale to dots
627  {
629  QgsTextRenderer::drawText( QRectF( cell.left() * context.renderContext().scaleFactor(),
630  cell.top() * context.renderContext().scaleFactor(),
631  cell.width() * context.renderContext().scaleFactor(),
632  cell.height() * context.renderContext().scaleFactor() ), 0,
634  }
635  }
636 
638 }
639 
640 void QgsLayoutTable::setCellMargin( const double margin )
641 {
642  if ( qgsDoubleNear( margin, mCellMargin ) )
643  {
644  return;
645  }
646 
647  mCellMargin = margin;
648 
649  //since spacing has changed, we need to recalculate the table size
651 
652  emit changed();
653 }
654 
656 {
657  if ( mode == mEmptyTableMode )
658  {
659  return;
660  }
661 
662  mEmptyTableMode = mode;
663 
664  //since appearance has changed, we need to recalculate the table size
666 
667  emit changed();
668 }
669 
670 void QgsLayoutTable::setEmptyTableMessage( const QString &message )
671 {
672  if ( message == mEmptyTableMessage )
673  {
674  return;
675  }
676 
677  mEmptyTableMessage = message;
678 
679  //since message has changed, we need to recalculate the table size
681 
682  emit changed();
683 }
684 
685 void QgsLayoutTable::setShowEmptyRows( const bool showEmpty )
686 {
687  if ( showEmpty == mShowEmptyRows )
688  {
689  return;
690  }
691 
692  mShowEmptyRows = showEmpty;
693  update();
694  emit changed();
695 }
696 
697 void QgsLayoutTable::setHeaderFont( const QFont &font )
698 {
699  mHeaderTextFormat.setFont( font );
700  if ( font.pointSizeF() > 0 )
701  {
702  mHeaderTextFormat.setSize( font.pointSizeF() );
704  }
705  else if ( font.pixelSize() > 0 )
706  {
707  mHeaderTextFormat.setSize( font.pixelSize() );
709  }
710 
711  //since font attributes have changed, we need to recalculate the table size
713 
714  emit changed();
715 }
716 
718 {
719  return mHeaderTextFormat.toQFont();
720 }
721 
722 void QgsLayoutTable::setHeaderFontColor( const QColor &color )
723 {
724  if ( color == mHeaderTextFormat.color() )
725  {
726  return;
727  }
728 
729  mHeaderTextFormat.setColor( color );
730  update();
731 
732  emit changed();
733 }
734 
736 {
737  return mHeaderTextFormat.color();
738 }
739 
741 {
742  mHeaderTextFormat = format;
743 
744  //since font attributes have changed, we need to recalculate the table size
746 
747  emit changed();
748 }
749 
751 {
752  return mHeaderTextFormat;
753 }
754 
756 {
757  if ( alignment == mHeaderHAlignment )
758  {
759  return;
760  }
761 
762  mHeaderHAlignment = alignment;
763  update();
764 
765  emit changed();
766 }
767 
769 {
770  if ( mode == mHeaderMode )
771  {
772  return;
773  }
774 
775  mHeaderMode = mode;
777 
778  emit changed();
779 }
780 
781 void QgsLayoutTable::setContentFont( const QFont &font )
782 {
783  mContentTextFormat.setFont( font );
784  if ( font.pointSizeF() > 0 )
785  {
786  mContentTextFormat.setSize( font.pointSizeF() );
788  }
789  else if ( font.pixelSize() > 0 )
790  {
791  mContentTextFormat.setSize( font.pixelSize() );
793  }
794 
795  //since font attributes have changed, we need to recalculate the table size
797 
798  emit changed();
799 }
800 
802 {
803  return mContentTextFormat.toQFont();
804 }
805 
806 void QgsLayoutTable::setContentFontColor( const QColor &color )
807 {
808  if ( color == mContentTextFormat.color() )
809  {
810  return;
811  }
812 
813  mContentTextFormat.setColor( color );
814  update();
815 
816  emit changed();
817 }
818 
820 {
821  return mContentTextFormat.color();
822 }
823 
825 {
826  mContentTextFormat = format;
827 
828  //since spacing has changed, we need to recalculate the table size
830 
831  emit changed();
832 }
833 
835 {
836  return mContentTextFormat;
837 }
838 
839 void QgsLayoutTable::setShowGrid( const bool showGrid )
840 {
841  if ( showGrid == mShowGrid )
842  {
843  return;
844  }
845 
847  //since grid spacing has changed, we need to recalculate the table size
849 
850  emit changed();
851 }
852 
853 void QgsLayoutTable::setGridStrokeWidth( const double width )
854 {
855  if ( qgsDoubleNear( width, mGridStrokeWidth ) )
856  {
857  return;
858  }
859 
860  mGridStrokeWidth = width;
861  //since grid spacing has changed, we need to recalculate the table size
863 
864  emit changed();
865 }
866 
867 void QgsLayoutTable::setGridColor( const QColor &color )
868 {
869  if ( color == mGridColor )
870  {
871  return;
872  }
873 
874  mGridColor = color;
875  update();
876 
877  emit changed();
878 }
879 
880 void QgsLayoutTable::setHorizontalGrid( const bool horizontalGrid )
881 {
883  {
884  return;
885  }
886 
888  //since grid spacing has changed, we need to recalculate the table size
890 
891  emit changed();
892 }
893 
894 void QgsLayoutTable::setVerticalGrid( const bool verticalGrid )
895 {
896  if ( verticalGrid == mVerticalGrid )
897  {
898  return;
899  }
900 
902  //since grid spacing has changed, we need to recalculate the table size
904 
905  emit changed();
906 }
907 
908 void QgsLayoutTable::setBackgroundColor( const QColor &color )
909 {
910  if ( color == mBackgroundColor )
911  {
912  return;
913  }
914 
915  mBackgroundColor = color;
916  update();
917 
918  emit changed();
919 }
920 
922 {
923  if ( behavior == mWrapBehavior )
924  {
925  return;
926  }
927 
928  mWrapBehavior = behavior;
930 
931  emit changed();
932 }
933 
935 {
936  //remove existing columns
937  mColumns = columns;
938 
939  // backward compatibility
940  // test if sorting is provided with the columns and call setSortColumns in such case
941  QgsLayoutTableSortColumns newSortColumns;
943  std::copy_if( mColumns.begin(), mColumns.end(), std::back_inserter( newSortColumns ), []( const QgsLayoutTableColumn & col ) {return col.sortByRank() > 0;} );
944  if ( !newSortColumns.isEmpty() )
945  {
946  std::sort( newSortColumns.begin(), newSortColumns.end(), []( const QgsLayoutTableColumn & a, const QgsLayoutTableColumn & b ) {return a.sortByRank() < b.sortByRank();} );
947  setSortColumns( newSortColumns );
948  }
950 }
951 
953 {
955 }
956 
958 {
959  if ( mCellStyles.contains( group ) )
960  delete mCellStyles.take( group );
961 
962  mCellStyles.insert( group, new QgsLayoutTableStyle( style ) );
963 }
964 
966 {
967  if ( !mCellStyles.contains( group ) )
968  return nullptr;
969 
970  return mCellStyles.value( group );
971 }
972 
973 QMap<int, QString> QgsLayoutTable::headerLabels() const
974 {
975  QMap<int, QString> headers;
976 
977  int i = 0;
978  for ( const QgsLayoutTableColumn &col : std::as_const( mColumns ) )
979  {
980  headers.insert( i, col.heading() );
981  i++;
982  }
983  return headers;
984 }
985 
987 {
988  std::unique_ptr< QgsExpressionContextScope > cellScope = std::make_unique< QgsExpressionContextScope >();
989  cellScope->setVariable( QStringLiteral( "row_number" ), row + 1, true );
990  cellScope->setVariable( QStringLiteral( "column_number" ), column + 1, true );
991  return cellScope.release();
992 }
993 
995 {
996  return QgsConditionalStyle();
997 }
998 
999 QSizeF QgsLayoutTable::fixedFrameSize( const int frameIndex ) const
1000 {
1001  Q_UNUSED( frameIndex )
1002  return QSizeF( mTableSize.width(), 0 );
1003 }
1004 
1005 QSizeF QgsLayoutTable::minFrameSize( const int frameIndex ) const
1006 {
1009 
1010  double height = 0;
1013  {
1014  //header required, force frame to be high enough for header
1015  for ( int col = 0; col < mColumns.size(); ++ col )
1016  {
1018  }
1019  }
1020  return QSizeF( 0, height );
1021 }
1022 
1024 {
1025  mMaxColumnWidthMap.clear();
1026  mMaxRowHeightMap.clear();
1027  mTableContents.clear();
1028 
1029  //get new contents
1030  if ( !getTableContents( mTableContents ) )
1031  {
1032  return;
1033  }
1034 }
1035 
1037 {
1038  mTableSize = QSizeF( totalWidth(), totalHeight() );
1040 }
1041 
1042 void QgsLayoutTable::initStyles()
1043 {
1044  mCellStyles.insert( OddColumns, new QgsLayoutTableStyle() );
1045  mCellStyles.insert( EvenColumns, new QgsLayoutTableStyle() );
1046  mCellStyles.insert( OddRows, new QgsLayoutTableStyle() );
1047  mCellStyles.insert( EvenRows, new QgsLayoutTableStyle() );
1048  mCellStyles.insert( FirstColumn, new QgsLayoutTableStyle() );
1049  mCellStyles.insert( LastColumn, new QgsLayoutTableStyle() );
1050  mCellStyles.insert( HeaderRow, new QgsLayoutTableStyle() );
1051  mCellStyles.insert( FirstRow, new QgsLayoutTableStyle() );
1052  mCellStyles.insert( LastRow, new QgsLayoutTableStyle() );
1053 
1054  mCellStyleNames.insert( OddColumns, QStringLiteral( "oddColumns" ) );
1055  mCellStyleNames.insert( EvenColumns, QStringLiteral( "evenColumns" ) );
1056  mCellStyleNames.insert( OddRows, QStringLiteral( "oddRows" ) );
1057  mCellStyleNames.insert( EvenRows, QStringLiteral( "evenRows" ) );
1058  mCellStyleNames.insert( FirstColumn, QStringLiteral( "firstColumn" ) );
1059  mCellStyleNames.insert( LastColumn, QStringLiteral( "lastColumn" ) );
1060  mCellStyleNames.insert( HeaderRow, QStringLiteral( "headerRow" ) );
1061  mCellStyleNames.insert( FirstRow, QStringLiteral( "firstRow" ) );
1062  mCellStyleNames.insert( LastRow, QStringLiteral( "lastRow" ) );
1063 }
1064 
1066 {
1067  mMaxColumnWidthMap.clear();
1068 
1069  //total number of cells (rows + 1 for header)
1070  int cols = mColumns.count();
1071  int cells = cols * ( mTableContents.count() + 1 );
1072  QVector< double > widths( cells );
1073 
1074  double currentCellTextWidth;
1075 
1078 
1079  //first, go through all the column headers and calculate the sizes
1080  int i = 0;
1081  for ( const QgsLayoutTableColumn &col : std::as_const( mColumns ) )
1082  {
1083  if ( col.width() > 0 )
1084  {
1085  //column has manually specified width
1086  widths[i] = col.width();
1087  }
1088  else if ( mHeaderMode != QgsLayoutTable::NoHeaders )
1089  {
1090  std::unique_ptr< QgsExpressionContextScope > headerCellScope = std::make_unique< QgsExpressionContextScope >();
1091  headerCellScope->setVariable( QStringLiteral( "column_number" ), i + 1, true );
1092  QgsExpressionContextScopePopper popper( context.expressionContext(), headerCellScope.release() );
1093 
1094  //column width set to automatic, so check content size
1095  const QStringList multiLineSplit = col.heading().split( '\n' );
1096  currentCellTextWidth = QgsTextRenderer::textWidth( context, textFormatForHeader( i ), multiLineSplit ) / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters );
1097  widths[i] = currentCellTextWidth;
1098  }
1099  else
1100  {
1101  widths[i] = 0.0;
1102  }
1103  i++;
1104  }
1105 
1106  //next, go through all the table contents and calculate the sizes
1107  QgsLayoutTableContents::const_iterator rowIt = mTableContents.constBegin();
1108  int row = 1;
1109  for ( ; rowIt != mTableContents.constEnd(); ++rowIt )
1110  {
1111  QgsLayoutTableRow::const_iterator colIt = rowIt->constBegin();
1112  int col = 0;
1113  for ( ; colIt != rowIt->constEnd(); ++colIt )
1114  {
1115  if ( mColumns.at( col ).width() <= 0 )
1116  {
1117  //column width set to automatic, so check content size
1118  const QStringList multiLineSplit = QgsExpressionUtils::toLocalizedString( *colIt ).split( '\n' );
1119 
1120  QgsTextFormat cellFormat = textFormatForCell( row - 1, col );
1121  QgsExpressionContextScopePopper popper( context.expressionContext(), scopeForCell( row - 1, col ) );
1122  cellFormat.updateDataDefinedProperties( context );
1123 
1124  currentCellTextWidth = QgsTextRenderer::textWidth( context, cellFormat, multiLineSplit ) / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters );
1125  widths[ row * cols + col ] = currentCellTextWidth;
1126  }
1127  else
1128  {
1129  widths[ row * cols + col ] = 0;
1130  }
1131 
1132  col++;
1133  }
1134  row++;
1135  }
1136 
1137  //calculate maximum
1138  for ( int col = 0; col < cols; ++col )
1139  {
1140  double maxColWidth = 0;
1141  for ( int row = 0; row < mTableContents.count() + 1; ++row )
1142  {
1143  maxColWidth = std::max( widths[ row * cols + col ], maxColWidth );
1144  }
1145  mMaxColumnWidthMap.insert( col, maxColWidth );
1146  }
1147 
1148  return true;
1149 }
1150 
1152 {
1153  mMaxRowHeightMap.clear();
1154 
1155  //total number of cells (rows + 1 for header)
1156  int cols = mColumns.count();
1157  int cells = cols * ( mTableContents.count() + 1 );
1158  QVector< double > heights( cells );
1159 
1162 
1163  //first, go through all the column headers and calculate the sizes
1164  int i = 0;
1165  for ( const QgsLayoutTableColumn &col : std::as_const( mColumns ) )
1166  {
1167  std::unique_ptr< QgsExpressionContextScope > headerCellScope = std::make_unique< QgsExpressionContextScope >();
1168  headerCellScope->setVariable( QStringLiteral( "column_number" ), i + 1, true );
1169  QgsExpressionContextScopePopper popper( context.expressionContext(), headerCellScope.release() );
1170 
1171  const QgsTextFormat cellFormat = textFormatForHeader( i );
1173  //height
1175  {
1176  heights[i] = 0;
1177  }
1178  else
1179  {
1180  heights[i] = QgsTextRenderer::textHeight( context,
1181  cellFormat,
1182  QStringList() << col.heading(), QgsTextRenderer::Rect,
1183  nullptr,
1184  mWrapBehavior == WrapText ? Qgis::TextRendererFlag::WrapLines : Qgis::TextRendererFlags(),
1186  )
1188  - headerDescentMm;
1189  }
1190  i++;
1191  }
1192 
1193  //next, go through all the table contents and calculate the sizes
1194  QgsLayoutTableContents::const_iterator rowIt = mTableContents.constBegin();
1195  int row = 1;
1196  for ( ; rowIt != mTableContents.constEnd(); ++rowIt )
1197  {
1198  QgsLayoutTableRow::const_iterator colIt = rowIt->constBegin();
1199  int i = 0;
1200  for ( ; colIt != rowIt->constEnd(); ++colIt )
1201  {
1202  QgsTextFormat cellFormat = textFormatForCell( row - 1, i );
1203  QgsExpressionContextScopePopper popper( context.expressionContext(), scopeForCell( row - 1, i ) );
1204  cellFormat.updateDataDefinedProperties( context );
1206  const QString localizedString { QgsExpressionUtils::toLocalizedString( *colIt ) };
1207 
1208  heights[ row * cols + i ] = QgsTextRenderer::textHeight( context,
1209  cellFormat,
1210  QStringList() << localizedString.split( '\n' ),
1212  nullptr,
1213  mWrapBehavior == WrapText ? Qgis::TextRendererFlag::WrapLines : Qgis::TextRendererFlags(),
1215  ) / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters ) - contentDescentMm;
1216 
1217  i++;
1218  }
1219  row++;
1220  }
1221 
1222  //calculate maximum
1223  for ( int row = 0; row < mTableContents.count() + 1; ++row )
1224  {
1225  double maxRowHeight = 0;
1226  for ( int col = 0; col < cols; ++col )
1227  {
1228  maxRowHeight = std::max( heights[ row * cols + col ], maxRowHeight );
1229  }
1230  mMaxRowHeightMap.insert( row, maxRowHeight );
1231  }
1232 
1233  return true;
1234 }
1235 
1237 {
1238  //check how much space each column needs
1239  if ( !calculateMaxColumnWidths() )
1240  {
1241  return 0;
1242  }
1243 
1244  //adapt frame to total width
1245  double totalWidth = 0;
1246  QMap<int, double>::const_iterator maxColWidthIt = mMaxColumnWidthMap.constBegin();
1247  for ( ; maxColWidthIt != mMaxColumnWidthMap.constEnd(); ++maxColWidthIt )
1248  {
1249  totalWidth += maxColWidthIt.value();
1250  }
1251  totalWidth += ( 2 * mMaxColumnWidthMap.size() * mCellMargin );
1252  totalWidth += ( mMaxColumnWidthMap.size() + 1 ) * ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1253 
1254  return totalWidth;
1255 }
1256 
1258 {
1259  //check how much space each row needs
1260  if ( !calculateMaxRowHeights() )
1261  {
1262  return 0;
1263  }
1264 
1265  double height = 0;
1266 
1269 
1270  //loop through all existing frames to calculate how many rows are visible in each
1271  //as the entire height of a frame may not be utilized for content rows
1272  int rowsAlreadyShown = 0;
1273  int numberExistingFrames = frameCount();
1274  int rowsVisibleInLastFrame = 0;
1275  double heightOfLastFrame = 0;
1276  for ( int idx = 0; idx < numberExistingFrames; ++idx )
1277  {
1278  bool hasHeader = ( ( mHeaderMode == QgsLayoutTable::FirstFrame && idx == 0 )
1280  heightOfLastFrame = frame( idx )->rect().height();
1281  rowsVisibleInLastFrame = rowsVisible( context, heightOfLastFrame, rowsAlreadyShown, hasHeader, false );
1282  rowsAlreadyShown += rowsVisibleInLastFrame;
1283  height += heightOfLastFrame;
1284  if ( rowsAlreadyShown >= mTableContents.length() )
1285  {
1286  //shown entire contents of table, nothing remaining
1287  return height;
1288  }
1289  }
1290 
1291  //calculate how many rows left to show
1292  int remainingRows = mTableContents.length() - rowsAlreadyShown;
1293 
1294  if ( remainingRows <= 0 )
1295  {
1296  //no remaining rows
1297  return height;
1298  }
1299 
1301  {
1302  QgsLayoutItemPage *page = mLayout->pageCollection()->page( mLayout->pageCollection()->pageCount() - 1 );
1303  if ( page )
1304  heightOfLastFrame = page->sizeWithUnits().height();
1305  }
1306 
1307  bool hasHeader = ( ( mHeaderMode == QgsLayoutTable::FirstFrame && numberExistingFrames < 1 )
1309 
1310  int numberFramesMissing = 0;
1311  while ( remainingRows > 0 )
1312  {
1313  numberFramesMissing++;
1314 
1315  rowsVisibleInLastFrame = rowsVisible( context, heightOfLastFrame, rowsAlreadyShown, hasHeader, false );
1316  if ( rowsVisibleInLastFrame < 1 )
1317  {
1318  //if no rows are visible in the last frame, calculation of missing frames
1319  //is impossible. So just return total height of existing frames
1320  return height;
1321  }
1322 
1323  rowsAlreadyShown += rowsVisibleInLastFrame;
1324  remainingRows = mTableContents.length() - rowsAlreadyShown;
1325  }
1326 
1327  //rows remain unshown -- how many extra frames would we need to complete the table?
1328  //assume all added frames are same size as final frame
1329  height += heightOfLastFrame * numberFramesMissing;
1330  return height;
1331 }
1332 
1333 void QgsLayoutTable::drawHorizontalGridLines( QgsLayoutItemRenderContext &context, int firstRow, int lastRow, bool drawHeaderLines ) const
1334 {
1335  //horizontal lines
1336  if ( lastRow - firstRow < 1 && !drawHeaderLines )
1337  {
1338  return;
1339  }
1340 
1341  QPainter *painter = context.renderContext().painter();
1342 
1344  double halfGridStrokeWidth = ( mShowGrid ? mGridStrokeWidth : 0 ) / 2.0;
1345  double currentY = 0;
1346  currentY = halfGridStrokeWidth;
1347  if ( drawHeaderLines )
1348  {
1349  painter->drawLine( QPointF( halfGridStrokeWidth, currentY ), QPointF( mTableSize.width() - halfGridStrokeWidth, currentY ) );
1350  currentY += ( mShowGrid ? mGridStrokeWidth : 0 );
1351  currentY += mMaxRowHeightMap[0] + 2 * mCellMargin;
1352  }
1353  for ( int row = firstRow; row < lastRow; ++row )
1354  {
1355  painter->drawLine( QPointF( halfGridStrokeWidth, currentY ), QPointF( mTableSize.width() - halfGridStrokeWidth, currentY ) );
1356  currentY += ( mShowGrid ? mGridStrokeWidth : 0 );
1357  double rowHeight = row < mTableContents.count() ? mMaxRowHeightMap[row + 1] : cellBodyHeightForEmptyRows;
1358  currentY += ( rowHeight + 2 * mCellMargin );
1359  }
1360  painter->drawLine( QPointF( halfGridStrokeWidth, currentY ), QPointF( mTableSize.width() - halfGridStrokeWidth, currentY ) );
1361 }
1362 
1363 QColor QgsLayoutTable::backgroundColor( int row, int column ) const
1364 {
1365  QColor color = mBackgroundColor;
1366  if ( QgsLayoutTableStyle *style = mCellStyles.value( OddColumns ) )
1367  if ( style->enabled && column % 2 == 0 )
1368  color = style->cellBackgroundColor;
1369  if ( QgsLayoutTableStyle *style = mCellStyles.value( EvenColumns ) )
1370  if ( style->enabled && column % 2 == 1 )
1371  color = style->cellBackgroundColor;
1372  if ( QgsLayoutTableStyle *style = mCellStyles.value( OddRows ) )
1373  if ( style->enabled && row % 2 == 0 )
1374  color = style->cellBackgroundColor;
1375  if ( QgsLayoutTableStyle *style = mCellStyles.value( EvenRows ) )
1376  if ( style->enabled && row % 2 == 1 )
1377  color = style->cellBackgroundColor;
1378  if ( QgsLayoutTableStyle *style = mCellStyles.value( FirstColumn ) )
1379  if ( style->enabled && column == 0 )
1380  color = style->cellBackgroundColor;
1381  if ( QgsLayoutTableStyle *style = mCellStyles.value( LastColumn ) )
1382  if ( style->enabled && column == mColumns.count() - 1 )
1383  color = style->cellBackgroundColor;
1384  if ( QgsLayoutTableStyle *style = mCellStyles.value( HeaderRow ) )
1385  if ( style->enabled && row == -1 )
1386  color = style->cellBackgroundColor;
1387  if ( QgsLayoutTableStyle *style = mCellStyles.value( FirstRow ) )
1388  if ( style->enabled && row == 0 )
1389  color = style->cellBackgroundColor;
1390  if ( QgsLayoutTableStyle *style = mCellStyles.value( LastRow ) )
1391  if ( style->enabled && row == mTableContents.count() - 1 )
1392  color = style->cellBackgroundColor;
1393 
1394  if ( row >= 0 )
1395  {
1396  QgsConditionalStyle conditionalStyle = conditionalCellStyle( row, column );
1397  if ( conditionalStyle.backgroundColor().isValid() )
1398  color = conditionalStyle.backgroundColor();
1399  }
1400 
1401  return color;
1402 }
1403 
1404 void QgsLayoutTable::drawVerticalGridLines( QgsLayoutItemRenderContext &context, const QMap<int, double> &maxWidthMap, int firstRow, int lastRow, bool hasHeader, bool mergeCells ) const
1405 {
1406  //vertical lines
1407  if ( lastRow - firstRow < 1 && !hasHeader )
1408  {
1409  return;
1410  }
1411 
1412  QPainter *painter = context.renderContext().painter();
1413 
1414  //calculate height of table within frame
1415  double tableHeight = 0;
1416  if ( hasHeader )
1417  {
1418  tableHeight += ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) + mCellMargin * 2 + mMaxRowHeightMap[0];
1419  }
1420  tableHeight += ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 );
1421  double headerHeight = tableHeight;
1422 
1424  for ( int row = firstRow; row < lastRow; ++row )
1425  {
1426  double rowHeight = row < mTableContents.count() ? mMaxRowHeightMap[row + 1] : cellBodyHeightForEmptyRows;
1427  tableHeight += rowHeight + ( mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0 ) + mCellMargin * 2;
1428  }
1429 
1430  double halfGridStrokeWidth = ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 ) / 2.0;
1431  double currentX = halfGridStrokeWidth;
1432  painter->drawLine( QPointF( currentX, halfGridStrokeWidth ), QPointF( currentX, tableHeight - halfGridStrokeWidth ) );
1433  currentX += ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1434  QMap<int, double>::const_iterator maxColWidthIt = maxWidthMap.constBegin();
1435  int col = 1;
1436  for ( ; maxColWidthIt != maxWidthMap.constEnd(); ++maxColWidthIt )
1437  {
1438  currentX += ( maxColWidthIt.value() + 2 * mCellMargin );
1439  if ( col == maxWidthMap.size() || !mergeCells )
1440  {
1441  painter->drawLine( QPointF( currentX, halfGridStrokeWidth ), QPointF( currentX, tableHeight - halfGridStrokeWidth ) );
1442  }
1443  else if ( hasHeader )
1444  {
1445  painter->drawLine( QPointF( currentX, halfGridStrokeWidth ), QPointF( currentX, headerHeight - halfGridStrokeWidth ) );
1446  }
1447 
1448  currentX += ( mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0 );
1449  col++;
1450  }
1451 }
1452 
1454 {
1456 
1457  //force recalculation of frame rects, so that they are set to the correct
1458  //fixed and minimum frame sizes
1460 }
1461 
1463 {
1464  return ( contents.indexOf( row ) >= 0 );
1465 }
1466 
1468 {
1469  return mContentTextFormat;
1470 }
1471 
1473 {
1474  return mHeaderTextFormat;
1475 }
1476 
1477 Qt::Alignment QgsLayoutTable::horizontalAlignmentForCell( int, int column ) const
1478 {
1479  return mColumns.value( column ).hAlignment();
1480 }
1481 
1482 Qt::Alignment QgsLayoutTable::verticalAlignmentForCell( int, int column ) const
1483 {
1484  return mColumns.value( column ).vAlignment();
1485 }
1486 
@ ApplyScalingWorkaroundForTextRendering
Whether a scaling workaround designed to stablise the rendering of small font sizes (or for painters ...
@ WrapLines
Automatically wrap long lines of text.
Conditional styling for a rule.
QColor backgroundColor() const
The background color for style.
QColor textColor() const
The text color set for style.
RAII class to pop scope from an expression context on destruction.
Single scope for storing variables and functions for use within a QgsExpressionContext.
static bool setFromXmlChildNode(QFont &font, const QDomElement &element, const QString &childNode)
Sets the properties of a font to match the properties stored in an XML child node.
QRectF extent() const
Returns the visible portion of the multi frame's content which is shown in this frame,...
Item representing the paper in a layout.
Contains settings and helpers relating to a render of a QgsLayoutItem.
Definition: qgslayoutitem.h:45
QgsRenderContext & renderContext()
Returns a reference to the context's render context.
Definition: qgslayoutitem.h:72
QgsLayoutSize sizeWithUnits() const
Returns the item's current size, including units.
Abstract base class for layout items with the ability to distribute the content to several frames (Qg...
int frameCount() const
Returns the number of frames associated with this multiframe.
QgsLayoutFrame * frame(int index) const
Returns the child frame at a specified index from the multiframe.
@ ExtendToNextPage
Creates new full page frames on the following page(s) until the entire multiframe content is visible.
void refresh() override
Refreshes the multiframe, causing a recalculation of any property overrides.
virtual void recalculateFrameSizes()
Recalculates the portion of the multiframe item which is shown in each of its component frames.
void update()
Forces a redraw of all child frames.
int frameIndex(QgsLayoutFrame *frame) const
Returns the index of a frame within the multiframe.
void recalculateFrameRects()
Forces a recalculation of all the associated frame's scene rectangles.
void changed()
Emitted when the object's properties change.
QPointer< QgsLayout > mLayout
double height() const
Returns the height of the size.
Definition: qgslayoutsize.h:90
Stores properties of a column for a QgsLayoutTable.
bool readXml(const QDomElement &columnElem)
Reads the column's properties from xml.
Styling option for a layout table cell.
bool readXml(const QDomElement &styleElem)
Reads the style's properties from XML.
QColor cellBackgroundColor
Cell background color.
bool enabled
Whether the styling option is enabled.
bool writeXml(QDomElement &styleElem, QDomDocument &doc) const
Writes the style's properties to XML for storage.
bool mHorizontalGrid
True if grid should be shown.
QSizeF fixedFrameSize(int frameIndex=-1) const override
Returns the fixed size for a frame, if desired.
void setColumns(const QgsLayoutTableColumns &columns)
Replaces the columns in the table with a specified list of QgsLayoutTableColumns.
Q_DECL_DEPRECATED void setHeaderFontColor(const QColor &color)
Sets the color used to draw header text in the table.
QgsLayoutTable(QgsLayout *layout)
Constructor for QgsLayoutTable, belonging to the specified layout.
virtual Qt::Alignment horizontalAlignmentForCell(int row, int column) const
Returns the horizontal alignment to use for the cell at the specified row and column.
QColor backgroundColor() const
Returns the color used for the background of the table.
virtual QgsConditionalStyle conditionalCellStyle(int row, int column) const
Returns the conditional style to use for the cell at row, column.
void setHeaderHAlignment(HeaderHAlignment alignment)
Sets the horizontal alignment for table headers.
void setShowEmptyRows(bool showEmpty)
Sets whether empty rows should be drawn.
QgsLayoutTableColumns & columns()
Returns a reference to the list of QgsLayoutTableColumns shown in the table.
Q_DECL_DEPRECATED QFont headerFont() const
Returns the font used to draw header text in the table.
QMap< int, double > mMaxColumnWidthMap
Map of maximum width for each column.
void setVerticalGrid(bool verticalGrid)
Sets whether the grid's vertical lines should be drawn in the table.
virtual void refreshAttributes()
Refreshes the contents shown in the table by querying for new data.
void setGridColor(const QColor &color)
Sets the color used for grid lines in the table.
double totalWidth()
Returns total width of table contents.
bool horizontalGrid() const
Returns whether the grid's horizontal lines are drawn in the table.
Q_DECL_DEPRECATED void setContentFontColor(const QColor &color)
Sets the color used to draw text in table body cells.
void setCellMargin(double margin)
Sets the margin distance in mm between cell borders and their contents.
void render(QgsLayoutItemRenderContext &context, const QRectF &renderExtent, int frameIndex) override
Renders a portion of the multiframe's content into a render context.
Q_DECL_DEPRECATED void setHeaderFont(const QFont &font)
Sets the font used to draw header text in the table.
void drawVerticalGridLines(QgsLayoutItemRenderContext &context, const QMap< int, double > &maxWidthMap, int firstRow, int lastRow, bool hasHeader, bool mergeCells=false) const
Draws the vertical grid lines for the table.
void setBackgroundColor(const QColor &color)
Sets the color used for background of table.
void setContentTextFormat(const QgsTextFormat &format)
Sets the format used to draw content text in the table.
QgsTextFormat contentTextFormat() const
Returns the format used to draw content text in the table.
QString mEmptyTableMessage
String to show in empty tables.
void setWrapBehavior(WrapBehavior behavior)
Sets the wrap behavior for the table, which controls how text within cells is automatically wrapped.
Q_DECL_DEPRECATED QColor headerFontColor() const
Returns the color used to draw header text in the table.
QPair< int, int > rowRange(QgsRenderContext &context, int frameIndex) const
Calculates a range of rows which should be visible in a given frame.
double mGridStrokeWidth
Width of grid lines.
EmptyTableMode mEmptyTableMode
Behavior for empty tables.
void setEmptyTableBehavior(EmptyTableMode mode)
Sets the behavior mode for empty tables with no content rows.
virtual QgsTextFormat textFormatForHeader(int column) const
Returns the text format to use for the header cell at the specified column.
void setHeaderMode(HeaderMode mode)
Sets the display mode for headers in the table.
Q_DECL_DEPRECATED void setContentFont(const QFont &font)
Sets the font used to draw text in table body cells.
WrapBehavior mWrapBehavior
bool verticalGrid() const
Returns whether the grid's vertical lines are drawn in the table.
QColor mBackgroundColor
Color for table background.
void recalculateTableSize()
Recalculates and updates the size of the table and all table frames.
void drawHorizontalGridLines(QgsLayoutItemRenderContext &context, int firstRow, int lastRow, bool drawHeaderLines) const
Draws the horizontal grid lines for the table.
virtual QgsExpressionContextScope * scopeForCell(int row, int column) const
Creates a new QgsExpressionContextScope for the cell at row, column.
QgsLayoutTableContents & contents()
Returns the current contents of the table.
Q_DECL_DEPRECATED QFont contentFont() const
Returns the font used to draw text in table body cells.
bool contentsContainsRow(const QgsLayoutTableContents &contents, const QgsLayoutTableRow &row) const
Checks whether a table contents contains a given row.
bool mShowGrid
True if grid should be shown.
CellStyleGroup
Row or column groups for cell styling.
@ FirstRow
Style first row only.
@ EvenColumns
Style even numbered columns.
@ EvenRows
Style even numbered rows.
@ HeaderRow
Style header row.
@ OddColumns
Style odd numbered columns.
@ FirstColumn
Style first column only.
@ LastColumn
Style last column only.
@ LastRow
Style last row only.
@ OddRows
Style odd numbered rows.
QgsTextFormat mHeaderTextFormat
void setHorizontalGrid(bool horizontalGrid)
Sets whether the grid's horizontal lines should be drawn in the table.
HeaderMode
Controls where headers are shown in the table.
@ FirstFrame
Header shown on first frame only.
@ AllFrames
Headers shown on all frames.
@ NoHeaders
No headers shown for table.
void setCellStyle(CellStyleGroup group, const QgsLayoutTableStyle &style)
Sets the cell style for a cell group.
QgsLayoutTableColumns mColumns
Columns to show in table.
const QgsLayoutTableStyle * cellStyle(CellStyleGroup group) const
Returns the cell style for a cell group.
void setShowGrid(bool showGrid)
Sets whether grid lines should be drawn in the table.
QgsTextFormat mContentTextFormat
virtual bool getTableContents(QgsLayoutTableContents &contents)=0
Fetches the contents used for the cells in the table.
QgsLayoutTableSortColumns & sortColumns()
Returns a reference to the list of QgsLayoutTableSortColumns shown in the table.
virtual bool calculateMaxColumnWidths()
Calculates the maximum width of text shown in columns.
~QgsLayoutTable() override
QgsTextFormat headerTextFormat() const
Returns the format used to draw header text in the table.
HeaderMode mHeaderMode
Header display mode.
HeaderHAlignment mHeaderHAlignment
Alignment for table headers.
int rowsVisible(QgsRenderContext &context, double frameHeight, int firstRow, bool includeHeader, bool includeEmptyRows) const
Calculates how many content rows would be visible within a frame of the specified height.
void recalculateFrameSizes() override
bool mShowEmptyRows
True if empty rows should be shown in the table.
QColor mGridColor
Color for grid lines.
QSizeF minFrameSize(int frameIndex=-1) const override
Returns the minimum size for a frames, if desired.
double mCellMargin
Margin between cell borders and cell text.
virtual Qt::Alignment verticalAlignmentForCell(int row, int column) const
Returns the vertical alignment to use for the cell at the specified row and column.
Q_DECL_DEPRECATED QColor contentFontColor() const
Returns the color used to draw text in table body cells.
double totalHeight()
Returns total height of table contents.
QgsLayoutTableContents mTableContents
Contents to show in table.
bool writePropertiesToElement(QDomElement &elem, QDomDocument &doc, const QgsReadWriteContext &context) const override
Stores multiframe state within an XML DOM element.
void setEmptyTableMessage(const QString &message)
Sets the message for empty tables with no content rows.
void setSortColumns(const QgsLayoutTableSortColumns &sortColumns)
Replaces the sorting columns in the table with a specified list of QgsLayoutTableSortColumns.
void setHeaderTextFormat(const QgsTextFormat &format)
Sets the format used to draw header text in the table.
QgsLayoutTableSortColumns mSortColumns
Columns to sort the table.
bool showGrid() const
Returns whether grid lines are drawn in the table.
virtual QMap< int, QString > headerLabels() const
Returns the text used in the column headers for the table.
void setGridStrokeWidth(double width)
Sets the width in mm for grid lines in the table.
virtual QgsTextFormat textFormatForCell(int row, int column) const
Returns the text format to use for the cell at the specified row and column.
bool readPropertiesFromElement(const QDomElement &itemElem, const QDomDocument &doc, const QgsReadWriteContext &context) override
Sets multiframe state from a DOM element.
QSizeF totalSize() const override
Returns the total size of the multiframe's content, in layout units.
bool mVerticalGrid
True if grid should be shown.
virtual bool calculateMaxRowHeights()
Calculates the maximum height of text shown in rows.
WrapBehavior
Controls how long strings in the table are handled.
@ WrapText
Text which doesn't fit inside the cell is wrapped. Note that this only applies to text in columns wit...
HeaderHAlignment
Controls how headers are horizontally aligned in a table.
@ HeaderRight
Align headers right.
@ HeaderLeft
Align headers left.
@ HeaderCenter
Align headers to center.
@ FollowColumn
Header uses the same alignment as the column.
EmptyTableMode
Controls how empty tables are displayed.
@ HideTable
Hides entire table if empty.
@ ShowMessage
Shows preset message instead of table contents.
void refresh() override
QMap< int, double > mMaxRowHeightMap
Map of maximum height for each row.
QMap< CellStyleGroup, QgsLayoutTableStyle * > mCellStyles
static QgsRenderContext createRenderContextForLayout(QgsLayout *layout, QPainter *painter, double dpi=-1)
Creates a render context suitable for the specified layout and painter destination.
Base class for layouts, which can contain items such as maps, labels, scalebars, etc.
Definition: qgslayout.h:51
The class is used as a container of context for various read/write operations on other objects.
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.
double convertToPainterUnits(double size, QgsUnitTypes::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale(), Qgis::RenderSubcomponentProperty property=Qgis::RenderSubcomponentProperty::Generic) const
Converts a size from the specified units to painter units (pixels).
bool testFlag(Qgis::RenderContextFlag flag) const
Check whether a particular flag is enabled.
void setFlag(Qgis::RenderContextFlag flag, bool on=true)
Enable or disable a particular flag (other flags are not affected)
Scoped object for saving and restoring a QPainter object's state.
Scoped object for temporary scaling of a QgsRenderContext for pixel based rendering.
static QColor decodeColor(const QString &str)
static QString encodeColor(const QColor &color)
Container for all settings relating to text rendering.
Definition: qgstextformat.h:41
void setColor(const QColor &color)
Sets the color that text will be rendered in.
void setSize(double size)
Sets the size for rendered text.
void setFont(const QFont &font)
Sets the font used for rendering text.
void updateDataDefinedProperties(QgsRenderContext &context)
Updates the format by evaluating current values of data defined properties.
void readXml(const QDomElement &elem, const QgsReadWriteContext &context)
Read settings from a DOM element.
QFont toQFont() const
Returns a QFont matching the relevant settings from this text format.
void setSizeUnit(QgsUnitTypes::RenderUnit unit)
Sets the units for the size of rendered text.
QDomElement writeXml(QDomDocument &doc, const QgsReadWriteContext &context) const
Write settings into a DOM element.
QColor color() const
Returns the color that text will be rendered in.
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.
@ AlignVCenter
Center align.
static void drawText(const QRectF &rect, double rotation, HAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, bool drawAsOutlines=true, VAlignment vAlignment=AlignTop, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags())
Draws text within a rectangle using the specified settings.
HAlignment
Horizontal alignment.
@ AlignLeft
Left align.
@ AlignRight
Right align.
@ AlignCenter
Center align.
static QFontMetricsF fontMetrics(QgsRenderContext &context, const QgsTextFormat &format, double scaleFactor=1.0)
Returns the font metrics for the given text format, when rendered in the specified render context.
static HAlignment convertQtHAlignment(Qt::Alignment alignment)
Converts a Qt horizontal alignment flag to a QgsTextRenderer::HAlignment value.
static double textHeight(const QgsRenderContext &context, const QgsTextFormat &format, const QStringList &textLines, DrawMode mode=Point, QFontMetricsF *fontMetrics=nullptr, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags(), double maxLineWidth=0)
Returns the height of a text based on a given format.
static VAlignment convertQtVAlignment(Qt::Alignment alignment)
Converts a Qt vertical alignment flag to a QgsTextRenderer::VAlignment value.
@ Rect
Text within rectangle draw mode.
static constexpr double FONT_WORKAROUND_SCALE
Scale factor for upscaling font sizes and downscaling destination painter devices.
@ RenderPoints
Points (e.g., for font sizes)
Definition: qgsunittypes.h:173
@ RenderPixels
Pixels.
Definition: qgsunittypes.h:171
@ RenderMillimeters
Millimeters.
Definition: qgsunittypes.h:169
QVector< QgsLayoutTableColumn > QgsLayoutTableColumns
List of column definitions for a QgsLayoutTable.
QVector< QgsLayoutTableColumn > QgsLayoutTableSortColumns
List of column definitions for sorting a QgsLayoutTable.
QVector< QgsLayoutTableRow > QgsLayoutTableContents
List of QgsLayoutTableRows, representing rows and column cell contents for a QgsLayoutTable.
QVector< QVariant > QgsLayoutTableRow
List of QVariants, representing a the contents of a single row in a QgsLayoutTable.
#define str(x)
Definition: qgis.cpp:37
#define Q_NOWARN_DEPRECATED_POP
Definition: qgis.h:2545
#define Q_NOWARN_DEPRECATED_PUSH
Definition: qgis.h:2544
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:1990