Skip to content

Commit d65a18c

Browse files
committedOct 6, 2017
Fix item snapping while resizing
1 parent 6289367 commit d65a18c

File tree

5 files changed

+603
-80
lines changed

5 files changed

+603
-80
lines changed
 

‎python/core/layout/qgslayoutsnapper.sip

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,35 @@ class QgsLayoutSnapper: QgsLayoutSerializableObject
9898
will automatically display and position these lines to indicate snapping positions to item bounds.
9999

100100
A list of items to ignore during the snapping can be specified via the ``ignoreItems`` list.
101+
102+
.. seealso:: snapRect()
101103
:rtype: QPointF
102104
%End
103105

106+
QRectF snapRect( const QRectF &rect, double scaleFactor, bool &snapped /Out/, QGraphicsLineItem *horizontalSnapLine = 0,
107+
QGraphicsLineItem *verticalSnapLine = 0,
108+
const QList< QgsLayoutItem * > *ignoreItems = 0 ) const;
109+
%Docstring
110+
Snaps a layout coordinate ``rect``. If ``rect`` was snapped, ``snapped`` will be set to true.
111+
112+
Snapping occurs by moving the rectangle alone. The rectangle will not be resized
113+
as a result of the snap operation.
114+
115+
The ``scaleFactor`` argument should be set to the transformation from
116+
scalar transform from layout coordinates to pixels, i.e. the
117+
graphics view transform().m11() value.
118+
119+
This method considers snapping to the grid, snap lines, etc.
120+
121+
If the ``horizontalSnapLine`` and ``verticalSnapLine`` arguments are specified, then the snapper
122+
will automatically display and position these lines to indicate snapping positions to item bounds.
123+
124+
A list of items to ignore during the snapping can be specified via the ``ignoreItems`` list.
125+
126+
.. seealso:: snapPoint()
127+
:rtype: QRectF
128+
%End
129+
104130
QPointF snapPointToGrid( QPointF point, double scaleFactor, bool &snappedX /Out/, bool &snappedY /Out/ ) const;
105131
%Docstring
106132
Snaps a layout coordinate ``point`` to the grid. If ``point``
@@ -113,6 +139,26 @@ class QgsLayoutSnapper: QgsLayoutSerializableObject
113139

114140
If snapToGrid() is disabled, this method will return the point
115141
unchanged.
142+
143+
.. seealso:: snapPointsToGrid()
144+
:rtype: QPointF
145+
%End
146+
147+
QPointF snapPointsToGrid( const QList< QPointF > &points, double scaleFactor, bool &snappedX /Out/, bool &snappedY /Out/ ) const;
148+
%Docstring
149+
Snaps a set of ``points`` to the grid. If the points
150+
were snapped, ``snapped`` will be set to true.
151+
152+
The ``scaleFactor`` argument should be set to the transformation from
153+
scalar transform from layout coordinates to pixels, i.e. the
154+
graphics view transform().m11() value.
155+
156+
If snapToGrid() is disabled, this method will not attempt to snap the points.
157+
158+
The returned value is the smallest delta which the points need to be shifted by in order to align
159+
one of the points to the grid.
160+
161+
.. seealso:: snapPointToGrid()
116162
:rtype: QPointF
117163
%End
118164

@@ -127,6 +173,26 @@ class QgsLayoutSnapper: QgsLayoutSerializableObject
127173

128174
If snapToGuides() is disabled, this method will return the point
129175
unchanged.
176+
177+
.. seealso:: snapPointsToGuides()
178+
:rtype: float
179+
%End
180+
181+
double snapPointsToGuides( const QList< double > &points, QgsLayoutGuide::Orientation orientation, double scaleFactor, bool &snapped /Out/ ) const;
182+
%Docstring
183+
Snaps a set of ``points`` to the guides. If the points
184+
were snapped, ``snapped`` will be set to true.
185+
186+
The ``scaleFactor`` argument should be set to the transformation from
187+
scalar transform from layout coordinates to pixels, i.e. the
188+
graphics view transform().m11() value.
189+
190+
If snapToGuides() is disabled, this method will not attempt to snap the points.
191+
192+
The returned value is the smallest delta which the points need to be shifted by in order to align
193+
one of the points to a guide.
194+
195+
.. seealso:: snapPointToGuides()
130196
:rtype: float
131197
%End
132198

@@ -147,6 +213,27 @@ class QgsLayoutSnapper: QgsLayoutSerializableObject
147213

148214
If ``snapLine`` is specified, the snapper will automatically show (or hide) the snap line
149215
based on the result of the snap, and position it at the correct location for the snap.
216+
217+
.. seealso:: snapPointsToItems()
218+
:rtype: float
219+
%End
220+
221+
double snapPointsToItems( const QList< double > &points, Qt::Orientation orientation, double scaleFactor, const QList< QgsLayoutItem * > &ignoreItems, bool &snapped /Out/,
222+
QGraphicsLineItem *snapLine = 0 ) const;
223+
%Docstring
224+
Snaps a set of ``points`` to the item bounds. If the points
225+
were snapped, ``snapped`` will be set to true.
226+
227+
The ``scaleFactor`` argument should be set to the transformation from
228+
scalar transform from layout coordinates to pixels, i.e. the
229+
graphics view transform().m11() value.
230+
231+
If snapToItems() is disabled, this method will not attempt to snap the points.
232+
233+
The returned value is the smallest delta which the points need to be shifted by in order to align
234+
one of the points to an item bound.
235+
236+
.. seealso:: snapPointToItems()
150237
:rtype: float
151238
%End
152239

