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