Skip to content

Commit b494a71

Browse files
committed
Port selection actions to layout
1 parent de96530 commit b494a71

File tree

6 files changed

+308
-1
lines changed

6 files changed

+308
-1
lines changed

python/gui/layout/qgslayoutview.sip

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,29 @@ class QgsLayoutView: QGraphicsView
171171

172172
void emitZoomLevelChanged();
173173

174+
175+
void selectAll();
176+
%Docstring
177+
Selects all items in the view.
178+
.. seealso:: deselectAll()
179+
.. seealso:: invertSelection()
180+
%End
181+
182+
void deselectAll();
183+
%Docstring
184+
Deselects all items in the view.
185+
.. seealso:: selectAll()
186+
.. seealso:: invertSelection()
187+
%End
188+
189+
void invertSelection();
190+
%Docstring
191+
Inverts the current selection, selecting deselected items
192+
and deselecting and selected items.
193+
.. seealso:: selectAll()
194+
.. seealso:: deselectAll()
195+
%End
196+
174197
void viewChanged();
175198
%Docstring
176199
Updates associated rulers and other widgets after view extent or zoom has changed.

src/app/layout/qgslayoutdesignerdialog.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla
191191
connect( mActionZoomActual, &QAction::triggered, mView, &QgsLayoutView::zoomActual );
192192
connect( mActionZoomToWidth, &QAction::triggered, mView, &QgsLayoutView::zoomWidth );
193193

194+
connect( mActionSelectAll, &QAction::triggered, mView, &QgsLayoutView::selectAll );
195+
connect( mActionDeselectAll, &QAction::triggered, mView, &QgsLayoutView::deselectAll );
196+
connect( mActionInvertSelection, &QAction::triggered, mView, &QgsLayoutView::invertSelection );
197+
194198
connect( mActionAddPages, &QAction::triggered, this, &QgsLayoutDesignerDialog::addPages );
195199

196200
//create status bar labels

src/gui/layout/qgslayoutview.cpp

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,81 @@ void QgsLayoutView::emitZoomLevelChanged()
278278
emit zoomLevelChanged();
279279
}
280280

