From 78ecef6f2a476043516eede1d3a8504481fc1e22 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 4 Feb 2014 18:34:47 +1100 Subject: [PATCH] [composer] Draw mouse handles and item bounds outside of item's frames, so that snapping occurs to edge of item frame (fix #8943) --- python/core/composer/qgscomposeritem.sip | 33 ++++++++++- python/core/composer/qgscomposershape.sip | 8 +++ src/app/composer/qgscomposeritemwidget.cpp | 4 +- .../composer/qgscomposerscalebarwidget.cpp | 4 +- src/app/composer/qgscomposershapewidget.cpp | 1 + src/core/composer/qgscomposeritem.cpp | 59 ++++++++++++++++++- src/core/composer/qgscomposeritem.h | 34 ++++++++++- src/core/composer/qgscomposermousehandles.cpp | 23 ++++---- src/core/composer/qgscomposershape.cpp | 11 ++++ src/core/composer/qgscomposershape.h | 9 +++ 10 files changed, 166 insertions(+), 20 deletions(-) diff --git a/python/core/composer/qgscomposeritem.sip b/python/core/composer/qgscomposeritem.sip index ab7a6fd098e..ffc58702b86 100644 --- a/python/core/composer/qgscomposeritem.sip +++ b/python/core/composer/qgscomposeritem.sip @@ -183,8 +183,9 @@ class QgsComposerItem : QObject, QGraphicsRectItem void setItemPosition( double x, double y, ItemPositionMode itemPoint = UpperLeft ); /**Sets item position and width / height in one go + * @param posIncludesFrame set to true if the position and size arguments include the item's frame border @note: this method was added in version 1.6*/ - void setItemPosition( double x, double y, double width, double height, ItemPositionMode itemPoint = UpperLeft ); + void setItemPosition( double x, double y, double width, double height, ItemPositionMode itemPoint = UpperLeft, bool posIncludesFrame = false ); /**Returns item's last used position mode. @note: This property has no effect on actual's item position, which is always the top-left corner. @@ -228,6 +229,31 @@ class QgsComposerItem : QObject, QGraphicsRectItem */ void setFrameEnabled( bool drawFrame ); + /** Sets frame outline width + * @param outlineWidth new width for outline frame + * @returns nothing + * @note introduced in 2.2 + * @see setFrameEnabled + */ + virtual void setFrameOutlineWidth( double outlineWidth ); + + /** Returns the estimated amount the item's frame bleeds outside the item's + * actual rectangle. For instance, if the item has a 2mm frame outline, then + * 1mm of this frame is drawn outside the item's rect. In this case the + * return value will be 1.0 + * @note introduced in 2.2 + */ + virtual double estimatedFrameBleed() const; + + /** Returns the item's rectangular bounds, including any bleed caused by the item's frame. + * The bounds are returned in the item's coordinate system (see Qt's QGraphicsItem docs for + * more details about QGraphicsItem coordinate systems). The results differ from Qt's rect() + * function, as rect() makes no allowances for the portion of outlines which are drawn + * outside of the item. + * @note introduced in 2.2 + * @see estimatedFrameBleed + */ + virtual QRectF rectWithFrame() const; /** Whether this item has a Background or not. * @returns true if there is a Background around this item, otherwise false. @@ -466,4 +492,9 @@ class QgsComposerItem : QObject, QGraphicsRectItem void itemChanged(); /**Emitted if the rectangle changes*/ void sizeChanged(); + /**Emitted if the item's frame style changes + * @note: this function was introduced in version 2.2 + */ + void frameChanged(); + }; diff --git a/python/core/composer/qgscomposershape.sip b/python/core/composer/qgscomposershape.sip index 70b9f9e30f0..99e3891f964 100644 --- a/python/core/composer/qgscomposershape.sip +++ b/python/core/composer/qgscomposershape.sip @@ -61,4 +61,12 @@ class QgsComposerShape: QgsComposerItem virtual void drawFrame( QPainter* p ); /* reimplement drawBackground, since it's not a rect, but a custom shape */ virtual void drawBackground( QPainter* p ); + /**reimplement estimatedFrameBleed, since frames on shapes are drawn using symbology + * rather than the item's pen */ + virtual double estimatedFrameBleed() const; + public slots: + /**Should be called after the shape's symbol is changed. Redraws the shape and recalculates + * its selection bounds. + * Note: added in version 2.1*/ + void refreshSymbol(); }; diff --git a/src/app/composer/qgscomposeritemwidget.cpp b/src/app/composer/qgscomposeritemwidget.cpp index c97f488a0d6..0555b74a8d9 100644 --- a/src/app/composer/qgscomposeritemwidget.cpp +++ b/src/app/composer/qgscomposeritemwidget.cpp @@ -196,9 +196,7 @@ void QgsComposerItemWidget::on_mOutlineWidthSpinBox_valueChanged( double d ) } mItem->beginCommand( tr( "Item outline width" ), QgsComposerMergeCommand::ItemOutlineWidth ); - QPen itemPen = mItem->pen(); - itemPen.setWidthF( d ); - mItem->setPen( itemPen ); + mItem->setFrameOutlineWidth( d ); mItem->endCommand(); } diff --git a/src/app/composer/qgscomposerscalebarwidget.cpp b/src/app/composer/qgscomposerscalebarwidget.cpp index 745df1a0171..f11224af569 100644 --- a/src/app/composer/qgscomposerscalebarwidget.cpp +++ b/src/app/composer/qgscomposerscalebarwidget.cpp @@ -206,9 +206,7 @@ void QgsComposerScaleBarWidget::on_mLineWidthSpinBox_valueChanged( double d ) mComposerScaleBar->beginCommand( tr( "Scalebar line width" ), QgsComposerMergeCommand::ScaleBarLineWidth ); disconnectUpdateSignal(); - QPen newPen( mComposerScaleBar->pen().color() ); - newPen.setWidthF( d ); - mComposerScaleBar->setPen( newPen ); + mComposerScaleBar->setFrameOutlineWidth( d ); mComposerScaleBar->update(); connectUpdateSignal(); mComposerScaleBar->endCommand(); diff --git a/src/app/composer/qgscomposershapewidget.cpp b/src/app/composer/qgscomposershapewidget.cpp index 9c81280be80..f43464f44e7 100644 --- a/src/app/composer/qgscomposershapewidget.cpp +++ b/src/app/composer/qgscomposershapewidget.cpp @@ -123,6 +123,7 @@ void QgsComposerShapeWidget::updateShapeStyle() { if ( mComposerShape ) { + mComposerShape->refreshSymbol(); QIcon icon = QgsSymbolLayerV2Utils::symbolPreviewIcon( mComposerShape->shapeStyleSymbol(), mShapeStyleButton->iconSize() ); mShapeStyleButton->setIcon( icon ); } diff --git a/src/core/composer/qgscomposeritem.cpp b/src/core/composer/qgscomposeritem.cpp index 1b677051367..61e1ce8c680 100644 --- a/src/core/composer/qgscomposeritem.cpp +++ b/src/core/composer/qgscomposeritem.cpp @@ -349,6 +349,41 @@ bool QgsComposerItem::_readXML( const QDomElement& itemElem, const QDomDocument& return true; } +void QgsComposerItem::setFrameEnabled( bool drawFrame ) +{ + mFrame = drawFrame; + emit frameChanged(); +} + +void QgsComposerItem::setFrameOutlineWidth( double outlineWidth ) +{ + QPen itemPen = pen(); + if ( itemPen.widthF() == outlineWidth ) + { + //no change + return; + } + itemPen.setWidthF( outlineWidth ); + setPen( itemPen ); + emit frameChanged(); +} + +double QgsComposerItem::estimatedFrameBleed() const +{ + if ( !hasFrame() ) + { + return 0; + } + + return pen().widthF() / 2.0; +} + +QRectF QgsComposerItem::rectWithFrame() const +{ + double frameBleed = estimatedFrameBleed(); + return rect().adjusted( -frameBleed, -frameBleed, frameBleed, frameBleed ); +} + void QgsComposerItem::beginCommand( const QString& commandText, QgsComposerMergeCommand::Context c ) { if ( mComposition ) @@ -432,7 +467,7 @@ void QgsComposerItem::setItemPosition( double x, double y, ItemPositionMode item setItemPosition( x, y, width, height, itemPoint ); } -void QgsComposerItem::setItemPosition( double x, double y, double width, double height, ItemPositionMode itemPoint ) +void QgsComposerItem::setItemPosition( double x, double y, double width, double height, ItemPositionMode itemPoint, bool posIncludesFrame ) { double upperLeftX = x; double upperLeftY = y; @@ -460,6 +495,28 @@ void QgsComposerItem::setItemPosition( double x, double y, double width, double upperLeftY -= height; } + if ( posIncludesFrame ) + { + //adjust position to account for frame size + + if ( mItemRotation == 0 ) + { + upperLeftX += estimatedFrameBleed(); + upperLeftY += estimatedFrameBleed(); + } + else + { + //adjust position for item rotation + QLineF lineToItemOrigin = QLineF( 0, 0, estimatedFrameBleed(), estimatedFrameBleed() ); + lineToItemOrigin.setAngle( -45 - mItemRotation ); + upperLeftX += lineToItemOrigin.x2(); + upperLeftY += lineToItemOrigin.y2(); + } + + width -= 2 * estimatedFrameBleed(); + height -= 2 * estimatedFrameBleed(); + } + setSceneRect( QRectF( upperLeftX, upperLeftY, width, height ) ); } diff --git a/src/core/composer/qgscomposeritem.h b/src/core/composer/qgscomposeritem.h index 95d228c57a6..cf9dc195b6a 100644 --- a/src/core/composer/qgscomposeritem.h +++ b/src/core/composer/qgscomposeritem.h @@ -137,8 +137,9 @@ class CORE_EXPORT QgsComposerItem: public QObject, public QGraphicsRectItem void setItemPosition( double x, double y, ItemPositionMode itemPoint = UpperLeft ); /**Sets item position and width / height in one go + *@param posIncludesFrame set to true if the position and size arguments include the item's frame border @note: this method was added in version 1.6*/ - void setItemPosition( double x, double y, double width, double height, ItemPositionMode itemPoint = UpperLeft ); + void setItemPosition( double x, double y, double width, double height, ItemPositionMode itemPoint = UpperLeft, bool posIncludesFrame = false ); /**Returns item's last used position mode. @note: This property has no effect on actual's item position, which is always the top-left corner. @@ -180,8 +181,33 @@ class CORE_EXPORT QgsComposerItem: public QObject, public QGraphicsRectItem * @note introduced in 1.8 * @see hasFrame */ - void setFrameEnabled( bool drawFrame ) {mFrame = drawFrame;} + void setFrameEnabled( bool drawFrame ); + /** Sets frame outline width + * @param outlineWidth new width for outline frame + * @returns nothing + * @note introduced in 2.2 + * @see setFrameEnabled + */ + virtual void setFrameOutlineWidth( double outlineWidth ); + + /** Returns the estimated amount the item's frame bleeds outside the item's + * actual rectangle. For instance, if the item has a 2mm frame outline, then + * 1mm of this frame is drawn outside the item's rect. In this case the + * return value will be 1.0 + * @note introduced in 2.2 + */ + virtual double estimatedFrameBleed() const; + + /** Returns the item's rectangular bounds, including any bleed caused by the item's frame. + * The bounds are returned in the item's coordinate system (see Qt's QGraphicsItem docs for + * more details about QGraphicsItem coordinate systems). The results differ from Qt's rect() + * function, as rect() makes no allowances for the portion of outlines which are drawn + * outside of the item. + * @note introduced in 2.2 + * @see estimatedFrameBleed + */ + virtual QRectF rectWithFrame() const; /** Whether this item has a Background or not. * @returns true if there is a Background around this item, otherwise false. @@ -456,6 +482,10 @@ class CORE_EXPORT QgsComposerItem: public QObject, public QGraphicsRectItem void itemChanged(); /**Emitted if the rectangle changes*/ void sizeChanged(); + /**Emitted if the item's frame style changes + * @note: this function was introduced in version 2.2 + */ + void frameChanged(); private: // id (not unique) QString mId; diff --git a/src/core/composer/qgscomposermousehandles.cpp b/src/core/composer/qgscomposermousehandles.cpp index dbe2e00308b..3305910e782 100644 --- a/src/core/composer/qgscomposermousehandles.cpp +++ b/src/core/composer/qgscomposermousehandles.cpp @@ -153,7 +153,7 @@ void QgsComposerMouseHandles::drawSelectedItemBounds( QPainter* painter ) { //if currently dragging, draw selected item bounds relative to current mouse position //first, get bounds of current item in scene coordinates - QPolygonF itemSceneBounds = ( *itemIter )->mapToScene(( *itemIter )->rect() ); + QPolygonF itemSceneBounds = ( *itemIter )->mapToScene(( *itemIter )->rectWithFrame() ); //now, translate it by the current movement amount //IMPORTANT - this is done in scene coordinates, since we don't want any rotation/non-translation transforms to affect the movement itemSceneBounds.translate( transform().dx(), transform().dy() ); @@ -166,7 +166,7 @@ void QgsComposerMouseHandles::drawSelectedItemBounds( QPainter* painter ) if ( selectedItems.size() > 1 ) { //get item bounds in mouse handle item's coordinate system - QRectF itemRect = mapRectFromItem(( *itemIter ), ( *itemIter )->rect() ); + QRectF itemRect = mapRectFromItem(( *itemIter ), ( *itemIter )->rectWithFrame() ); //now, resize it relative to the current resized dimensions of the mouse handles QgsComposition::relativeResizeRect( itemRect, QRectF( -mResizeMoveX, -mResizeMoveY, mBeginHandleWidth, mBeginHandleHeight ), mResizeRect ); itemBounds = QPolygonF( itemRect ); @@ -180,7 +180,7 @@ void QgsComposerMouseHandles::drawSelectedItemBounds( QPainter* painter ) else { //not resizing or moving, so just map from scene bounds - itemBounds = mapRectFromItem(( *itemIter ), ( *itemIter )->rect() ); + itemBounds = mapRectFromItem(( *itemIter ), ( *itemIter )->rectWithFrame() ); } painter->drawPolygon( itemBounds ); } @@ -189,7 +189,7 @@ void QgsComposerMouseHandles::drawSelectedItemBounds( QPainter* painter ) void QgsComposerMouseHandles::selectionChanged() { - //listen out for selected items' sizeChanged signal + //listen out for selected items' size and rotation changed signals QList itemList = composition()->items(); QList::iterator itemIt = itemList.begin(); for ( ; itemIt != itemList.end(); ++itemIt ) @@ -201,10 +201,13 @@ void QgsComposerMouseHandles::selectionChanged() { QObject::connect( item, SIGNAL( sizeChanged() ), this, SLOT( selectedItemSizeChanged() ) ); QObject::connect( item, SIGNAL( itemRotationChanged( double ) ), this, SLOT( selectedItemRotationChanged() ) ); + QObject::connect( item, SIGNAL( frameChanged( ) ), this, SLOT( selectedItemSizeChanged() ) ); } else { QObject::disconnect( item, SIGNAL( sizeChanged() ), this, 0 ); + QObject::disconnect( item, SIGNAL( itemRotationChanged( double ) ), this, 0 ); + QObject::disconnect( item, SIGNAL( frameChanged( ) ), this, 0 ); } } } @@ -279,12 +282,12 @@ QRectF QgsComposerMouseHandles::selectionBounds() const QList::iterator itemIter = selectedItems.begin(); //start with handle bounds of first selected item - QRectF bounds = mapFromItem(( *itemIter ), ( *itemIter )->rect() ).boundingRect(); + QRectF bounds = mapFromItem(( *itemIter ), ( *itemIter )->rectWithFrame() ).boundingRect(); //iterate through remaining items, expanding the bounds as required for ( ++itemIter; itemIter != selectedItems.end(); ++itemIter ) { - bounds = bounds.united( mapFromItem(( *itemIter ), ( *itemIter )->rect() ).boundingRect() ); + bounds = bounds.united( mapFromItem(( *itemIter ), ( *itemIter )->rectWithFrame() ).boundingRect() ); } return bounds; @@ -627,19 +630,19 @@ void QgsComposerMouseHandles::mouseReleaseEvent( QGraphicsSceneMouseEvent* event QRectF itemRect; if ( selectedItems.size() == 1 ) { - //only a single item is selected, so set it's size to the final resized mouse handle size + //only a single item is selected, so set its size to the final resized mouse handle size itemRect = mResizeRect; } else { //multiple items selected, so each needs to be scaled relatively to the final size of the mouse handles - itemRect = mapRectFromItem(( *itemIter ), ( *itemIter )->rect() ); + itemRect = mapRectFromItem(( *itemIter ), ( *itemIter )->rectWithFrame() ); QgsComposition::relativeResizeRect( itemRect, QRectF( -mResizeMoveX, -mResizeMoveY, mBeginHandleWidth, mBeginHandleHeight ), mResizeRect ); } itemRect = itemRect.normalized(); QPointF newPos = mapToScene( itemRect.topLeft() ); - ( *itemIter )->setItemPosition( newPos.x(), newPos.y(), itemRect.width(), itemRect.height() ); + ( *itemIter )->setItemPosition( newPos.x(), newPos.y(), itemRect.width(), itemRect.height(), QgsComposerItem::UpperLeft, true ); subcommand->saveAfterState(); } @@ -1255,7 +1258,7 @@ void QgsComposerMouseHandles::collectAlignCoordinates( QMap< double, const QgsCo } else { - itemRect = currentItem->sceneBoundingRect(); + itemRect = currentItem->mapRectToScene( currentItem->rectWithFrame() ); } alignCoordsX.insert( itemRect.left(), currentItem ); alignCoordsX.insert( itemRect.right(), currentItem ); diff --git a/src/core/composer/qgscomposershape.cpp b/src/core/composer/qgscomposershape.cpp index de0a9d7b853..82d23b76f65 100644 --- a/src/core/composer/qgscomposershape.cpp +++ b/src/core/composer/qgscomposershape.cpp @@ -59,6 +59,13 @@ void QgsComposerShape::setShapeStyleSymbol( QgsFillSymbolV2* symbol ) delete mShapeStyleSymbol; mShapeStyleSymbol = symbol; update(); + emit frameChanged(); +} + +void QgsComposerShape::refreshSymbol() +{ + update(); + emit frameChanged(); } void QgsComposerShape::createDefaultShapeStyleSymbol() @@ -242,6 +249,10 @@ void QgsComposerShape::drawBackground( QPainter* p ) } } +double QgsComposerShape::estimatedFrameBleed() const +{ + return QgsSymbolLayerV2Utils::estimateMaxSymbolBleed( mShapeStyleSymbol ); +} bool QgsComposerShape::writeXML( QDomElement& elem, QDomDocument & doc ) const { diff --git a/src/core/composer/qgscomposershape.h b/src/core/composer/qgscomposershape.h index 052fd418eaa..588401e2b29 100644 --- a/src/core/composer/qgscomposershape.h +++ b/src/core/composer/qgscomposershape.h @@ -85,6 +85,15 @@ class CORE_EXPORT QgsComposerShape: public QgsComposerItem virtual void drawFrame( QPainter* p ); /* reimplement drawBackground, since it's not a rect, but a custom shape */ virtual void drawBackground( QPainter* p ); + /**reimplement estimatedFrameBleed, since frames on shapes are drawn using symbology + * rather than the item's pen */ + virtual double estimatedFrameBleed() const; + + public slots: + /**Should be called after the shape's symbol is changed. Redraws the shape and recalculates + * its selection bounds. + * Note: added in version 2.1*/ + void refreshSymbol(); private: /**Ellipse, rectangle or triangle*/