Skip to content

Commit 420821f

Browse files
committedNov 7, 2017
Fix correct frame bounding rects for shapes and node items
1 parent 9a08fad commit 420821f

13 files changed

+196
-6
lines changed
 

‎python/core/layout/qgslayoutitemnodeitem.sip

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ Returns the number of nodes in the shape.
108108
Deselects any selected nodes.
109109
%End
110110

111+
virtual QRectF boundingRect() const;
112+
113+
114+
virtual double estimatedFrameBleed() const;
115+
116+
111117
protected:
112118

113119
QgsLayoutNodesItem( QgsLayout *layout );
@@ -133,6 +139,7 @@ Returns the number of nodes in the shape.
133139

134140

135141

142+
136143
virtual bool _addNode( const int nodeIndex, QPointF newNode, const double radius ) = 0;
137144
%Docstring
138145
Method called in addNode.

‎python/core/layout/qgslayoutitemshape.sip

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ class QgsLayoutItemShape : QgsLayoutItem
8181
virtual QRectF boundingRect() const;
8282

8383

84+
virtual double estimatedFrameBleed() const;
85+
86+
8487
protected:
8588

8689
virtual void draw( QgsRenderContext &context, const QStyleOptionGraphicsItem *itemStyle = 0 );

‎src/core/layout/qgslayoutitemnodeitem.cpp

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ void QgsLayoutNodesItem::setNodes( const QPolygonF &nodes )
2929
updateSceneRect();
3030
}
3131

32+
QRectF QgsLayoutNodesItem::boundingRect() const
33+
{
34+
return mCurrentRectangle;
35+
}
36+
37+
double QgsLayoutNodesItem::estimatedFrameBleed() const
38+
{
39+
return mMaxSymbolBleed;
40+
}
41+
3242
QgsLayoutNodesItem::QgsLayoutNodesItem( QgsLayout *layout )
3343
: QgsLayoutItem( layout )
3444
{
@@ -55,6 +65,8 @@ void QgsLayoutNodesItem::init()
5565
setCacheMode( QGraphicsItem::NoCache );
5666
setBackgroundEnabled( false );
5767
setFrameEnabled( false );
68+
69+
connect( this, &QgsLayoutNodesItem::sizePositionChanged, this, &QgsLayoutNodesItem::updateBoundingRect );
5870
}
5971

6072
void QgsLayoutNodesItem::draw( QgsRenderContext &context, const QStyleOptionGraphicsItem *style )
@@ -311,18 +323,24 @@ void QgsLayoutNodesItem::updateSceneRect()
311323
const QRectF br = mPolygon.boundingRect();
312324

313325
const QPointF topLeft = mapToScene( br.topLeft() );
326+
//will trigger updateBoundingRect if necessary
314327
attemptSetSceneRect( QRectF( topLeft.x(), topLeft.y(), br.width(), br.height() ) );
315328

316329
// update polygon position
317330
mPolygon.translate( -br.topLeft().x(), -br.topLeft().y() );
331+
}
332+
333+
void QgsLayoutNodesItem::updateBoundingRect()
334+
{
335+
QRectF br = rect();
336+
br.adjust( -mMaxSymbolBleed, -mMaxSymbolBleed, mMaxSymbolBleed, mMaxSymbolBleed );
337+
mCurrentRectangle = br;
318338

319339
// update
320340
prepareGeometryChange();
321341
update();
322342
}
323343