281+
void QgsLayoutView::selectAll()
282+
{
283+
if ( !currentLayout() )
284+
{
285+
return;
286+
}
287+
288+
//select all items in layout
289+
QgsLayoutItem *focusedItem = nullptr;
290+
const QList<QGraphicsItem *> itemList = currentLayout()->items();
291+
for ( QGraphicsItem *graphicsItem : itemList )
292+
{
293+
QgsLayoutItem *item = dynamic_cast<QgsLayoutItem *>( graphicsItem );
294+
QgsLayoutItemPage *paperItem = dynamic_cast<QgsLayoutItemPage *>( graphicsItem );
295+
if ( item && !paperItem )
296+
{
297+
if ( !item->isLocked() )
298+
{
299+
item->setSelected( true );
300+
if ( !focusedItem )
301+
focusedItem = item;
302+
}
303+
else
304+
{
305+
//deselect all locked items
306+
item->setSelected( false );
307+
}
308+
}
309+
}
310+
emit itemFocused( focusedItem );
311+
}
312+
313+
void QgsLayoutView::deselectAll()
314+
{
315+
if ( !currentLayout() )
316+
{
317+
return;
318+
}
319+
320+
currentLayout()->deselectAll();
321+
}
322+
323+
void QgsLayoutView::invertSelection()
324+
{
325+
if ( !currentLayout() )
326+
{
327+
return;
328+
}
329+
330+
QgsLayoutItem *focusedItem = nullptr;
331+
//check all items in layout
332+
const QList<QGraphicsItem *> itemList = currentLayout()->items();
333+
for ( QGraphicsItem *graphicsItem : itemList )
334+
{
335+
QgsLayoutItem *item = dynamic_cast<QgsLayoutItem *>( graphicsItem );
336+
QgsLayoutItemPage *paperItem = dynamic_cast<QgsLayoutItemPage *>( graphicsItem );
337+
if ( item && !paperItem )
338+
{
339+
//flip selected state for items (and deselect any locked items)
340+
if ( item->isSelected() || item->isLocked() )
341+
{
342+
item->setSelected( false );
343+
}
344+
else
345+
{
346+
item->setSelected( true );
347+
if ( !focusedItem )
348+
focusedItem = item;
349+
}
350+
}
351+
}
352+
if ( focusedItem )
353+
emit itemFocused( focusedItem );
354+
}
355+
281356
void QgsLayoutView::mousePressEvent( QMouseEvent *event )
282357
{
283358
mSnapMarker->setVisible( false );

src/gui/layout/qgslayoutview.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,33 @@ class GUI_EXPORT QgsLayoutView: public QGraphicsView
210210
// methods also adds noise to the API.
211211
void emitZoomLevelChanged();
212212

213+
// Why are these select methods in the view and not in the scene (QgsLayout)?
214+
// Well, in my opinion selections are purely a GUI concept. Ideally
215+
// NONE of the selection handling would be done in core, but we're restrained
216+
// by the QGraphicsScene API here.
217+
218+
/**
219+
* Selects all items in the view.
220+
* \see deselectAll()
221+
* \see invertSelection()
222+
*/
223+
void selectAll();
224+
225+
/**
226+
* Deselects all items in the view.
227+
* \see selectAll()
228+
* \see invertSelection()
229+
*/
230+
void deselectAll();
231+
232+
/**
233+
* Inverts the current selection, selecting deselected items
234+
* and deselecting and selected items.
235+
* \see selectAll()
236+
* \see deselectAll()
237+
*/
238+
void invertSelection();
239+
213240
/**
214241
* Updates associated rulers and other widgets after view extent or zoom has changed.
215242
* This should be called after calling any of the QGraphicsView

src/ui/layout/qgslayoutdesignerbase.ui

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@
144144
</property>
145145
<addaction name="mActionUndo"/>
146146
<addaction name="mActionRedo"/>
147+
<addaction name="separator"/>
148+
<addaction name="mActionSelectAll"/>
149+
<addaction name="mActionDeselectAll"/>
150+
<addaction name="mActionInvertSelection"/>
151+
<addaction name="mActionSelectNextBelow"/>
152+
<addaction name="mActionSelectNextAbove"/>
147153
</widget>
148154
<addaction name="mLayoutMenu"/>
149155
<addaction name="menuEdit"/>
@@ -466,6 +472,70 @@
466472
<string>Ctrl+Alt+;</string>
467473
</property>
468474
</action>
475+
<action name="mActionDeselectAll">
476+
<property name="icon">
477+
<iconset resource="../../../images/images.qrc">
478+
<normaloff>:/images/themes/default/mActionDeselectAll.svg</normaloff>:/images/themes/default/mActionDeselectAll.svg</iconset>
479+
</property>
480+
<property name="text">
481+
<string>D&amp;eselect All</string>
482+
</property>
483+
<property name="toolTip">
484+
<string>Deselect all</string>
485+
</property>
486+
<property name="shortcut">
487+
<string>Ctrl+Shift+A</string>
488+
</property>
489+
</action>
490+
<action name="mActionSelectAll">
491+
<property name="icon">
492+
<iconset resource="../../../images/images.qrc">
493+
<normaloff>:/images/themes/default/mActionSelectAll.svg</normaloff>:/images/themes/default/mActionSelectAll.svg</iconset>
494+
</property>
495+
<property name="text">
496+
<string>&amp;Select All</string>
497+
</property>
498+
<property name="toolTip">
499+
<string>Select all items</string>
500+
</property>
501+
<property name="shortcut">
502+
<string>Ctrl+A</string>
503+
</property>
504+
</action>
505+
<action name="mActionInvertSelection">
506+
<property name="icon">
507+
<iconset resource="../../../images/images.qrc">
508+
<normaloff>:/images/themes/default/mActionInvertSelection.svg</normaloff>:/images/themes/default/mActionInvertSelection.svg</iconset>
509+
</property>
510+
<property name="text">
511+
<string>&amp;Invert Selection</string>
512+
</property>
513+
<property name="toolTip">
514+
<string>Invert selection</string>
515+
</property>
516+
</action>
517+
<action name="mActionSelectNextBelow">
518+
<property name="text">
519+
<string>Select Next Item &amp;Below</string>
520+
</property>
521+
<property name="toolTip">
522+
<string>Select next item below</string>
523+
</property>
524+
<property name="shortcut">
525+
<string>Ctrl+Alt+[</string>
526+
</property>
527+
</action>
528+
<action name="mActionSelectNextAbove">
529+
<property name="text">
530+
<string>Select Next Item &amp;Above</string>
531+
</property>
532+
<property name="toolTip">
533+
<string>Select next item above</string>
534+
</property>
535+
<property name="shortcut">
536+
<string>Ctrl+Alt+]</string>
537+
</property>
538+
</action>
469539
</widget>
470540
<resources>
471541
<include location="../../../images/images.qrc"/>

tests/src/python/test_qgslayoutview.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@
1414

1515
import qgis # NOQA
1616

17-
from qgis.core import QgsProject, QgsLayout, QgsUnitTypes
17+
from qgis.core import (QgsProject,
18+
QgsLayout,
19+
QgsUnitTypes,
20+
QgsLayoutItemMap)
1821
from qgis.gui import QgsLayoutView
1922
from qgis.PyQt.QtCore import QRectF
2023
from qgis.PyQt.QtGui import QTransform
24+
from qgis.PyQt.QtTest import QSignalSpy
2125

2226
from qgis.testing import start_app, unittest
2327

@@ -71,6 +75,110 @@ def testLayoutScalePixels(self):
7175
view.setZoomLevel(0.5)
7276
self.assertEqual(view.transform().m11(), 0.5)
7377

78+
def testSelectAll(self):
79+
p = QgsProject()
80+
l = QgsLayout(p)
81+
82+
# add some items
83+
item1 = QgsLayoutItemMap(l)
84+
l.addItem(item1)
85+
item2 = QgsLayoutItemMap(l)
86+
l.addItem(item2)
87+
item3 = QgsLayoutItemMap(l)
88+
item3.setLocked(True)
89+
l.addItem(item3)
90+
91+
view = QgsLayoutView()
92+
# no layout, no crash
93+
view.selectAll()
94+
95+
view.setCurrentLayout(l)
96+
97+
focused_item_spy = QSignalSpy(view.itemFocused)
98+
99+
view.selectAll()
100+
self.assertTrue(item1.isSelected())
101+
self.assertTrue(item2.isSelected())
102+
self.assertFalse(item3.isSelected()) # locked
103+
104+
self.assertEqual(len(focused_item_spy), 1)
105+
106+
item3.setSelected(True) # locked item selection should be cleared
107+
view.selectAll()
108+
self.assertTrue(item1.isSelected())
109+
self.assertTrue(item2.isSelected())
110+
self.assertFalse(item3.isSelected()) # locked
111+
112+
def testDeselectAll(self):
113+
p = QgsProject()
114+
l = QgsLayout(p)
115+
116+
# add some items
117+
item1 = QgsLayoutItemMap(l)
118+
l.addItem(item1)
119+
item2 = QgsLayoutItemMap(l)
120+
l.addItem(item2)
121+
item3 = QgsLayoutItemMap(l)
122+
item3.setLocked(True)
123+
l.addItem(item3)
124+
125+
view = QgsLayoutView()
126+
# no layout, no crash
127+
view.deselectAll()
128+
129+
view.setCurrentLayout(l)
130+
131+
focused_item_spy = QSignalSpy(view.itemFocused)
132+
133+
view.deselectAll()
134+
self.assertFalse(item1.isSelected())
135+
self.assertFalse(item2.isSelected())
136+
self.assertFalse(item3.isSelected())
137+
138+
self.assertEqual(len(focused_item_spy), 1)
139+
140+
item1.setSelected(True)
141+
item2.setSelected(True)
142+
item3.setSelected(True)
143+
view.deselectAll()
144+
self.assertFalse(item1.isSelected())
145+
self.assertFalse(item2.isSelected())
146+
self.assertFalse(item3.isSelected())
147+
148+
def testInvertSelection(self):
149+
p = QgsProject()
150+
l = QgsLayout(p)
151+
152+
# add some items
153+
item1 = QgsLayoutItemMap(l)
154+
l.addItem(item1)
155+
item2 = QgsLayoutItemMap(l)
156+
l.addItem(item2)
157+
item3 = QgsLayoutItemMap(l)
158+
item3.setLocked(True)
159+
l.addItem(item3)
160+
161+
view = QgsLayoutView()
162+
# no layout, no crash
163+
view.invertSelection()
164+
165+
view.setCurrentLayout(l)
166+
167+
focused_item_spy = QSignalSpy(view.itemFocused)
168+
169+
view.invertSelection()
170+
self.assertTrue(item1.isSelected())
171+
self.assertTrue(item2.isSelected())
172+
self.assertFalse(item3.isSelected()) # locked
173+
174+
self.assertEqual(len(focused_item_spy), 1)
175+
176+
item3.setSelected(True) # locked item selection should be cleared
177+
view.invertSelection()
178+
self.assertFalse(item1.isSelected())
179+
self.assertFalse(item2.isSelected())
180+
self.assertFalse(item3.isSelected()) # locked
181+
74182

75183
if __name__ == '__main__':
76184
unittest.main()

0 commit comments

Comments
 (0)