QGIS API Documentation  3.21.0-Master (5b68dc587e)
qgslayoutaligner.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgslayoutaligner.cpp
3  --------------------
4  begin : October 2017
5  copyright : (C) 2017 by Nyall Dawson
6  email : nyall dot dawson at gmail dot com
7  ***************************************************************************/
8 /***************************************************************************
9  * *
10  * This program is free software; you can redistribute it and/or modify *
11  * it under the terms of the GNU General Public License as published by *
12  * the Free Software Foundation; either version 2 of the License, or *
13  * (at your option) any later version. *
14  * *
15  ***************************************************************************/
16 
17 #include "qgslayoutaligner.h"
18 #include "qgslayoutitem.h"
19 #include "qgslayout.h"
20 #include "qgslayoutundostack.h"
21 
22 void QgsLayoutAligner::alignItems( QgsLayout *layout, const QList<QgsLayoutItem *> &items, QgsLayoutAligner::Alignment alignment )
23 {
24  if ( !layout || items.size() < 2 )
25  {
26  return;
27  }
28 
29  const QRectF itemBBox = boundingRectOfItems( items );
30  if ( !itemBBox.isValid() )
31  {
32  return;
33  }
34 
35  double refCoord = 0;
36  switch ( alignment )
37  {
38  case AlignLeft:
39  refCoord = itemBBox.left();
40  break;
41  case AlignHCenter:
42  refCoord = itemBBox.center().x();
43  break;
44  case AlignRight:
45  refCoord = itemBBox.right();
46  break;
47  case AlignTop:
48  refCoord = itemBBox.top();
49  break;
50  case AlignVCenter:
51  refCoord = itemBBox.center().y();
52  break;
53  case AlignBottom:
54  refCoord = itemBBox.bottom();
55  break;
56  }
57 
58  layout->undoStack()->beginMacro( undoText( alignment ) );
59  for ( QgsLayoutItem *item : items )
60  {
61  layout->undoStack()->beginCommand( item, QString() );
62 
63  QPointF shifted = item->pos();
64  switch ( alignment )
65  {
66  case AlignLeft:
67  shifted.setX( refCoord );
68  break;
69  case AlignHCenter:
70  shifted.setX( refCoord - item->rect().width() / 2.0 );
71  break;
72  case AlignRight:
73  shifted.setX( refCoord - item->rect().width() );
74  break;
75  case AlignTop:
76  shifted.setY( refCoord );
77  break;
78  case AlignVCenter:
79  shifted.setY( refCoord - item->rect().height() / 2.0 );
80  break;
81  case AlignBottom:
82  shifted.setY( refCoord - item->rect().height() );
83  break;
84  }
85 
86  // need to keep item units
87  const QgsLayoutPoint newPos = layout->convertFromLayoutUnits( shifted, item->positionWithUnits().units() );
88  item->attemptMove( newPos, false );
89 
90  layout->undoStack()->endCommand();
91  }
92  layout->undoStack()->endMacro();
93 }
94 
95 void QgsLayoutAligner::distributeItems( QgsLayout *layout, const QList<QgsLayoutItem *> &items, QgsLayoutAligner::Distribution distribution )
96 {
97  if ( items.size() < 2 )
98  return;
99 
100  // equispaced distribution doesn't follow the same approach of the other distribution types
101  if ( distribution == DistributeHSpace || distribution == DistributeVSpace )
102  {
103  distributeEquispacedItems( layout, items, distribution );
104  return;
105  }
106 
107  auto collectReferenceCoord = [distribution]( QgsLayoutItem * item )->double
108  {
109  const QRectF itemBBox = item->sceneBoundingRect();
110  switch ( distribution )
111  {
112  case DistributeLeft:
113  return itemBBox.left();
114  case DistributeHCenter:
115  return itemBBox.center().x();
116  case DistributeRight:
117  return itemBBox.right();
118  case DistributeTop:
119  return itemBBox.top();
120  case DistributeVCenter:
121  return itemBBox.center().y();
122  case DistributeBottom:
123  return itemBBox.bottom();
124  case DistributeHSpace:
125  case DistributeVSpace:
126  // not reachable branch, just to avoid compilation warning
127  return std::numeric_limits<double>::quiet_NaN();
128  }
129  // no warnings
130  return itemBBox.left();
131  };
132 
133 
134  double minCoord = std::numeric_limits<double>::max();
135  double maxCoord = std::numeric_limits<double>::lowest();
136  QMap< double, QgsLayoutItem * > itemCoords;
137  for ( QgsLayoutItem *item : items )
138  {
139  const double refCoord = collectReferenceCoord( item );
140  minCoord = std::min( minCoord, refCoord );
141  maxCoord = std::max( maxCoord, refCoord );
142  itemCoords.insert( refCoord, item );
143  }
144 
145  const double step = ( maxCoord - minCoord ) / ( items.size() - 1 );
146 
147  auto distributeItemToCoord = [layout, distribution]( QgsLayoutItem * item, double refCoord )
148  {
149  QPointF shifted = item->pos();
150  switch ( distribution )
151  {
152  case DistributeLeft:
153  shifted.setX( refCoord );
154  break;
155  case DistributeHCenter:
156  shifted.setX( refCoord - item->rect().width() / 2.0 );
157  break;
158  case DistributeRight:
159  shifted.setX( refCoord - item->rect().width() );
160  break;
161  case DistributeTop:
162  shifted.setY( refCoord );
163  break;
164  case DistributeVCenter:
165  shifted.setY( refCoord - item->rect().height() / 2.0 );
166  break;
167  case DistributeBottom:
168  shifted.setY( refCoord - item->rect().height() );
169  break;
170  case DistributeHSpace:
171  case DistributeVSpace:
172  // not reachable branch, just to avoid compilation warning
173  break;
174  }
175 
176  // need to keep item units
177  const QgsLayoutPoint newPos = layout->convertFromLayoutUnits( shifted, item->positionWithUnits().units() );
178  item->attemptMove( newPos, false );
179  };
180 
181 
182  layout->undoStack()->beginMacro( undoText( distribution ) );
183  double currentVal = minCoord;
184  for ( auto itemIt = itemCoords.constBegin(); itemIt != itemCoords.constEnd(); ++itemIt )
185  {
186  layout->undoStack()->beginCommand( itemIt.value(), QString() );
187  distributeItemToCoord( itemIt.value(), currentVal );
188  layout->undoStack()->endCommand();
189 
190  currentVal += step;
191  }
192  layout->undoStack()->endMacro();
193 }
194 
195 void QgsLayoutAligner::resizeItems( QgsLayout *layout, const QList<QgsLayoutItem *> &items, QgsLayoutAligner::Resize resize )
196 {
197  if ( !( items.size() >= 2 || ( items.size() == 1 && resize == ResizeToSquare ) ) )
198  return;
199 
200  auto collectSize = [resize]( QgsLayoutItem * item )->double
201  {
202  const QRectF itemBBox = item->sceneBoundingRect();
203  switch ( resize )
204  {
205  case ResizeNarrowest:
206  case ResizeWidest:
207  case ResizeToSquare:
208  return itemBBox.width();
209  case ResizeShortest:
210  case ResizeTallest:
211  return itemBBox.height();
212  }
213  // no warnings
214  return itemBBox.width();
215  };
216 
217  double newSize = collectSize( items.at( 0 ) );
218  for ( QgsLayoutItem *item : items )
219  {
220  const double size = collectSize( item );
221  switch ( resize )
222  {
223  case ResizeNarrowest:
224  case ResizeShortest:
225  newSize = std::min( size, newSize );
226  break;
227  case ResizeTallest:
228  case ResizeWidest:
229  newSize = std::max( size, newSize );
230  break;
231  case ResizeToSquare:
232  break;
233  }
234  }
235 
236  auto resizeItemToSize = [layout, resize]( QgsLayoutItem * item, double size )
237  {
238  QSizeF newSize = item->rect().size();
239  switch ( resize )
240  {
241  case ResizeNarrowest:
242  case ResizeWidest:
243  newSize.setWidth( size );
244  break;
245  case ResizeTallest:
246  case ResizeShortest:
247  newSize.setHeight( size );
248  break;
249  case ResizeToSquare:
250  {
251  if ( newSize.width() > newSize.height() )
252  newSize.setHeight( newSize.width() );
253  else
254  newSize.setWidth( newSize.height() );
255  break;
256  }
257  }
258 
259  // need to keep item units
260  const QgsLayoutSize newSizeWithUnits = layout->convertFromLayoutUnits( newSize, item->sizeWithUnits().units() );
261  item->attemptResize( newSizeWithUnits );
262  };
263 
264  layout->undoStack()->beginMacro( undoText( resize ) );
265  for ( QgsLayoutItem *item : items )
266  {
267  layout->undoStack()->beginCommand( item, QString() );
268  resizeItemToSize( item, newSize );
269  layout->undoStack()->endCommand();
270  }
271  layout->undoStack()->endMacro();
272 }
273 
274 QRectF QgsLayoutAligner::boundingRectOfItems( const QList<QgsLayoutItem *> &items )
275 {
276  if ( items.empty() )
277  {
278  return QRectF();
279  }
280 
281  auto it = items.constBegin();
282  //set the box to the first item
283  QgsLayoutItem *currentItem = *it;
284  it++;
285  double minX = currentItem->pos().x();
286  double minY = currentItem->pos().y();
287  double maxX = minX + currentItem->rect().width();
288  double maxY = minY + currentItem->rect().height();
289 
290  double currentMinX, currentMinY, currentMaxX, currentMaxY;
291 
292  for ( ; it != items.constEnd(); ++it )
293  {
294  currentItem = *it;
295  currentMinX = currentItem->pos().x();
296  currentMinY = currentItem->pos().y();
297  currentMaxX = currentMinX + currentItem->rect().width();
298  currentMaxY = currentMinY + currentItem->rect().height();
299 
300  if ( currentMinX < minX )
301  minX = currentMinX;
302  if ( currentMaxX > maxX )
303  maxX = currentMaxX;
304  if ( currentMinY < minY )
305  minY = currentMinY;
306  if ( currentMaxY > maxY )
307  maxY = currentMaxY;
308  }
309 
310  return QRectF( QPointF( minX, minY ), QPointF( maxX, maxY ) );
311 }
312 
313 QString QgsLayoutAligner::undoText( Distribution distribution )
314 {
315  switch ( distribution )
316  {
317  case DistributeLeft:
318  return QObject::tr( "Distribute Items by Left" );
319  case DistributeHCenter:
320  return QObject::tr( "Distribute Items by Horizontal Center" );
321  case DistributeHSpace:
322  return QObject::tr( "Distribute Horizontal Spacing Equally" );
323  case DistributeRight:
324  return QObject::tr( "Distribute Items by Right" );
325  case DistributeTop:
326  return QObject::tr( "Distribute Items by Top" );
327  case DistributeVCenter:
328  return QObject::tr( "Distribute Items by Vertical Center" );
329  case DistributeVSpace:
330  return QObject::tr( "Distribute Vertical Spacing Equally" );
331  case DistributeBottom:
332  return QObject::tr( "Distribute Items by Bottom" );
333  }
334  return QString(); //no warnings
335 }
336 
337 QString QgsLayoutAligner::undoText( QgsLayoutAligner::Resize resize )
338 {
339  switch ( resize )
340  {
341  case ResizeNarrowest:
342  return QObject::tr( "Resize Items to Narrowest" );
343  case ResizeWidest:
344  return QObject::tr( "Resize Items to Widest" );
345  case ResizeShortest:
346  return QObject::tr( "Resize Items to Shortest" );
347  case ResizeTallest:
348  return QObject::tr( "Resize Items to Tallest" );
349  case ResizeToSquare:
350  return QObject::tr( "Resize Items to Square" );
351  }
352  return QString(); //no warnings
353 }
354 
355 QString QgsLayoutAligner::undoText( Alignment alignment )
356 {
357  switch ( alignment )
358  {
359  case AlignLeft:
360  return QObject::tr( "Align Items to Left" );
361  case AlignHCenter:
362  return QObject::tr( "Align Items to Center" );
363  case AlignRight:
364  return QObject::tr( "Align Items to Right" );
365  case AlignTop:
366  return QObject::tr( "Align Items to Top" );
367  case AlignVCenter:
368  return QObject::tr( "Align Items to Vertical Center" );
369  case AlignBottom:
370  return QObject::tr( "Align Items to Bottom" );
371  }
372  return QString(); //no warnings
373 }
374 
375 void QgsLayoutAligner::distributeEquispacedItems( QgsLayout *layout, const QList<QgsLayoutItem *> &items, QgsLayoutAligner::Distribution distribution )
376 {
377  double length = 0.0;
378  double minCoord = std::numeric_limits<double>::max();
379  double maxCoord = std::numeric_limits<double>::lowest();
380  QMap< double, QgsLayoutItem * > itemCoords;
381 
382  for ( QgsLayoutItem *item : items )
383  {
384  const QRectF itemBBox = item->sceneBoundingRect();
385 
386  const double item_min = ( distribution == DistributeHSpace ? itemBBox.left() :
387  itemBBox.top() );
388  const double item_max = ( distribution == DistributeHSpace ? itemBBox.right() :
389  itemBBox.bottom() );
390 
391  minCoord = std::min( minCoord, item_min );
392  maxCoord = std::max( maxCoord, item_max );
393  length += ( item_max - item_min );
394  itemCoords.insert( item_min, item );
395  }
396  const double step = ( maxCoord - minCoord - length ) / ( items.size() - 1 );
397 
398  double currentVal = minCoord;
399  layout->undoStack()->beginMacro( undoText( distribution ) );
400  for ( auto itemIt = itemCoords.constBegin(); itemIt != itemCoords.constEnd(); ++itemIt )
401  {
402  QgsLayoutItem *item = itemIt.value();
403  QPointF shifted = item->pos();
404 
405  layout->undoStack()->beginCommand( itemIt.value(), QString() );
406 
407  if ( distribution == DistributeHSpace )
408  {
409  shifted.setX( currentVal );
410  }
411  else
412  {
413  shifted.setY( currentVal );
414  }
415 
416  const QgsLayoutPoint newPos = layout->convertFromLayoutUnits( shifted, item->positionWithUnits().units() );
417  item->attemptMove( newPos, false );
418 
419  layout->undoStack()->endCommand();
420 
421  currentVal += ( distribution == DistributeHSpace ? item->rect().width() :
422  item->rect().height() ) + step;
423  }
424  layout->undoStack()->endMacro();
425  return;
426 }
Alignment
Alignment options.
@ AlignVCenter
Align vertical centers.
@ AlignLeft
Align left edges.
@ AlignBottom
Align bottom edges.
@ AlignRight
Align right edges.
@ AlignTop
Align top edges.
@ AlignHCenter
Align horizontal centers.
Resize
Resize options.
@ ResizeNarrowest
Resize width to match narrowest width.
@ ResizeShortest
Resize height to match shortest height.
@ ResizeTallest
Resize height to match tallest height.
@ ResizeToSquare
Resize items to square.
@ ResizeWidest
Resize width to match widest width.
static void alignItems(QgsLayout *layout, const QList< QgsLayoutItem * > &items, Alignment alignment)
Aligns a set of items from a layout in place.
static void distributeItems(QgsLayout *layout, const QList< QgsLayoutItem * > &items, Distribution distribution)
Distributes a set of items from a layout in place.
static void resizeItems(QgsLayout *layout, const QList< QgsLayoutItem * > &items, Resize resize)
Resizes a set of items from a layout in place.
Distribution
Distribution options.
@ DistributeHSpace
Distribute horizontal equispaced.
@ DistributeVCenter
Distribute vertical centers.
@ DistributeBottom
Distribute bottom edges.
@ DistributeLeft
Distribute left edges.
@ DistributeHCenter
Distribute horizontal centers.
@ DistributeRight
Distribute right edges.
@ DistributeVSpace
Distribute vertical equispaced.
@ DistributeTop
Distribute top edges.
Base class for graphical items within a QgsLayout.
QgsLayoutPoint positionWithUnits() const
Returns the item's current position, including units.
virtual void attemptMove(const QgsLayoutPoint &point, bool useReferencePoint=true, bool includesFrame=false, int page=-1)
Attempts to move the item to a specified point.
This class provides a method of storing points, consisting of an x and y coordinate,...
QgsUnitTypes::LayoutUnit units() const
Returns the units for the point.
This class provides a method of storing sizes, consisting of a width and height, for use in QGIS layo...
Definition: qgslayoutsize.h:41
void endCommand()
Saves final state of an object and pushes the active command to the undo history.
void beginMacro(const QString &commandText)
Starts a macro command, with the given descriptive commandText.
void beginCommand(QgsLayoutUndoObjectInterface *object, const QString &commandText, int id=0)
Begins a new undo command for the specified object.
void endMacro()
Ends a macro command.
Base class for layouts, which can contain items such as maps, labels, scalebars, etc.
Definition: qgslayout.h:51
QgsLayoutMeasurement convertFromLayoutUnits(double length, QgsUnitTypes::LayoutUnit unit) const
Converts a length measurement from the layout's native units to a specified target unit.
Definition: qgslayout.cpp:344
QgsLayoutUndoStack * undoStack()
Returns a pointer to the layout's undo stack, which manages undo/redo states for the layout and it's ...
Definition: qgslayout.cpp:686