‎src/core/layout/qgslayoutsnapper.cpp

Lines changed: 158 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -112,109 +112,217 @@ QPointF QgsLayoutSnapper::snapPoint( QPointF point, double scaleFactor, bool &sn
112112
return point;
113113
}
114114

115+
QRectF QgsLayoutSnapper::snapRect( const QRectF &rect, double scaleFactor, bool &snapped, QGraphicsLineItem *horizontalSnapLine, QGraphicsLineItem *verticalSnapLine, const QList<QgsLayoutItem *> *ignoreItems ) const
116+
{
117+
snapped = false;
118+
QRectF snappedRect = rect;
119+
120+
QList< double > xCoords;
121+
xCoords << rect.left() << rect.center().x() << rect.right();
122+
QList< double > yCoords;
123+
yCoords << rect.top() << rect.center().y() << rect.bottom();
124+
125+
// highest priority - guides
126+
bool snappedXToGuides = false;
127+
double deltaX = snapPointsToGuides( xCoords, QgsLayoutGuide::Vertical, scaleFactor, snappedXToGuides );
128+
if ( snappedXToGuides )
129+
{
130+
snapped = true;
131+
snappedRect.translate( deltaX, 0 );
132+
if ( verticalSnapLine )
133+
verticalSnapLine->setVisible( false );
134+
}
135+
bool snappedYToGuides = false;
136+
double deltaY = snapPointsToGuides( yCoords, QgsLayoutGuide::Horizontal, scaleFactor, snappedYToGuides );
137+
if ( snappedYToGuides )
138+
{
139+
snapped = true;
140+
snappedRect.translate( 0, deltaY );
141+
if ( horizontalSnapLine )
142+
horizontalSnapLine->setVisible( false );
143+
}
144+
145+
bool snappedXToItems = false;
146+
bool snappedYToItems = false;
147+
if ( !snappedXToGuides )
148+
{
149+
deltaX = snapPointsToItems( xCoords, Qt::Horizontal, scaleFactor, ignoreItems ? *ignoreItems : QList< QgsLayoutItem * >(), snappedXToItems, verticalSnapLine );
150+
if ( snappedXToItems )
151+
{
152+
snapped = true;
153+
snappedRect.translate( deltaX, 0 );
154+
}
155+
}
156+
if ( !snappedYToGuides )
157+
{
158+
deltaY = snapPointsToItems( yCoords, Qt::Vertical, scaleFactor, ignoreItems ? *ignoreItems : QList< QgsLayoutItem * >(), snappedYToItems, horizontalSnapLine );
159+
if ( snappedYToItems )
160+
{
161+
snapped = true;
162+
snappedRect.translate( 0, deltaY );
163+
}
164+
}
165+
166+
bool snappedXToGrid = false;
167+
bool snappedYToGrid = false;
168+
QList< QPointF > points;
169+
points << rect.topLeft() << rect.topRight() << rect.bottomLeft() << rect.bottomRight();
170+
QPointF res = snapPointsToGrid( points, scaleFactor, snappedXToGrid, snappedYToGrid );
171+
if ( snappedXToGrid && !snappedXToGuides && !snappedXToItems )
172+
{
173+
snapped = true;
174+
snappedRect.translate( res.x(), 0 );
175+
}
176+
if ( snappedYToGrid && !snappedYToGuides && !snappedYToItems )
177+
{
178+
snapped = true;
179+
snappedRect.translate( 0, res.y() );
180+
}
181+
182+
return snappedRect;
183+
}
184+
115185
QPointF QgsLayoutSnapper::snapPointToGrid( QPointF point, double scaleFactor, bool &snappedX, bool &snappedY ) const
186+
{
187+
QPointF delta = snapPointsToGrid( QList< QPointF >() << point, scaleFactor, snappedX, snappedY );
188+
return point + delta;
189+
}
190+
191+
QPointF QgsLayoutSnapper::snapPointsToGrid( const QList<QPointF> &points, double scaleFactor, bool &snappedX, bool &snappedY ) const
116192
{
117193
snappedX = false;
118194
snappedY = false;
119195
if ( !mLayout || !mSnapToGrid )
120196
{
121-
return point;
197+
return QPointF( 0, 0 );
122198
}
123199
const QgsLayoutGridSettings &grid = mLayout->gridSettings();
124200
if ( grid.resolution().length() <= 0 )
125-
return point;
201+
return QPointF( 0, 0 );
202+
203+
double deltaX = 0;
204+
double deltaY = 0;
205+
double smallestDiffX = DBL_MAX;
206+
double smallestDiffY = DBL_MAX;
207+
for ( QPointF point : points )
208+
{
209+
//calculate y offset to current page
210+
QPointF pagePoint = mLayout->pageCollection()->positionOnPage( point );
126211

127-
//calculate y offset to current page
128-
QPointF pagePoint = mLayout->pageCollection()->positionOnPage( point );
212+
double yPage = pagePoint.y(); //y-coordinate relative to current page
213+
double yAtTopOfPage = mLayout->pageCollection()->page( mLayout->pageCollection()->pageNumberForPoint( point ) )->pos().y();
129214

130-
double yPage = pagePoint.y(); //y-coordinate relative to current page
131-
double yAtTopOfPage = mLayout->pageCollection()->page( mLayout->pageCollection()->pageNumberForPoint( point ) )->pos().y();
215+
//snap x coordinate
216+
double gridRes = mLayout->convertToLayoutUnits( grid.resolution() );
217+
QPointF gridOffset = mLayout->convertToLayoutUnits( grid.offset() );
218+
int xRatio = static_cast< int >( ( point.x() - gridOffset.x() ) / gridRes + 0.5 ); //NOLINT
219+
int yRatio = static_cast< int >( ( yPage - gridOffset.y() ) / gridRes + 0.5 ); //NOLINT
132220

133-
//snap x coordinate
134-
double gridRes = mLayout->convertToLayoutUnits( grid.resolution() );
135-
QPointF gridOffset = mLayout->convertToLayoutUnits( grid.offset() );
136-
int xRatio = static_cast< int >( ( point.x() - gridOffset.x() ) / gridRes + 0.5 ); //NOLINT
137-
int yRatio = static_cast< int >( ( yPage - gridOffset.y() ) / gridRes + 0.5 ); //NOLINT
221+
double xSnapped = xRatio * gridRes + gridOffset.x();
222+
double ySnapped = yRatio * gridRes + gridOffset.y() + yAtTopOfPage;
138223

139-
double xSnapped = xRatio * gridRes + gridOffset.x();
140-
double ySnapped = yRatio * gridRes + gridOffset.y() + yAtTopOfPage;
224+
double currentDiffX = std::fabs( xSnapped - point.x() );
225+
if ( currentDiffX < smallestDiffX )
226+
{
227+
smallestDiffX = currentDiffX;
228+
deltaX = xSnapped - point.x();
229+
}
230+
231+
double currentDiffY = std::fabs( ySnapped - point.y() );
232+
if ( currentDiffY < smallestDiffY )
233+
{
234+
smallestDiffY = currentDiffY;
235+
deltaY = ySnapped - point.y();
236+
}
237+
}
141238

142239
//convert snap tolerance from pixels to layout units
143240
double alignThreshold = mTolerance / scaleFactor;
144241

145-
if ( std::fabs( xSnapped - point.x() ) > alignThreshold )
146-
{
147-
//snap distance is outside of tolerance
148-
xSnapped = point.x();
149-
}
150-
else
242+
QPointF delta( 0, 0 );
243+
if ( smallestDiffX <= alignThreshold )
151244
{
245+
//snap distance is inside of tolerance
152246
snappedX = true;
247+
delta.setX( deltaX );
153248
}
154-
if ( std::fabs( ySnapped - point.y() ) > alignThreshold )
155-
{
156-
//snap distance is outside of tolerance
157-
ySnapped = point.y();
158-
}
159-
else
249+
if ( smallestDiffY <= alignThreshold )
160250
{
251+
//snap distance is inside of tolerance
161252
snappedY = true;
253+
delta.setY( deltaY );
162254
}
163255

164-
return QPointF( xSnapped, ySnapped );
256+
return delta;
165257
}
166258

