Skip to content

Commit 17d6da9

Browse files
committedJul 23, 2014
[FEATURE][composer] Holding shift while drawing new lines constrains lines to horizontal/vertical/diagonals, while drawing rectangles constrains items to squares. Holding control switches to a draw-from-center mode. Sponsored by City of Uster, Switzerland.
1 parent 622e2d1 commit 17d6da9

File tree

6 files changed

+252
-31
lines changed

6 files changed

+252
-31
lines changed
 

‎python/core/composer/qgscomposerutils.sip

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,19 @@ class QgsComposerUtils
3333
* @param y in/out: y cooreinate before / after the rotation
3434
*/
3535
static void rotate( const double angle, double& x, double& y );
36+
37+
/**Ensures that an angle is in the range 0 <= angle < 360
38+
* @param angle angle in degrees
39+
* @returns equivalent angle within the range [0, 360)
40+
* @see snappedAngle
41+
*/
42+
static double normalizedAngle( const double angle );
43+
44+
/**Snaps an angle to its closest 45 degree angle
45+
* @param angle angle in degrees
46+
* @returns angle snapped to 0, 45/90/135/180/225/270 or 315 degrees
47+
*/
48+
static double snappedAngle( const double angle );
3649

3750
/**Calculates the largest scaled version of originalRect which fits within boundsRect, when it is rotated by
3851
* a specified amount.

‎src/core/composer/qgscomposerutils.cpp

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,60 @@ void QgsComposerUtils::rotate( const double angle, double &x, double &y )
9292
y = yRot;
9393
}
9494

95+
double QgsComposerUtils::normalizedAngle( const double angle )
96+
{
97+
double clippedAngle = angle;
98+
if ( clippedAngle >= 360.0 || clippedAngle <= -360.0 )
99+
{
100+
clippedAngle = fmod( clippedAngle, 360.0 );
101+
}
102+
if ( clippedAngle < 0.0 )
103+
{
104+
clippedAngle += 360.0;
105+
}
106+
return clippedAngle;
107+
}
108+
109+
double QgsComposerUtils::snappedAngle( const double angle )
110+
{
111+
//normalize angle to 0-360 degrees
112+
double clippedAngle = normalizedAngle( angle );
113+
114+
//snap angle to 45 degree
115+
if ( clippedAngle >= 22.5 && clippedAngle < 67.5 )
116+
{
117+
return 45.0;
118+
}
119+
else if ( clippedAngle >= 67.5 && clippedAngle < 112.5 )
120+
{
121+
return 90.0;
122+
}
123+
else if ( clippedAngle >= 112.5 && clippedAngle < 157.5 )
124+
{
125+
return 135.0;
126+
}
127+
else if ( clippedAngle >= 157.5 && clippedAngle < 202.5 )
128+
{
129+
return 180.0;
130+
}
131+
else if ( clippedAngle >= 202.5 && clippedAngle < 247.5 )
132+
{
133+
return 225.0;
134+
}
135+
else if ( clippedAngle >= 247.5 && clippedAngle < 292.5 )
136+
{
137+
return 270.0;
138+
}
139+
else if ( clippedAngle >= 292.5 && clippedAngle < 337.5 )
140+
{
141+
return 315.0;
142+
}
143+
else
144+
{
145+
return 0.0;
146+
}
147+
}
148+
95149
QRectF QgsComposerUtils::largestRotatedRectWithinBounds( const QRectF originalRect, const QRectF boundsRect, const double rotation )
96150
{
97151
double originalWidth = originalRect.width();
@@ -100,7 +154,7 @@ QRectF QgsComposerUtils::largestRotatedRectWithinBounds( const QRectF originalRe
100154
double boundsHeight = boundsRect.height();
101155
double ratioBoundsRect = boundsWidth / boundsHeight;
102156

103-
double clippedRotation = fmod( rotation, 360.0 );
157+
double clippedRotation = normalizedAngle( rotation );
104158

105159
//shortcut for some rotation values
106160
if ( clippedRotation == 0 || clippedRotation == 90 || clippedRotation == 180 || clippedRotation == 270 )

‎src/core/composer/qgscomposerutils.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,19 @@ class CORE_EXPORT QgsComposerUtils
5555
*/
5656
static void rotate( const double angle, double& x, double& y );
5757

