Skip to content

Commit d950f17

Browse files
committed
Add item bounds based snapping to QgsLayoutSnapper
1 parent 172d484 commit d950f17

File tree

6 files changed

+346
-15
lines changed

6 files changed

+346
-15
lines changed

python/core/layout/qgslayout.sip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class QgsLayout : QGraphicsScene, QgsExpressionContextGenerator, QgsLayoutUndoOb
2626
ZItem,
2727
ZGrid,
2828
ZGuide,
29+
ZSmartGuide,
2930
ZMouseHandles,
3031
ZMapTool,
3132
ZSnapIndicator,

python/core/layout/qgslayoutsnapper.sip

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,21 @@ class QgsLayoutSnapper: QgsLayoutSerializableObject
6969
.. seealso:: snapToGuides()
7070
%End
7171

72-
QPointF snapPoint( QPointF point, double scaleFactor, bool &snapped /Out/ ) const;
72+
bool snapToItems() const;
73+
%Docstring
74+
Returns true if snapping to items is enabled.
75+
.. seealso:: setSnapToItems()
76+
:rtype: bool
77+
%End
78+
79+
void setSnapToItems( bool enabled );
80+
%Docstring
81+
Sets whether snapping to items is ``enabled``.
82+
.. seealso:: snapToItems()
83+
%End
84+
85+
QPointF snapPoint( QPointF point, double scaleFactor, bool &snapped /Out/, QGraphicsLineItem *horizontalSnapLine = 0,
86+
QGraphicsLineItem *verticalSnapLine = 0 ) const;
7387
%Docstring
7488
Snaps a layout coordinate ``point``. If ``point`` was snapped, ``snapped`` will be set to true.
7589

@@ -78,6 +92,9 @@ class QgsLayoutSnapper: QgsLayoutSerializableObject
7892
graphics view transform().m11() value.
7993

8094
This method considers snapping to the grid, snap lines, etc.
95+
96+
If the ``horizontalSnapLine`` and ``verticalSnapLine`` arguments are specified, then the snapper
97+
will automatically display and position these lines to indicate snapping positions to item bounds.
8198
:rtype: QPointF
8299
%End
83100

@@ -98,18 +115,38 @@ class QgsLayoutSnapper: QgsLayoutSerializableObject
98115

99116
double snapPointToGuides( double original, QgsLayoutGuide::Orientation orientation, double scaleFactor, bool &snapped /Out/ ) const;
100117
%Docstring
101-
Snaps a layout coordinate ``point`` to the grid. If ``point``
118+
Snaps an ``original`` layout coordinate to the guides. If the point
102119
was snapped, ``snapped`` will be set to true.
103120

104121
The ``scaleFactor`` argument should be set to the transformation from
105122
scalar transform from layout coordinates to pixels, i.e. the
106123
graphics view transform().m11() value.
107124

108-
If snapToGrid() is disabled, this method will return the point
125+
If snapToGuides() is disabled, this method will return the point
109126
unchanged.
110127
:rtype: float
111128
%End
112129

130+
double snapPointToItems( double original, Qt::Orientation orientation, double scaleFactor, const QList< QgsLayoutItem * > &ignoreItems, bool &snapped /Out/,
131+
QGraphicsLineItem *snapLine = 0 ) const;
132+
%Docstring
133+
Snaps an ``original`` layout coordinate to the item bounds. If the point
134+
was snapped, ``snapped`` will be set to true.
135+
136+
The ``scaleFactor`` argument should be set to the transformation from
137+
scalar transform from layout coordinates to pixels, i.e. the
138+
graphics view transform().m11() value.
139+
140+
If snapToItems() is disabled, this method will return the point
141+
unchanged.
142+
143+
A list of items to ignore during the snapping can be specified via the ``ignoreItems`` list.
144+
145+
If ``snapLine`` is specified, the snapper will automatically show (or hide) the snap line
146+
based on the result of the snap, and position it at the correct location for the snap.
147+
:rtype: float
148+
%End
149+
113150
virtual bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const;
114151

115152
%Docstring

