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