58+
/**Ensures that an angle is in the range 0 <= angle < 360
59+
* @param angle angle in degrees
60+
* @returns equivalent angle within the range [0, 360)
61+
* @see snappedAngle
62+
*/
63+
static double normalizedAngle( const double angle );
64+
65+
/**Snaps an angle to its closest 45 degree angle
66+
* @param angle angle in degrees
67+
* @returns angle snapped to 0, 45/90/135/180/225/270 or 315 degrees
68+
*/
69+
static double snappedAngle( const double angle );
70+
5871
/**Calculates the largest scaled version of originalRect which fits within boundsRect, when it is rotated by
5972
* a specified amount.
6073
* @param originalRect QRectF to be rotated and scaled

‎src/gui/qgscomposerview.cpp

Lines changed: 92 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
#include "qgspaperitem.h"
4646
#include "qgsmapcanvas.h" //for QgsMapCanvas::WheelAction
4747
#include "qgscursors.h"
48+
#include "qgscomposerutils.h"
4849

4950
QgsComposerView::QgsComposerView( QWidget* parent, const char* name, Qt::WindowFlags f )
5051
: QGraphicsView( parent )
@@ -333,6 +334,7 @@ void QgsComposerView::mousePressEvent( QMouseEvent* e )
333334
{
334335
mRubberBandStartPos = QPointF( snappedScenePoint.x(), snappedScenePoint.y() );
335336
mRubberBandLineItem = new QGraphicsLineItem( snappedScenePoint.x(), snappedScenePoint.y(), snappedScenePoint.x(), snappedScenePoint.y() );
337+
mRubberBandLineItem->setPen( QPen( QBrush( QColor( 227, 22, 22, 200 ) ), 0 ) );
336338
mRubberBandLineItem->setZValue( 1000 );
337339
scene()->addItem( mRubberBandLineItem );
338340
scene()->update();
@@ -352,6 +354,8 @@ void QgsComposerView::mousePressEvent( QMouseEvent* e )
352354
{
353355
QTransform t;
354356
mRubberBandItem = new QGraphicsRectItem( 0, 0, 0, 0 );
357+
mRubberBandItem->setBrush( Qt::NoBrush );
358+
mRubberBandItem->setPen( QPen( QBrush( QColor( 227, 22, 22, 200 ) ), 0 ) );
355359
mRubberBandStartPos = QPointF( snappedScenePoint.x(), snappedScenePoint.y() );
356360
t.translate( snappedScenePoint.x(), snappedScenePoint.y() );
357361
mRubberBandItem->setTransform( t );
@@ -486,8 +490,8 @@ void QgsComposerView::startMarqueeSelect( QPointF & scenePoint )
486490

487491
QTransform t;
488492
mRubberBandItem = new QGraphicsRectItem( 0, 0, 0, 0 );
489-
mRubberBandItem->setBrush( QBrush( QColor( 225, 50, 70, 25 ) ) );
490-
mRubberBandItem->setPen( QPen( Qt::DotLine ) );
493+
mRubberBandItem->setBrush( QBrush( QColor( 224, 178, 76, 63 ) ) );
494+
mRubberBandItem->setPen( QPen( QBrush( QColor( 254, 58, 29, 100 ) ), 0, Qt::DotLine ) );
491495
mRubberBandStartPos = QPointF( scenePoint.x(), scenePoint.y() );
492496
t.translate( scenePoint.x(), scenePoint.y() );
493497
mRubberBandItem->setTransform( t );
@@ -742,9 +746,7 @@ void QgsComposerView::mouseReleaseEvent( QMouseEvent* e )
742746
}
743747
else
744748
{
745-
QPointF scenePoint = mapToScene( e->pos() );
746-
QPointF snappedScenePoint = composition()->snapPointToGrid( scenePoint );
747-
QgsComposerArrow* composerArrow = new QgsComposerArrow( mRubberBandStartPos, QPointF( snappedScenePoint.x(), snappedScenePoint.y() ), composition() );
749+
QgsComposerArrow* composerArrow = new QgsComposerArrow( mRubberBandLineItem->line().p1(), mRubberBandLineItem->line().p2(), composition() );
748750
composition()->addComposerArrow( composerArrow );
749751

750752
composition()->clearSelection();
@@ -925,6 +927,19 @@ void QgsComposerView::mouseMoveEvent( QMouseEvent* e )
925927
return;
926928
}
927929

930+
bool shiftModifier = false;
931+
bool controlModifier = false;
932+
if ( e->modifiers() & Qt::ShiftModifier )
933+
{
934+
//shift key depressed
935+
shiftModifier = true;
936+
}
937+
if ( e->modifiers() & Qt::ControlModifier )
938+
{
939+
//control key depressed
940+
controlModifier = true;
941+
}
942+
928943
mMouseCurrentXY = e->pos();
929944
//update cursor position in composer status bar
930945
emit cursorPosChanged( mapToScene( e->pos() ) );
@@ -960,7 +975,7 @@ void QgsComposerView::mouseMoveEvent( QMouseEvent* e )
960975

961976
if ( mMarqueeSelect || mMarqueeZoom )
962977
{
963-
updateRubberBand( scenePoint );
978+
updateRubberBandRect( scenePoint );
964979
return;
965980
}
966981

@@ -972,10 +987,7 @@ void QgsComposerView::mouseMoveEvent( QMouseEvent* e )
972987

973988
case AddArrow:
974989
{
975-
if ( mRubberBandLineItem )
976-
{
977-
mRubberBandLineItem->setLine( mRubberBandStartPos.x(), mRubberBandStartPos.y(), scenePoint.x(), scenePoint.y() );
978-
}
990+
updateRubberBandLine( scenePoint, shiftModifier );
979991
break;
980992
}
981993

@@ -990,7 +1002,7 @@ void QgsComposerView::mouseMoveEvent( QMouseEvent* e )
9901002
case AddTable:
9911003
//adjust rubber band item
9921004
{
993-
updateRubberBand( scenePoint );
1005+
updateRubberBandRect( scenePoint, shiftModifier, controlModifier );
9941006
break;
9951007
}
9961008

@@ -1011,8 +1023,13 @@ void QgsComposerView::mouseMoveEvent( QMouseEvent* e )
10111023
}
10121024
}
10131025

1014-
void QgsComposerView::updateRubberBand( QPointF & pos )
1026+
void QgsComposerView::updateRubberBandRect( QPointF & pos, const bool constrainSquare, const bool fromCenter )
10151027
{
1028+
if ( !mRubberBandItem )
1029+
{
1030+
return;
1031+
}
1032+
10161033
double x = 0;
10171034
double y = 0;
10181035
double width = 0;
@@ -1021,35 +1038,82 @@ void QgsComposerView::updateRubberBand( QPointF & pos )
10211038
double dx = pos.x() - mRubberBandStartPos.x();
10221039
double dy = pos.y() - mRubberBandStartPos.y();
10231040

1024-
if ( dx < 0 )
1041+
if ( constrainSquare )
10251042
{
1026-
x = pos.x();
1027-
width = -dx;
1043+
if ( fabs( dx ) > fabs( dy ) )
1044+
{
1045+
width = fabs( dx );
1046+
height = width;
1047+
}
1048+
else
1049+
{
1050+
height = fabs( dy );
1051+
width = height;
1052+
}
1053+
1054+
x = mRubberBandStartPos.x() - (( dx < 0 ) ? width : 0 );
1055+
y = mRubberBandStartPos.y() - (( dy < 0 ) ? height : 0 );
10281056
}
10291057
else
10301058
{
1031-
x = mRubberBandStartPos.x();
1032-
width = dx;
1059+
//not constraining
1060+
if ( dx < 0 )
1061+
{
1062+
x = pos.x();
1063+
width = -dx;
1064+
}
1065+
else
1066+
{
1067+
x = mRubberBandStartPos.x();
1068+
width = dx;
1069+
}
1070+
1071+
if ( dy < 0 )
1072+
{
1073+
y = pos.y();
1074+
height = -dy;
1075+
}
1076+
else
1077+
{
1078+
y = mRubberBandStartPos.y();
1079+
height = dy;
1080+
}
10331081
}
10341082

1035-
if ( dy < 0 )
1083+
if ( fromCenter )
10361084
{
1037-
y = pos.y();
1038-
height = -dy;
1085+
x = mRubberBandStartPos.x() - width;
1086+
y = mRubberBandStartPos.y() - height;
1087+
width *= 2.0;
1088+
height *= 2.0;
10391089
}
1040-
else
1090+
1091+
mRubberBandItem->setRect( 0, 0, width, height );
1092+
QTransform t;
1093+
t.translate( x, y );
1094+
mRubberBandItem->setTransform( t );
1095+
}
1096+
1097+
void QgsComposerView::updateRubberBandLine( const QPointF &pos, const bool constrainAngles )
1098+
{
1099+
if ( !mRubberBandLineItem )
10411100
{
1042-
y = mRubberBandStartPos.y();
1043-
height = dy;
1101+
return;
10441102
}
10451103

1046-
if ( mRubberBandItem )
1104+
//snap to grid
1105+
QPointF snappedScenePoint = composition()->snapPointToGrid( pos );
1106+
1107+
QLineF newLine = QLineF( mRubberBandStartPos, snappedScenePoint );
1108+
1109+
if ( constrainAngles )
10471110
{
1048-
mRubberBandItem->setRect( 0, 0, width, height );
1049-
QTransform t;
1050-
t.translate( x, y );
1051-
mRubberBandItem->setTransform( t );
1111+
//movement is contrained to 45 degree angles
1112+
double angle = QgsComposerUtils::snappedAngle( newLine.angle() );
1113+
newLine.setAngle( angle );
10521114
}
1115+
1116+
mRubberBandLineItem->setLine( newLine );
10531117
}
10541118

10551119
void QgsComposerView::mouseDoubleClickEvent( QMouseEvent* e )

‎src/gui/qgscomposerview.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,10 @@ class GUI_EXPORT QgsComposerView: public QGraphicsView
229229

230230
/**Zoom composition from a mouse wheel event*/
231231
void wheelZoom( QWheelEvent * event );
232-
/**Redraws the rubber band*/
233-
void updateRubberBand( QPointF & pos );
232+
/**Redraws the rectangular rubber band*/
233+
void updateRubberBandRect( QPointF & pos, const bool constrainSquare = false, const bool fromCenter = false );
234+
/**Redraws the linear rubber band*/
235+
void updateRubberBandLine( const QPointF & pos, const bool constrainAngles = false );
234236
/**Removes the rubber band and cleans up*/
235237
void removeRubberBand();
236238

