Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion src/core/layout/qgslayout.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
#include "qgslayoutrendercontext.h"
#include "qgslayoutreportcontext.h"
#include "qgslayoutundostack.h"
#include "qgsmessagelog.h"
#include "qgsproject.h"

#include <QElapsedTimer>
#include "qgsreadwritecontext.h"
#include "qgsruntimeprofiler.h"
#include "qgssettingsentryimpl.h"
Expand Down Expand Up @@ -778,9 +781,21 @@ QgsLayoutItemGroup *QgsLayout::groupItems( const QList<QgsLayoutItem *> &items )
return nullptr;
}

QElapsedTimer perfTimer; perfTimer.start();

mUndoStack->beginMacro( tr( "Group Items" ) );
auto itemGroup = std::make_unique<QgsLayoutItemGroup>( 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<QgsLayoutItem *> orderedItems = items;
std::sort( orderedItems.begin(), orderedItems.end(),
[]( QgsLayoutItem * a, QgsLayoutItem * b )
{
return a->zValue() > b->zValue();
} );
for ( QgsLayoutItem *item : orderedItems )
{
itemGroup->addItem( item );
}
Expand All @@ -792,6 +807,20 @@ QgsLayoutItemGroup *QgsLayout::groupItems( const QList<QgsLayoutItem *> &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;
Expand All @@ -805,6 +834,9 @@ QList<QgsLayoutItem *> 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
Expand All @@ -819,6 +851,19 @@ QList<QgsLayoutItem *> 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;
}

Expand Down
46 changes: 46 additions & 0 deletions src/core/layout/qgslayoutitemgroup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,52 @@ QList<QgsLayoutItem *> 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() )
Expand Down
27 changes: 26 additions & 1 deletion src/core/layout/qgslayoutitemgroup.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<QgsLayoutItem *> 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;

Expand Down
147 changes: 117 additions & 30 deletions src/core/layout/qgslayoutmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
#include "qgslayout.h"
#include "qgslayoutitemgroup.h"
#include "qgslogger.h"
#include "qgsmessagelog.h"

#include <QApplication>
#include <QDomDocument>
#include <QDomElement>
#include <QElapsedTimer>
#include <QGraphicsItem>
#include <QIODevice>
#include <QIcon>
Expand All @@ -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<QgsLayoutItem *>( 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<QgsLayoutItem *>( 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<QgsLayoutItem *> QgsLayoutModel::topLevelItemsInScene() const
{
QList<QgsLayoutItem *> result;
result.reserve( mItemsInScene.size() );
for ( QgsLayoutItem *item : mItemsInScene )
{
if ( !item->parentGroup() )
result.append( item );
}
return result;
}

QList<QgsLayoutItem *> QgsLayoutModel::childItemsInScene( QgsLayoutItemGroup *group ) const
{
QList<QgsLayoutItem *> 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<QgsLayoutItem *> 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
Expand All @@ -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<QgsLayoutItem *> 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<QgsLayoutItemGroup *>( parentItem );
if ( !group )
return QModelIndex();

const QList<QgsLayoutItem *> 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();
}

Expand All @@ -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<QgsLayoutItem *>( 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<QgsLayoutItem *> siblings = childItemsInScene( grandparent );
parentRow = siblings.indexOf( parentGroup );
}
else
{
const QList<QgsLayoutItem *> 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<QgsLayoutItemGroup *>( parentItem );
if ( !group )
return 0;
}
#endif

//no children for now
return 0;
return childItemsInScene( group ).size();
}

int QgsLayoutModel::columnCount( const QModelIndex &parent ) const
Expand Down Expand Up @@ -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<QgsLayoutItem *> 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<QgsLayoutItem *> 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
Expand Down
Loading
Loading