diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index a97db3a1bb18..618c35cd7cd5 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -35,7 +35,10 @@ #include "qgslayoutrendercontext.h" #include "qgslayoutreportcontext.h" #include "qgslayoutundostack.h" +#include "qgsmessagelog.h" #include "qgsproject.h" + +#include #include "qgsreadwritecontext.h" #include "qgsruntimeprofiler.h" #include "qgssettingsentryimpl.h" @@ -778,9 +781,21 @@ QgsLayoutItemGroup *QgsLayout::groupItems( const QList &items ) return nullptr; } + QElapsedTimer perfTimer; perfTimer.start(); + mUndoStack->beginMacro( tr( "Group Items" ) ); auto itemGroup = std::make_unique( this ); - for ( QgsLayoutItem *item : items ) + + // Sort the selection by current global z-order (descending) so the + // group's local z-stack reflects what the user saw on the canvas: + // the visually-topmost selected item is the topmost group member. + QList orderedItems = items; + std::sort( orderedItems.begin(), orderedItems.end(), + []( QgsLayoutItem * a, QgsLayoutItem * b ) + { + return a->zValue() > b->zValue(); + } ); + for ( QgsLayoutItem *item : orderedItems ) { itemGroup->addItem( item ); } @@ -792,6 +807,20 @@ QgsLayoutItemGroup *QgsLayout::groupItems( const QList &items ) mProject->setDirty( true ); mUndoStack->endMacro(); + const qint64 beforeReset = perfTimer.elapsed(); + + // The members' parentGroup() just flipped from null to returnGroup; + // QAbstractItemModel never saw this so the tree's parent/rowCount + // would be stale until next refresh. Force a reset so members appear + // nested under the new group. + mItemsModel->emitModelReset(); + + QgsMessageLog::logMessage( + QStringLiteral( "groupItems: count=%1 stack-and-reset=%2ms reset-only=%3ms total=%4ms" ) + .arg( items.size() ).arg( beforeReset ) + .arg( perfTimer.elapsed() - beforeReset ) + .arg( perfTimer.elapsed() ), + QStringLiteral( "LayoutPerf" ), Qgis::MessageLevel::Info ); // cppcheck-suppress returnDanglingLifetime return returnGroup; @@ -805,6 +834,9 @@ QList QgsLayout::ungroupItems( QgsLayoutItemGroup *group ) return ungroupedItems; } + QElapsedTimer perfTimer; perfTimer.start(); + const int childCount = group->items().size(); + mUndoStack->beginMacro( tr( "Ungroup Items" ) ); // Call this before removing group items so it can keep note // of contents @@ -819,6 +851,19 @@ QList QgsLayout::ungroupItems( QgsLayoutItemGroup *group ) removeLayoutItem( group ); mUndoStack->endMacro(); + const qint64 beforeReset = perfTimer.elapsed(); + // Same staleness as groupItems(): the members' parentGroup() just + // flipped from group to null. Force a reset so they reappear at top + // level in the items panel. + mItemsModel->emitModelReset(); + + QgsMessageLog::logMessage( + QStringLiteral( "ungroupItems: count=%1 unstack-and-reset=%2ms reset-only=%3ms total=%4ms" ) + .arg( childCount ).arg( beforeReset ) + .arg( perfTimer.elapsed() - beforeReset ) + .arg( perfTimer.elapsed() ), + QStringLiteral( "LayoutPerf" ), Qgis::MessageLevel::Info ); + return ungroupedItems; } diff --git a/src/core/layout/qgslayoutitemgroup.cpp b/src/core/layout/qgslayoutitemgroup.cpp index 9870ad920cbd..e1d164b2f506 100644 --- a/src/core/layout/qgslayoutitemgroup.cpp +++ b/src/core/layout/qgslayoutitemgroup.cpp @@ -120,6 +120,52 @@ QList QgsLayoutItemGroup::items() const return val; } +static int indexOfItemPtr( const QList< QPointer< QgsLayoutItem > > &list, QgsLayoutItem *item ) +{ + for ( int i = 0; i < list.size(); ++i ) + { + if ( list.at( i ).data() == item ) + return i; + } + return -1; +} + +bool QgsLayoutItemGroup::reorderItemUp( QgsLayoutItem *item ) +{ + int idx = indexOfItemPtr( mItems, item ); + if ( idx <= 0 ) + return false; // not in group, or already at top + mItems.move( idx, idx - 1 ); + return true; +} + +bool QgsLayoutItemGroup::reorderItemDown( QgsLayoutItem *item ) +{ + int idx = indexOfItemPtr( mItems, item ); + if ( idx < 0 || idx >= mItems.size() - 1 ) + return false; // not in group, or already at bottom + mItems.move( idx, idx + 1 ); + return true; +} + +bool QgsLayoutItemGroup::reorderItemToTop( QgsLayoutItem *item ) +{ + int idx = indexOfItemPtr( mItems, item ); + if ( idx <= 0 ) + return false; + mItems.move( idx, 0 ); + return true; +} + +bool QgsLayoutItemGroup::reorderItemToBottom( QgsLayoutItem *item ) +{ + int idx = indexOfItemPtr( mItems, item ); + if ( idx < 0 || idx >= mItems.size() - 1 ) + return false; + mItems.move( idx, mItems.size() - 1 ); + return true; +} + void QgsLayoutItemGroup::setVisibility( const bool visible ) { if ( !shouldBlockUndoCommands() ) diff --git a/src/core/layout/qgslayoutitemgroup.h b/src/core/layout/qgslayoutitemgroup.h index 76c30d12f347..d20c1da7cf69 100644 --- a/src/core/layout/qgslayoutitemgroup.h +++ b/src/core/layout/qgslayoutitemgroup.h @@ -60,10 +60,35 @@ class CORE_EXPORT QgsLayoutItemGroup : public QgsLayoutItem void removeItems(); /** - * Returns a list of items contained by the group. + * Returns a list of items contained by the group, in local z-order + * (index 0 = topmost within the group, last index = bottommost). */ QList items() const; + /** + * Moves an \a item one step toward the top of the group's local z-stack. + * Returns TRUE if \a item was moved. + */ + bool reorderItemUp( QgsLayoutItem *item ); + + /** + * Moves an \a item one step toward the bottom of the group's local z-stack. + * Returns TRUE if \a item was moved. + */ + bool reorderItemDown( QgsLayoutItem *item ); + + /** + * Moves an \a item to the top of the group's local z-stack. + * Returns TRUE if \a item was moved. + */ + bool reorderItemToTop( QgsLayoutItem *item ); + + /** + * Moves an \a item to the bottom of the group's local z-stack. + * Returns TRUE if \a item was moved. + */ + bool reorderItemToBottom( QgsLayoutItem *item ); + //overridden to also hide grouped items void setVisibility( bool visible ) override; diff --git a/src/core/layout/qgslayoutmodel.cpp b/src/core/layout/qgslayoutmodel.cpp index 9ac23e913b2e..7f2b9b64364b 100644 --- a/src/core/layout/qgslayoutmodel.cpp +++ b/src/core/layout/qgslayoutmodel.cpp @@ -21,10 +21,12 @@ #include "qgslayout.h" #include "qgslayoutitemgroup.h" #include "qgslogger.h" +#include "qgsmessagelog.h" #include #include #include +#include #include #include #include @@ -44,13 +46,55 @@ QgsLayoutModel::QgsLayoutModel( QgsLayout *layout, QObject *parent ) QgsLayoutItem *QgsLayoutModel::itemFromIndex( const QModelIndex &index ) const { //try to return the QgsLayoutItem corresponding to a QModelIndex - if ( !index.isValid() || index.row() == 0 ) + if ( !index.isValid() ) { return nullptr; } - QgsLayoutItem *item = static_cast( index.internalPointer() ); - return item; + // Internal pointer is the QgsLayoutItem * (or nullptr for the root sentinel + // at top-level row 0). The static_cast naturally yields nullptr for that case. + return static_cast( index.internalPointer() ); +} + +void QgsLayoutModel::emitModelReset() +{ + QElapsedTimer t; t.start(); + const int top = topLevelItemsInScene().size(); + const int scene = mItemsInScene.size(); + beginResetModel(); + endResetModel(); + QgsMessageLog::logMessage( + QStringLiteral( "emitModelReset: top=%1 scene=%2 elapsed=%3ms" ) + .arg( top ).arg( scene ).arg( t.elapsed() ), + QStringLiteral( "LayoutPerf" ), Qgis::MessageLevel::Info ); +} + +QList QgsLayoutModel::topLevelItemsInScene() const +{ + QList result; + result.reserve( mItemsInScene.size() ); + for ( QgsLayoutItem *item : mItemsInScene ) + { + if ( !item->parentGroup() ) + result.append( item ); + } + return result; +} + +QList QgsLayoutModel::childItemsInScene( QgsLayoutItemGroup *group ) const +{ + QList result; + if ( !group ) + return result; + // Honor the group's local z-stack (mItems order) so reorderItemUp/Down + // is reflected in the tree. Only include items currently in the scene. + const QList groupItems = group->items(); + for ( QgsLayoutItem *item : groupItems ) + { + if ( item && mItemsInScene.contains( item ) ) + result.append( item ); + } + return result; } QModelIndex QgsLayoutModel::index( int row, int column, const QModelIndex &parent ) const @@ -61,17 +105,33 @@ QModelIndex QgsLayoutModel::index( int row, int column, const QModelIndex &paren return QModelIndex(); } - if ( !parent.isValid() && row == 0 ) + if ( !parent.isValid() ) { - return createIndex( row, column, nullptr ); + if ( row == 0 ) + { + // root sentinel — paper item placeholder, hidden by the items panel proxy + return createIndex( row, column, nullptr ); + } + + const QList top = topLevelItemsInScene(); + if ( row >= 1 && row <= top.size() ) + { + return createIndex( row, column, top.at( row - 1 ) ); + } + return QModelIndex(); } - else if ( !parent.isValid() && row >= 1 && row < mItemsInScene.size() + 1 ) + + // parent must be a group for there to be children + QgsLayoutItem *parentItem = itemFromIndex( parent ); + QgsLayoutItemGroup *group = qobject_cast( parentItem ); + if ( !group ) + return QModelIndex(); + + const QList children = childItemsInScene( group ); + if ( row >= 0 && row < children.size() ) { - //return an index for the layout item at this position - return createIndex( row, column, mItemsInScene.at( row - 1 ) ); + return createIndex( row, column, children.at( row ) ); } - - //only top level supported for now return QModelIndex(); } @@ -93,31 +153,50 @@ void QgsLayoutModel::refreshItemsInScene() QModelIndex QgsLayoutModel::parent( const QModelIndex &index ) const { - Q_UNUSED( index ) + if ( !index.isValid() ) + return QModelIndex(); - //all items are top level for now - return QModelIndex(); + QgsLayoutItem *item = static_cast( index.internalPointer() ); + if ( !item ) + return QModelIndex(); + + QgsLayoutItemGroup *parentGroup = item->parentGroup(); + if ( !parentGroup ) + return QModelIndex(); + + // Find parent's row in ITS parent's child list + QgsLayoutItemGroup *grandparent = parentGroup->parentGroup(); + int parentRow = -1; + if ( grandparent ) + { + const QList siblings = childItemsInScene( grandparent ); + parentRow = siblings.indexOf( parentGroup ); + } + else + { + const QList top = topLevelItemsInScene(); + parentRow = top.indexOf( parentGroup ); + if ( parentRow >= 0 ) + parentRow += 1; // shift past the root sentinel at top level + } + if ( parentRow < 0 ) + return QModelIndex(); + return createIndex( parentRow, 0, parentGroup ); } int QgsLayoutModel::rowCount( const QModelIndex &parent ) const { if ( !parent.isValid() ) { - return mItemsInScene.size() + 1; + // top-level rows = sentinel + items with no parent group + return topLevelItemsInScene().size() + 1; } -#if 0 - QGraphicsItem *parentItem = itemFromIndex( parent ); - - if ( parentItem ) - { - // return child count for item + QgsLayoutItem *parentItem = itemFromIndex( parent ); + QgsLayoutItemGroup *group = qobject_cast( parentItem ); + if ( !group ) return 0; - } -#endif - - //no children for now - return 0; + return childItemsInScene( group ).size(); } int QgsLayoutModel::columnCount( const QModelIndex &parent ) const @@ -906,14 +985,22 @@ QModelIndex QgsLayoutModel::indexForItem( QgsLayoutItem *item, const int column return QModelIndex(); } - int row = mItemsInScene.indexOf( item ); - if ( row == -1 ) + QgsLayoutItemGroup *parentGroup = item->parentGroup(); + if ( !parentGroup ) { - //not found - return QModelIndex(); + const QList top = topLevelItemsInScene(); + int row = top.indexOf( item ); + if ( row < 0 ) + return QModelIndex(); + return index( row + 1, column ); // +1 for sentinel } - return index( row + 1, column ); + const QList siblings = childItemsInScene( parentGroup ); + int row = siblings.indexOf( item ); + if ( row < 0 ) + return QModelIndex(); + QModelIndex parentIdx = indexForItem( parentGroup, 0 ); + return index( row, column, parentIdx ); } ///@cond PRIVATE diff --git a/src/core/layout/qgslayoutmodel.h b/src/core/layout/qgslayoutmodel.h index 2386738611fb..b07e563841a2 100644 --- a/src/core/layout/qgslayoutmodel.h +++ b/src/core/layout/qgslayoutmodel.h @@ -249,6 +249,14 @@ class CORE_EXPORT QgsLayoutModel : public QAbstractItemModel */ QModelIndex indexForItem( QgsLayoutItem *item, int column = 0 ); + /** + * Re-emits modelReset around an externally-driven structural change + * (e.g. grouping or ungrouping) so the tree picks up new + * parentGroup() relationships. Cheap fallback for operations that + * would otherwise need granular beginMoveRows / endMoveRows. + */ + void emitModelReset(); + public slots: ///@cond PRIVATE @@ -289,6 +297,20 @@ class CORE_EXPORT QgsLayoutModel : public QAbstractItemModel */ void rebuildSceneItemList(); + /** + * Returns items from mItemsInScene that have no parent group. + * Order preserves the existing global z-order slice. + */ + QList topLevelItemsInScene() const; + + /** + * Returns items from mItemsInScene whose parentGroup() is \a group. + * Order preserves the existing global z-order slice — once + * QgsLayoutItemGroup gains its own local z-stack this will defer + * to that ordering. + */ + QList childItemsInScene( class QgsLayoutItemGroup *group ) const; + friend class TestQgsLayoutModel; friend class TestQgsLayoutGui; }; diff --git a/src/gui/layout/qgslayoutitemslistview.cpp b/src/gui/layout/qgslayoutitemslistview.cpp index be41409a6c85..04ecdcb6b624 100644 --- a/src/gui/layout/qgslayoutitemslistview.cpp +++ b/src/gui/layout/qgslayoutitemslistview.cpp @@ -20,11 +20,15 @@ #include "qgslayoutitemgroup.h" #include "qgslayoutmodel.h" #include "qgslayoutview.h" +#include "qgsmessagelog.h" +#include #include #include #include +#include + #include "moc_qgslayoutitemslistview.cpp" QgsLayoutItemsListViewModel::QgsLayoutItemsListViewModel( QgsLayoutModel *model, QObject *parent ) @@ -49,10 +53,14 @@ void QgsLayoutItemsListViewModel::setSelected( const QModelIndex &index ) mModel->setSelected( mapToSource( index ) ); } -bool QgsLayoutItemsListViewModel::filterAcceptsRow( int sourceRow, const QModelIndex & ) const +bool QgsLayoutItemsListViewModel::filterAcceptsRow( int sourceRow, const QModelIndex &sourceParent ) const { - if ( sourceRow == 0 ) - return false; // hide empty null item row + // Only the root sentinel (top-level row 0, which is the paper / null item) + // should be hidden. Group children at local row 0 are real items and + // must NOT be filtered, otherwise the topmost member of every group + // disappears from the items panel after grouping. + if ( sourceRow == 0 && !sourceParent.isValid() ) + return false; return true; } @@ -97,7 +105,9 @@ QgsLayoutItemsListView::QgsLayoutItemsListView( QWidget *parent, QgsLayoutDesign setDropIndicatorShown( true ); setDragDropMode( QAbstractItemView::InternalMove ); setContextMenuPolicy( Qt::CustomContextMenu ); - setIndentation( 0 ); + setIndentation( 16 ); + setRootIsDecorated( true ); + setAnimated( true ); // Allow multi selection from the list view setSelectionMode( QAbstractItemView::ExtendedSelection ); @@ -115,11 +125,74 @@ void QgsLayoutItemsListView::setCurrentLayout( QgsLayout *layout ) header()->setSectionResizeMode( 0, QHeaderView::Fixed ); header()->setSectionResizeMode( 1, QHeaderView::Fixed ); - setColumnWidth( 0, Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'x' ) * 4 ); setColumnWidth( 1, Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'x' ) * 4 ); header()->setSectionsMovable( false ); + adjustVisibilityColumnWidth(); + + // Queued so updateSelection() does not run synchronously from inside + // a source model row insertion (e.g. when grouping items), which + // would emit dataChanged mid-transaction and corrupt the proxy. + connect( selectionModel(), &QItemSelectionModel::selectionChanged, this, &QgsLayoutItemsListView::updateSelection, Qt::QueuedConnection ); - connect( selectionModel(), &QItemSelectionModel::selectionChanged, this, &QgsLayoutItemsListView::updateSelection ); + // After group / ungroup the source model resets; expand all groups so + // freshly-nested children are visible right away, and re-fit the + // visibility column so the indented checkboxes always have room. + connect( mModel, &QAbstractItemModel::modelReset, this, [this]() + { + QElapsedTimer t; t.start(); + expandAll(); + const qint64 expandMs = t.elapsed(); + adjustVisibilityColumnWidth(); + const qint64 totalMs = t.elapsed(); + QgsMessageLog::logMessage( + QStringLiteral( "panel.modelReset: expandAll=%1ms total=%2ms" ) + .arg( expandMs ).arg( totalMs ), + QStringLiteral( "LayoutPerf" ), Qgis::MessageLevel::Info ); + } ); + expandAll(); +} + +int QgsLayoutItemsListView::computeMaxNestingDepth() const +{ + if ( !mModel ) + return 0; + + int deepest = 0; + std::function walk = + [&]( const QModelIndex &parent, int depth ) + { + const int rows = mModel->rowCount( parent ); + if ( rows == 0 ) + return; + if ( depth > deepest ) + deepest = depth; + for ( int r = 0; r < rows; ++r ) + { + walk( mModel->index( r, 0, parent ), depth + 1 ); + } + }; + walk( QModelIndex(), 0 ); + return deepest; +} + +void QgsLayoutItemsListView::adjustVisibilityColumnWidth() +{ + QElapsedTimer t; t.start(); + const int base = Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'x' ) * 4; + const int depth = computeMaxNestingDepth(); + const qint64 depthMs = t.elapsed(); + // QTreeView puts the disclosure arrow + per-level indentation in the + // first column. When nesting is present the column needs noticeably + // more room than the bare checkbox width — double the base AND add + // one indentation step per level so deeper nesting still fits. + const int width = depth > 0 + ? base * 2 + depth * indentation() + : base; + setColumnWidth( 0, width ); + QgsMessageLog::logMessage( + QStringLiteral( "panel.adjustVisColumn: depth=%1 width=%2 depth-walk=%3ms total=%4ms" ) + .arg( depth ).arg( width ).arg( depthMs ).arg( t.elapsed() ), + QStringLiteral( "LayoutPerf" ), Qgis::MessageLevel::Info ); } void QgsLayoutItemsListView::keyPressEvent( QKeyEvent *event ) @@ -150,8 +223,8 @@ void QgsLayoutItemsListView::updateSelection() { // Do nothing if we are currently updating the selection // because user has selected/deselected some items in the - // graphics view - if ( !mModel || mUpdatingFromView ) + // graphics view, or if we are already inside this method. + if ( !mModel || mUpdatingFromView || mUpdatingSelection ) return; // Set the updating flag @@ -196,14 +269,10 @@ void QgsLayoutItemsListView::updateSelection() item->setSelected( true ); } - // find top level group this item is contained within, and mark the group as selected - QgsLayoutItemGroup *group = item->parentGroup(); - while ( group && group->parentGroup() ) - { - group = group->parentGroup(); - } - if ( group && group != item ) - group->setSelected( true ); + // The items panel is the precise/direct selection surface (mirrors + // the Adobe Layers panel): clicking a child selects only that child, + // not its enclosing group. Use the canvas selection tool for + // whole-group selection on plain click. } // Reset the updating flag mUpdatingSelection = false; diff --git a/src/gui/layout/qgslayoutitemslistview.h b/src/gui/layout/qgslayoutitemslistview.h index 8539f468d2e7..a5b5790bb3ea 100644 --- a/src/gui/layout/qgslayoutitemslistview.h +++ b/src/gui/layout/qgslayoutitemslistview.h @@ -103,6 +103,11 @@ class GUI_EXPORT QgsLayoutItemsListView : public QTreeView bool mUpdatingSelection = false; bool mUpdatingFromView = false; + + //! Walks the model and returns the deepest nesting level (0 = no groups). + int computeMaxNestingDepth() const; + //! Recomputes the visibility column width based on current nesting depth. + void adjustVisibilityColumnWidth(); }; #endif // QGSLAYOUTITEMSLISTVIEW_H diff --git a/src/gui/layout/qgslayoutviewtoolselect.cpp b/src/gui/layout/qgslayoutviewtoolselect.cpp index 275f90d14d76..721d022974ca 100644 --- a/src/gui/layout/qgslayoutviewtoolselect.cpp +++ b/src/gui/layout/qgslayoutviewtoolselect.cpp @@ -17,9 +17,14 @@ #include +#include +#include + #include "qgslayout.h" +#include "qgslayoutitem.h" #include "qgslayoutitemgroup.h" #include "qgslayoutitempage.h" +#include "qgslayoutitemregistry.h" #include "qgslayoutmousehandles.h" #include "qgslayoutview.h" #include "qgslayoutviewmouseevent.h" @@ -114,14 +119,22 @@ void QgsLayoutViewToolSelect::layoutPressEvent( QgsLayoutViewMouseEvent *event ) selectedItem = layout()->layoutItemAt( event->layoutPoint(), true, searchToleranceInLayoutUnits() ); } - // if selected item is in a group, we actually get the top-level group it's part of - QgsLayoutItemGroup *group = selectedItem ? selectedItem->parentGroup() : nullptr; - while ( group && group->parentGroup() ) + // If the selected item is in a group, we normally promote to the top-level + // group it belongs to so the whole group is selected on a plain click. + // CTRL skips this promotion so the user can drill into nested members + // directly (mirrors Adobe Group Selection / Direct Selection semantics) + // — this composes naturally with the existing CTRL stack-cycle above. + const bool drillIntoGroup = event->modifiers() & Qt::ControlModifier; + if ( !drillIntoGroup ) { - group = group->parentGroup(); + QgsLayoutItemGroup *group = selectedItem ? selectedItem->parentGroup() : nullptr; + while ( group && group->parentGroup() ) + { + group = group->parentGroup(); + } + if ( group ) + selectedItem = group; } - if ( group ) - selectedItem = group; if ( !selectedItem ) { @@ -309,6 +322,13 @@ void QgsLayoutViewToolSelect::wheelEvent( QWheelEvent *event ) void QgsLayoutViewToolSelect::keyPressEvent( QKeyEvent *event ) { + if ( event->key() == Qt::Key_Escape && mIsolatedGroup ) + { + exitIsolation(); + event->accept(); + return; + } + if ( mMouseHandles->isDragging() || mMouseHandles->isResizing() ) { return; @@ -319,8 +339,109 @@ void QgsLayoutViewToolSelect::keyPressEvent( QKeyEvent *event ) } } +QgsLayoutItemGroup *QgsLayoutViewToolSelect::isolatedGroup() const +{ + return mIsolatedGroup; +} + +void QgsLayoutViewToolSelect::layoutDoubleClickEvent( QgsLayoutViewMouseEvent *event ) +{ + if ( event->button() != Qt::LeftButton ) + { + event->ignore(); + return; + } + + QgsLayoutItem *hit = layout()->layoutItemAt( event->layoutPoint(), true, + searchToleranceInLayoutUnits() ); + if ( !hit ) + { + // Double-click on empty area exits isolation + if ( mIsolatedGroup ) + { + exitIsolation(); + event->accept(); + return; + } + event->ignore(); + return; + } + + // The double-click target is either a group itself, or its innermost + // containing group — that's the one we isolate. + QgsLayoutItemGroup *target = qobject_cast( hit ); + if ( !target ) + target = hit->parentGroup(); + if ( !target ) + { + event->ignore(); + return; + } + + enterIsolation( target ); + event->accept(); +} + +void QgsLayoutViewToolSelect::enterIsolation( QgsLayoutItemGroup *group ) +{ + if ( !group || !layout() ) + return; + + // If already isolating something else, restore first. + if ( mIsolatedGroup && mIsolatedGroup != group ) + exitIsolation(); + + // Build the set of items considered "inside" the isolation: the group + // itself plus all transitive descendants. + QSet inside; + inside.insert( group ); + QList stack = group->items(); + while ( !stack.isEmpty() ) + { + QgsLayoutItem *it = stack.takeLast(); + if ( !it || inside.contains( it ) ) + continue; + inside.insert( it ); + if ( QgsLayoutItemGroup *nested = qobject_cast( it ) ) + stack.append( nested->items() ); + } + + // Dim everything else (excluding pages, which we always leave fully visible). + const QList sceneItems = layout()->items(); + for ( QGraphicsItem *gi : sceneItems ) + { + QgsLayoutItem *li = dynamic_cast( gi ); + if ( !li ) + continue; + if ( li->type() == QgsLayoutItemRegistry::LayoutPage ) + continue; + if ( inside.contains( li ) ) + continue; + if ( !mDimmedItems.contains( li ) ) + mDimmedItems.insert( li, li->opacity() ); + li->setOpacity( sIsolationDimOpacity ); + } + mIsolatedGroup = group; +} + +void QgsLayoutViewToolSelect::exitIsolation() +{ + if ( !mIsolatedGroup && mDimmedItems.isEmpty() ) + return; + + for ( auto it = mDimmedItems.constBegin(); it != mDimmedItems.constEnd(); ++it ) + { + if ( it.key() ) + it.key()->setOpacity( it.value() ); + } + mDimmedItems.clear(); + mIsolatedGroup = nullptr; +} + void QgsLayoutViewToolSelect::deactivate() { + if ( mIsolatedGroup ) + exitIsolation(); if ( mIsSelecting ) { mRubberBand->finish(); diff --git a/src/gui/layout/qgslayoutviewtoolselect.h b/src/gui/layout/qgslayoutviewtoolselect.h index f45b0c1ae060..67e775135d2f 100644 --- a/src/gui/layout/qgslayoutviewtoolselect.h +++ b/src/gui/layout/qgslayoutviewtoolselect.h @@ -23,7 +23,12 @@ #include "qgslayoutviewrubberband.h" #include "qgslayoutviewtool.h" +#include +#include + class QgsLayoutMouseHandles; +class QgsLayoutItemGroup; +class QgsLayoutItem; /** * \ingroup gui @@ -43,10 +48,30 @@ class GUI_EXPORT QgsLayoutViewToolSelect : public QgsLayoutViewTool void layoutPressEvent( QgsLayoutViewMouseEvent *event ) override; void layoutMoveEvent( QgsLayoutViewMouseEvent *event ) override; void layoutReleaseEvent( QgsLayoutViewMouseEvent *event ) override; + void layoutDoubleClickEvent( QgsLayoutViewMouseEvent *event ) override; void wheelEvent( QWheelEvent *event ) override; void keyPressEvent( QKeyEvent *event ) override; void deactivate() override; + /** + * Returns the currently isolated group, or NULLPTR when no group is + * being isolated. + */ + QgsLayoutItemGroup *isolatedGroup() const; + + /** + * Enters isolation mode for \a group. While isolated, items outside + * the group are visually dimmed so the user can edit group members + * without distraction. Mirrors Adobe Illustrator's group isolation. + */ + void enterIsolation( QgsLayoutItemGroup *group ); + + /** + * Exits isolation mode and restores all items to their normal opacity. + * No-op if no group is currently isolated. + */ + void exitIsolation(); + ///@cond PRIVATE /** @@ -81,6 +106,14 @@ class GUI_EXPORT QgsLayoutViewToolSelect : public QgsLayoutViewTool //! Search tolerance in millimeters for selecting items static const double sSearchToleranceInMillimeters; + + //! Group currently isolated (NULLPTR = no isolation) + QPointer mIsolatedGroup; + + //! Items dimmed by enterIsolation, mapped to their original opacity + QHash mDimmedItems; + + static constexpr qreal sIsolationDimOpacity = 0.25; }; #endif // QGSLAYOUTVIEWTOOLSELECT_H diff --git a/src/gui/qgsmaplayerstylemanagerwidget.cpp b/src/gui/qgsmaplayerstylemanagerwidget.cpp index 61166ba851e5..472928c43378 100644 --- a/src/gui/qgsmaplayerstylemanagerwidget.cpp +++ b/src/gui/qgsmaplayerstylemanagerwidget.cpp @@ -70,7 +70,7 @@ QgsMapLayerStyleManagerWidget::QgsMapLayerStyleManagerWidget( QgsMapLayer *layer QAction *loadDefaultAction = toolbar->addAction( tr( "Restore Default" ) ); connect( loadDefaultAction, &QAction::triggered, this, &QgsMapLayerStyleManagerWidget::loadDefault ); - connect( mStyleList, &QAbstractItemView::clicked, this, &QgsMapLayerStyleManagerWidget::styleClicked ); + connect( mStyleList, &QAbstractItemView::clicked, this, &QgsMapLayerStyleManagerWidget::styleClicked, Qt::QueuedConnection ); setLayout( new QVBoxLayout() ); layout()->setContentsMargins( 0, 0, 0, 0 );