324-
325-
326344
bool QgsLayoutNodesItem::writePropertiesToElement( QDomElement &elem, QDomDocument &doc, const QgsReadWriteContext &context ) const
327345
{
328346
// style

‎src/core/layout/qgslayoutitemnodeitem.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ class CORE_EXPORT QgsLayoutNodesItem: public QgsLayoutItem
107107
*/
108108
void deselectNode() { mSelectedNode = -1; }
109109

110+
// Depending on the symbol style, the bounding rectangle can be larger than the shape
111+
QRectF boundingRect() const override;
112+
113+
// Reimplement estimatedFrameBleed, since frames on shapes are drawn using symbology
114+
// rather than the item's pen
115+
double estimatedFrameBleed() const override;
116+
110117
protected:
111118

112119
/**
@@ -132,6 +139,9 @@ class CORE_EXPORT QgsLayoutNodesItem: public QgsLayoutItem
132139
//! Shape's nodes.
133140
QPolygonF mPolygon;
134141

142+
//! Max symbol bleed
143+
double mMaxSymbolBleed = 0.0;
144+
135145
//! Method called in addNode.
136146
virtual bool _addNode( const int nodeIndex, QPointF newNode, const double radius ) = 0;
137147

@@ -159,6 +169,10 @@ class CORE_EXPORT QgsLayoutNodesItem: public QgsLayoutItem
159169
//! Update the current scene rectangle for this item.
160170
void updateSceneRect();
161171

172+
private slots:
173+
174+
void updateBoundingRect();
175+
162176
private:
163177

164178
void init();
@@ -171,6 +185,9 @@ class CORE_EXPORT QgsLayoutNodesItem: public QgsLayoutItem
171185
* the painting. */
172186
bool mDrawNodes = false;
173187

188+
//! Current bounding rectangle of shape
189+
QRectF mCurrentRectangle;
190+
174191
//! Draw nodes
175192
void drawNodes( QgsRenderContext &context, const QStyleOptionGraphicsItem *itemStyle = nullptr ) const;
176193
void drawSelectedNode( QgsRenderContext &context, const QStyleOptionGraphicsItem *itemStyle = nullptr ) const;

‎src/core/layout/qgslayoutitempolygon.cpp

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,19 @@ void QgsLayoutItemPolygon::createDefaultPolygonStyleSymbol()
7373

7474
mPolygonStyleSymbol.reset( QgsFillSymbol::createSimple( properties ) );
7575

76+
refreshSymbol();
77+
}
78+
79+
void QgsLayoutItemPolygon::refreshSymbol()
80+
{
81+
if ( layout() )
82+
{
83+
QgsRenderContext rc = QgsLayoutUtils::createRenderContextForLayout( layout(), nullptr, layout()->context().dpi() );
84+
mMaxSymbolBleed = ( 25.4 / layout()->context().dpi() ) * QgsSymbolLayerUtils::estimateMaxSymbolBleed( mPolygonStyleSymbol.get(), rc );
85+
}
86+
87+
updateSceneRect();
88+
7689
emit frameChanged();
7790
}
7891

@@ -108,8 +121,7 @@ void QgsLayoutItemPolygon::_readXmlStyle( const QDomElement &elmt, const QgsRead
108121
void QgsLayoutItemPolygon::setSymbol( QgsFillSymbol *symbol )
109122
{
110123
mPolygonStyleSymbol.reset( static_cast<QgsFillSymbol *>( symbol->clone() ) );
111-
update();
112-
emit frameChanged();
124+
refreshSymbol();
113125
}
114126

115127
void QgsLayoutItemPolygon::_writeXmlStyle( QDomDocument &doc, QDomElement &elmt, const QgsReadWriteContext &context ) const

‎src/core/layout/qgslayoutitempolygon.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ class CORE_EXPORT QgsLayoutItemPolygon: public QgsLayoutNodesItem
8181
std::unique_ptr<QgsFillSymbol> mPolygonStyleSymbol;
8282
//! Create a default symbol.
8383
void createDefaultPolygonStyleSymbol();
84+
85+
/**
86+
* Should be called after the shape's symbol is changed. Redraws the shape and recalculates
87+
* its selection bounds.
88+
*/
89+
void refreshSymbol();
8490
};
8591

8692
#endif // QGSLAYOUTITEMPOLYGON_H

‎src/core/layout/qgslayoutitempolyline.cpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
#include "qgslayoutitemregistry.h"
1919
#include "qgssymbollayerutils.h"
2020
#include "qgssymbol.h"
21+
#include "qgslayout.h"
2122
#include "qgsmapsettings.h"
23+
#include "qgslayoututils.h"
2224
#include <limits>
2325

2426
QgsLayoutItemPolyline::QgsLayoutItemPolyline( QgsLayout *layout )
@@ -96,6 +98,18 @@ void QgsLayoutItemPolyline::createDefaultPolylineStyleSymbol()
9698
properties.insert( QStringLiteral( "capstyle" ), QStringLiteral( "square" ) );
9799

