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