‎tests/src/core/testqgscomposerutils.cpp

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class TestQgsComposerUtils: public QObject
3636
void drawArrowHead(); //test drawing an arrow head
3737
void angle(); //test angle utility function
3838
void rotate(); //test rotation helper function
39+
void normalizedAngle(); //test normalised angle function
40+
void snappedAngle(); //test snapped angle function
3941
void largestRotatedRect(); //test largest rotated rect helper function
4042
void pointsToMM(); //test conversion of point size to mm
4143
void mmToPoints(); //test conversion of mm to point size
@@ -157,6 +159,79 @@ void TestQgsComposerUtils::rotate()
157159
}
158160
}
159161

162+
void TestQgsComposerUtils::normalizedAngle()
163+
{
164+
QList< QPair< double, double > > testVals;
165+
testVals << qMakePair( 0.0, 0.0 );
166+
testVals << qMakePair( 90.0, 90.0 );
167+
testVals << qMakePair( 180.0, 180.0 );
168+
testVals << qMakePair( 270.0, 270.0 );
169+
testVals << qMakePair( 360.0, 0.0 );
170+
testVals << qMakePair( 390.0, 30.0 );
171+
testVals << qMakePair( 720.0, 0.0 );
172+
testVals << qMakePair( 730.0, 10.0 );
173+
testVals << qMakePair( -10.0, 350.0 );
174+
testVals << qMakePair( -360.0, 0.0 );
175+
testVals << qMakePair( -370.0, 350.0 );
176+
testVals << qMakePair( -760.0, 320.0 );
177+
178+
//test normalized angle helper function
179+
QList< QPair< double, double > >::const_iterator it = testVals.constBegin();
180+
for ( ; it != testVals.constEnd(); ++it )
181+
{
182+
QVERIFY( qgsDoubleNear( QgsComposerUtils::normalizedAngle(( *it ).first ), ( *it ).second ) );
183+
}
184+
}
185+
186+
void TestQgsComposerUtils::snappedAngle()
187+
{
188+
QList< QPair< double, double > > testVals;
189+
testVals << qMakePair( 0.0, 0.0 );
190+
testVals << qMakePair( 10.0, 0.0 );
191+
testVals << qMakePair( 20.0, 0.0 );
192+
testVals << qMakePair( 30.0, 45.0 );
193+
testVals << qMakePair( 40.0, 45.0 );
194+
testVals << qMakePair( 50.0, 45.0 );
195+
testVals << qMakePair( 60.0, 45.0 );
196+
testVals << qMakePair( 70.0, 90.0 );
197+
testVals << qMakePair( 80.0, 90.0 );
198+
testVals << qMakePair( 90.0, 90.0 );
199+
testVals << qMakePair( 100.0, 90.0 );
200+
testVals << qMakePair( 110.0, 90.0 );
201+
testVals << qMakePair( 120.0, 135.0 );
202+
testVals << qMakePair( 130.0, 135.0 );
203+
testVals << qMakePair( 140.0, 135.0 );
204+
testVals << qMakePair( 150.0, 135.0 );
205+
testVals << qMakePair( 160.0, 180.0 );
206+
testVals << qMakePair( 170.0, 180.0 );
207+
testVals << qMakePair( 180.0, 180.0 );
208+
testVals << qMakePair( 190.0, 180.0 );
209+
testVals << qMakePair( 200.0, 180.0 );
210+
testVals << qMakePair( 210.0, 225.0 );
211+
testVals << qMakePair( 220.0, 225.0 );
212+
testVals << qMakePair( 230.0, 225.0 );
213+
testVals << qMakePair( 240.0, 225.0 );
214+
testVals << qMakePair( 250.0, 270.0 );
215+
testVals << qMakePair( 260.0, 270.0 );
216+
testVals << qMakePair( 270.0, 270.0 );
217+
testVals << qMakePair( 280.0, 270.0 );
218+
testVals << qMakePair( 290.0, 270.0 );
219+
testVals << qMakePair( 300.0, 315.0 );
220+
testVals << qMakePair( 310.0, 315.0 );
221+
testVals << qMakePair( 320.0, 315.0 );
222+
testVals << qMakePair( 330.0, 315.0 );
223+
testVals << qMakePair( 340.0, 0.0 );
224+
testVals << qMakePair( 350.0, 0.0 );
225+
testVals << qMakePair( 360.0, 0.0 );
226+
227+
//test snapped angle helper function
228+
QList< QPair< double, double > >::const_iterator it = testVals.constBegin();
229+
for ( ; it != testVals.constEnd(); ++it )
230+
{
231+
QVERIFY( qgsDoubleNear( QgsComposerUtils::snappedAngle(( *it ).first ), ( *it ).second ) );
232+
}
233+
}
234+
160235
void TestQgsComposerUtils::largestRotatedRect()
161236
{
162237
QRectF wideRect = QRectF( 0, 0, 2, 1 );

2 commit comments

Comments
 (2)

olivierdalang commented on Jul 23, 2014

@olivierdalang
Contributor

@nyalldawson : thanks for this feature !
The adobe suite as well as some other software use the alt modifier instead of the ctrl modifier to "draw from center", did you consider using it ? (not sure how standard it is though, but alt seems more natural to me)

nyalldawson commented on Jul 23, 2014

@nyalldawson
CollaboratorAuthor

@olivierdalang I'll do a bit of an audit and check. I'm usually inclined to just follow what the adobe suite uses, so I'll switch the baheviour unless there's a strong trend to use ctrl

Please sign in to comment.