Skip to content

Commit 714920f

Browse files
committedNov 24, 2017
Start of multiframe porting
1 parent 6278245 commit 714920f

15 files changed

+2064
-0
lines changed
 

‎python/core/core_auto.sip

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@
406406
%Include layout/qgslayoutcontext.sip
407407
%Include layout/qgslayouteffect.sip
408408
%Include layout/qgslayoutguidecollection.sip
409+
%Include layout/qgslayoutframe.sip
409410
%Include layout/qgslayoutitem.sip
410411
%Include layout/qgslayoutitemgroup.sip
411412
%Include layout/qgslayoutitemlabel.sip
@@ -422,6 +423,7 @@
422423
%Include layout/qgslayoutitemregistry.sip
423424
%Include layout/qgslayoutitemshape.sip
424425
%Include layout/qgslayoutmodel.sip
426+
%Include layout/qgslayoutmultiframe.sip
425427
%Include layout/qgslayoutpagecollection.sip
426428
%Include layout/qgslayoutobject.sip
427429
%Include layout/qgslayoutundostack.sip

‎python/core/layout/qgslayout.sip

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,28 @@ class QgsLayout : QGraphicsScene, QgsExpressionContextGenerator, QgsLayoutUndoOb
379379
The item will also be deleted.
380380
%End
381381

382+
void addMultiFrame( QgsLayoutMultiFrame *multiFrame /Transfer/ );
383+
%Docstring
384+
Adds a ``multiFrame`` to the layout. The object is owned by the layout until removeMultiFrame() is called.
385+
.. seealso:: removeMultiFrame()
386+
.. seealso:: multiFrames()
387+
%End
388+
389+
void removeMultiFrame( QgsLayoutMultiFrame *multiFrame );
390+
%Docstring
391+
Removes a ``multiFrame`` from the layout (but does not delete it).
392+
.. seealso:: addMultiFrame()
393+
.. seealso:: multiFrames()
394+
%End
395+
396+
QSet< QgsLayoutMultiFrame * > multiFrames() const;
397+
%Docstring
398+
Returns a list of multi frames contained in the layout.
399+
.. seealso:: addMultiFrame()
400+
.. seealso:: removeMultiFrame()
401+
:rtype: set of QgsLayoutMultiFrame
402+
%End
403+
382404
QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const;
383405
%Docstring
384406
Returns the layout's state encapsulated in a DOM element.

‎python/core/layout/qgslayoutframe.sip

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/************************************************************************
2+
* This file has been generated automatically from *
3+
* *
4+
* src/core/layout/qgslayoutframe.h *
5+
* *
6+
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
7+
************************************************************************/
8+
9+
10+
11+
12+
class QgsLayoutFrame: QgsLayoutItem
13+
{
14+
%Docstring
15+
Base class for frame items, which form a layout multiframe item.
16+
.. versionadded:: 3.0
17+
%End
18+
19+
%TypeHeaderCode
20+
#include "qgslayoutframe.h"
21+
%End
22+
public:
23+
24+
QgsLayoutFrame( QgsLayout *layout, QgsLayoutMultiFrame *multiFrame );
25+
%Docstring
26+
Constructor for QgsLayoutFrame, with the specified parent ``layout``
27+
and belonging to a ``multiFrame``.
28+
%End
29+
30+
virtual int type() const;
31+
32+
virtual QString stringType() const;
33+
34+
35+
virtual QString displayName() const;
36+
37+
38+
void setContentSection( const QRectF &section );
39+
%Docstring
40+
Sets the visible part of the multiframe's content which is visible within
41+
this frame (relative to the total multiframe extent in layout units).
42+
.. seealso:: extent()
43+
%End
44+
45+
QgsLayoutMultiFrame *multiFrame() const;
46+
%Docstring
47+
Returns the parent multiframe for the frame.
48+
:rtype: QgsLayoutMultiFrame
49+
%End
50+
51+
52+
QRectF extent() const;
53+
%Docstring
54+
Returns the visible portion of the multi frame's content which
55+
is shown in this frame, in layout units.
56+
.. seealso:: setContentSection()
57+
:rtype: QRectF
58+
%End
59+
60+
bool hidePageIfEmpty() const;
61+
%Docstring
62+
Returns whether the page should be hidden (ie, not included in layout exports) if this frame is empty
63+
:return: true if page should be hidden if frame is empty
64+
.. seealso:: setHidePageIfEmpty()
65+
:rtype: bool
66+
%End
67+
68+
void setHidePageIfEmpty( const bool hidePageIfEmpty );
69+
%Docstring
70+
Sets whether the page should be hidden (ie, not included in layout exports) if this frame is empty
71+
\param hidePageIfEmpty set to true if page should be hidden if frame is empty
72+
.. seealso:: hidePageIfEmpty()
73+
%End
74+
75+
bool hideBackgroundIfEmpty() const;
76+
%Docstring
77+
Returns whether the background and frame stroke should be hidden if this frame is empty
78+
:return: true if background and stroke should be hidden if frame is empty
79+
.. seealso:: setHideBackgroundIfEmpty()
80+
:rtype: bool
81+
%End
82+
83+
void setHideBackgroundIfEmpty( const bool hideBackgroundIfEmpty );
84+
%Docstring
85+
Sets whether the background and frame stroke should be hidden if this frame is empty
86+
\param hideBackgroundIfEmpty set to true if background and stroke should be hidden if frame is empty
87+
.. seealso:: hideBackgroundIfEmpty()
88+
%End
89+
90+
bool isEmpty() const;
91+
%Docstring
92+
Returns whether the frame is empty.
93+
.. seealso:: hidePageIfEmpty()
94+
:rtype: bool
95+
%End
96+
97+
virtual QgsExpressionContext createExpressionContext() const;
98+
99+
100+
protected:
101+
102+
virtual void draw( QgsRenderContext &context, const QStyleOptionGraphicsItem *itemStyle = 0 );
103+
104+
void drawFrame( QgsRenderContext &context );
105+
void drawBackground( QgsRenderContext &context );
106+
107+
};
108+
109+
/************************************************************************
110+
* This file has been generated automatically from *
111+
* *
112+
* src/core/layout/qgslayoutframe.h *
113+
* *
114+
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
115+
************************************************************************/

