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