167259
double QgsLayoutSnapper::snapPointToGuides( double original, QgsLayoutGuide::Orientation orientation, double scaleFactor, bool &snapped ) const
260+
{
261+
double delta = snapPointsToGuides( QList< double >() << original, orientation, scaleFactor, snapped );
262+
return original + delta;
263+
}
264+
265+
double QgsLayoutSnapper::snapPointsToGuides( const QList<double> &points, QgsLayoutGuide::Orientation orientation, double scaleFactor, bool &snapped ) const
168266
{
169267
snapped = false;
170268
if ( !mLayout || !mSnapToGuides )
171269
{
172-
return original;
270+
return 0;
173271
}
174272

175273
//convert snap tolerance from pixels to layout units
176274
double alignThreshold = mTolerance / scaleFactor;
177275

178-
double bestPos = original;
276+
double bestDelta = 0;
179277
double smallestDiff = DBL_MAX;
180278

181-
Q_FOREACH ( QgsLayoutGuide *guide, mLayout->guides().guides( orientation ) )
279+
for ( double p : points )
182280
{
183-
double guidePos = guide->layoutPosition();
184-
double diff = std::fabs( original - guidePos );
185-
if ( diff < smallestDiff )
281+
Q_FOREACH ( QgsLayoutGuide *guide, mLayout->guides().guides( orientation ) )
186282
{
187-
smallestDiff = diff;
188-
bestPos = guidePos;
283+
double guidePos = guide->layoutPosition();
284+
double diff = std::fabs( p - guidePos );
285+
if ( diff < smallestDiff )
286+
{
287+
smallestDiff = diff;
288+
bestDelta = guidePos - p;
289+
}
189290
}
190291
}
191292

192293
if ( smallestDiff <= alignThreshold )
193294
{
194295
snapped = true;
195-
return bestPos;
296+
return bestDelta;
196297
}
197298
else
198299
{
199-
return original;
300+
return 0;
200301
}
201302
}
202303

