QGIS API Documentation  3.10.0-A Coruña (6c816b4204)
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  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  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  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  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  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  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  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  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  QRectF itemBBox = item->sceneBoundingRect();
385 
386  double item_min = ( distribution == DistributeHSpace ? itemBBox.left() :
387  itemBBox.top() );
388  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  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 }
QgsLayoutPoint positionWithUnits() const
Returns the item&#39;s current position, including units.
Base class for graphical items within a QgsLayout.
Align vertical centers.
QgsLayoutUndoStack * undoStack()
Returns a pointer to the layout&#39;s undo stack, which manages undo/redo states for the layout and it&#39;s ...
Definition: qgslayout.cpp:685
Resize items to square.
Distribute horizontal equispaced.
Resize height to match shortest height.
static void alignItems(QgsLayout *layout, const QList< QgsLayoutItem * > &items, Alignment alignment)
Aligns a set of items from a layout in place.
Distribute vertical centers.
This class provides a method of storing points, consisting of an x and y coordinate, for use in QGIS layouts.
QgsUnitTypes::LayoutUnit units() const
Returns the units for the point.
void endCommand()
Saves final state of an object and pushes the active command to the undo history. ...
Distribution
Distribution options.
static void distributeItems(QgsLayout *layout, const QList< QgsLayoutItem * > &items, Distribution distribution)
Distributes a set of items from a layout in place.
Resize height to match tallest height.
Distribute right edges.
Base class for layouts, which can contain items such as maps, labels, scalebars, etc.
Definition: qgslayout.h:49
void beginMacro(const QString &commandText)
Starts a macro command, with the given descriptive commandText.
Resize width to match widest width.
void beginCommand(QgsLayoutUndoObjectInterface *object, const QString &commandText, int id=0)
Begins a new undo command for the specified object.
virtual void attemptMove(const QgsLayoutPoint &point, bool useReferencePoint=true, bool includesFrame=false, int page=-1)
Attempts to move the item to a specified point.
Align horizontal centers.
QgsLayoutMeasurement convertFromLayoutUnits(double length, QgsUnitTypes::LayoutUnit unit) const
Converts a length measurement from the layout&#39;s native units to a specified target unit...
Definition: qgslayout.cpp:343
Alignment
Alignment options.
Distribute vertical equispaced.
Distribute horizontal centers.
Resize
Resize options.
void endMacro()
Ends a macro command.
This class provides a method of storing sizes, consisting of a width and height, for use in QGIS layo...
Definition: qgslayoutsize.h:40
static void resizeItems(QgsLayout *layout, const QList< QgsLayoutItem * > &items, Resize resize)
Resizes a set of items from a layout in place.
Distribute bottom edges.
Resize width to match narrowest width.