Branch data Line data Source code
1 : : /***************************************************************************
2 : : qgslayoutsnapper.cpp
3 : : --------------------
4 : : begin : July 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 "qgslayoutsnapper.h"
18 : : #include "qgslayout.h"
19 : : #include "qgsreadwritecontext.h"
20 : : #include "qgsproject.h"
21 : : #include "qgslayoutpagecollection.h"
22 : : #include "qgssettings.h"
23 : :
24 : 0 : QgsLayoutSnapper::QgsLayoutSnapper( QgsLayout *layout )
25 : 0 : : mLayout( layout )
26 : 0 : {
27 : 0 : QgsSettings s;
28 : 0 : mTolerance = s.value( QStringLiteral( "LayoutDesigner/defaultSnapTolerancePixels" ), 5, QgsSettings::Gui ).toInt();
29 : 0 : }
30 : :
31 : 0 : QgsLayout *QgsLayoutSnapper::layout()
32 : : {
33 : 0 : return mLayout;
34 : : }
35 : :
36 : 0 : void QgsLayoutSnapper::setSnapTolerance( const int snapTolerance )
37 : : {
38 : 0 : mTolerance = snapTolerance;
39 : 0 : }
40 : :
41 : 0 : void QgsLayoutSnapper::setSnapToGrid( bool enabled )
42 : : {
43 : 0 : mSnapToGrid = enabled;
44 : 0 : }
45 : :
46 : 0 : void QgsLayoutSnapper::setSnapToGuides( bool enabled )
47 : : {
48 : 0 : mSnapToGuides = enabled;
49 : 0 : }
50 : :
51 : 0 : void QgsLayoutSnapper::setSnapToItems( bool enabled )
52 : : {
53 : 0 : mSnapToItems = enabled;
54 : 0 : }
55 : :
56 : 0 : QPointF QgsLayoutSnapper::snapPoint( QPointF point, double scaleFactor, bool &snapped, QGraphicsLineItem *horizontalSnapLine, QGraphicsLineItem *verticalSnapLine,
57 : : const QList< QgsLayoutItem * > *ignoreItems ) const
58 : : {
59 : 0 : snapped = false;
60 : :
61 : : // highest priority - guides
62 : 0 : bool snappedXToGuides = false;
63 : 0 : double newX = snapPointToGuides( point.x(), Qt::Vertical, scaleFactor, snappedXToGuides );
64 : 0 : if ( snappedXToGuides )
65 : : {
66 : 0 : snapped = true;
67 : 0 : point.setX( newX );
68 : 0 : if ( verticalSnapLine )
69 : 0 : verticalSnapLine->setVisible( false );
70 : 0 : }
71 : 0 : bool snappedYToGuides = false;
72 : 0 : double newY = snapPointToGuides( point.y(), Qt::Horizontal, scaleFactor, snappedYToGuides );
73 : 0 : if ( snappedYToGuides )
74 : : {
75 : 0 : snapped = true;
76 : 0 : point.setY( newY );
77 : 0 : if ( horizontalSnapLine )
78 : 0 : horizontalSnapLine->setVisible( false );
79 : 0 : }
80 : :
81 : 0 : bool snappedXToItems = false;
82 : 0 : bool snappedYToItems = false;
83 : 0 : if ( !snappedXToGuides )
84 : : {
85 : 0 : newX = snapPointToItems( point.x(), Qt::Horizontal, scaleFactor, ignoreItems ? *ignoreItems : QList< QgsLayoutItem * >(), snappedXToItems, verticalSnapLine );
86 : 0 : if ( snappedXToItems )
87 : : {
88 : 0 : snapped = true;
89 : 0 : point.setX( newX );
90 : 0 : }
91 : 0 : }
92 : 0 : if ( !snappedYToGuides )
93 : : {
94 : 0 : newY = snapPointToItems( point.y(), Qt::Vertical, scaleFactor, ignoreItems ? *ignoreItems : QList< QgsLayoutItem * >(), snappedYToItems, horizontalSnapLine );
95 : 0 : if ( snappedYToItems )
96 : : {
97 : 0 : snapped = true;
98 : 0 : point.setY( newY );
99 : 0 : }
100 : 0 : }
101 : :
102 : 0 : bool snappedXToGrid = false;
103 : 0 : bool snappedYToGrid = false;
104 : 0 : QPointF res = snapPointToGrid( point, scaleFactor, snappedXToGrid, snappedYToGrid );
105 : 0 : if ( snappedXToGrid && !snappedXToGuides && !snappedXToItems )
106 : : {
107 : 0 : snapped = true;
108 : 0 : point.setX( res.x() );
109 : 0 : }
110 : 0 : if ( snappedYToGrid && !snappedYToGuides && !snappedYToItems )
111 : : {
112 : 0 : snapped = true;
113 : 0 : point.setY( res.y() );
114 : 0 : }
115 : :
116 : 0 : return point;
117 : 0 : }
118 : :
119 : 0 : QRectF QgsLayoutSnapper::snapRect( const QRectF &rect, double scaleFactor, bool &snapped, QGraphicsLineItem *horizontalSnapLine, QGraphicsLineItem *verticalSnapLine, const QList<QgsLayoutItem *> *ignoreItems ) const
120 : : {
121 : 0 : snapped = false;
122 : 0 : QRectF snappedRect = rect;
123 : :
124 : 0 : QList< double > xCoords;
125 : 0 : xCoords << rect.left() << rect.center().x() << rect.right();
126 : 0 : QList< double > yCoords;
127 : 0 : yCoords << rect.top() << rect.center().y() << rect.bottom();
128 : :
129 : : // highest priority - guides
130 : 0 : bool snappedXToGuides = false;
131 : 0 : double deltaX = snapPointsToGuides( xCoords, Qt::Vertical, scaleFactor, snappedXToGuides );
132 : 0 : if ( snappedXToGuides )
133 : : {
134 : 0 : snapped = true;
135 : 0 : snappedRect.translate( deltaX, 0 );
136 : 0 : if ( verticalSnapLine )
137 : 0 : verticalSnapLine->setVisible( false );
138 : 0 : }
139 : 0 : bool snappedYToGuides = false;
140 : 0 : double deltaY = snapPointsToGuides( yCoords, Qt::Horizontal, scaleFactor, snappedYToGuides );
141 : 0 : if ( snappedYToGuides )
142 : : {
143 : 0 : snapped = true;
144 : 0 : snappedRect.translate( 0, deltaY );
145 : 0 : if ( horizontalSnapLine )
146 : 0 : horizontalSnapLine->setVisible( false );
147 : 0 : }
148 : :
149 : 0 : bool snappedXToItems = false;
150 : 0 : bool snappedYToItems = false;
151 : 0 : if ( !snappedXToGuides )
152 : : {
153 : 0 : deltaX = snapPointsToItems( xCoords, Qt::Horizontal, scaleFactor, ignoreItems ? *ignoreItems : QList< QgsLayoutItem * >(), snappedXToItems, verticalSnapLine );
154 : 0 : if ( snappedXToItems )
155 : : {
156 : 0 : snapped = true;
157 : 0 : snappedRect.translate( deltaX, 0 );
158 : 0 : }
159 : 0 : }
160 : 0 : if ( !snappedYToGuides )
161 : : {
162 : 0 : deltaY = snapPointsToItems( yCoords, Qt::Vertical, scaleFactor, ignoreItems ? *ignoreItems : QList< QgsLayoutItem * >(), snappedYToItems, horizontalSnapLine );
163 : 0 : if ( snappedYToItems )
164 : : {
165 : 0 : snapped = true;
166 : 0 : snappedRect.translate( 0, deltaY );
167 : 0 : }
168 : 0 : }
169 : :
170 : 0 : bool snappedXToGrid = false;
171 : 0 : bool snappedYToGrid = false;
172 : 0 : QList< QPointF > points;
173 : 0 : points << rect.topLeft() << rect.topRight() << rect.bottomLeft() << rect.bottomRight();
174 : 0 : QPointF res = snapPointsToGrid( points, scaleFactor, snappedXToGrid, snappedYToGrid );
175 : 0 : if ( snappedXToGrid && !snappedXToGuides && !snappedXToItems )
176 : : {
177 : 0 : snapped = true;
178 : 0 : snappedRect.translate( res.x(), 0 );
179 : 0 : }
180 : 0 : if ( snappedYToGrid && !snappedYToGuides && !snappedYToItems )
181 : : {
182 : 0 : snapped = true;
183 : 0 : snappedRect.translate( 0, res.y() );
184 : 0 : }
185 : :
186 : : return snappedRect;
187 : 0 : }
188 : :
189 : 0 : QPointF QgsLayoutSnapper::snapPointToGrid( QPointF point, double scaleFactor, bool &snappedX, bool &snappedY ) const
190 : : {
191 : 0 : QPointF delta = snapPointsToGrid( QList< QPointF >() << point, scaleFactor, snappedX, snappedY );
192 : 0 : return point + delta;
193 : 0 : }
194 : :
195 : 0 : QPointF QgsLayoutSnapper::snapPointsToGrid( const QList<QPointF> &points, double scaleFactor, bool &snappedX, bool &snappedY ) const
196 : : {
197 : 0 : snappedX = false;
198 : 0 : snappedY = false;
199 : 0 : if ( !mLayout || !mSnapToGrid )
200 : : {
201 : 0 : return QPointF( 0, 0 );
202 : : }
203 : 0 : const QgsLayoutGridSettings &grid = mLayout->gridSettings();
204 : 0 : if ( grid.resolution().length() <= 0 )
205 : 0 : return QPointF( 0, 0 );
206 : :
207 : 0 : double deltaX = 0;
208 : 0 : double deltaY = 0;
209 : 0 : double smallestDiffX = std::numeric_limits<double>::max();
210 : 0 : double smallestDiffY = std::numeric_limits<double>::max();
211 : 0 : for ( QPointF point : points )
212 : : {
213 : : //calculate y offset to current page
214 : 0 : QPointF pagePoint = mLayout->pageCollection()->positionOnPage( point );
215 : :
216 : 0 : double yPage = pagePoint.y(); //y-coordinate relative to current page
217 : 0 : double yAtTopOfPage = mLayout->pageCollection()->page( mLayout->pageCollection()->pageNumberForPoint( point ) )->pos().y();
218 : :
219 : : //snap x coordinate
220 : 0 : double gridRes = mLayout->convertToLayoutUnits( grid.resolution() );
221 : 0 : QPointF gridOffset = mLayout->convertToLayoutUnits( grid.offset() );
222 : 0 : int xRatio = static_cast< int >( ( point.x() - gridOffset.x() ) / gridRes + 0.5 ); //NOLINT
223 : 0 : int yRatio = static_cast< int >( ( yPage - gridOffset.y() ) / gridRes + 0.5 ); //NOLINT
224 : :
225 : 0 : double xSnapped = xRatio * gridRes + gridOffset.x();
226 : 0 : double ySnapped = yRatio * gridRes + gridOffset.y() + yAtTopOfPage;
227 : :
228 : 0 : double currentDiffX = std::fabs( xSnapped - point.x() );
229 : 0 : if ( currentDiffX < smallestDiffX )
230 : : {
231 : 0 : smallestDiffX = currentDiffX;
232 : 0 : deltaX = xSnapped - point.x();
233 : 0 : }
234 : :
235 : 0 : double currentDiffY = std::fabs( ySnapped - point.y() );
236 : 0 : if ( currentDiffY < smallestDiffY )
237 : : {
238 : 0 : smallestDiffY = currentDiffY;
239 : 0 : deltaY = ySnapped - point.y();
240 : 0 : }
241 : : }
242 : :
243 : : //convert snap tolerance from pixels to layout units
244 : 0 : double alignThreshold = mTolerance / scaleFactor;
245 : :
246 : 0 : QPointF delta( 0, 0 );
247 : 0 : if ( smallestDiffX <= alignThreshold )
248 : : {
249 : : //snap distance is inside of tolerance
250 : 0 : snappedX = true;
251 : 0 : delta.setX( deltaX );
252 : 0 : }
253 : 0 : if ( smallestDiffY <= alignThreshold )
254 : : {
255 : : //snap distance is inside of tolerance
256 : 0 : snappedY = true;
257 : 0 : delta.setY( deltaY );
258 : 0 : }
259 : :
260 : 0 : return delta;
261 : 0 : }
262 : :
263 : 0 : double QgsLayoutSnapper::snapPointToGuides( double original, Qt::Orientation orientation, double scaleFactor, bool &snapped ) const
264 : : {
265 : 0 : double delta = snapPointsToGuides( QList< double >() << original, orientation, scaleFactor, snapped );
266 : 0 : return original + delta;
267 : 0 : }
268 : 0 :
269 : 0 : double QgsLayoutSnapper::snapPointsToGuides( const QList<double> &points, Qt::Orientation orientation, double scaleFactor, bool &snapped ) const
270 : 0 : {
271 : 0 : snapped = false;
272 : 0 : if ( !mLayout || !mSnapToGuides )
273 : : {
274 : 0 : return 0;
275 : : }
276 : :
277 : : //convert snap tolerance from pixels to layout units
278 : 0 : double alignThreshold = mTolerance / scaleFactor;
279 : :
280 : 0 : double bestDelta = 0;
281 : 0 : double smallestDiff = std::numeric_limits<double>::max();
282 : :
283 : 0 : for ( double p : points )
284 : : {
285 : 0 : const auto constGuides = mLayout->guides().guides( orientation );
286 : 0 : for ( QgsLayoutGuide *guide : constGuides )
287 : : {
288 : 0 : double guidePos = guide->layoutPosition();
289 : 0 : double diff = std::fabs( p - guidePos );
290 : 0 : if ( diff < smallestDiff )
291 : : {
292 : 0 : smallestDiff = diff;
293 : 0 : bestDelta = guidePos - p;
294 : 0 : }
295 : : }
296 : 0 : }
297 : :
298 : 0 : if ( smallestDiff <= alignThreshold )
299 : : {
300 : 0 : snapped = true;
301 : 0 : return bestDelta;
302 : : }
303 : : else
304 : : {
305 : 0 : return 0;
306 : : }
307 : 0 : }
308 : :
309 : 0 : double QgsLayoutSnapper::snapPointToItems( double original, Qt::Orientation orientation, double scaleFactor, const QList<QgsLayoutItem *> &ignoreItems, bool &snapped,
310 : : QGraphicsLineItem *snapLine ) const
311 : : {
312 : 0 : double delta = snapPointsToItems( QList< double >() << original, orientation, scaleFactor, ignoreItems, snapped, snapLine );
313 : 0 : return original + delta;
314 : 0 : }
315 : :
316 : 0 : double QgsLayoutSnapper::snapPointsToItems( const QList<double> &points, Qt::Orientation orientation, double scaleFactor, const QList<QgsLayoutItem *> &ignoreItems, bool &snapped, QGraphicsLineItem *snapLine ) const
317 : : {
318 : 0 : snapped = false;
319 : 0 : if ( !mLayout || !mSnapToItems )
320 : : {
321 : 0 : if ( snapLine )
322 : 0 : snapLine->setVisible( false );
323 : 0 : return 0;
324 : : }
325 : :
326 : 0 : double alignThreshold = mTolerance / scaleFactor;
327 : :
328 : 0 : double bestDelta = 0;
329 : 0 : double smallestDiff = std::numeric_limits<double>::max();
330 : 0 : double closest = 0;
331 : 0 : const QList<QGraphicsItem *> itemList = mLayout->items();
332 : 0 : QList< double > currentCoords;
333 : 0 : for ( QGraphicsItem *item : itemList )
334 : : {
335 : 0 : QgsLayoutItem *currentItem = dynamic_cast< QgsLayoutItem *>( item );
336 : 0 : if ( !currentItem || ignoreItems.contains( currentItem ) )
337 : 0 : continue;
338 : 0 : if ( currentItem->type() == QgsLayoutItemRegistry::LayoutGroup )
339 : 0 : continue; // don't snap to group bounds, instead we snap to group item bounds
340 : 0 : if ( !currentItem->isVisible() )
341 : 0 : continue; // don't snap to invisible items
342 : :
343 : 0 : QRectF itemRect;
344 : 0 : if ( dynamic_cast<const QgsLayoutItemPage *>( currentItem ) )
345 : : {
346 : : //if snapping to paper use the paper item's rect rather then the bounding rect,
347 : : //since we want to snap to the page edge and not any outlines drawn around the page
348 : 0 : itemRect = currentItem->mapRectToScene( currentItem->rect() );
349 : 0 : }
350 : : else
351 : : {
352 : 0 : itemRect = currentItem->mapRectToScene( currentItem->rectWithFrame() );
353 : : }
354 : :
355 : 0 : currentCoords.clear();
356 : 0 : switch ( orientation )
357 : : {
358 : : case Qt::Horizontal:
359 : : {
360 : 0 : currentCoords << itemRect.left();
361 : 0 : currentCoords << itemRect.right();
362 : 0 : currentCoords << itemRect.center().x();
363 : 0 : break;
364 : : }
365 : :
366 : : case Qt::Vertical:
367 : : {
368 : 0 : currentCoords << itemRect.top();
369 : 0 : currentCoords << itemRect.center().y();
370 : 0 : currentCoords << itemRect.bottom();
371 : 0 : break;
372 : : }
373 : : }
374 : :
375 : 0 : for ( double val : std::as_const( currentCoords ) )
376 : : {
377 : 0 : for ( double p : points )
378 : : {
379 : 0 : double dist = std::fabs( p - val );
380 : 0 : if ( dist <= alignThreshold && dist < smallestDiff )
381 : : {
382 : 0 : snapped = true;
383 : 0 : smallestDiff = dist;
384 : 0 : bestDelta = val - p;
385 : 0 : closest = val;
386 : 0 : }
387 : : }
388 : : }
389 : : }
390 : :
391 : 0 : if ( snapLine )
392 : : {
393 : 0 : if ( snapped )
394 : : {
395 : 0 : snapLine->setVisible( true );
396 : 0 : switch ( orientation )
397 : : {
398 : : case Qt::Vertical:
399 : : {
400 : 0 : snapLine->setLine( QLineF( -100000, closest, 100000, closest ) );
401 : 0 : break;
402 : : }
403 : :
404 : : case Qt::Horizontal:
405 : : {
406 : 0 : snapLine->setLine( QLineF( closest, -100000, closest, 100000 ) );
407 : 0 : break;
408 : : }
409 : : }
410 : 0 : }
411 : : else
412 : : {
413 : 0 : snapLine->setVisible( false );
414 : : }
415 : 0 : }
416 : :
417 : 0 : return bestDelta;
418 : 0 : }
419 : :
420 : :
421 : 0 : bool QgsLayoutSnapper::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext & ) const
422 : : {
423 : 0 : QDomElement element = document.createElement( QStringLiteral( "Snapper" ) );
424 : :
425 : 0 : element.setAttribute( QStringLiteral( "tolerance" ), mTolerance );
426 : 0 : element.setAttribute( QStringLiteral( "snapToGrid" ), mSnapToGrid );
427 : 0 : element.setAttribute( QStringLiteral( "snapToGuides" ), mSnapToGuides );
428 : 0 : element.setAttribute( QStringLiteral( "snapToItems" ), mSnapToItems );
429 : :
430 : 0 : parentElement.appendChild( element );
431 : : return true;
432 : 0 : }
433 : :
434 : 0 : bool QgsLayoutSnapper::readXml( const QDomElement &e, const QDomDocument &, const QgsReadWriteContext & )
435 : : {
436 : 0 : QDomElement element = e;
437 : 0 : if ( element.nodeName() != QLatin1String( "Snapper" ) )
438 : : {
439 : 0 : element = element.firstChildElement( QStringLiteral( "Snapper" ) );
440 : 0 : }
441 : :
442 : 0 : if ( element.nodeName() != QLatin1String( "Snapper" ) )
443 : : {
444 : 0 : return false;
445 : : }
446 : :
447 : 0 : mTolerance = element.attribute( QStringLiteral( "tolerance" ), QStringLiteral( "5" ) ).toInt();
448 : 0 : mSnapToGrid = element.attribute( QStringLiteral( "snapToGrid" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
449 : 0 : mSnapToGuides = element.attribute( QStringLiteral( "snapToGuides" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
450 : 0 : mSnapToItems = element.attribute( QStringLiteral( "snapToItems" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
451 : 0 : return true;
452 : 0 : }
|