203304
double QgsLayoutSnapper::snapPointToItems( double original, Qt::Orientation orientation, double scaleFactor, const QList<QgsLayoutItem *> &ignoreItems, bool &snapped,
204305
QGraphicsLineItem *snapLine ) const
306+
{
307+
double delta = snapPointsToItems( QList< double >() << original, orientation, scaleFactor, ignoreItems, snapped, snapLine );
308+
return original + delta;
309+
}
310+
311+
double QgsLayoutSnapper::snapPointsToItems( const QList<double> &points, Qt::Orientation orientation, double scaleFactor, const QList<QgsLayoutItem *> &ignoreItems, bool &snapped, QGraphicsLineItem *snapLine ) const
205312
{
206313
snapped = false;
207314
if ( !mLayout || !mSnapToItems )
208315
{
209316
if ( snapLine )
210317
snapLine->setVisible( false );
211-
return original;
318+
return 0;
212319
}
213320

214321
double alignThreshold = mTolerance / scaleFactor;
215322

216-
double closest = original;
217-
double closestDist = DBL_MAX;
323+
double bestDelta = 0;
324+
double smallestDiff = DBL_MAX;
325+
double closest = 0;
218326
const QList<QGraphicsItem *> itemList = mLayout->items();
219327
QList< double > currentCoords;
220328
for ( QGraphicsItem *item : itemList )
@@ -264,12 +372,16 @@ double QgsLayoutSnapper::snapPointToItems( double original, Qt::Orientation orie
264372

265373
for ( double val : qgsAsConst( currentCoords ) )
266374
{
267-
double dist = std::fabs( original - val );
268-
if ( dist <= alignThreshold && dist < closestDist )
375+
for ( double p : points )
269376
{
270-
snapped = true;
271-
closestDist = dist;
272-
closest = val;
377+
double dist = std::fabs( p - val );
378+
if ( dist <= alignThreshold && dist < smallestDiff )
379+
{
380+
snapped = true;
381+
smallestDiff = dist;
382+
bestDelta = val - p;
383+
closest = val;
384+
}
273385
}
274386
}
275387
}
@@ -300,9 +412,10 @@ double QgsLayoutSnapper::snapPointToItems( double original, Qt::Orientation orie
300412
}
301413
}
302414

303-
return closest;
415+
return bestDelta;
304416
}
305417