98100
mPolylineStyleSymbol.reset( QgsLineSymbol::createSimple( properties ) );
101+
refreshSymbol();
102+
}
103+
104+
void QgsLayoutItemPolyline::refreshSymbol()
105+
{
106+
if ( layout() )
107+
{
108+
QgsRenderContext rc = QgsLayoutUtils::createRenderContextForLayout( layout(), nullptr, layout()->context().dpi() );
109+
mMaxSymbolBleed = ( 25.4 / layout()->context().dpi() ) * QgsSymbolLayerUtils::estimateMaxSymbolBleed( mPolylineStyleSymbol.get(), rc );
110+
}
111+
112+
updateSceneRect();
99113

100114
emit frameChanged();
101115
}
@@ -127,8 +141,7 @@ void QgsLayoutItemPolyline::_readXmlStyle( const QDomElement &elmt, const QgsRea
127141
void QgsLayoutItemPolyline::setSymbol( QgsLineSymbol *symbol )
128142
{
129143
mPolylineStyleSymbol.reset( static_cast<QgsLineSymbol *>( symbol->clone() ) );
130-
update();
131-
emit frameChanged();
144+
refreshSymbol();
132145
}
133146

134147
void QgsLayoutItemPolyline::_writeXmlStyle( QDomDocument &doc, QDomElement &elmt, const QgsReadWriteContext &context ) const

‎src/core/layout/qgslayoutitempolyline.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ class CORE_EXPORT QgsLayoutItemPolyline: public QgsLayoutNodesItem
8282

8383
//! Create a default symbol.
8484
void createDefaultPolylineStyleSymbol();
85+
86+
/**
87+
* Should be called after the shape's symbol is changed. Redraws the shape and recalculates
88+
* its selection bounds.
89+
*/
90+
void refreshSymbol();
8591
};
8692

8793
#endif // QGSLAYOUTITEMPOLYLINE_H

‎src/core/layout/qgslayoutitemshape.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ QRectF QgsLayoutItemShape::boundingRect() const
129129
return mCurrentRectangle;
130130
}
131131

132+
double QgsLayoutItemShape::estimatedFrameBleed() const
133+
{
134+
return mMaxSymbolBleed;
135+
}
136+
132137
void QgsLayoutItemShape::draw( QgsRenderContext &context, const QStyleOptionGraphicsItem * )
133138
{
134139
QPainter *painter = context.painter();

‎src/core/layout/qgslayoutitemshape.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ class CORE_EXPORT QgsLayoutItemShape : public QgsLayoutItem
9595
// Depending on the symbol style, the bounding rectangle can be larger than the shape
9696
QRectF boundingRect() const override;
9797

98+
// Reimplement estimatedFrameBleed, since frames on shapes are drawn using symbology
99+
// rather than the item's pen
100+
double estimatedFrameBleed() const override;
101+
98102
protected:
99103

100104
void draw( QgsRenderContext &context, const QStyleOptionGraphicsItem *itemStyle = nullptr ) override;

‎tests/src/core/testqgslayoutshapes.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class TestQgsLayoutShapes : public QObject
4848
void roundedRectangle(); //test if rounded rectangle shape is functioning
4949
void symbol(); //test is styling shapes via symbol is working
5050
void readWriteXml();
51+
void bounds();
5152

5253
private:
5354

@@ -253,5 +254,36 @@ void TestQgsLayoutShapes::readWriteXml()
253254
QCOMPARE( copy->symbol()->symbolLayer( 0 )->strokeColor().name(), QStringLiteral( "#ffff00" ) );
254255
}
255256

257+
void TestQgsLayoutShapes::bounds()
258+
{
259+
QgsProject p;
260+
QgsLayout l( &p );
261+
QgsLayoutItemShape *shape = new QgsLayoutItemShape( &l );
262+
shape->attemptMove( QgsLayoutPoint( 20, 20 ) );
263+
shape->attemptResize( QgsLayoutSize( 150, 100 ) );
264+
265+
QgsSimpleFillSymbolLayer *simpleFill = new QgsSimpleFillSymbolLayer();
266+
QgsFillSymbol *fillSymbol = new QgsFillSymbol();
267+
fillSymbol->changeSymbolLayer( 0, simpleFill );
268+
simpleFill->setColor( Qt::green );
269+
simpleFill->setStrokeColor( Qt::yellow );
270+
simpleFill->setStrokeWidth( 6 );
271+
shape->setSymbol( fillSymbol );
272+
273+
// scene bounding rect should include symbol outline
274+
QRectF bounds = shape->sceneBoundingRect();
275+
QCOMPARE( bounds.left(), 17.0 );
276+
QCOMPARE( bounds.right(), 173.0 );
277+
QCOMPARE( bounds.top(), 17.0 );
278+
QCOMPARE( bounds.bottom(), 123.0 );
279+
280+
// rectWithFrame should include symbol outline too
281+
bounds = shape->rectWithFrame();
282+
QCOMPARE( bounds.left(), -3.0 );
283+
QCOMPARE( bounds.right(), 153.0 );
284+
QCOMPARE( bounds.top(), -3.0 );
285+
QCOMPARE( bounds.bottom(), 103.0 );
286+
}
287+
256288
QGSTEST_MAIN( TestQgsLayoutShapes )
257289
#include "testqgslayoutshapes.moc"

