diff --git a/python/core/composer/qgscomposerutils.sip b/python/core/composer/qgscomposerutils.sip index eddda487b77..a07029aa9ba 100644 --- a/python/core/composer/qgscomposerutils.sip +++ b/python/core/composer/qgscomposerutils.sip @@ -33,6 +33,19 @@ class QgsComposerUtils * @param y in/out: y cooreinate before / after the rotation */ static void rotate( const double angle, double& x, double& y ); + + /**Ensures that an angle is in the range 0 <= angle < 360 + * @param angle angle in degrees + * @returns equivalent angle within the range [0, 360) + * @see snappedAngle + */ + static double normalizedAngle( const double angle ); + + /**Snaps an angle to its closest 45 degree angle + * @param angle angle in degrees + * @returns angle snapped to 0, 45/90/135/180/225/270 or 315 degrees + */ + static double snappedAngle( const double angle ); /**Calculates the largest scaled version of originalRect which fits within boundsRect, when it is rotated by * a specified amount. diff --git a/src/core/composer/qgscomposerutils.cpp b/src/core/composer/qgscomposerutils.cpp index 725087ebeae..72ec87fbbf1 100644 --- a/src/core/composer/qgscomposerutils.cpp +++ b/src/core/composer/qgscomposerutils.cpp @@ -92,6 +92,60 @@ void QgsComposerUtils::rotate( const double angle, double &x, double &y ) y = yRot; } +double QgsComposerUtils::normalizedAngle( const double angle ) +{ + double clippedAngle = angle; + if ( clippedAngle >= 360.0 || clippedAngle <= -360.0 ) + { + clippedAngle = fmod( clippedAngle, 360.0 ); + } + if ( clippedAngle < 0.0 ) + { + clippedAngle += 360.0; + } + return clippedAngle; +} + +double QgsComposerUtils::snappedAngle( const double angle ) +{ + //normalize angle to 0-360 degrees + double clippedAngle = normalizedAngle( angle ); + + //snap angle to 45 degree + if ( clippedAngle >= 22.5 && clippedAngle < 67.5 ) + { + return 45.0; + } + else if ( clippedAngle >= 67.5 && clippedAngle < 112.5 ) + { + return 90.0; + } + else if ( clippedAngle >= 112.5 && clippedAngle < 157.5 ) + { + return 135.0; + } + else if ( clippedAngle >= 157.5 && clippedAngle < 202.5 ) + { + return 180.0; + } + else if ( clippedAngle >= 202.5 && clippedAngle < 247.5 ) + { + return 225.0; + } + else if ( clippedAngle >= 247.5 && clippedAngle < 292.5 ) + { + return 270.0; + } + else if ( clippedAngle >= 292.5 && clippedAngle < 337.5 ) + { + return 315.0; + } + else + { + return 0.0; + } +} + QRectF QgsComposerUtils::largestRotatedRectWithinBounds( const QRectF originalRect, const QRectF boundsRect, const double rotation ) { double originalWidth = originalRect.width(); @@ -100,7 +154,7 @@ QRectF QgsComposerUtils::largestRotatedRectWithinBounds( const QRectF originalRe double boundsHeight = boundsRect.height(); double ratioBoundsRect = boundsWidth / boundsHeight; - double clippedRotation = fmod( rotation, 360.0 ); + double clippedRotation = normalizedAngle( rotation ); //shortcut for some rotation values if ( clippedRotation == 0 || clippedRotation == 90 || clippedRotation == 180 || clippedRotation == 270 ) diff --git a/src/core/composer/qgscomposerutils.h b/src/core/composer/qgscomposerutils.h index a428a19fe53..880653fc663 100644 --- a/src/core/composer/qgscomposerutils.h +++ b/src/core/composer/qgscomposerutils.h @@ -55,6 +55,19 @@ class CORE_EXPORT QgsComposerUtils */ static void rotate( const double angle, double& x, double& y ); + /**Ensures that an angle is in the range 0 <= angle < 360 + * @param angle angle in degrees + * @returns equivalent angle within the range [0, 360) + * @see snappedAngle + */ + static double normalizedAngle( const double angle ); + + /**Snaps an angle to its closest 45 degree angle + * @param angle angle in degrees + * @returns angle snapped to 0, 45/90/135/180/225/270 or 315 degrees + */ + static double snappedAngle( const double angle ); + /**Calculates the largest scaled version of originalRect which fits within boundsRect, when it is rotated by * a specified amount. * @param originalRect QRectF to be rotated and scaled diff --git a/src/gui/qgscomposerview.cpp b/src/gui/qgscomposerview.cpp index 2c7ed7be788..81dcd84e56f 100644 --- a/src/gui/qgscomposerview.cpp +++ b/src/gui/qgscomposerview.cpp @@ -45,6 +45,7 @@ #include "qgspaperitem.h" #include "qgsmapcanvas.h" //for QgsMapCanvas::WheelAction #include "qgscursors.h" +#include "qgscomposerutils.h" QgsComposerView::QgsComposerView( QWidget* parent, const char* name, Qt::WindowFlags f ) : QGraphicsView( parent ) @@ -333,6 +334,7 @@ void QgsComposerView::mousePressEvent( QMouseEvent* e ) { mRubberBandStartPos = QPointF( snappedScenePoint.x(), snappedScenePoint.y() ); mRubberBandLineItem = new QGraphicsLineItem( snappedScenePoint.x(), snappedScenePoint.y(), snappedScenePoint.x(), snappedScenePoint.y() ); + mRubberBandLineItem->setPen( QPen( QBrush( QColor( 227, 22, 22, 200 ) ), 0 ) ); mRubberBandLineItem->setZValue( 1000 ); scene()->addItem( mRubberBandLineItem ); scene()->update(); @@ -352,6 +354,8 @@ void QgsComposerView::mousePressEvent( QMouseEvent* e ) { QTransform t; mRubberBandItem = new QGraphicsRectItem( 0, 0, 0, 0 ); + mRubberBandItem->setBrush( Qt::NoBrush ); + mRubberBandItem->setPen( QPen( QBrush( QColor( 227, 22, 22, 200 ) ), 0 ) ); mRubberBandStartPos = QPointF( snappedScenePoint.x(), snappedScenePoint.y() ); t.translate( snappedScenePoint.x(), snappedScenePoint.y() ); mRubberBandItem->setTransform( t ); @@ -486,8 +490,8 @@ void QgsComposerView::startMarqueeSelect( QPointF & scenePoint ) QTransform t; mRubberBandItem = new QGraphicsRectItem( 0, 0, 0, 0 ); - mRubberBandItem->setBrush( QBrush( QColor( 225, 50, 70, 25 ) ) ); - mRubberBandItem->setPen( QPen( Qt::DotLine ) ); + mRubberBandItem->setBrush( QBrush( QColor( 224, 178, 76, 63 ) ) ); + mRubberBandItem->setPen( QPen( QBrush( QColor( 254, 58, 29, 100 ) ), 0, Qt::DotLine ) ); mRubberBandStartPos = QPointF( scenePoint.x(), scenePoint.y() ); t.translate( scenePoint.x(), scenePoint.y() ); mRubberBandItem->setTransform( t ); @@ -742,9 +746,7 @@ void QgsComposerView::mouseReleaseEvent( QMouseEvent* e ) } else { - QPointF scenePoint = mapToScene( e->pos() ); - QPointF snappedScenePoint = composition()->snapPointToGrid( scenePoint ); - QgsComposerArrow* composerArrow = new QgsComposerArrow( mRubberBandStartPos, QPointF( snappedScenePoint.x(), snappedScenePoint.y() ), composition() ); + QgsComposerArrow* composerArrow = new QgsComposerArrow( mRubberBandLineItem->line().p1(), mRubberBandLineItem->line().p2(), composition() ); composition()->addComposerArrow( composerArrow ); composition()->clearSelection(); @@ -925,6 +927,19 @@ void QgsComposerView::mouseMoveEvent( QMouseEvent* e ) return; } + bool shiftModifier = false; + bool controlModifier = false; + if ( e->modifiers() & Qt::ShiftModifier ) + { + //shift key depressed + shiftModifier = true; + } + if ( e->modifiers() & Qt::ControlModifier ) + { + //control key depressed + controlModifier = true; + } + mMouseCurrentXY = e->pos(); //update cursor position in composer status bar emit cursorPosChanged( mapToScene( e->pos() ) ); @@ -960,7 +975,7 @@ void QgsComposerView::mouseMoveEvent( QMouseEvent* e ) if ( mMarqueeSelect || mMarqueeZoom ) { - updateRubberBand( scenePoint ); + updateRubberBandRect( scenePoint ); return; } @@ -972,10 +987,7 @@ void QgsComposerView::mouseMoveEvent( QMouseEvent* e ) case AddArrow: { - if ( mRubberBandLineItem ) - { - mRubberBandLineItem->setLine( mRubberBandStartPos.x(), mRubberBandStartPos.y(), scenePoint.x(), scenePoint.y() ); - } + updateRubberBandLine( scenePoint, shiftModifier ); break; } @@ -990,7 +1002,7 @@ void QgsComposerView::mouseMoveEvent( QMouseEvent* e ) case AddTable: //adjust rubber band item { - updateRubberBand( scenePoint ); + updateRubberBandRect( scenePoint, shiftModifier, controlModifier ); break; } @@ -1011,8 +1023,13 @@ void QgsComposerView::mouseMoveEvent( QMouseEvent* e ) } } -void QgsComposerView::updateRubberBand( QPointF & pos ) +void QgsComposerView::updateRubberBandRect( QPointF & pos, const bool constrainSquare, const bool fromCenter ) { + if ( !mRubberBandItem ) + { + return; + } + double x = 0; double y = 0; double width = 0; @@ -1021,35 +1038,82 @@ void QgsComposerView::updateRubberBand( QPointF & pos ) double dx = pos.x() - mRubberBandStartPos.x(); double dy = pos.y() - mRubberBandStartPos.y(); - if ( dx < 0 ) + if ( constrainSquare ) { - x = pos.x(); - width = -dx; + if ( fabs( dx ) > fabs( dy ) ) + { + width = fabs( dx ); + height = width; + } + else + { + height = fabs( dy ); + width = height; + } + + x = mRubberBandStartPos.x() - (( dx < 0 ) ? width : 0 ); + y = mRubberBandStartPos.y() - (( dy < 0 ) ? height : 0 ); } else { - x = mRubberBandStartPos.x(); - width = dx; + //not constraining + if ( dx < 0 ) + { + x = pos.x(); + width = -dx; + } + else + { + x = mRubberBandStartPos.x(); + width = dx; + } + + if ( dy < 0 ) + { + y = pos.y(); + height = -dy; + } + else + { + y = mRubberBandStartPos.y(); + height = dy; + } } - if ( dy < 0 ) + if ( fromCenter ) { - y = pos.y(); - height = -dy; - } - else - { - y = mRubberBandStartPos.y(); - height = dy; + x = mRubberBandStartPos.x() - width; + y = mRubberBandStartPos.y() - height; + width *= 2.0; + height *= 2.0; } - if ( mRubberBandItem ) + mRubberBandItem->setRect( 0, 0, width, height ); + QTransform t; + t.translate( x, y ); + mRubberBandItem->setTransform( t ); +} + +void QgsComposerView::updateRubberBandLine( const QPointF &pos, const bool constrainAngles ) +{ + if ( !mRubberBandLineItem ) { - mRubberBandItem->setRect( 0, 0, width, height ); - QTransform t; - t.translate( x, y ); - mRubberBandItem->setTransform( t ); + return; } + + //snap to grid + QPointF snappedScenePoint = composition()->snapPointToGrid( pos ); + + QLineF newLine = QLineF( mRubberBandStartPos, snappedScenePoint ); + + if ( constrainAngles ) + { + //movement is contrained to 45 degree angles + double angle = QgsComposerUtils::snappedAngle( newLine.angle() ); + newLine.setAngle( angle ); + } + + mRubberBandLineItem->setLine( newLine ); } void QgsComposerView::mouseDoubleClickEvent( QMouseEvent* e ) diff --git a/src/gui/qgscomposerview.h b/src/gui/qgscomposerview.h index cf73dc0ff23..29a2b7e8c54 100644 --- a/src/gui/qgscomposerview.h +++ b/src/gui/qgscomposerview.h @@ -229,8 +229,10 @@ class GUI_EXPORT QgsComposerView: public QGraphicsView /**Zoom composition from a mouse wheel event*/ void wheelZoom( QWheelEvent * event ); - /**Redraws the rubber band*/ - void updateRubberBand( QPointF & pos ); + /**Redraws the rectangular rubber band*/ + void updateRubberBandRect( QPointF & pos, const bool constrainSquare = false, const bool fromCenter = false ); + /**Redraws the linear rubber band*/ + void updateRubberBandLine( const QPointF & pos, const bool constrainAngles = false ); /**Removes the rubber band and cleans up*/ void removeRubberBand(); diff --git a/tests/src/core/testqgscomposerutils.cpp b/tests/src/core/testqgscomposerutils.cpp index 2be8143fc6c..4146cc49059 100644 --- a/tests/src/core/testqgscomposerutils.cpp +++ b/tests/src/core/testqgscomposerutils.cpp @@ -36,6 +36,8 @@ class TestQgsComposerUtils: public QObject void drawArrowHead(); //test drawing an arrow head void angle(); //test angle utility function void rotate(); //test rotation helper function + void normalizedAngle(); //test normalised angle function + void snappedAngle(); //test snapped angle function void largestRotatedRect(); //test largest rotated rect helper function void pointsToMM(); //test conversion of point size to mm void mmToPoints(); //test conversion of mm to point size @@ -157,6 +159,79 @@ void TestQgsComposerUtils::rotate() } } +void TestQgsComposerUtils::normalizedAngle() +{ + QList< QPair< double, double > > testVals; + testVals << qMakePair( 0.0, 0.0 ); + testVals << qMakePair( 90.0, 90.0 ); + testVals << qMakePair( 180.0, 180.0 ); + testVals << qMakePair( 270.0, 270.0 ); + testVals << qMakePair( 360.0, 0.0 ); + testVals << qMakePair( 390.0, 30.0 ); + testVals << qMakePair( 720.0, 0.0 ); + testVals << qMakePair( 730.0, 10.0 ); + testVals << qMakePair( -10.0, 350.0 ); + testVals << qMakePair( -360.0, 0.0 ); + testVals << qMakePair( -370.0, 350.0 ); + testVals << qMakePair( -760.0, 320.0 ); + + //test normalized angle helper function + QList< QPair< double, double > >::const_iterator it = testVals.constBegin(); + for ( ; it != testVals.constEnd(); ++it ) + { + QVERIFY( qgsDoubleNear( QgsComposerUtils::normalizedAngle(( *it ).first ), ( *it ).second ) ); + } +} + +void TestQgsComposerUtils::snappedAngle() +{ + QList< QPair< double, double > > testVals; + testVals << qMakePair( 0.0, 0.0 ); + testVals << qMakePair( 10.0, 0.0 ); + testVals << qMakePair( 20.0, 0.0 ); + testVals << qMakePair( 30.0, 45.0 ); + testVals << qMakePair( 40.0, 45.0 ); + testVals << qMakePair( 50.0, 45.0 ); + testVals << qMakePair( 60.0, 45.0 ); + testVals << qMakePair( 70.0, 90.0 ); + testVals << qMakePair( 80.0, 90.0 ); + testVals << qMakePair( 90.0, 90.0 ); + testVals << qMakePair( 100.0, 90.0 ); + testVals << qMakePair( 110.0, 90.0 ); + testVals << qMakePair( 120.0, 135.0 ); + testVals << qMakePair( 130.0, 135.0 ); + testVals << qMakePair( 140.0, 135.0 ); + testVals << qMakePair( 150.0, 135.0 ); + testVals << qMakePair( 160.0, 180.0 ); + testVals << qMakePair( 170.0, 180.0 ); + testVals << qMakePair( 180.0, 180.0 ); + testVals << qMakePair( 190.0, 180.0 ); + testVals << qMakePair( 200.0, 180.0 ); + testVals << qMakePair( 210.0, 225.0 ); + testVals << qMakePair( 220.0, 225.0 ); + testVals << qMakePair( 230.0, 225.0 ); + testVals << qMakePair( 240.0, 225.0 ); + testVals << qMakePair( 250.0, 270.0 ); + testVals << qMakePair( 260.0, 270.0 ); + testVals << qMakePair( 270.0, 270.0 ); + testVals << qMakePair( 280.0, 270.0 ); + testVals << qMakePair( 290.0, 270.0 ); + testVals << qMakePair( 300.0, 315.0 ); + testVals << qMakePair( 310.0, 315.0 ); + testVals << qMakePair( 320.0, 315.0 ); + testVals << qMakePair( 330.0, 315.0 ); + testVals << qMakePair( 340.0, 0.0 ); + testVals << qMakePair( 350.0, 0.0 ); + testVals << qMakePair( 360.0, 0.0 ); + + //test snapped angle helper function + QList< QPair< double, double > >::const_iterator it = testVals.constBegin(); + for ( ; it != testVals.constEnd(); ++it ) + { + QVERIFY( qgsDoubleNear( QgsComposerUtils::snappedAngle(( *it ).first ), ( *it ).second ) ); + } +} + void TestQgsComposerUtils::largestRotatedRect() { QRectF wideRect = QRectF( 0, 0, 2, 1 );