Branch data Line data Source code
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 : 0 : void QgsLayoutAligner::alignItems( QgsLayout *layout, const QList<QgsLayoutItem *> &items, QgsLayoutAligner::Alignment alignment )
23 : : {
24 : 0 : if ( !layout || items.size() < 2 )
25 : : {
26 : 0 : return;
27 : : }
28 : :
29 : 0 : QRectF itemBBox = boundingRectOfItems( items );
30 : 0 : if ( !itemBBox.isValid() )
31 : : {
32 : 0 : return;
33 : : }
34 : :
35 : 0 : double refCoord = 0;
36 : 0 : switch ( alignment )
37 : : {
38 : : case AlignLeft:
39 : 0 : refCoord = itemBBox.left();
40 : 0 : break;
41 : : case AlignHCenter:
42 : 0 : refCoord = itemBBox.center().x();
43 : 0 : break;
44 : : case AlignRight:
45 : 0 : refCoord = itemBBox.right();
46 : 0 : break;
47 : : case AlignTop:
48 : 0 : refCoord = itemBBox.top();
49 : 0 : break;
50 : : case AlignVCenter:
51 : 0 : refCoord = itemBBox.center().y();
52 : 0 : break;
53 : : case AlignBottom:
54 : 0 : refCoord = itemBBox.bottom();
55 : 0 : break;
56 : : }
57 : :
58 : 0 : layout->undoStack()->beginMacro( undoText( alignment ) );
59 : 0 : for ( QgsLayoutItem *item : items )
60 : : {
61 : 0 : layout->undoStack()->beginCommand( item, QString() );
62 : :
63 : 0 : QPointF shifted = item->pos();
64 : 0 : switch ( alignment )
65 : : {
66 : : case AlignLeft:
67 : 0 : shifted.setX( refCoord );
68 : 0 : break;
69 : : case AlignHCenter:
70 : 0 : shifted.setX( refCoord - item->rect().width() / 2.0 );
71 : 0 : break;
72 : : case AlignRight:
73 : 0 : shifted.setX( refCoord - item->rect().width() );
74 : 0 : break;
75 : : case AlignTop:
76 : 0 : shifted.setY( refCoord );
77 : 0 : break;
78 : : case AlignVCenter:
79 : 0 : shifted.setY( refCoord - item->rect().height() / 2.0 );
80 : 0 : break;
81 : : case AlignBottom:
82 : 0 : shifted.setY( refCoord - item->rect().height() );
83 : 0 : break;
84 : : }
85 : :
86 : : // need to keep item units
87 : 0 : QgsLayoutPoint newPos = layout->convertFromLayoutUnits( shifted, item->positionWithUnits().units() );
88 : 0 : item->attemptMove( newPos, false );
89 : :
90 : 0 : layout->undoStack()->endCommand();
91 : : }
92 : 0 : layout->undoStack()->endMacro();
93 : 0 : }
94 : :
95 : 0 : void QgsLayoutAligner::distributeItems( QgsLayout *layout, const QList<QgsLayoutItem *> &items, QgsLayoutAligner::Distribution distribution )
96 : : {
97 : 0 : if ( items.size() < 2 )
98 : 0 : return;
99 : :
100 : : // equispaced distribution doesn't follow the same approach of the other distribution types
101 : 0 : if ( distribution == DistributeHSpace || distribution == DistributeVSpace )
102 : : {
103 : 0 : distributeEquispacedItems( layout, items, distribution );
104 : 0 : return;
105 : : }
106 : :
107 : 0 : auto collectReferenceCoord = [distribution]( QgsLayoutItem * item )->double
108 : : {
109 : 0 : QRectF itemBBox = item->sceneBoundingRect();
110 : 0 : switch ( distribution )
111 : : {
112 : : case DistributeLeft:
113 : 0 : return itemBBox.left();
114 : : case DistributeHCenter:
115 : 0 : return itemBBox.center().x();
116 : : case DistributeRight:
117 : 0 : return itemBBox.right();
118 : : case DistributeTop:
119 : 0 : return itemBBox.top();
120 : : case DistributeVCenter:
121 : 0 : return itemBBox.center().y();
122 : : case DistributeBottom:
123 : 0 : return itemBBox.bottom();
124 : : case DistributeHSpace:
125 : : case DistributeVSpace:
126 : : // not reachable branch, just to avoid compilation warning
127 : 0 : return std::numeric_limits<double>::quiet_NaN();
128 : : }
129 : : // no warnings
130 : 0 : return itemBBox.left();
131 : 0 : };
132 : :
133 : :
134 : 0 : double minCoord = std::numeric_limits<double>::max();
135 : 0 : double maxCoord = std::numeric_limits<double>::lowest();
136 : 0 : QMap< double, QgsLayoutItem * > itemCoords;
137 : 0 : for ( QgsLayoutItem *item : items )
138 : : {
139 : 0 : double refCoord = collectReferenceCoord( item );
140 : 0 : minCoord = std::min( minCoord, refCoord );
141 : 0 : maxCoord = std::max( maxCoord, refCoord );
142 : 0 : itemCoords.insert( refCoord, item );
143 : : }
144 : :
145 : 0 : const double step = ( maxCoord - minCoord ) / ( items.size() - 1 );
146 : :
147 : 0 : auto distributeItemToCoord = [layout, distribution]( QgsLayoutItem * item, double refCoord )
148 : : {
149 : 0 : QPointF shifted = item->pos();
150 : 0 : switch ( distribution )
151 : : {
152 : : case DistributeLeft:
153 : 0 : shifted.setX( refCoord );
154 : 0 : break;
155 : : case DistributeHCenter:
156 : 0 : shifted.setX( refCoord - item->rect().width() / 2.0 );
157 : 0 : break;
158 : : case DistributeRight:
159 : 0 : shifted.setX( refCoord - item->rect().width() );
160 : 0 : break;
161 : : case DistributeTop:
162 : 0 : shifted.setY( refCoord );
163 : 0 : break;
164 : : case DistributeVCenter:
165 : 0 : shifted.setY( refCoord - item->rect().height() / 2.0 );
166 : 0 : break;
167 : : case DistributeBottom:
168 : 0 : shifted.setY( refCoord - item->rect().height() );
169 : 0 : break;
170 : : case DistributeHSpace:
171 : : case DistributeVSpace:
172 : : // not reachable branch, just to avoid compilation warning
173 : 0 : break;
174 : : }
175 : :
176 : : // need to keep item units
177 : 0 : QgsLayoutPoint newPos = layout->convertFromLayoutUnits( shifted, item->positionWithUnits().units() );
178 : 0 : item->attemptMove( newPos, false );
179 : 0 : };
180 : :
181 : :
182 : 0 : layout->undoStack()->beginMacro( undoText( distribution ) );
183 : 0 : double currentVal = minCoord;
184 : 0 : for ( auto itemIt = itemCoords.constBegin(); itemIt != itemCoords.constEnd(); ++itemIt )
185 : : {
186 : 0 : layout->undoStack()->beginCommand( itemIt.value(), QString() );
187 : 0 : distributeItemToCoord( itemIt.value(), currentVal );
188 : 0 : layout->undoStack()->endCommand();
189 : :
190 : 0 : currentVal += step;
191 : 0 : }
192 : 0 : layout->undoStack()->endMacro();
193 : 0 : }
194 : :
195 : 0 : void QgsLayoutAligner::resizeItems( QgsLayout *layout, const QList<QgsLayoutItem *> &items, QgsLayoutAligner::Resize resize )
196 : : {
197 : 0 : if ( !( items.size() >= 2 || ( items.size() == 1 && resize == ResizeToSquare ) ) )
198 : 0 : return;
199 : :
200 : 0 : auto collectSize = [resize]( QgsLayoutItem * item )->double
201 : : {
202 : 0 : QRectF itemBBox = item->sceneBoundingRect();
203 : 0 : switch ( resize )
204 : : {
205 : : case ResizeNarrowest:
206 : : case ResizeWidest:
207 : : case ResizeToSquare:
208 : 0 : return itemBBox.width();
209 : : case ResizeShortest:
210 : : case ResizeTallest:
211 : 0 : return itemBBox.height();
212 : : }
213 : : // no warnings
214 : 0 : return itemBBox.width();
215 : 0 : };
216 : :
217 : 0 : double newSize = collectSize( items.at( 0 ) );
218 : 0 : for ( QgsLayoutItem *item : items )
219 : : {
220 : 0 : double size = collectSize( item );
221 : 0 : switch ( resize )
222 : : {
223 : : case ResizeNarrowest:
224 : : case ResizeShortest:
225 : 0 : newSize = std::min( size, newSize );
226 : 0 : break;
227 : : case ResizeTallest:
228 : : case ResizeWidest:
229 : 0 : newSize = std::max( size, newSize );
230 : 0 : break;
231 : : case ResizeToSquare:
232 : 0 : break;
233 : : }
234 : : }
235 : :
236 : 0 : auto resizeItemToSize = [layout, resize]( QgsLayoutItem * item, double size )
237 : : {
238 : 0 : QSizeF newSize = item->rect().size();
239 : 0 : switch ( resize )
240 : : {
241 : : case ResizeNarrowest:
242 : : case ResizeWidest:
243 : 0 : newSize.setWidth( size );
244 : 0 : break;
245 : : case ResizeTallest:
246 : : case ResizeShortest:
247 : 0 : newSize.setHeight( size );
248 : 0 : break;
249 : : case ResizeToSquare:
250 : : {
251 : 0 : if ( newSize.width() > newSize.height() )
252 : 0 : newSize.setHeight( newSize.width() );
253 : : else
254 : 0 : newSize.setWidth( newSize.height() );
255 : 0 : break;
256 : : }
257 : : }
258 : :
259 : : // need to keep item units
260 : 0 : QgsLayoutSize newSizeWithUnits = layout->convertFromLayoutUnits( newSize, item->sizeWithUnits().units() );
261 : 0 : item->attemptResize( newSizeWithUnits );
262 : 0 : };
263 : :
264 : 0 : layout->undoStack()->beginMacro( undoText( resize ) );
265 : 0 : for ( QgsLayoutItem *item : items )
266 : : {
267 : 0 : layout->undoStack()->beginCommand( item, QString() );
268 : 0 : resizeItemToSize( item, newSize );
269 : 0 : layout->undoStack()->endCommand();
270 : : }
271 : 0 : layout->undoStack()->endMacro();
272 : 0 : }
273 : :
274 : 0 : QRectF QgsLayoutAligner::boundingRectOfItems( const QList<QgsLayoutItem *> &items )
275 : : {
276 : 0 : if ( items.empty() )
277 : : {
278 : 0 : return QRectF();
279 : : }
280 : :
281 : 0 : auto it = items.constBegin();
282 : : //set the box to the first item
283 : 0 : QgsLayoutItem *currentItem = *it;
284 : 0 : it++;
285 : 0 : double minX = currentItem->pos().x();
286 : 0 : double minY = currentItem->pos().y();
287 : 0 : double maxX = minX + currentItem->rect().width();
288 : 0 : double maxY = minY + currentItem->rect().height();
289 : :
290 : : double currentMinX, currentMinY, currentMaxX, currentMaxY;
291 : :
292 : 0 : for ( ; it != items.constEnd(); ++it )
293 : : {
294 : 0 : currentItem = *it;
295 : 0 : currentMinX = currentItem->pos().x();
296 : 0 : currentMinY = currentItem->pos().y();
297 : 0 : currentMaxX = currentMinX + currentItem->rect().width();
298 : 0 : currentMaxY = currentMinY + currentItem->rect().height();
299 : :
300 : 0 : if ( currentMinX < minX )
301 : 0 : minX = currentMinX;
302 : 0 : if ( currentMaxX > maxX )
303 : 0 : maxX = currentMaxX;
304 : 0 : if ( currentMinY < minY )
305 : 0 : minY = currentMinY;
306 : 0 : if ( currentMaxY > maxY )
307 : 0 : maxY = currentMaxY;
308 : 0 : }
309 : :
310 : 0 : return QRectF( QPointF( minX, minY ), QPointF( maxX, maxY ) );
311 : 0 : }
312 : :
313 : 0 : QString QgsLayoutAligner::undoText( Distribution distribution )
314 : : {
315 : 0 : switch ( distribution )
316 : : {
317 : : case DistributeLeft:
318 : 0 : return QObject::tr( "Distribute Items by Left" );
319 : : case DistributeHCenter:
320 : 0 : return QObject::tr( "Distribute Items by Horizontal Center" );
321 : : case DistributeHSpace:
322 : 0 : return QObject::tr( "Distribute Horizontal Spacing Equally" );
323 : : case DistributeRight:
324 : 0 : return QObject::tr( "Distribute Items by Right" );
325 : : case DistributeTop:
326 : 0 : return QObject::tr( "Distribute Items by Top" );
327 : : case DistributeVCenter:
328 : 0 : return QObject::tr( "Distribute Items by Vertical Center" );
329 : : case DistributeVSpace:
330 : 0 : return QObject::tr( "Distribute Vertical Spacing Equally" );
331 : : case DistributeBottom:
332 : 0 : return QObject::tr( "Distribute Items by Bottom" );
333 : : }
334 : 0 : return QString(); //no warnings
335 : 0 : }
336 : :
337 : 0 : QString QgsLayoutAligner::undoText( QgsLayoutAligner::Resize resize )
338 : : {
339 : 0 : switch ( resize )
340 : : {
341 : : case ResizeNarrowest:
342 : 0 : return QObject::tr( "Resize Items to Narrowest" );
343 : : case ResizeWidest:
344 : 0 : return QObject::tr( "Resize Items to Widest" );
345 : : case ResizeShortest:
346 : 0 : return QObject::tr( "Resize Items to Shortest" );
347 : : case ResizeTallest:
348 : 0 : return QObject::tr( "Resize Items to Tallest" );
349 : : case ResizeToSquare:
350 : 0 : return QObject::tr( "Resize Items to Square" );
351 : : }
352 : 0 : return QString(); //no warnings
353 : 0 : }
354 : :
355 : 0 : QString QgsLayoutAligner::undoText( Alignment alignment )
356 : : {
357 : 0 : switch ( alignment )
358 : : {
359 : : case AlignLeft:
360 : 0 : return QObject::tr( "Align Items to Left" );
361 : : case AlignHCenter:
362 : 0 : return QObject::tr( "Align Items to Center" );
363 : : case AlignRight:
364 : 0 : return QObject::tr( "Align Items to Right" );
365 : : case AlignTop:
366 : 0 : return QObject::tr( "Align Items to Top" );
367 : : case AlignVCenter:
368 : 0 : return QObject::tr( "Align Items to Vertical Center" );
369 : : case AlignBottom:
370 : 0 : return QObject::tr( "Align Items to Bottom" );
371 : : }
372 : 0 : return QString(); //no warnings
373 : 0 : }
374 : :
375 : 0 : void QgsLayoutAligner::distributeEquispacedItems( QgsLayout *layout, const QList<QgsLayoutItem *> &items, QgsLayoutAligner::Distribution distribution )
376 : : {
377 : 0 : double length = 0.0;
378 : 0 : double minCoord = std::numeric_limits<double>::max();
379 : 0 : double maxCoord = std::numeric_limits<double>::lowest();
380 : 0 : QMap< double, QgsLayoutItem * > itemCoords;
381 : :
382 : 0 : for ( QgsLayoutItem *item : items )
383 : : {
384 : 0 : QRectF itemBBox = item->sceneBoundingRect();
385 : :
386 : 0 : double item_min = ( distribution == DistributeHSpace ? itemBBox.left() :
387 : 0 : itemBBox.top() );
388 : 0 : double item_max = ( distribution == DistributeHSpace ? itemBBox.right() :
389 : 0 : itemBBox.bottom() );
390 : :
391 : 0 : minCoord = std::min( minCoord, item_min );
392 : 0 : maxCoord = std::max( maxCoord, item_max );
393 : 0 : length += ( item_max - item_min );
394 : 0 : itemCoords.insert( item_min, item );
395 : : }
396 : 0 : const double step = ( maxCoord - minCoord - length ) / ( items.size() - 1 );
397 : :
398 : 0 : double currentVal = minCoord;
399 : 0 : layout->undoStack()->beginMacro( undoText( distribution ) );
400 : 0 : for ( auto itemIt = itemCoords.constBegin(); itemIt != itemCoords.constEnd(); ++itemIt )
401 : : {
402 : 0 : QgsLayoutItem *item = itemIt.value();
403 : 0 : QPointF shifted = item->pos();
404 : :
405 : 0 : layout->undoStack()->beginCommand( itemIt.value(), QString() );
406 : :
407 : 0 : if ( distribution == DistributeHSpace )
408 : : {
409 : 0 : shifted.setX( currentVal );
410 : 0 : }
411 : : else
412 : : {
413 : 0 : shifted.setY( currentVal );
414 : : }
415 : :
416 : 0 : QgsLayoutPoint newPos = layout->convertFromLayoutUnits( shifted, item->positionWithUnits().units() );
417 : 0 : item->attemptMove( newPos, false );
418 : :
419 : 0 : layout->undoStack()->endCommand();
420 : :
421 : 0 : currentVal += ( distribution == DistributeHSpace ? item->rect().width() :
422 : 0 : item->rect().height() ) + step;
423 : 0 : }
424 : 0 : layout->undoStack()->endMacro();
425 : : return;
426 : 0 : }
|