Skip to content

Commit

Permalink
Add item bounds based snapping to QgsLayoutSnapper
Browse files Browse the repository at this point in the history
  • Loading branch information
nyalldawson committed Oct 6, 2017
1 parent 172d484 commit d950f17
Show file tree
Hide file tree
Showing 6 changed files with 346 additions and 15 deletions.
1 change: 1 addition & 0 deletions python/core/layout/qgslayout.sip
Expand Up @@ -26,6 +26,7 @@ class QgsLayout : QGraphicsScene, QgsExpressionContextGenerator, QgsLayoutUndoOb
ZItem,
ZGrid,
ZGuide,
ZSmartGuide,
ZMouseHandles,
ZMapTool,
ZSnapIndicator,
Expand Down
43 changes: 40 additions & 3 deletions python/core/layout/qgslayoutsnapper.sip
Expand Up @@ -69,7 +69,21 @@ class QgsLayoutSnapper: QgsLayoutSerializableObject
.. seealso:: snapToGuides()
%End

QPointF snapPoint( QPointF point, double scaleFactor, bool &snapped /Out/ ) const;
bool snapToItems() const;
%Docstring
Returns true if snapping to items is enabled.
.. seealso:: setSnapToItems()
:rtype: bool
%End

void setSnapToItems( bool enabled );
%Docstring
Sets whether snapping to items is ``enabled``.
.. seealso:: snapToItems()
%End

QPointF snapPoint( QPointF point, double scaleFactor, bool &snapped /Out/, QGraphicsLineItem *horizontalSnapLine = 0,
QGraphicsLineItem *verticalSnapLine = 0 ) const;
%Docstring
Snaps a layout coordinate ``point``. If ``point`` was snapped, ``snapped`` will be set to true.

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

This method considers snapping to the grid, snap lines, etc.

If the ``horizontalSnapLine`` and ``verticalSnapLine`` arguments are specified, then the snapper
will automatically display and position these lines to indicate snapping positions to item bounds.
:rtype: QPointF
%End

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

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

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

If snapToGrid() is disabled, this method will return the point
If snapToGuides() is disabled, this method will return the point
unchanged.
:rtype: float
%End

double snapPointToItems( double original, Qt::Orientation orientation, double scaleFactor, const QList< QgsLayoutItem * > &ignoreItems, bool &snapped /Out/,
QGraphicsLineItem *snapLine = 0 ) const;
%Docstring
Snaps an ``original`` layout coordinate to the item bounds. If the point
was snapped, ``snapped`` will be set to true.

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

If snapToItems() is disabled, this method will return the point
unchanged.

A list of items to ignore during the snapping can be specified via the ``ignoreItems`` list.

If ``snapLine`` is specified, the snapper will automatically show (or hide) the snap line
based on the result of the snap, and position it at the correct location for the snap.
:rtype: float
%End

virtual bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const;

%Docstring
Expand Down
5 changes: 3 additions & 2 deletions src/core/layout/qgslayout.h
Expand Up @@ -45,8 +45,9 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
{
ZPage = 0, //!< Z-value for page (paper) items
ZItem = 1, //!< Minimum z value for items
ZGrid = 9998, //!< Z-value for page grids
ZGuide = 9999, //!< Z-value for page guides
ZGrid = 9997, //!< Z-value for page grids
ZGuide = 9998, //!< Z-value for page guides
ZSmartGuide = 9999, //!< Z-value for smart (item bounds based) guides
ZMouseHandles = 10000, //!< Z-value for mouse handles
ZMapTool = 10001, //!< Z-value for temporary map tool items
ZSnapIndicator = 10002, //!< Z-value for snapping indicator
Expand Down
143 changes: 138 additions & 5 deletions src/core/layout/qgslayoutsnapper.cpp
Expand Up @@ -44,7 +44,12 @@ void QgsLayoutSnapper::setSnapToGuides( bool enabled )
mSnapToGuides = enabled;
}

QPointF QgsLayoutSnapper::snapPoint( QPointF point, double scaleFactor, bool &snapped ) const
void QgsLayoutSnapper::setSnapToItems( bool enabled )
{
mSnapToItems = enabled;
}