‎python/core/layout/qgslayoutitemregistry.sip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ class QgsLayoutItemRegistry : QObject
109109
LayoutShape,
110110
LayoutPolygon,
111111
LayoutPolyline,
112+
LayoutFrame,
112113

113114
// item
114115
PluginItem,
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
/************************************************************************
2+
* This file has been generated automatically from *
3+
* *
4+
* src/core/layout/qgslayoutmultiframe.h *
5+
* *
6+
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
7+
************************************************************************/
8+
9+
10+
11+
12+
13+
class QgsLayoutMultiFrame: QgsLayoutObject
14+
{
15+
%Docstring
16+
Abstract base class for layout items with the ability to distribute the content to
17+
several frames (QgsLayoutFrame items).
18+
.. versionadded:: 3.0
19+
%End
20+
21+
%TypeHeaderCode
22+
#include "qgslayoutmultiframe.h"
23+
%End
24+
public:
25+
26+
enum ResizeMode
27+
{
28+
UseExistingFrames,
29+
ExtendToNextPage,
30+
RepeatOnEveryPage,
31+
RepeatUntilFinished
32+
};
33+
34+
QgsLayoutMultiFrame( QgsLayout *layout /TransferThis/ );
35+
%Docstring
36+
Construct a new multiframe item, attached to the specified ``layout``.
37+
%End
38+
39+
~QgsLayoutMultiFrame();
40+
41+
virtual QSizeF totalSize() const = 0;
42+
%Docstring
43+
Returns the total size of the multiframe's content, in layout units.
44+
:rtype: QSizeF
45+
%End
46+
47+
virtual QSizeF fixedFrameSize( const int frameIndex = -1 ) const;
48+
%Docstring
49+
Returns the fixed size for a frame, if desired. If the fixed frame size changes,
50+
the sizes of all frames can be recalculated by calling recalculateFrameRects().
51+
\param frameIndex frame number
52+
:return: fixed size for frame. If the size has a width or height of 0, then
53+
the frame size is not fixed in that direction and frames can have variable width
54+
or height accordingly.
55+
.. seealso:: minFrameSize()
56+
.. seealso:: recalculateFrameRects()
57+
:rtype: QSizeF
58+
%End
59+
60+
virtual QSizeF minFrameSize( const int frameIndex = -1 ) const;
61+
%Docstring
62+
Returns the minimum size for a frames, if desired. If the minimum
63+
size changes, the sizes of all frames can be recalculated by calling
64+
recalculateFrameRects().
65+
\param frameIndex frame number
66+
:return: minimum size for frame. If the size has a width or height of 0, then
67+
the frame size has no minimum in that direction.
68+
.. seealso:: fixedFrameSize()
69+
.. seealso:: recalculateFrameRects()
70+
:rtype: QSizeF
71+
%End
72+
73+
virtual void render( QgsRenderContext &context, const QRectF &renderExtent, const int frameIndex,
74+
const QStyleOptionGraphicsItem *itemStyle = 0 ) = 0;
75+
%Docstring
76+
Renders a portion of the multiframe's content into a render ``context``.
77+
\param context destination render painter
78+
\param renderExtent visible extent of content to render into the painter.
79+
\param frameIndex frame number for content
80+
\param itemStyle item style options for graphics item rendering
81+
%End
82+
83+
virtual void addFrame( QgsLayoutFrame *frame /Transfer/, bool recalcFrameSizes = true );
84+
%Docstring
85+
Adds a ``frame`` to the multiframe.
86+
87+
If ``recalcFrameSizes`` is set to true, then a recalculation of all existing frame sizes will be forced.
88+
89+
.. seealso:: removeFrame()
90+
%End
91+
92+
virtual double findNearbyPageBreak( double yPos );
93+
%Docstring
94+
Finds the optimal position to break a frame at.
95+
\param yPos maximum vertical position for break, in layout units.
96+
:return: the optimal breakable position which occurs in the multi frame close
97+
to and before the specified yPos
98+
:rtype: float
99+
%End
100+
101+
void removeFrame( int index, bool removeEmptyPages = false );
102+
%Docstring
103+
Removes a frame by ``index`` from the multiframe. This method automatically removes the frame from the
104+
layout too.
105+
106+
If ``removeEmptyPages`` is set to true, then pages which are empty after the frame is removed will
107+
also be removed from the layout.
108+
109+
.. seealso:: addFrame()
110+
.. seealso:: deleteFrames()
111+
%End
112+
113+
void deleteFrames();
114+
%Docstring
115+
Removes and deletes all child frames.
116+
.. seealso:: removeFrame()
117+
%End
118+
119+
void setResizeMode( ResizeMode mode );
120+
%Docstring
121+
Sets the resize ``mode`` for the multiframe, and recalculates frame sizes to match.
122+
.. seealso:: resizeMode()
123+
%End
124+
125+
ResizeMode resizeMode() const;
126+
%Docstring
127+
Returns the resize mode for the multiframe.
128+
.. seealso:: setResizeMode()
129+
:rtype: ResizeMode
130+
%End
131+
132+
virtual bool writeXml( QDomElement &elem, QDomDocument &doc, bool ignoreFrames = false ) const = 0;
133+
%Docstring
134+
Stores state information about multiframe in DOM element. Implementations of writeXml
135+
should also call the _writeXML method to save general multiframe properties.
136+
\param elem is DOM element
137+
\param doc is the DOM document
138+
\param ignoreFrames set to false to avoid writing state information about child frames into DOM
139+
.. seealso:: _writeXML
140+
:rtype: bool
141+
%End
142+
143+
bool _writeXml( QDomElement &elem, QDomDocument &doc, bool ignoreFrames = false ) const;
144+
%Docstring
145+
Stores state information about base multiframe object in DOM element. Implementations of writeXml
146+
should call this method.
147+
\param elem is DOM element
148+
\param doc is the DOM document
149+
\param ignoreFrames set to false to avoid writing state information about child frames into DOM
150+
.. seealso:: writeXml
151+
:rtype: bool
152+
%End
153+
154+
virtual bool readXml( const QDomElement &itemElem, const QDomDocument &doc, bool ignoreFrames = false ) = 0;
155+
%Docstring
156+
Reads multiframe state information from a DOM element. Implementations of readXml
157+
should also call the _readXML method to restore general multiframe properties.
158+
\param itemElem is DOM element
159+
\param doc is the DOM document
160+
\param ignoreFrames set to false to avoid read state information about child frames from DOM
161+
.. seealso:: _readXML
162+
:rtype: bool
163+
%End
164+
165+
bool _readXml( const QDomElement &itemElem, const QDomDocument &doc, bool ignoreFrames = false );
166+
%Docstring
167+
Restores state information about base multiframe object from a DOM element. Implementations of readXml
168+
should call this method.
169+
\param itemElem is DOM element
170+
\param doc is the DOM document
171+
\param ignoreFrames set to false to avoid reading state information about child frames from DOM
172+
.. seealso:: readXml
173+
:rtype: bool
174+
%End
175+
176+
177+
int frameCount() const;
178+
%Docstring
179+
Returns the number of frames associated with this multiframe.
180+
.. seealso:: frames()
181+
:rtype: int
182+
%End
183+
184+
QgsLayoutFrame *frame( int index ) const;
185+
%Docstring
186+
Returns the child frame at a specified ``index`` from the multiframe.
187+
.. seealso:: frameIndex()
188+
:rtype: QgsLayoutFrame
189+
%End
190+
191+
int frameIndex( QgsLayoutFrame *frame ) const;
192+
%Docstring
193+
Returns the index of a ``frame`` within the multiframe.
194+
:return: index for frame if found, -1 if frame not found in multiframe
195+
.. seealso:: frame()
196+
:rtype: int
197+
%End
198+
199+
QgsLayoutFrame *createNewFrame( QgsLayoutFrame *currentFrame, QPointF pos, QSizeF size );
200+
%Docstring
201+
Creates a new frame and adds it to the multi frame and layout.
202+
\param currentFrame an existing QgsLayoutFrame from which to copy the size
203+
and general frame properties (e.g., frame style, background, rendering settings).
204+
\param pos position of top-left corner of the new frame, in layout units
205+
\param size size of the new frame, in layout units
206+
:rtype: QgsLayoutFrame
207+
%End
208+
209+
virtual QString displayName() const;
210+
%Docstring
211+
Returns the multiframe display name.
212+
:rtype: str
213+
%End
214+
215+
public slots:
216+
217+
void update();
218+
%Docstring
219+
Forces a redraw of all child frames.
220+
%End
221+
222+
virtual void recalculateFrameSizes();
223+
%Docstring
224+
Recalculates the portion of the multiframe item which is shown in each of its
225+
component frames. If the resize mode is set to anything but UseExistingFrames then
226+
this may cause new frames to be added or frames to be removed, in order to fit
227+
the current size of the multiframe's content.
228+
.. seealso:: recalculateFrameRects()
229+
%End
230+
231+
void recalculateFrameRects();
232+
%Docstring
233+
Forces a recalculation of all the associated frame's scene rectangles. This
234+
method is useful for multiframes which implement a minFrameSize() or
235+
fixedFrameSize() method.
236+
.. seealso:: minFrameSize()
237+
.. seealso:: fixedFrameSize()
238+
.. seealso:: recalculateFrameSizes
239+
%End
240+
241+
signals:
242+
243+
void changed();
244+
%Docstring
245+
Emitted when the properties of a multi frame have changed, and the GUI item widget
246+
must be updated.
247+
%End
248+
249+
void contentsChanged();
250+
%Docstring
251+
Emitted when the contents of the multi frame have changed and the frames
252+
must be redrawn.
253+
%End
254+
255+
protected:
256+
257+
258+
259+
protected slots:
260+
261+
void handlePageChange();
262+
%Docstring
263+
Adapts to changed number of layout pages if resize type is RepeatOnEveryPage.
264+
%End
265+
266+
void handleFrameRemoval();
267+
%Docstring
268+
Called when a frame is removed. Updates frame list and recalculates
269+
content of remaining frames.
270+
%End
271+
272+
273+
};
274+
275+
/************************************************************************
276+
* This file has been generated automatically from *
277+
* *
278+
* src/core/layout/qgslayoutmultiframe.h *
279+
* *
280+
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
281+
************************************************************************/

