diff --git a/python/gui/layout/qgslayoutview.sip b/python/gui/layout/qgslayoutview.sip index 74a45119419..aca319ee7b5 100644 --- a/python/gui/layout/qgslayoutview.sip +++ b/python/gui/layout/qgslayoutview.sip @@ -177,6 +177,8 @@ class QgsLayoutView: QGraphicsView Selects all items in the view. .. seealso:: deselectAll() .. seealso:: invertSelection() +.. seealso:: selectNextItemAbove() +.. seealso:: selectNextItemBelow() %End void deselectAll(); @@ -192,6 +194,22 @@ class QgsLayoutView: QGraphicsView and deselecting and selected items. .. seealso:: selectAll() .. seealso:: deselectAll() +%End + + void selectNextItemAbove(); +%Docstring + Selects the next item above the existing selection, by item z order. +.. seealso:: selectNextItemBelow() +.. seealso:: selectAll() +.. seealso:: deselectAll() +%End + + void selectNextItemBelow(); +%Docstring + Selects the next item below the existing selection, by item z order. +.. seealso:: selectNextItemAbove() +.. seealso:: selectAll() +.. seealso:: deselectAll() %End void viewChanged(); diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 2f3f90ee901..effe503b4a8 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -194,9 +194,14 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla connect( mActionSelectAll, &QAction::triggered, mView, &QgsLayoutView::selectAll ); connect( mActionDeselectAll, &QAction::triggered, mView, &QgsLayoutView::deselectAll ); connect( mActionInvertSelection, &QAction::triggered, mView, &QgsLayoutView::invertSelection ); + connect( mActionSelectNextAbove, &QAction::triggered, mView, &QgsLayoutView::selectNextItemAbove ); + connect( mActionSelectNextBelow, &QAction::triggered, mView, &QgsLayoutView::selectNextItemBelow ); connect( mActionAddPages, &QAction::triggered, this, &QgsLayoutDesignerDialog::addPages ); + connect( mActionUnlockAll, &QAction::triggered, this, &QgsLayoutDesignerDialog::unlockAllItems ); + connect( mActionLockItems, &QAction::triggered, this, &QgsLayoutDesignerDialog::lockSelectedItems ); + //create status bar labels mStatusCursorXLabel = new QLabel( mStatusBar ); mStatusCursorXLabel->setMinimumWidth( 100 ); @@ -477,6 +482,22 @@ void QgsLayoutDesignerDialog::snapToItems( bool enabled ) mLayout->snapper().setSnapToItems( enabled ); } +void QgsLayoutDesignerDialog::unlockAllItems() +{ + if ( mLayout ) + { + mLayout->unlockAllItems(); + } +} + +void QgsLayoutDesignerDialog::lockSelectedItems() +{ + if ( mLayout ) + { + mLayout->lockSelectedItems(); + } +} + void QgsLayoutDesignerDialog::closeEvent( QCloseEvent * ) { emit aboutToClose(); diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index f6a8501bcb3..3a3b5de7609 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -149,6 +149,18 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner */ void snapToItems( bool enabled ); + /** + * Unlocks all locked items in the layout. + * \see lockSelectedItems() + */ + void unlockAllItems(); + + /** + * Locks any selected items in the layout. + * \see unlockAllItems() + */ + void lockSelectedItems(); + signals: /** diff --git a/src/gui/layout/qgslayoutview.cpp b/src/gui/layout/qgslayoutview.cpp index fe352240515..06d75f318c8 100644 --- a/src/gui/layout/qgslayoutview.cpp +++ b/src/gui/layout/qgslayoutview.cpp @@ -24,6 +24,7 @@ #include "qgslayoutviewtooltemporarymousepan.h" #include "qgslayoutmousehandles.h" #include "qgslayoutruler.h" +#include "qgslayoutmodel.h" #include "qgssettings.h" #include "qgsrectangle.h" #include "qgsapplication.h" @@ -353,6 +354,50 @@ void QgsLayoutView::invertSelection() emit itemFocused( focusedItem ); } + +void selectNextByZOrder( QgsLayout *layout, bool above ) +{ + if ( !layout ) + return; + + QgsLayoutItem *previousSelectedItem = nullptr; + const QList selectedItems = layout->selectedLayoutItems(); + if ( !selectedItems.isEmpty() ) + { + previousSelectedItem = selectedItems.at( 0 ); + } + + if ( !previousSelectedItem ) + { + return; + } + + //select item with target z value + QgsLayoutItem *selectedItem = nullptr; + if ( !above ) + selectedItem = layout->itemsModel()->findItemBelow( previousSelectedItem ); + else + selectedItem = layout->itemsModel()->findItemAbove( previousSelectedItem ); + + if ( !selectedItem ) + { + return; + } + + //OK, found a good target item + layout->setSelectedItem( selectedItem ); +} + +void QgsLayoutView::selectNextItemAbove() +{ + selectNextByZOrder( currentLayout(), true ); +} + +void QgsLayoutView::selectNextItemBelow() +{ + selectNextByZOrder( currentLayout(), false ); +} + void QgsLayoutView::mousePressEvent( QMouseEvent *event ) { mSnapMarker->setVisible( false ); diff --git a/src/gui/layout/qgslayoutview.h b/src/gui/layout/qgslayoutview.h index 856ff7ffe2c..717b1f43f8b 100644 --- a/src/gui/layout/qgslayoutview.h +++ b/src/gui/layout/qgslayoutview.h @@ -219,6 +219,8 @@ class GUI_EXPORT QgsLayoutView: public QGraphicsView * Selects all items in the view. * \see deselectAll() * \see invertSelection() + * \see selectNextItemAbove() + * \see selectNextItemBelow() */ void selectAll(); @@ -237,6 +239,22 @@ class GUI_EXPORT QgsLayoutView: public QGraphicsView */ void invertSelection(); + /** + * Selects the next item above the existing selection, by item z order. + * \see selectNextItemBelow() + * \see selectAll() + * \see deselectAll() + */ + void selectNextItemAbove(); + + /** + * Selects the next item below the existing selection, by item z order. + * \see selectNextItemAbove() + * \see selectAll() + * \see deselectAll() + */ + void selectNextItemBelow(); + /** * Updates associated rulers and other widgets after view extent or zoom has changed. * This should be called after calling any of the QGraphicsView diff --git a/src/ui/layout/qgslayoutdesignerbase.ui b/src/ui/layout/qgslayoutdesignerbase.ui index 128cbb77a9c..96fcf6c0e62 100644 --- a/src/ui/layout/qgslayoutdesignerbase.ui +++ b/src/ui/layout/qgslayoutdesignerbase.ui @@ -151,9 +151,17 @@ + + + Layout + + + + + @@ -171,6 +179,19 @@ + + + toolBar + + + TopToolBarArea + + + false + + + + &Close @@ -536,6 +557,33 @@ Ctrl+Alt+] + + + + :/images/themes/default/locked.svg:/images/themes/default/locked.svg + + + Loc&k Selected Items + + + Ctrl+L + + + + + + :/images/themes/default/unlocked.svg:/images/themes/default/unlocked.svg + + + Unl&ock All + + + Unlock All Items + + + Ctrl+Shift+L + + diff --git a/tests/src/python/test_qgslayoutview.py b/tests/src/python/test_qgslayoutview.py index 3587b6015a2..f3385f19c1d 100644 --- a/tests/src/python/test_qgslayoutview.py +++ b/tests/src/python/test_qgslayoutview.py @@ -179,6 +179,78 @@ class TestQgsLayoutView(unittest.TestCase): self.assertFalse(item2.isSelected()) self.assertFalse(item3.isSelected()) # locked + def testSelectNextByZOrder(self): + p = QgsProject() + l = QgsLayout(p) + + # add some items + item1 = QgsLayoutItemMap(l) + l.addItem(item1) + item2 = QgsLayoutItemMap(l) + l.addItem(item2) + item3 = QgsLayoutItemMap(l) + item3.setLocked(True) + l.addItem(item3) + + view = QgsLayoutView() + # no layout, no crash + view.selectNextItemAbove() + view.selectNextItemBelow() + + view.setCurrentLayout(l) + + focused_item_spy = QSignalSpy(view.itemFocused) + + # no selection + view.selectNextItemAbove() + view.selectNextItemBelow() + self.assertEqual(len(focused_item_spy), 0) + + l.setSelectedItem(item1) + self.assertEqual(len(focused_item_spy), 1) + # already bottom most + view.selectNextItemBelow() + self.assertTrue(item1.isSelected()) + self.assertFalse(item2.isSelected()) + self.assertFalse(item3.isSelected()) + self.assertEqual(len(focused_item_spy), 1) + + view.selectNextItemAbove() + self.assertFalse(item1.isSelected()) + self.assertTrue(item2.isSelected()) + self.assertFalse(item3.isSelected()) + self.assertEqual(len(focused_item_spy), 2) + + view.selectNextItemAbove() + self.assertFalse(item1.isSelected()) + self.assertFalse(item2.isSelected()) + self.assertTrue(item3.isSelected()) + self.assertEqual(len(focused_item_spy), 3) + + view.selectNextItemAbove() # already top most + self.assertFalse(item1.isSelected()) + self.assertFalse(item2.isSelected()) + self.assertTrue(item3.isSelected()) + self.assertEqual(len(focused_item_spy), 3) + + view.selectNextItemBelow() + self.assertFalse(item1.isSelected()) + self.assertTrue(item2.isSelected()) + self.assertFalse(item3.isSelected()) + self.assertEqual(len(focused_item_spy), 4) + + view.selectNextItemBelow() + self.assertTrue(item1.isSelected()) + self.assertFalse(item2.isSelected()) + self.assertFalse(item3.isSelected()) + self.assertEqual(len(focused_item_spy), 5) + + view.selectNextItemBelow() # back to bottom most + self.assertTrue(item1.isSelected()) + self.assertFalse(item2.isSelected()) + self.assertFalse(item3.isSelected()) + self.assertEqual(len(focused_item_spy), 5) + if __name__ == '__main__': unittest.main()