src/core/layout/qgslayout.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
4545
{
4646
ZPage = 0, //!< Z-value for page (paper) items
4747
ZItem = 1, //!< Minimum z value for items
48-
ZGrid = 9998, //!< Z-value for page grids
49-
ZGuide = 9999, //!< Z-value for page guides
48+
ZGrid = 9997, //!< Z-value for page grids
49+
ZGuide = 9998, //!< Z-value for page guides
50+
ZSmartGuide = 9999, //!< Z-value for smart (item bounds based) guides
5051
ZMouseHandles = 10000, //!< Z-value for mouse handles
5152
ZMapTool = 10001, //!< Z-value for temporary map tool items
5253
ZSnapIndicator = 10002, //!< Z-value for snapping indicator

src/core/layout/qgslayoutsnapper.cpp

Lines changed: 138 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ void QgsLayoutSnapper::setSnapToGuides( bool enabled )
4444
mSnapToGuides = enabled;
4545
}
4646

47-
QPointF QgsLayoutSnapper::snapPoint( QPointF point, double scaleFactor, bool &snapped ) const
47+
void QgsLayoutSnapper::setSnapToItems( bool enabled )
48+
{
49+
mSnapToItems = enabled;
50+
}
51+
52+
QPointF QgsLayoutSnapper::snapPoint( QPointF point, double scaleFactor, bool &snapped, QGraphicsLineItem *horizontalSnapLine, QGraphicsLineItem *verticalSnapLine ) const
4853
{
4954
snapped = false;
5055

@@ -55,24 +60,49 @@ QPointF QgsLayoutSnapper::snapPoint( QPointF point, double scaleFactor, bool &sn
5560
{
5661
snapped = true;
5762
point.setX( newX );
63+
if ( verticalSnapLine )
64+
verticalSnapLine->setVisible( false );
5865
}
5966
bool snappedYToGuides = false;
6067
double newY = snapPointToGuides( point.y(), QgsLayoutGuide::Horizontal, scaleFactor, snappedYToGuides );
6168
if ( snappedYToGuides )
6269
{
6370
snapped = true;
6471
point.setY( newY );
72+
if ( horizontalSnapLine )
73+
horizontalSnapLine->setVisible( false );
74+
}
75+
76+
bool snappedXToItems = false;
77+
bool snappedYToItems = false;
78+
if ( !snappedXToGuides )
79+
{
80+
newX = snapPointToItems( point.x(), Qt::Horizontal, scaleFactor, QList< QgsLayoutItem * >(), snappedXToItems, verticalSnapLine );
81+
if ( snappedXToItems )
82+
{
83+
snapped = true;
84+
point.setX( newX );
85+
}
86+
}
87+
if ( !snappedYToGuides )
88+
{
89+
newY = snapPointToItems( point.y(), Qt::Vertical, scaleFactor, QList< QgsLayoutItem * >(), snappedYToItems, horizontalSnapLine );
90+
if ( snappedYToItems )
91+
{
92+
snapped = true;
93+
point.setY( newY );
94+
}
6595
}
6696

6797
bool snappedXToGrid = false;
6898
bool snappedYToGrid = false;
6999
QPointF res = snapPointToGrid( point, scaleFactor, snappedXToGrid, snappedYToGrid );
70-
if ( snappedXToGrid && !snappedXToGuides )
100+
if ( snappedXToGrid && !snappedXToGuides && !snappedXToItems )
71101
{
72102
snapped = true;
73103
point.setX( res.x() );
74104
}
75-
if ( snappedYToGrid && !snappedYToGuides )
105+
if ( snappedYToGrid && !snappedYToGuides && !snappedYToItems )
76106
{
77107
snapped = true;
78108
point.setY( res.y() );
@@ -169,13 +199,117 @@ double QgsLayoutSnapper::snapPointToGuides( double original, QgsLayoutGuide::Ori
169199
}
170200
}
171201

202+
double QgsLayoutSnapper::snapPointToItems( double original, Qt::Orientation orientation, double scaleFactor, const QList<QgsLayoutItem *> &ignoreItems, bool &snapped,
203+
QGraphicsLineItem *snapLine ) const
204+
{
205+
snapped = false;
206+
if ( !mLayout || !mSnapToItems )
207+
{
208+
if ( snapLine )
209+
snapLine->setVisible( false );
210+
return original;
211+
}
212+
213+
double alignThreshold = mTolerance / scaleFactor;
214+
215+
double closest = original;
216+
double closestDist = DBL_MAX;
217+
const QList<QGraphicsItem *> itemList = mLayout->items();
218+
QList< double > currentCoords;
219+
for ( QGraphicsItem *item : itemList )
220+
{
221+
QgsLayoutItem *currentItem = dynamic_cast< QgsLayoutItem *>( item );
222+
if ( ignoreItems.contains( currentItem ) )
223+
continue;
224+
225+
//don't snap to selected items, since they're the ones that will be snapping to something else
226+
//also ignore group members - only snap to bounds of group itself
227+
//also ignore hidden items
228+
if ( !currentItem /* TODO || currentItem->selected() || currentItem->isGroupMember() */ || !currentItem->isVisible() )
229+
{
230+
continue;
231+
}
232+
QRectF itemRect;
233+
if ( dynamic_cast<const QgsLayoutItemPage *>( currentItem ) )
234+
{
235+
//if snapping to paper use the paper item's rect rather then the bounding rect,
236+
//since we want to snap to the page edge and not any outlines drawn around the page
237+
itemRect = currentItem->mapRectToScene( currentItem->rect() );
238+
}
239+
else
240+
{
241+
itemRect = currentItem->mapRectToScene( currentItem->rectWithFrame() );
242+
}
243+
244+
currentCoords.clear();
245+
switch ( orientation )
246+
{
247+
case Qt::Horizontal:
248+
{
249+
currentCoords << itemRect.left();
250+
currentCoords << itemRect.right();
251+
currentCoords << itemRect.center().x();
252+
break;
253+
}
254+
255+
case Qt::Vertical:
256+
{
257+
currentCoords << itemRect.top();
258+
currentCoords << itemRect.center().y();
259+
currentCoords << itemRect.bottom();
260+
break;
261+
}
262+
}
263+
264+
for ( double val : qgsAsConst( currentCoords ) )
265+
{
266+
double dist = std::fabs( original - val );
267+
if ( dist <= alignThreshold && dist < closestDist )
268+
{
269+
snapped = true;
270+
closestDist = dist;
271+
closest = val;
272+
}
273+
}
274+
}
275+
276+
if ( snapLine )
277+
{
278+
if ( snapped )
279+
{
280+
snapLine->setVisible( true );
281+
switch ( orientation )
282+
{
283+
case Qt::Vertical:
284+
{
285+
snapLine->setLine( QLineF( 0, closest, 300, closest ) );
286+
break;
287+
}
288+
289+
case Qt::Horizontal:
290+
{
291+
snapLine->setLine( QLineF( closest, 0, closest, 300 ) );
292+
break;
293+
}
294+
}
295+
}
296+
else
297+
{
298+
snapLine->setVisible( false );
299+
}
300+
}
301+
302+
return closest;
303+
}
304+
172305
bool QgsLayoutSnapper::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext & ) const
173306
{
174307
QDomElement element = document.createElement( QStringLiteral( "Snapper" ) );
175308

176309
element.setAttribute( QStringLiteral( "tolerance" ), mTolerance );
177310
element.setAttribute( QStringLiteral( "snapToGrid" ), mSnapToGrid );
178311
element.setAttribute( QStringLiteral( "snapToGuides" ), mSnapToGuides );
312+
element.setAttribute( QStringLiteral( "snapToItems" ), mSnapToItems );
179313

180314
parentElement.appendChild( element );
181315
return true;
@@ -197,7 +331,6 @@ bool QgsLayoutSnapper::readXml( const QDomElement &e, const QDomDocument &, cons
197331
mTolerance = element.attribute( QStringLiteral( "tolerance" ), QStringLiteral( "5" ) ).toInt();
198332
mSnapToGrid = element.attribute( QStringLiteral( "snapToGrid" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
199333
mSnapToGuides = element.attribute( QStringLiteral( "snapToGuides" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
334+
mSnapToItems = element.attribute( QStringLiteral( "snapToItems" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
200335
return true;
201336
}
202-
203-

src/core/layout/qgslayoutsnapper.h

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,18 @@ class CORE_EXPORT QgsLayoutSnapper: public QgsLayoutSerializableObject
8282
*/
8383
void setSnapToGuides( bool enabled );
8484

85+
/**
86+
* Returns true if snapping to items is enabled.
87+
* \see setSnapToItems()
88+
*/
89+
bool snapToItems() const { return mSnapToItems; }
90+
91+
/**
92+
* Sets whether snapping to items is \a enabled.
93+
* \see snapToItems()
94+
*/
95+
void setSnapToItems( bool enabled );
96+
8597
/**
8698
* Snaps a layout coordinate \a point. If \a point was snapped, \a snapped will be set to true.
8799
*
@@ -90,8 +102,12 @@ class CORE_EXPORT QgsLayoutSnapper: public QgsLayoutSerializableObject
90102
* graphics view transform().m11() value.
91103
*
92104
* This method considers snapping to the grid, snap lines, etc.
105+
*
106+
* If the \a horizontalSnapLine and \a verticalSnapLine arguments are specified, then the snapper
107+
* will automatically display and position these lines to indicate snapping positions to item bounds.
93108
*/
94-
QPointF snapPoint( QPointF point, double scaleFactor, bool &snapped SIP_OUT ) const;
109+
QPointF snapPoint( QPointF point, double scaleFactor, bool &snapped SIP_OUT, QGraphicsLineItem *horizontalSnapLine = nullptr,
110+
QGraphicsLineItem *verticalSnapLine = nullptr ) const;
95111

96112
/**
97113
* Snaps a layout coordinate \a point to the grid. If \a point
@@ -108,18 +124,37 @@ class CORE_EXPORT QgsLayoutSnapper: public QgsLayoutSerializableObject
108124
QPointF snapPointToGrid( QPointF point, double scaleFactor, bool &snappedX SIP_OUT, bool &snappedY SIP_OUT ) const;
109125

110126
/**
111-
* Snaps a layout coordinate \a point to the grid. If \a point
127+
* Snaps an \a original layout coordinate to the guides. If the point
112128
* was snapped, \a snapped will be set to true.
113129
*
114130
* The \a scaleFactor argument should be set to the transformation from
115131
* scalar transform from layout coordinates to pixels, i.e. the
116132
* graphics view transform().m11() value.
117133
*
118-
* If snapToGrid() is disabled, this method will return the point
134+
* If snapToGuides() is disabled, this method will return the point
119135
* unchanged.
120136
*/
121137
double snapPointToGuides( double original, QgsLayoutGuide::Orientation orientation, double scaleFactor, bool &snapped SIP_OUT ) const;
122138

139+
/**
140+
* Snaps an \a original layout coordinate to the item bounds. If the point
141+
* was snapped, \a snapped will be set to true.
142+
*
143+
* The \a scaleFactor argument should be set to the transformation from
144+
* scalar transform from layout coordinates to pixels, i.e. the
145+
* graphics view transform().m11() value.
146+
*
147+
* If snapToItems() is disabled, this method will return the point
148+
* unchanged.
149+
*
150+
* A list of items to ignore during the snapping can be specified via the \a ignoreItems list.
151+
*
152+
* If \a snapLine is specified, the snapper will automatically show (or hide) the snap line
153+
* based on the result of the snap, and position it at the correct location for the snap.
154+
*/
155+
double snapPointToItems( double original, Qt::Orientation orientation, double scaleFactor, const QList< QgsLayoutItem * > &ignoreItems, bool &snapped SIP_OUT,
156+
QGraphicsLineItem *snapLine = nullptr ) const;
157+
123158
/**
124159
* Stores the snapper's state in a DOM element. The \a parentElement should refer to the parent layout's DOM element.
125160
* \see readXml()
@@ -147,6 +182,7 @@ class CORE_EXPORT QgsLayoutSnapper: public QgsLayoutSerializableObject
147182
int mTolerance = 5;
148183
bool mSnapToGrid = false;
149184
bool mSnapToGuides = true;
185+
bool mSnapToItems = true;
150186

151187
friend class QgsLayoutSnapperUndoCommand;
152188

0 commit comments

Comments
 (0)