QPointF QgsLayoutSnapper::snapPoint( QPointF point, double scaleFactor, bool &snapped, QGraphicsLineItem *horizontalSnapLine, QGraphicsLineItem *verticalSnapLine ) const
{
snapped = false;

Expand All @@ -55,24 +60,49 @@ QPointF QgsLayoutSnapper::snapPoint( QPointF point, double scaleFactor, bool &sn
{
snapped = true;
point.setX( newX );
if ( verticalSnapLine )
verticalSnapLine->setVisible( false );
}
bool snappedYToGuides = false;
double newY = snapPointToGuides( point.y(), QgsLayoutGuide::Horizontal, scaleFactor, snappedYToGuides );
if ( snappedYToGuides )
{
snapped = true;
point.setY( newY );
if ( horizontalSnapLine )
horizontalSnapLine->setVisible( false );
}

bool snappedXToItems = false;
bool snappedYToItems = false;
if ( !snappedXToGuides )
{
newX = snapPointToItems( point.x(), Qt::Horizontal, scaleFactor, QList< QgsLayoutItem * >(), snappedXToItems, verticalSnapLine );
if ( snappedXToItems )
{
snapped = true;
point.setX( newX );
}
}
if ( !snappedYToGuides )
{
newY = snapPointToItems( point.y(), Qt::Vertical, scaleFactor, QList< QgsLayoutItem * >(), snappedYToItems, horizontalSnapLine );
if ( snappedYToItems )
{
snapped = true;
point.setY( newY );
}
}

bool snappedXToGrid = false;
bool snappedYToGrid = false;
QPointF res = snapPointToGrid( point, scaleFactor, snappedXToGrid, snappedYToGrid );
if ( snappedXToGrid && !snappedXToGuides )
if ( snappedXToGrid && !snappedXToGuides && !snappedXToItems )
{
snapped = true;
point.setX( res.x() );
}
if ( snappedYToGrid && !snappedYToGuides )
if ( snappedYToGrid && !snappedYToGuides && !snappedYToItems )
{
snapped = true;
point.setY( res.y() );
Expand Down Expand Up @@ -169,13 +199,117 @@ double QgsLayoutSnapper::snapPointToGuides( double original, QgsLayoutGuide::Ori
}
}

double QgsLayoutSnapper::snapPointToItems( double original, Qt::Orientation orientation, double scaleFactor, const QList<QgsLayoutItem *> &ignoreItems, bool &snapped,
QGraphicsLineItem *snapLine ) const
{
snapped = false;
if ( !mLayout || !mSnapToItems )
{
if ( snapLine )
snapLine->setVisible( false );
return original;
}

double alignThreshold = mTolerance / scaleFactor;

double closest = original;
double closestDist = DBL_MAX;
const QList<QGraphicsItem *> itemList = mLayout->items();
QList< double > currentCoords;
for ( QGraphicsItem *item : itemList )
{
QgsLayoutItem *currentItem = dynamic_cast< QgsLayoutItem *>( item );
if ( ignoreItems.contains( currentItem ) )
continue;

//don't snap to selected items, since they're the ones that will be snapping to something else
//also ignore group members - only snap to bounds of group itself
//also ignore hidden items
if ( !currentItem /* TODO || currentItem->selected() || currentItem->isGroupMember() */ || !currentItem->isVisible() )
{
continue;
}
QRectF itemRect;
if ( dynamic_cast<const QgsLayoutItemPage *>( currentItem ) )
{
//if snapping to paper use the paper item's rect rather then the bounding rect,
//since we want to snap to the page edge and not any outlines drawn around the page
itemRect = currentItem->mapRectToScene( currentItem->rect() );
}
else
{
itemRect = currentItem->mapRectToScene( currentItem->rectWithFrame() );
}

currentCoords.clear();
switch ( orientation )
{
case Qt::Horizontal:
{
currentCoords << itemRect.left();
currentCoords << itemRect.right();
currentCoords << itemRect.center().x();
break;
}

case Qt::Vertical:
{
currentCoords << itemRect.top();
currentCoords << itemRect.center().y();
currentCoords << itemRect.bottom();
break;
}
}

for ( double val : qgsAsConst( currentCoords ) )
{
double dist = std::fabs( original - val );
if ( dist <= alignThreshold && dist < closestDist )
{
snapped = true;
closestDist = dist;
closest = val;
}
}
}

if ( snapLine )
{
if ( snapped )
{
snapLine->setVisible( true );
switch ( orientation )
{
case Qt::Vertical:
{
snapLine->setLine( QLineF( 0, closest, 300, closest ) );
break;
}

case Qt::Horizontal:
{
snapLine->setLine( QLineF( closest, 0, closest, 300 ) );
break;
}
}
}
else
{
snapLine->setVisible( false );
}
}

return closest;
}

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

element.setAttribute( QStringLiteral( "tolerance" ), mTolerance );
element.setAttribute( QStringLiteral( "snapToGrid" ), mSnapToGrid );
element.setAttribute( QStringLiteral( "snapToGuides" ), mSnapToGuides );
element.setAttribute( QStringLiteral( "snapToItems" ), mSnapToItems );

parentElement.appendChild( element );
return true;
Expand All @@ -197,7 +331,6 @@ bool QgsLayoutSnapper::readXml( const QDomElement &e, const QDomDocument &, cons
mTolerance = element.attribute( QStringLiteral( "tolerance" ), QStringLiteral( "5" ) ).toInt();
mSnapToGrid = element.attribute( QStringLiteral( "snapToGrid" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
mSnapToGuides = element.attribute( QStringLiteral( "snapToGuides" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
mSnapToItems = element.attribute( QStringLiteral( "snapToItems" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
return true;
}


42 changes: 39 additions & 3 deletions src/core/layout/qgslayoutsnapper.h
Expand Up @@ -82,6 +82,18 @@ class CORE_EXPORT QgsLayoutSnapper: public QgsLayoutSerializableObject
*/
void setSnapToGuides( bool enabled );

/**
* Returns true if snapping to items is enabled.
* \see setSnapToItems()
*/
bool snapToItems() const { return mSnapToItems; }

/**
* Sets whether snapping to items is \a enabled.
* \see snapToItems()
*/
void setSnapToItems( bool enabled );

/**
* Snaps a layout coordinate \a point. If \a point was snapped, \a snapped will be set to true.
*
Expand All @@ -90,8 +102,12 @@ class CORE_EXPORT QgsLayoutSnapper: public QgsLayoutSerializableObject
* graphics view transform().m11() value.
*
* This method considers snapping to the grid, snap lines, etc.
*
* If the \a horizontalSnapLine and \a verticalSnapLine arguments are specified, then the snapper
* will automatically display and position these lines to indicate snapping positions to item bounds.
*/
QPointF snapPoint( QPointF point, double scaleFactor, bool &snapped SIP_OUT ) const;
QPointF snapPoint( QPointF point, double scaleFactor, bool &snapped SIP_OUT, QGraphicsLineItem *horizontalSnapLine = nullptr,
QGraphicsLineItem *verticalSnapLine = nullptr ) const;

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

/**
* Snaps a layout coordinate \a point to the grid. If \a point
* Snaps an \a original layout coordinate to the guides. If the point
* was snapped, \a snapped will be set to true.
*
* The \a scaleFactor argument should be set to the transformation from
* scalar transform from layout coordinates to pixels, i.e. the
* graphics view transform().m11() value.
*
* If snapToGrid() is disabled, this method will return the point
* If snapToGuides() is disabled, this method will return the point
* unchanged.
*/
double snapPointToGuides( double original, QgsLayoutGuide::Orientation orientation, double scaleFactor, bool &snapped SIP_OUT ) const;

/**
* Snaps an \a original layout coordinate to the item bounds. If the point
* was snapped, \a snapped will be set to true.
*
* The \a scaleFactor argument should be set to the transformation from
* scalar transform from layout coordinates to pixels, i.e. the
* graphics view transform().m11() value.
*
* If snapToItems() is disabled, this method will return the point
* unchanged.
*
* A list of items to ignore during the snapping can be specified via the \a ignoreItems list.
*
* If \a snapLine is specified, the snapper will automatically show (or hide) the snap line
* based on the result of the snap, and position it at the correct location for the snap.
*/
double snapPointToItems( double original, Qt::Orientation orientation, double scaleFactor, const QList< QgsLayoutItem * > &ignoreItems, bool &snapped SIP_OUT,
QGraphicsLineItem *snapLine = nullptr ) const;

/**
* Stores the snapper's state in a DOM element. The \a parentElement should refer to the parent layout's DOM element.
* \see readXml()
Expand Down Expand Up @@ -147,6 +182,7 @@ class CORE_EXPORT QgsLayoutSnapper: public QgsLayoutSerializableObject
int mTolerance = 5;
bool mSnapToGrid = false;
bool mSnapToGuides = true;
bool mSnapToItems = true;

friend class QgsLayoutSnapperUndoCommand;

Expand Down

0 comments on commit d950f17

Please sign in to comment.