‎tests/src/python/test_qgslayoutpolygon.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,41 @@ def testReadWriteXml(self):
279279
self.assertEqual(shape2.symbol().symbolLayer(0).color().name(), '#008000')
280280
self.assertEqual(shape2.symbol().symbolLayer(0).strokeColor().name(), '#ff0000')
281281

282+
def testBounds(self):
283+
pr = QgsProject()
284+
l = QgsLayout(pr)
285+
286+
p = QPolygonF()
287+
p.append(QPointF(50.0, 30.0))
288+
p.append(QPointF(100.0, 10.0))
289+
p.append(QPointF(200.0, 100.0))
290+
shape = QgsLayoutItemPolygon(p, l)
291+
292+
props = {}
293+
props["color"] = "green"
294+
props["style"] = "solid"
295+
props["style_border"] = "solid"
296+
props["color_border"] = "red"
297+
props["width_border"] = "6.0"
298+
props["joinstyle"] = "miter"
299+
300+
style = QgsFillSymbol.createSimple(props)
301+
shape.setSymbol(style)
302+
303+
# scene bounding rect should include symbol outline
304+
bounds = shape.sceneBoundingRect()
305+
self.assertEqual(bounds.left(), 47.0)
306+
self.assertEqual(bounds.right(), 203.0)
307+
self.assertEqual(bounds.top(), 7.0)
308+
self.assertEqual(bounds.bottom(), 103.0)
309+
310+
# rectWithFrame should include symbol outline too
311+
bounds = shape.rectWithFrame()
312+
self.assertEqual(bounds.left(), -3.0)
313+
self.assertEqual(bounds.right(), 153.0)
314+
self.assertEqual(bounds.top(), -3.0)
315+
self.assertEqual(bounds.bottom(), 93.0)
316+
282317

283318
if __name__ == '__main__':
284319
unittest.main()

‎tests/src/python/test_qgslayoutpolyline.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,38 @@ def testReadWriteXml(self):
274274
self.assertEqual(shape2.nodes(), shape.nodes())
275275
self.assertEqual(shape2.symbol().symbolLayer(0).color().name(), '#ff0000')
276276

277+
def testBounds(self):
278+
pr = QgsProject()
279+
l = QgsLayout(pr)
280+
281+
p = QPolygonF()
282+
p.append(QPointF(50.0, 30.0))
283+
p.append(QPointF(100.0, 10.0))
284+
p.append(QPointF(200.0, 100.0))
285+
shape = QgsLayoutItemPolyline(p, l)
286+
287+
props = {}
288+
props["color"] = "255,0,0,255"
289+
props["width"] = "6.0"
290+
props["capstyle"] = "square"
291+
292+
style = QgsLineSymbol.createSimple(props)
293+
shape.setSymbol(style)
294+
295+
# scene bounding rect should include symbol outline
296+
bounds = shape.sceneBoundingRect()
297+
self.assertEqual(bounds.left(), 47.0)
298+
self.assertEqual(bounds.right(), 203.0)
299+
self.assertEqual(bounds.top(), 7.0)
300+
self.assertEqual(bounds.bottom(), 103.0)
301+
302+
# rectWithFrame should include symbol outline too
303+
bounds = shape.rectWithFrame()
304+
self.assertEqual(bounds.left(), -3.0)
305+
self.assertEqual(bounds.right(), 153.0)
306+
self.assertEqual(bounds.top(), -3.0)
307+
self.assertEqual(bounds.bottom(), 93.0)
308+
277309

278310
if __name__ == '__main__':
279311
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.