‎src/core/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ SET(QGIS_CORE_SRCS
369369
layout/qgslayoutexporter.cpp
370370
layout/qgslayoutgridsettings.cpp
371371
layout/qgslayoutguidecollection.cpp
372+
layout/qgslayoutframe.cpp
372373
layout/qgslayoutitem.cpp
373374
layout/qgslayoutitemgroup.cpp
374375
layout/qgslayoutitemgroupundocommand.cpp
@@ -389,6 +390,7 @@ SET(QGIS_CORE_SRCS
389390
layout/qgslayoutmeasurement.cpp
390391
layout/qgslayoutmeasurementconverter.cpp
391392
layout/qgslayoutmodel.cpp
393+
layout/qgslayoutmultiframe.cpp
392394
layout/qgslayoutobject.cpp
393395
layout/qgslayoutpagecollection.cpp
394396
layout/qgslayoutserializableobject.cpp
@@ -729,6 +731,7 @@ SET(QGIS_CORE_MOC_HDRS
729731
layout/qgslayoutcontext.h
730732
layout/qgslayouteffect.h
731733
layout/qgslayoutguidecollection.h
734+
layout/qgslayoutframe.h
732735
layout/qgslayoutitem.h
733736
layout/qgslayoutitemgroup.h
734737
layout/qgslayoutitemgroupundocommand.h
@@ -746,6 +749,7 @@ SET(QGIS_CORE_MOC_HDRS
746749
layout/qgslayoutitemregistry.h
747750
layout/qgslayoutitemshape.h
748751
layout/qgslayoutmodel.h
752+
layout/qgslayoutmultiframe.h
749753
layout/qgslayoutpagecollection.h
750754
layout/qgslayoutobject.h
751755
layout/qgslayoutundostack.h

‎src/core/layout/qgslayout.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ QgsLayout::~QgsLayout()
4343
// no need for undo commands when we're destroying the layout
4444
mBlockUndoCommands = true;
4545

46+
deleteAndRemoveMultiFrames();
47+
4648
// make sure that all layout items are removed before
4749
// this class is deconstructed - to avoid segfaults
4850
// when layout items access in destructor layout that isn't valid anymore
@@ -413,6 +415,24 @@ void QgsLayout::removeLayoutItem( QgsLayoutItem *item )
413415
}
414416
}
415417

418+
void QgsLayout::addMultiFrame( QgsLayoutMultiFrame *multiFrame )
419+
{
420+
if ( !multiFrame )
421+
return;
422+
423+
mMultiFrames.insert( multiFrame );
424+
}
425+
426+
void QgsLayout::removeMultiFrame( QgsLayoutMultiFrame *multiFrame )
427+
{
428+
mMultiFrames.remove( multiFrame );
429+
}
430+
431+
QSet<QgsLayoutMultiFrame *> QgsLayout::multiFrames() const
432+
{
433+
return mMultiFrames;
434+
}
435+
416436
QgsLayoutUndoStack *QgsLayout::undoStack()
417437
{
418438
return mUndoStack.get();
@@ -581,6 +601,12 @@ void QgsLayout::removeLayoutItemPrivate( QgsLayoutItem *item )
581601
delete item;
582602
}
583603

604+
void QgsLayout::deleteAndRemoveMultiFrames()
605+
{
606+
qDeleteAll( mMultiFrames );
607+
mMultiFrames.clear();
608+
}
609+
584610
void QgsLayout::updateZValues( const bool addUndoCommands )
585611
{
586612
int counter = mItemsModel->zOrderListSize();

‎src/core/layout/qgslayout.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
class QgsLayoutItemMap;
3131
class QgsLayoutModel;
32+
class QgsLayoutMultiFrame;
3233

3334
/**
3435
* \ingroup core
@@ -428,6 +429,27 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
428429
*/
429430
void removeLayoutItem( QgsLayoutItem *item );
430431

432+
/**
433+
* Adds a \a multiFrame to the layout. The object is owned by the layout until removeMultiFrame() is called.
434+
* \see removeMultiFrame()
435+
* \see multiFrames()
436+
*/
437+
void addMultiFrame( QgsLayoutMultiFrame *multiFrame SIP_TRANSFER );
438+
439+
/**
440+
* Removes a \a multiFrame from the layout (but does not delete it).
441+
* \see addMultiFrame()
442+
* \see multiFrames()
443+
*/
444+
void removeMultiFrame( QgsLayoutMultiFrame *multiFrame );
445+
446+
/**
447+
* Returns a list of multi frames contained in the layout.
448+
* \see addMultiFrame()
449+
* \see removeMultiFrame()
450+
*/
451+
QSet< QgsLayoutMultiFrame * > multiFrames() const;
452+
431453
/**
432454
* Returns the layout's state encapsulated in a DOM element.
433455
* \see readXml()
@@ -528,6 +550,9 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
528550

529551
bool mBlockUndoCommands = false;
530552

553+
//! List of multiframe objects
554+
QSet<QgsLayoutMultiFrame *> mMultiFrames;
555+
531556
//! Writes only the layout settings (not member settings like grid settings, etc) to XML
532557
void writeXmlLayoutSettings( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const;
533558
//! Reads only the layout settings (not member settings like grid settings, etc) from XML
@@ -543,6 +568,8 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
543568
*/
544569
void removeLayoutItemPrivate( QgsLayoutItem *item );
545570

571+
void deleteAndRemoveMultiFrames();
572+
546573
friend class QgsLayoutItemAddItemCommand;
547574
friend class QgsLayoutItemDeleteUndoCommand;
548575
friend class QgsLayoutItemUndoCommand;

‎src/core/layout/qgslayoutframe.cpp

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/***************************************************************************
2+
qgslayoutframe.cpp
3+
------------------
4+
begin : October 2017
5+
copyright : (C) 2017 by Nyall Dawson
6+
email : nyall dot dawson at gmail dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#include "qgslayoutframe.h"
17+
#include "qgslayoutmultiframe.h"
18+
#include "qgslayoutitemregistry.h"
19+
#include "qgslayout.h"
20+
21+
QgsLayoutFrame::QgsLayoutFrame( QgsLayout *layout, QgsLayoutMultiFrame *multiFrame )
22+
: QgsLayoutItem( layout )
23+
, mMultiFrame( multiFrame )
24+
{
25+
26+
//default to no background
27+
setBackgroundEnabled( false );
28+
29+
if ( multiFrame )
30+
{
31+
//repaint frame when multiframe content changes
32+
connect( multiFrame, &QgsLayoutMultiFrame::contentsChanged, this, [ = ]
33+
{
34+
update();
35+
} );
36+
37+
#if 0 //TODO
38+
//force recalculation of rect, so that multiframe specified sizes can be applied
39+
setSceneRect( QRectF( pos().x(), pos().y(), rect().width(), rect().height() ) );
40+
#endif
41+
}
42+
}
43+
44+
QgsLayoutMultiFrame *QgsLayoutFrame::multiFrame() const
45+
{
46+
return mMultiFrame;
47+
}
48+
#if 0// TODO
49+
bool QgsLayoutFrame::writeXml( QDomElement &elem, QDomDocument &doc ) const
50+
{
51+
QDomElement frameElem = doc.createElement( QStringLiteral( "ComposerFrame" ) );
52+
frameElem.setAttribute( QStringLiteral( "sectionX" ), QString::number( mSection.x() ) );
53+
frameElem.setAttribute( QStringLiteral( "sectionY" ), QString::number( mSection.y() ) );
54+
frameElem.setAttribute( QStringLiteral( "sectionWidth" ), QString::number( mSection.width() ) );
55+
frameElem.setAttribute( QStringLiteral( "sectionHeight" ), QString::number( mSection.height() ) );
56+
frameElem.setAttribute( QStringLiteral( "hidePageIfEmpty" ), mHidePageIfEmpty );
57+
frameElem.setAttribute( QStringLiteral( "hideBackgroundIfEmpty" ), mHideBackgroundIfEmpty );
58+
elem.appendChild( frameElem );
59+
60+
return _writeXml( frameElem, doc );
61+
}
62+
63+
bool QgsLayoutFrame::readXml( const QDomElement &itemElem, const QDomDocument &doc )
64+
{
65+
double x = itemElem.attribute( QStringLiteral( "sectionX" ) ).toDouble();
66+
double y = itemElem.attribute( QStringLiteral( "sectionY" ) ).toDouble();
67+
double width = itemElem.attribute( QStringLiteral( "sectionWidth" ) ).toDouble();
68+
double height = itemElem.attribute( QStringLiteral( "sectionHeight" ) ).toDouble();
69+
mSection = QRectF( x, y, width, height );
70+
mHidePageIfEmpty = itemElem.attribute( QStringLiteral( "hidePageIfEmpty" ), QStringLiteral( "0" ) ).toInt();
71+
mHideBackgroundIfEmpty = itemElem.attribute( QStringLiteral( "hideBackgroundIfEmpty" ), QStringLiteral( "0" ) ).toInt();
72+
QDomElement composerItem = itemElem.firstChildElement( QStringLiteral( "ComposerItem" ) );
73+
if ( composerItem.isNull() )
74+
{
75+
return false;
76+
}
77+
return _readXml( composerItem, doc );
78+
}
79+
#endif
80+
81+
int QgsLayoutFrame::type() const
82+
{
83+
return QgsLayoutItemRegistry::LayoutFrame;
84+
}
85+
86+
QString QgsLayoutFrame::stringType() const
87+
{
88+
return QStringLiteral( "ItemFrame" );
89+
}
90+
91+
void QgsLayoutFrame::setHidePageIfEmpty( const bool hidePageIfEmpty )
92+
{
93+
mHidePageIfEmpty = hidePageIfEmpty;
94+
}
95+
96+
void QgsLayoutFrame::setHideBackgroundIfEmpty( const bool hideBackgroundIfEmpty )
97+
{
98+
if ( hideBackgroundIfEmpty == mHideBackgroundIfEmpty )
99+
{
100+
return;
101+
}
102+
103+
mHideBackgroundIfEmpty = hideBackgroundIfEmpty;
104+
update();
105+
}
106+
107+
bool QgsLayoutFrame::isEmpty() const
108+
{
109+
if ( !mMultiFrame )
110+
{
111+
return true;
112+
}
113+
114+
double multiFrameHeight = mMultiFrame->totalSize().height();
115+
if ( multiFrameHeight <= mSection.top() )
116+
{
117+
//multiframe height is less than top of this frame's visible portion
118+
return true;
119+
}
120+
121+
return false;
122+
123+
}
124+
125+
QgsExpressionContext QgsLayoutFrame::createExpressionContext() const
126+
{
127+
if ( !mMultiFrame )
128+
return QgsLayoutItem::createExpressionContext();
129+
130+
//start with multiframe's context
131+
QgsExpressionContext context = mMultiFrame->createExpressionContext();
132+
133+
#if 0 //TODO
134+
//add frame's individual context
135+
context.appendScope( QgsExpressionContextUtils::layoutItemScope( this ) );
136+
#endif
137+
138+
return context;
139+
}
140+
141+
142+
QString QgsLayoutFrame::displayName() const
143+
{
144+
if ( !id().isEmpty() )
145+
{
146+
return id();
147+
}
148+
149+
if ( mMultiFrame )
150+
{
151+
return mMultiFrame->displayName();
152+
}
153+
154+
return tr( "<Frame>" );
155+
}
156+
157+
#if 0 //TODO
158+
void QgsLayoutFrame::setSceneRect( const QRectF &rectangle )
159+
{
160+
QRectF fixedRect = rectangle;
161+
162+
if ( mMultiFrame )
163+
{
164+
//calculate index of frame
165+
int frameIndex = mMultiFrame->frameIndex( this );
166+
167+
QSizeF fixedSize = mMultiFrame->fixedFrameSize( frameIndex );
168+
if ( fixedSize.width() > 0 )
169+
{
170+
fixedRect.setWidth( fixedSize.width() );
171+
}
172+
if ( fixedSize.height() > 0 )
173+
{
174+
fixedRect.setHeight( fixedSize.height() );
175+
}
176+
177+
//check minimum size
178+
QSizeF minSize = mMultiFrame->minFrameSize( frameIndex );
179+
fixedRect.setWidth( std::max( minSize.width(), fixedRect.width() ) );
180+
fixedRect.setHeight( std::max( minSize.height(), fixedRect.height() ) );
181+
}
182+
183+
QgsComposerItem::setSceneRect( fixedRect );
184+
}
185+
#endif
186+
187+
188+
void QgsLayoutFrame::draw( QgsRenderContext &context, const QStyleOptionGraphicsItem *itemStyle )
189+
{
190+
if ( mMultiFrame )
191+
{
192+
//calculate index of frame
193+
int frameIndex = mMultiFrame->frameIndex( this );
194+
mMultiFrame->render( context, mSection, frameIndex, itemStyle );
195+
}
196+
}
197+
198+
void QgsLayoutFrame::drawFrame( QgsRenderContext &context )
199+
{
200+
if ( !isEmpty() || !mHideBackgroundIfEmpty )
201+
{
202+
QgsLayoutItem::drawFrame( context );
203+
}
204+
}
205+
206+
void QgsLayoutFrame::drawBackground( QgsRenderContext &context )
207+
{
208+
if ( !isEmpty() || !mHideBackgroundIfEmpty )
209+
{
210+
QgsLayoutItem::drawBackground( context );
211+
}
212+
}
213+
214+
#if 0 //TODO
215+
void QgsLayoutFrame::beginItemCommand( const QString &text )
216+
{
217+
if ( mComposition )
218+
{
219+
mComposition->beginMultiFrameCommand( multiFrame(), text );
220+
}
221+
}
222+
223+
void QgsLayoutFrame::endItemCommand()
224+
{
225+
if ( mComposition )
226+
{
227+
mComposition->endMultiFrameCommand();
228+
}
229+
}
230+
#endif

‎src/core/layout/qgslayoutframe.h

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/***************************************************************************
2+
qgslayoutframe.cpp
3+
------------------
4+
begin : October 2017
5+
copyright : (C) 2017 by Nyall Dawson
6+
email : nyall dot dawson at gmail dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#ifndef QGSLAYOUTFRAME_H
17+
#define QGSLAYOUTFRAME_H
18+
19+
#include "qgis_core.h"
20+
#include "qgis.h"
21+
#include "qgslayoutitem.h"
22+
23+
class QgsLayout;
24+
class QgsLayoutMultiFrame;
25+
26+
/**
27+
* \ingroup core
28+
* Base class for frame items, which form a layout multiframe item.
29+
* \since QGIS 3.0
30+
*/
31+
class CORE_EXPORT QgsLayoutFrame: public QgsLayoutItem
32+
{
33+
Q_OBJECT
34+
35+
public:
36+
37+
/**
38+
* Constructor for QgsLayoutFrame, with the specified parent \a layout
39+
* and belonging to a \a multiFrame.
40+
*/
41+
QgsLayoutFrame( QgsLayout *layout, QgsLayoutMultiFrame *multiFrame );
42+
43+
int type() const override;
44+
QString stringType() const override;
45+
46+
//Overridden to allow multiframe to set display name
47+
QString displayName() const override;
48+
49+
/**
50+
* Sets the visible part of the multiframe's content which is visible within
51+
* this frame (relative to the total multiframe extent in layout units).
52+
* \see extent()
53+
*/
54+
void setContentSection( const QRectF &section ) { mSection = section; }
55+
56+
/**
57+
* Returns the parent multiframe for the frame.
58+
*/
59+
QgsLayoutMultiFrame *multiFrame() const;
60+
61+
#if 0 //TODO
62+
//Overridden to handle fixed frame sizes set by multi frame
63+
void setSceneRect( const QRectF &rectangle ) override;
64+
65+
void paint( QPainter *painter, const QStyleOptionGraphicsItem *itemStyle, QWidget *pWidget ) override;
66+
void beginItemCommand( const QString &text ) override;
67+
void endItemCommand() override;
68+
bool writeXml( QDomElement &elem, QDomDocument &doc ) const override;
69+
bool readXml( const QDomElement &itemElem, const QDomDocument &doc ) override;
70+
#endif
71+
72+
/**
73+
* Returns the visible portion of the multi frame's content which
74+
* is shown in this frame, in layout units.
75+
* \see setContentSection()
76+
*/
77+
QRectF extent() const { return mSection; }
78+
79+
/**
80+
* Returns whether the page should be hidden (ie, not included in layout exports) if this frame is empty
81+
* \returns true if page should be hidden if frame is empty
82+
* \see setHidePageIfEmpty()
83+
*/
84+
bool hidePageIfEmpty() const { return mHidePageIfEmpty; }
85+
86+
/**
87+
* Sets whether the page should be hidden (ie, not included in layout exports) if this frame is empty
88+
* \param hidePageIfEmpty set to true if page should be hidden if frame is empty
89+
* \see hidePageIfEmpty()
90+
*/
91+
void setHidePageIfEmpty( const bool hidePageIfEmpty );
92+
93+
/**
94+
* Returns whether the background and frame stroke should be hidden if this frame is empty
95+
* \returns true if background and stroke should be hidden if frame is empty
96+
* \see setHideBackgroundIfEmpty()
97+
*/
98+
bool hideBackgroundIfEmpty() const { return mHideBackgroundIfEmpty; }
99+
100+
/**
101+
* Sets whether the background and frame stroke should be hidden if this frame is empty
102+
* \param hideBackgroundIfEmpty set to true if background and stroke should be hidden if frame is empty
103+
* \see hideBackgroundIfEmpty()
104+
*/
105+
void setHideBackgroundIfEmpty( const bool hideBackgroundIfEmpty );
106+
107+
/**
108+
* Returns whether the frame is empty.
109+
* \see hidePageIfEmpty()
110+
*/
111+
bool isEmpty() const;
112+
113+
QgsExpressionContext createExpressionContext() const override;
114+
115+
protected:
116+
117+
void draw( QgsRenderContext &context, const QStyleOptionGraphicsItem *itemStyle = nullptr ) override;
118+
void drawFrame( QgsRenderContext &context );
119+
void drawBackground( QgsRenderContext &context );
120+
121+
private:
122+
QgsLayoutFrame() = delete;
123+
QgsLayoutMultiFrame *mMultiFrame = nullptr;
124+
QRectF mSection;
125+
126+
//! If true, layout will not export page if this frame is empty
127+
bool mHidePageIfEmpty = false;
128+
//! If true, background and outside frame will not be drawn if frame is empty
129+
bool mHideBackgroundIfEmpty = false;
130+
131+
friend class QgsLayoutMultiFrame;
132+
133+
};
134+
135+
#endif // QGSLAYOUTFRAME_H

‎src/core/layout/qgslayoutitemregistry.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ class CORE_EXPORT QgsLayoutItemRegistry : public QObject
191191
LayoutShape, //!< Shape item
192192
LayoutPolygon, //!< Polygon shape item
193193
LayoutPolyline, //!< Polyline shape item
194+
LayoutFrame, //!< Frame item, part of a QgsLayoutMultiFrame object
194195

195196
// item types provided by plugins
196197
PluginItem, //!< Starting point for plugin item types

‎src/core/layout/qgslayoutmultiframe.cpp

Lines changed: 431 additions & 0 deletions
Large diffs are not rendered by default.

‎src/core/layout/qgslayoutmultiframe.h

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
/***************************************************************************
2+
qgslayoutmultiframe.h
3+
--------------------
4+
begin : October 2017
5+
copyright : (C) 2017 by Nyall Dawson
6+
email : nyall dot dawson at gmail dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#ifndef QGSLAYOUTMULTIFRAME_H
17+
#define QGSLAYOUTMULTIFRAME_H
18+
19+
#include "qgis_core.h"
20+
#include "qgis.h"
21+
#include "qgslayoutobject.h"
22+
#include <QObject>
23+
#include <QSizeF>
24+
#include <QPointF>
25+
26+
class QgsLayoutFrame;
27+
class QgsLayoutItem;
28+
class QgsLayout;
29+
class QDomDocument;
30+
class QDomElement;
31+
class QRectF;
32+
class QPainter;
33+
class QStyleOptionGraphicsItem;
34+
class QgsRenderContext;
35+
36+
/**
37+
* \ingroup core
38+
* \class QgsLayoutMultiFrame
39+
* Abstract base class for layout items with the ability to distribute the content to
40+
* several frames (QgsLayoutFrame items).
41+
* \since QGIS 3.0
42+
*/
43+
44+
class CORE_EXPORT QgsLayoutMultiFrame: public QgsLayoutObject
45+
{
46+
47+
Q_OBJECT
48+
49+
public:
50+
51+
/**
52+
* Specifies the behavior for creating new frames to fit the multiframe's content
53+
*/
54+
enum ResizeMode
55+
{
56+
UseExistingFrames = 0, //!< Don't automatically create new frames, just use existing frames
57+
ExtendToNextPage, //!< Creates new full page frames on the following page(s) until the entire multiframe content is visible
58+
RepeatOnEveryPage, //!< Repeats the same frame on every page
59+
RepeatUntilFinished /*!< creates new frames with the same position and dimensions as the existing frame on the following page(s),
60+
until the entire multiframe content is visible */
61+
};
62+
63+
/**
64+
* Construct a new multiframe item, attached to the specified \a layout.
65+
*/
66+
QgsLayoutMultiFrame( QgsLayout *layout SIP_TRANSFERTHIS );
67+
68+
~QgsLayoutMultiFrame();
69+
70+
/**
71+
* Returns the total size of the multiframe's content, in layout units.
72+
*/
73+
virtual QSizeF totalSize() const = 0;
74+
75+
/**
76+
* Returns the fixed size for a frame, if desired. If the fixed frame size changes,
77+
* the sizes of all frames can be recalculated by calling recalculateFrameRects().
78+
* \param frameIndex frame number
79+
* \returns fixed size for frame. If the size has a width or height of 0, then
80+
* the frame size is not fixed in that direction and frames can have variable width
81+
* or height accordingly.
82+
* \see minFrameSize()
83+
* \see recalculateFrameRects()
84+
*/
85+
virtual QSizeF fixedFrameSize( const int frameIndex = -1 ) const;
86+
87+
/**
88+
* Returns the minimum size for a frames, if desired. If the minimum
89+
* size changes, the sizes of all frames can be recalculated by calling
90+
* recalculateFrameRects().
91+
* \param frameIndex frame number
92+
* \returns minimum size for frame. If the size has a width or height of 0, then
93+
* the frame size has no minimum in that direction.
94+
* \see fixedFrameSize()
95+
* \see recalculateFrameRects()
96+
*/
97+
virtual QSizeF minFrameSize( const int frameIndex = -1 ) const;
98+
99+
/**
100+
* Renders a portion of the multiframe's content into a render \a context.
101+
* \param context destination render painter
102+
* \param renderExtent visible extent of content to render into the painter.
103+
* \param frameIndex frame number for content
104+
* \param itemStyle item style options for graphics item rendering
105+
*/
106+
virtual void render( QgsRenderContext &context, const QRectF &renderExtent, const int frameIndex,
107+
const QStyleOptionGraphicsItem *itemStyle = nullptr ) = 0;
108+
109+
/**
110+
* Adds a \a frame to the multiframe.
111+
*
112+
* If \a recalcFrameSizes is set to true, then a recalculation of all existing frame sizes will be forced.
113+
*
114+
* \see removeFrame()
115+
*/
116+
virtual void addFrame( QgsLayoutFrame *frame SIP_TRANSFER, bool recalcFrameSizes = true );
117+
118+
/**
119+
* Finds the optimal position to break a frame at.
120+
* \param yPos maximum vertical position for break, in layout units.
121+
* \returns the optimal breakable position which occurs in the multi frame close
122+
* to and before the specified yPos
123+
*/
124+
virtual double findNearbyPageBreak( double yPos );
125+
126+
/**
127+
* Removes a frame by \a index from the multiframe. This method automatically removes the frame from the
128+
* layout too.
129+
*
130+
* If \a removeEmptyPages is set to true, then pages which are empty after the frame is removed will
131+
* also be removed from the layout.
132+
*
133+
* \see addFrame()
134+
* \see deleteFrames()
135+
*/
136+
void removeFrame( int index, bool removeEmptyPages = false );
137+
138+
/**
139+
* Removes and deletes all child frames.
140+
* \see removeFrame()
141+
*/
142+
void deleteFrames();
143+
144+
/**
145+
* Sets the resize \a mode for the multiframe, and recalculates frame sizes to match.
146+
* \see resizeMode()
147+
*/
148+
void setResizeMode( ResizeMode mode );
149+
150+
/**
151+
* Returns the resize mode for the multiframe.
152+
* \see setResizeMode()
153+
*/
154+
ResizeMode resizeMode() const { return mResizeMode; }
155+
156+
/**
157+
* Stores state information about multiframe in DOM element. Implementations of writeXml
158+
* should also call the _writeXML method to save general multiframe properties.
159+
* \param elem is DOM element
160+
* \param doc is the DOM document
161+
* \param ignoreFrames set to false to avoid writing state information about child frames into DOM
162+
* \see _writeXML
163+
*/
164+
virtual bool writeXml( QDomElement &elem, QDomDocument &doc, bool ignoreFrames = false ) const = 0;
165+
166+
/**
167+
* Stores state information about base multiframe object in DOM element. Implementations of writeXml
168+
* should call this method.
169+
* \param elem is DOM element
170+
* \param doc is the DOM document
171+
* \param ignoreFrames set to false to avoid writing state information about child frames into DOM
172+
* \see writeXml
173+
*/
174+
bool _writeXml( QDomElement &elem, QDomDocument &doc, bool ignoreFrames = false ) const;
175+
176+
/**
177+
* Reads multiframe state information from a DOM element. Implementations of readXml
178+
* should also call the _readXML method to restore general multiframe properties.
179+
* \param itemElem is DOM element
180+
* \param doc is the DOM document
181+
* \param ignoreFrames set to false to avoid read state information about child frames from DOM
182+
* \see _readXML
183+
*/
184+
virtual bool readXml( const QDomElement &itemElem, const QDomDocument &doc, bool ignoreFrames = false ) = 0;
185+
186+
/**
187+
* Restores state information about base multiframe object from a DOM element. Implementations of readXml
188+
* should call this method.
189+
* \param itemElem is DOM element
190+
* \param doc is the DOM document
191+
* \param ignoreFrames set to false to avoid reading state information about child frames from DOM
192+
* \see readXml
193+
*/
194+
bool _readXml( const QDomElement &itemElem, const QDomDocument &doc, bool ignoreFrames = false );
195+
196+
/**
197+
* Returns a list of all child frames for this multiframe.
198+
* \see frameCount()
199+
* \note Not available in Python bindings
200+
*/
201+
QList<QgsLayoutFrame *> frames() const SIP_SKIP;
202+
203+
/**
204+
* Returns the number of frames associated with this multiframe.
205+
* \see frames()
206+
*/
207+
int frameCount() const { return mFrameItems.size(); }
208+
209+
/**
210+
* Returns the child frame at a specified \a index from the multiframe.
211+
* \see frameIndex()
212+
*/
213+
QgsLayoutFrame *frame( int index ) const;
214+
215+
/**
216+
* Returns the index of a \a frame within the multiframe.
217+
* \returns index for frame if found, -1 if frame not found in multiframe
218+
* \see frame()
219+
*/
220+
int frameIndex( QgsLayoutFrame *frame ) const;
221+
222+
/**
223+
* Creates a new frame and adds it to the multi frame and layout.
224+
* \param currentFrame an existing QgsLayoutFrame from which to copy the size
225+
* and general frame properties (e.g., frame style, background, rendering settings).
226+
* \param pos position of top-left corner of the new frame, in layout units
227+
* \param size size of the new frame, in layout units
228+
*/
229+
QgsLayoutFrame *createNewFrame( QgsLayoutFrame *currentFrame, QPointF pos, QSizeF size );
230+
231+
/**
232+
* Returns the multiframe display name.
233+
*/
234+
virtual QString displayName() const;
235+
236+
public slots:
237+
238+
/**
239+
* Forces a redraw of all child frames.
240+
*/
241+
void update();
242+
243+
/**
244+
* Recalculates the portion of the multiframe item which is shown in each of its
245+
* component frames. If the resize mode is set to anything but UseExistingFrames then
246+
* this may cause new frames to be added or frames to be removed, in order to fit
247+
* the current size of the multiframe's content.
248+
* \see recalculateFrameRects()
249+
*/
250+
virtual void recalculateFrameSizes();
251+
252+
/**
253+
* Forces a recalculation of all the associated frame's scene rectangles. This
254+
* method is useful for multiframes which implement a minFrameSize() or
255+
* fixedFrameSize() method.
256+
* \see minFrameSize()
257+
* \see fixedFrameSize()
258+
* \see recalculateFrameSizes
259+
*/
260+
void recalculateFrameRects();
261+
262+
signals:
263+
264+
/**
265+
* Emitted when the properties of a multi frame have changed, and the GUI item widget
266+
* must be updated.
267+
*/
268+
void changed();
269+
270+
/**
271+
* Emitted when the contents of the multi frame have changed and the frames
272+
* must be redrawn.
273+
*/
274+
void contentsChanged();
275+
276+
protected:
277+
278+
QList<QgsLayoutFrame *> mFrameItems;
279+
280+
ResizeMode mResizeMode = UseExistingFrames;
281+
282+
protected slots:
283+
284+
/**
285+
* Adapts to changed number of layout pages if resize type is RepeatOnEveryPage.
286+
*/
287+
void handlePageChange();
288+
289+
/**
290+
* Called when a frame is removed. Updates frame list and recalculates
291+
* content of remaining frames.
292+
*/
293+
void handleFrameRemoval();
294+
295+
296+
private:
297+
QgsLayoutMultiFrame() = delete;
298+
299+
bool mIsRecalculatingSize = false;
300+
301+
bool mBlockUpdates = false;
302+
};
303+
304+
#endif // QGSLAYOUTMULTIFRAME_H

‎tests/src/core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ SET(TESTS
140140
testqgslayoutmapgrid.cpp
141141
testqgslayoutmapoverview.cpp
142142
testqgslayoutmodel.cpp
143+
testqgslayoutmultiframe.cpp
143144
testqgslayoutobject.cpp
144145
testqgslayoutpage.cpp
145146
testqgslayoutpicture.cpp

‎tests/src/core/testqgslayoutmultiframe.cpp

Lines changed: 484 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.