418+
306419
bool QgsLayoutSnapper::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext & ) const
307420
{
308421
QDomElement element = document.createElement( QStringLiteral( "Snapper" ) );

‎src/core/layout/qgslayoutsnapper.h

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,36 @@ class CORE_EXPORT QgsLayoutSnapper: public QgsLayoutSerializableObject
107107
* will automatically display and position these lines to indicate snapping positions to item bounds.
108108
*
109109
* A list of items to ignore during the snapping can be specified via the \a ignoreItems list.
110+
111+
* \see snapRect()
110112
*/
111113
QPointF snapPoint( QPointF point, double scaleFactor, bool &snapped SIP_OUT, QGraphicsLineItem *horizontalSnapLine = nullptr,
112114
QGraphicsLineItem *verticalSnapLine = nullptr,
113115
const QList< QgsLayoutItem * > *ignoreItems = nullptr ) const;
114116

117+
/**
118+
* Snaps a layout coordinate \a rect. If \a rect was snapped, \a snapped will be set to true.
119+
*
120+
* Snapping occurs by moving the rectangle alone. The rectangle will not be resized
121+
* as a result of the snap operation.
122+
*
123+
* The \a scaleFactor argument should be set to the transformation from
124+
* scalar transform from layout coordinates to pixels, i.e. the
125+
* graphics view transform().m11() value.
126+
*
127+
* This method considers snapping to the grid, snap lines, etc.
128+
*
129+
* If the \a horizontalSnapLine and \a verticalSnapLine arguments are specified, then the snapper
130+
* will automatically display and position these lines to indicate snapping positions to item bounds.
131+
*
132+
* A list of items to ignore during the snapping can be specified via the \a ignoreItems list.
133+
*
134+
* \see snapPoint()
135+
*/
136+
QRectF snapRect( const QRectF &rect, double scaleFactor, bool &snapped SIP_OUT, QGraphicsLineItem *horizontalSnapLine = nullptr,
137+
QGraphicsLineItem *verticalSnapLine = nullptr,
138+
const QList< QgsLayoutItem * > *ignoreItems = nullptr ) const;
139+
115140
/**
116141
* Snaps a layout coordinate \a point to the grid. If \a point
117142
* was snapped horizontally, \a snappedX will be set to true. If \a point
@@ -123,9 +148,28 @@ class CORE_EXPORT QgsLayoutSnapper: public QgsLayoutSerializableObject
123148
*
124149
* If snapToGrid() is disabled, this method will return the point
125150
* unchanged.
151+
*
152+
* \see snapPointsToGrid()
126153
*/
127154
QPointF snapPointToGrid( QPointF point, double scaleFactor, bool &snappedX SIP_OUT, bool &snappedY SIP_OUT ) const;
128155

156+
/**
157+
* Snaps a set of \a points to the grid. If the points
158+
* were snapped, \a snapped will be set to true.
159+
*
160+
* The \a scaleFactor argument should be set to the transformation from
161+
* scalar transform from layout coordinates to pixels, i.e. the
162+
* graphics view transform().m11() value.
163+
*
164+
* If snapToGrid() is disabled, this method will not attempt to snap the points.
165+
*
166+
* The returned value is the smallest delta which the points need to be shifted by in order to align
167+
* one of the points to the grid.
168+
*
169+
* \see snapPointToGrid()
170+
*/
171+
QPointF snapPointsToGrid( const QList< QPointF > &points, double scaleFactor, bool &snappedX SIP_OUT, bool &snappedY SIP_OUT ) const;
172+
129173
/**
130174
* Snaps an \a original layout coordinate to the guides. If the point
131175
* was snapped, \a snapped will be set to true.
@@ -136,9 +180,28 @@ class CORE_EXPORT QgsLayoutSnapper: public QgsLayoutSerializableObject
136180
*
137181
* If snapToGuides() is disabled, this method will return the point
138182
* unchanged.
183+
*
184+
* \see snapPointsToGuides()
139185
*/
140186
double snapPointToGuides( double original, QgsLayoutGuide::Orientation orientation, double scaleFactor, bool &snapped SIP_OUT ) const;
141187

188+
/**
189+
* Snaps a set of \a points to the guides. If the points
190+
* were snapped, \a snapped will be set to true.
191+
*
192+
* The \a scaleFactor argument should be set to the transformation from
193+
* scalar transform from layout coordinates to pixels, i.e. the
194+
* graphics view transform().m11() value.
195+
*
196+
* If snapToGuides() is disabled, this method will not attempt to snap the points.
197+
*
198+
* The returned value is the smallest delta which the points need to be shifted by in order to align
199+
* one of the points to a guide.
200+
*
201+
* \see snapPointToGuides()
202+
*/
203+
double snapPointsToGuides( const QList< double > &points, QgsLayoutGuide::Orientation orientation, double scaleFactor, bool &snapped SIP_OUT ) const;
204+
142205
/**
143206
* Snaps an \a original layout coordinate to the item bounds. If the point
144207
* was snapped, \a snapped will be set to true.
@@ -154,10 +217,30 @@ class CORE_EXPORT QgsLayoutSnapper: public QgsLayoutSerializableObject
154217
*
155218
* If \a snapLine is specified, the snapper will automatically show (or hide) the snap line
156219
* based on the result of the snap, and position it at the correct location for the snap.
220+
*
221+
* \see snapPointsToItems()
157222
*/
158223
double snapPointToItems( double original, Qt::Orientation orientation, double scaleFactor, const QList< QgsLayoutItem * > &ignoreItems, bool &snapped SIP_OUT,
159224
QGraphicsLineItem *snapLine = nullptr ) const;
160225

226+
/**
227+
* Snaps a set of \a points to the item bounds. If the points
228+
* were snapped, \a snapped will be set to true.
229+
*
230+
* The \a scaleFactor argument should be set to the transformation from
231+
* scalar transform from layout coordinates to pixels, i.e. the
232+
* graphics view transform().m11() value.
233+
*
234+
* If snapToItems() is disabled, this method will not attempt to snap the points.
235+
*
236+
* The returned value is the smallest delta which the points need to be shifted by in order to align
237+
* one of the points to an item bound.
238+
*
239+
* \see snapPointToItems()
240+
*/
241+
double snapPointsToItems( const QList< double > &points, Qt::Orientation orientation, double scaleFactor, const QList< QgsLayoutItem * > &ignoreItems, bool &snapped SIP_OUT,
242+
QGraphicsLineItem *snapLine = nullptr ) const;
243+
161244
/**
162245
* Stores the snapper's state in a DOM element. The \a parentElement should refer to the parent layout's DOM element.
163246
* \see readXml()

‎src/gui/layout/qgslayoutmousehandles.cpp

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -706,10 +706,6 @@ void QgsLayoutMouseHandles::resetStatusBar()
706706

707707
QPointF QgsLayoutMouseHandles::snapPoint( QPointF originalPoint, QgsLayoutMouseHandles::SnapGuideMode mode, bool snapHorizontal, bool snapVertical )
708708
{
709-
//align item
710-
double alignX = 0;
711-
double alignY = 0;
712-
713709
bool snapped = false;
714710

715711
const QList< QgsLayoutItem * > selectedItems = mLayout->selectedLayoutItems();
@@ -719,42 +715,16 @@ QPointF QgsLayoutMouseHandles::snapPoint( QPointF originalPoint, QgsLayoutMouseH
719715
switch ( mode )
720716
{
721717
case Item:
722-
//snappedPoint = alignItem( alignX, alignY, point.x(), point.y() );
718+
snappedPoint = mLayout->snapper().snapRect( rect().translated( originalPoint ), mView->transform().m11(), snapped, snapHorizontal ? mHorizontalSnapLine.get() : nullptr,
719+
snapVertical ? mVerticalSnapLine.get() : nullptr, &selectedItems ).topLeft();
723720
break;
724721
case Point:
725722
snappedPoint = mLayout->snapper().snapPoint( originalPoint, mView->transform().m11(), snapped, snapHorizontal ? mHorizontalSnapLine.get() : nullptr,
726723
snapVertical ? mVerticalSnapLine.get() : nullptr, &selectedItems );
727724
break;
728725
}
729-
#if 0
730-
if ( !qgsDoubleNear( alignX, -1.0 ) )
731-
{
732-
QGraphicsLineItem *item = hAlignSnapItem();
733-
int numPages = mComposition->numPages();
734-
double yLineCoord = 300; //default in case there is no single page
735-
if ( numPages > 0 )
736-
{
737-
yLineCoord = mComposition->paperHeight() * numPages + mComposition->spaceBetweenPages() * ( numPages - 1 );
738-
}
739-
item->setLine( QLineF( alignX, 0, alignX, yLineCoord ) );
740-
item->show();
741-
}
742-
else
743-
{
744-
deleteHAlignSnapItem();
745-
}
746-
if ( !qgsDoubleNear( alignY, -1.0 ) )
747-
{
748-
QGraphicsLineItem *item = vAlignSnapItem();
749-
item->setLine( QLineF( 0, alignY, mComposition->paperWidth(), alignY ) );
750-
item->show();
751-
}
752-
else
753-
{
754-
deleteVAlignSnapItem();
755-
}
756-
#endif
757-
return snappedPoint;
726+
727+
return snapped ? snappedPoint : originalPoint;
758728
}
759729

760730
void QgsLayoutMouseHandles::hideAlignItems()

‎tests/src/python/test_qgslayoutsnapper.py

Lines changed: 271 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
QgsReadWriteContext,
2727
QgsLayoutItemMap,
2828
QgsLayoutSize)
29-
from qgis.PyQt.QtCore import QPointF, Qt
29+
from qgis.PyQt.QtCore import QPointF, Qt, QRectF
3030
from qgis.PyQt.QtWidgets import QGraphicsLineItem
3131
from qgis.PyQt.QtXml import QDomDocument
3232

@@ -129,6 +129,65 @@ def testSnapPointToGrid(self):
129129
self.assertFalse(snappedY)
130130
self.assertEqual(point, QPointF(12, 23))
131131

132+
def testSnapPointsToGrid(self):
133+
p = QgsProject()
134+
l = QgsLayout(p)
135+
# need a page to snap to grid
136+
page = QgsLayoutItemPage(l)
137+
page.setPageSize('A4')
138+
l.pageCollection().addPage(page)
139+
s = QgsLayoutSnapper(l)
140+
141+
l.gridSettings().setResolution(QgsLayoutMeasurement(5, QgsUnitTypes.LayoutMillimeters))
142+
143+
s.setSnapToGrid(True)
144+
s.setSnapTolerance(1)
145+
146+
delta, snappedX, snappedY = s.snapPointsToGrid([QPointF(1, 0.5)], 1)
147+
self.assertTrue(snappedX)
148+
self.assertTrue(snappedY)
149+
self.assertEqual(delta, QPointF(-1, -0.5))
150+
151+
point, snappedX, snappedY = s.snapPointsToGrid([QPointF(9, 2), QPointF(12, 6)], 1)
152+
self.assertTrue(snappedX)
153+
self.assertTrue(snappedY)
154+
self.assertEqual(point, QPointF(1, -1))
155+
156+
point, snappedX, snappedY = s.snapPointsToGrid([QPointF(9, 2), QPointF(12, 7)], 1)
157+
self.assertTrue(snappedX)
158+
self.assertFalse(snappedY)
159+
self.assertEqual(point, QPointF(1, 0))
160+
161+
point, snappedX, snappedY = s.snapPointsToGrid([QPointF(8, 2), QPointF(12, 6)], 1)
162+
self.assertFalse(snappedX)
163+
self.assertTrue(snappedY)
164+
self.assertEqual(point, QPointF(0, -1))
165+
166+
# grid disabled
167+
s.setSnapToGrid(False)
168+
point, snappedX, snappedY = s.snapPointsToGrid([QPointF(1, 1)], 1)
169+
self.assertFalse(snappedX)
170+
self.assertFalse(snappedY)
171+
self.assertEqual(point, QPointF(0, 0))
172+
s.setSnapToGrid(True)
173+
174+
# with different pixel scale
175+
point, snappedX, snappedY = s.snapPointsToGrid([QPointF(0.5, 0.5)], 1)
176+
self.assertTrue(snappedX)
177+
self.assertTrue(snappedY)
178+
self.assertEqual(point, QPointF(-.5, -.5))
179+
point, snappedX, snappedY = s.snapPointsToGrid([QPointF(0.5, 0.5)], 3)
180+
self.assertFalse(snappedX)
181+
self.assertFalse(snappedY)
182+
self.assertEqual(point, QPointF(0, 0))
183+
184+
# with offset grid
185+
l.gridSettings().setOffset(QgsLayoutPoint(2, 0))
186+
point, snappedX, snappedY = s.snapPointsToGrid([QPointF(13, 23)], 1)
187+
self.assertTrue(snappedX)
188+
self.assertFalse(snappedY)
189+
self.assertEqual(point, QPointF(-1, 0))
190+
132191
def testSnapPointToGuides(self):
133192
p = QgsProject()
134193
l = QgsLayout(p)
@@ -172,6 +231,60 @@ def testSnapPointToGuides(self):
172231
point, snapped = s.snapPointToGuides(0.5, QgsLayoutGuide.Horizontal, 3)
173232
self.assertFalse(snapped)
174233

234+
def testSnapPointsToGuides(self):
235+
p = QgsProject()
236+
l = QgsLayout(p)
237+
page = QgsLayoutItemPage(l)
238+
page.setPageSize('A4')
239+
l.pageCollection().addPage(page)
240+
s = QgsLayoutSnapper(l)
241+
guides = l.guides()
242+
243+
s.setSnapToGuides(True)
244+
s.setSnapTolerance(1)
245+
246+
# no guides
247+
delta, snapped = s.snapPointsToGuides([0.5], QgsLayoutGuide.Vertical, 1)
248+
self.assertFalse(snapped)
249+
250+
guides.addGuide(QgsLayoutGuide(QgsLayoutGuide.Vertical, QgsLayoutMeasurement(1), page))
251+
point, snapped = s.snapPointsToGuides([0.7], QgsLayoutGuide.Vertical, 1)
252+
self.assertTrue(snapped)
253+
self.assertAlmostEqual(point, 0.3, 5)
254+
255+
point, snapped = s.snapPointsToGuides([0.7, 1.2], QgsLayoutGuide.Vertical, 1)
256+
self.assertTrue(snapped)
257+
self.assertAlmostEqual(point, -0.2, 5)
258+
259+
# outside tolerance
260+
point, snapped = s.snapPointsToGuides([5.5], QgsLayoutGuide.Vertical, 1)
261+
self.assertFalse(snapped)
262+
263+
# snapping off
264+
s.setSnapToGuides(False)
265+
point, snapped = s.snapPointsToGuides([0.5], QgsLayoutGuide.Vertical, 1)
266+
self.assertFalse(snapped)
267+
268+
s.setSnapToGuides(True)
269+
270+
# snap to hoz
271+
point, snapped = s.snapPointsToGuides([0.5], QgsLayoutGuide.Horizontal, 1)
272+
self.assertFalse(snapped)
273+
guides.addGuide(QgsLayoutGuide(QgsLayoutGuide.Horizontal, QgsLayoutMeasurement(1), page))
274+
point, snapped = s.snapPointsToGuides([0.7], QgsLayoutGuide.Horizontal, 1)
275+
self.assertTrue(snapped)
276+
self.assertAlmostEqual(point, 0.3, 5)
277+
point, snapped = s.snapPointsToGuides([0.7, 1.2], QgsLayoutGuide.Horizontal, 1)
278+
self.assertTrue(snapped)
279+
self.assertAlmostEqual(point, -0.2, 5)
280+
point, snapped = s.snapPointsToGuides([0.7, 0.9, 1.2], QgsLayoutGuide.Horizontal, 1)
281+
self.assertTrue(snapped)
282+
self.assertAlmostEqual(point, 0.1, 5)
283+
284+
# with different pixel scale
285+
point, snapped = s.snapPointsToGuides([0.5, 1.5], QgsLayoutGuide.Horizontal, 3)
286+
self.assertFalse(snapped)
287+
175288
def testSnapPointToItems(self):
176289
p = QgsProject()
177290
l = QgsLayout(p)
@@ -264,6 +377,104 @@ def testSnapPointToItems(self):
264377
point, snapped = s.snapPointToItems(20.5, Qt.Vertical, 3, [])
265378
self.assertFalse(snapped)
266379

380+
def testSnapPointsToItems(self):
381+
p = QgsProject()
382+
l = QgsLayout(p)
383+
page = QgsLayoutItemPage(l)
384+
page.setPageSize('A4')
385+
#l.pageCollection().addPage(page)
386+
s = QgsLayoutSnapper(l)
387+
guides = l.guides()
388+
389+
s.setSnapToItems(True)
390+
s.setSnapTolerance(1)
391+
392+
# no items
393+
point, snapped = s.snapPointsToItems([0.5], Qt.Horizontal, 1, [])
394+
self.assertFalse(snapped)
395+
396+
line = QGraphicsLineItem()
397+
line.setVisible(True)
398+
point, snapped = s.snapPointsToItems([0.5], Qt.Horizontal, 1, [], line)
399+
self.assertFalse(line.isVisible())
400+
401+
guides.addGuide(QgsLayoutGuide(QgsLayoutGuide.Vertical, QgsLayoutMeasurement(1), page))
402+
403+
# add an item
404+
item1 = QgsLayoutItemMap(l)
405+
item1.attemptMove(QgsLayoutPoint(4, 8, QgsUnitTypes.LayoutMillimeters))
406+
item1.attemptResize(QgsLayoutSize(18, 12, QgsUnitTypes.LayoutMillimeters))
407+
l.addItem(item1)
408+
409+
point, snapped = s.snapPointsToItems([3.5], Qt.Horizontal, 1, [], line)
410+
self.assertTrue(snapped)
411+
self.assertEqual(point, 0.5)
412+
self.assertTrue(line.isVisible())
413+
point, snapped = s.snapPointsToItems([4.5], Qt.Horizontal, 1, [])
414+
self.assertTrue(snapped)
415+
self.assertEqual(point, -0.5)
416+
point, snapped = s.snapPointsToItems([4.6, 4.5], Qt.Horizontal, 1, [])
417+
self.assertTrue(snapped)
418+
self.assertEqual(point, -0.5)
419+
point, snapped = s.snapPointsToItems([4.6, 4.5, 3.7], Qt.Horizontal, 1, [])
420+
self.assertTrue(snapped)
421+
self.assertAlmostEqual(point, 0.3, 5)
422+
423+
# ignoring item
424+
point, snapped = s.snapPointsToItems([4.5], Qt.Horizontal, 1, [item1])
425+
self.assertFalse(snapped)
426+
427+
# outside tolerance
428+
point, snapped = s.snapPointsToItems([5.5], Qt.Horizontal, 1, [], line)
429+
self.assertFalse(snapped)
430+
self.assertFalse(line.isVisible())
431+
432+
# snap to center
433+
point, snapped = s.snapPointsToItems([12.5], Qt.Horizontal, 1, [])
434+
self.assertTrue(snapped)
435+
self.assertEqual(point, 0.5)
436+
437+
# snap to right
438+
point, snapped = s.snapPointsToItems([22.5], Qt.Horizontal, 1, [])
439+
self.assertTrue(snapped)
440+
self.assertEqual(point, -0.5)
441+
442+
#snap to top
443+
point, snapped = s.snapPointsToItems([7.5], Qt.Vertical, 1, [], line)
444+
self.assertTrue(snapped)
445+
self.assertEqual(point, 0.5)
446+
self.assertTrue(line.isVisible())
447+
point, snapped = s.snapPointsToItems([8.5], Qt.Vertical, 1, [])
448+
self.assertTrue(snapped)
449+
self.assertEqual(point, -0.5)
450+
451+
# outside tolerance
452+
point, snapped = s.snapPointsToItems([5.5], Qt.Vertical, 1, [], line)
453+
self.assertFalse(snapped)
454+
self.assertFalse(line.isVisible())
455+
456+
# snap to center
457+
point, snapped = s.snapPointsToItems([13.5], Qt.Vertical, 1, [])
458+
self.assertTrue(snapped)
459+
self.assertEqual(point, 0.5)
460+
461+
# snap to bottom
462+
point, snapped = s.snapPointsToItems([20.5], Qt.Vertical, 1, [])
463+
self.assertTrue(snapped)
464+
self.assertEqual(point, -0.5)
465+
466+
# snapping off
467+
s.setSnapToItems(False)
468+
line.setVisible(True)
469+
point, snapped = s.snapPointsToItems([20.5], Qt.Vertical, 1, [], line)
470+
self.assertFalse(snapped)
471+
self.assertFalse(line.isVisible())
472+
473+
# with different pixel scale
474+
s.setSnapToItems(True)
475+
point, snapped = s.snapPointsToItems([20.5], Qt.Vertical, 3, [])
476+
self.assertFalse(snapped)
477+
267478
def testSnapPoint(self):
268479
p = QgsProject()
269480
l = QgsLayout(p)
@@ -319,6 +530,65 @@ def testSnapPoint(self):
319530
self.assertTrue(snapped)
320531
self.assertEqual(point, QPointF(0, 0))
321532

533+
def testSnapRect(self):
534+
p = QgsProject()
535+
l = QgsLayout(p)
536+
page = QgsLayoutItemPage(l)
537+
page.setPageSize('A4')
538+
l.pageCollection().addPage(page)
539+
s = QgsLayoutSnapper(l)
540+
guides = l.guides()
541+
542+
# first test snapping to grid
543+
l.gridSettings().setResolution(QgsLayoutMeasurement(5, QgsUnitTypes.LayoutMillimeters))
544+
s.setSnapToItems(False)
545+
s.setSnapToGrid(True)
546+
s.setSnapTolerance(1)
547+
548+
rect, snapped = s.snapRect(QRectF(1, 1, 2, 1), 1)
549+
self.assertTrue(snapped)
550+
self.assertEqual(rect, QRectF(0, 0, 2, 1))
551+
rect, snapped = s.snapRect(QRectF(1, 1, 3.5, 3.5), 1)
552+
self.assertTrue(snapped)
553+
self.assertEqual(rect, QRectF(1.5, 1.5, 3.5, 3.5))
554+
555+
s.setSnapToItems(False)
556+
s.setSnapToGrid(False)
557+
rect, snapped = s.snapRect(QRectF(1, 1, 3.5, 3.5), 1)
558+
self.assertFalse(snapped)
559+
self.assertEqual(rect, QRectF(1, 1, 3.5, 3.5))
560+
561+
# test that guide takes precedence
562+
s.setSnapToGrid(True)
563+
s.setSnapToGuides(True)
564+
guides.addGuide(QgsLayoutGuide(QgsLayoutGuide.Horizontal, QgsLayoutMeasurement(0.5), page))
565+
rect, snapped = s.snapRect(QRectF(1, 1, 2, 3), 1)
566+
self.assertTrue(snapped)
567+
self.assertEqual(rect, QRectF(0.0, 0.5, 2.0, 3.0))
568+
569+
# add an item
570+
item1 = QgsLayoutItemMap(l)
571+
item1.attemptMove(QgsLayoutPoint(121, 1.1, QgsUnitTypes.LayoutMillimeters))
572+
l.addItem(item1)
573+
574+
# test that guide takes precedence over item
575+
s.setSnapToGrid(True)
576+
s.setSnapToGuides(True)
577+
s.setSnapToItems(True)
578+
rect, snapped = s.snapRect(QRectF(1, 1, 2, 3), 1)
579+
self.assertTrue(snapped)
580+
self.assertEqual(rect, QRectF(0.0, 0.5, 2.0, 3.0))
581+
# but items take precedence over grid
582+
s.setSnapToGuides(False)
583+
rect, snapped = s.snapRect(QRectF(1, 1, 2, 3), 1)
584+
self.assertTrue(snapped)
585+
self.assertEqual(rect, QRectF(0.0, 1.1, 2.0, 3.0))
586+
587+
# ... unless item is ignored!
588+
rect, snapped = s.snapRect(QRectF(1, 1, 2, 3), 1, None, None, [item1])
589+
self.assertTrue(snapped)
590+
self.assertEqual(rect, QRectF(0.0, 0.0, 2.0, 3.0))
591+
322592
def testReadWriteXml(self):
323593
p = QgsProject()
324594
l = QgsLayout(p)

0 commit comments

Comments
 (0)
Please sign in to comment.