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