diff --git a/images/images.qrc b/images/images.qrc
index d4ffd3d4e6e6..432b3ae6ee11 100644
--- a/images/images.qrc
+++ b/images/images.qrc
@@ -341,6 +341,7 @@
themes/default/mActionLabel.svg
themes/default/mActionLabeling.svg
themes/default/mActionLayers.svg
+ themes/default/mActionListActions.svg
themes/default/mActionLocalCumulativeCutStretch.svg
themes/default/mActionLocalHistogramStretch.svg
themes/default/mActionLockItems.svg
diff --git a/images/themes/default/mActionListActions.svg b/images/themes/default/mActionListActions.svg
new file mode 100644
index 000000000000..f0196e8adc90
--- /dev/null
+++ b/images/themes/default/mActionListActions.svg
@@ -0,0 +1,185 @@
+
+
diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h
index 6430bace9410..ea5df89a6951 100644
--- a/src/app/qgisapp.h
+++ b/src/app/qgisapp.h
@@ -184,7 +184,6 @@ class QgsCustomizationDialog;
#include "qgsoptionsutils.h"
#include "qgsoptionswidgetfactory.h"
#include "qgspointxy.h"
-#include "qgsrasterminmaxorigin.h"
#include "qgsrecentprojectsitemsmodel.h"
#include "qgsvectorlayersaveasdialog.h"
#include "qobjectuniqueptr.h"
diff --git a/src/app/qgscustomization.cpp b/src/app/qgscustomization.cpp
index be8008af9017..dc6312671ddc 100644
--- a/src/app/qgscustomization.cpp
+++ b/src/app/qgscustomization.cpp
@@ -151,12 +151,39 @@ unsigned int QgsCustomization::QgsItem::childrenCount() const
return mChildItemList.size();
}
+void QgsCustomization::QgsItem::insertChild( int position, std::unique_ptr item )
+{
+ if ( position < 0 && position >= static_cast( mChildItemList.size() ) )
+ {
+ QgsDebugError( u"Insert item impossible, invalid position"_s );
+ return;
+ }
+
+ mChildItemList.insert( std::next( mChildItemList.cbegin(), position ), std::move( item ) );
+ QgsItem *pitem = mChildItemList.at( position ).get();
+ mChildItems[pitem->name()] = pitem;
+}
+
+void QgsCustomization::QgsItem::deleteChild( int position )
+{
+ if ( position < 0 && position >= static_cast( mChildItemList.size() ) )
+ {
+ QgsDebugError( u"Delete item impossible, invalid position"_s );
+ return;
+ }
+
+ mChildItems.take( mChildItemList.at( position )->name() );
+ mChildItemList.erase( std::next( mChildItemList.cbegin(), position ) );
+}
+
void QgsCustomization::QgsItem::writeXml( QDomDocument &doc, QDomElement &parent ) const
{
QDomElement itemElem = doc.createElement( xmlTag() );
itemElem.setAttribute( u"name"_s, mName );
itemElem.setAttribute( u"visible"_s, ( mVisible ? "true" : "false" ) );
+ writeXmlItem( itemElem );
+
for ( const std::unique_ptr &childItem : mChildItemList )
{
childItem->writeXml( doc, itemElem );
@@ -174,6 +201,8 @@ QString QgsCustomization::QgsItem::readXml( const QDomElement &elem )
return QObject::tr( "Invalid XML file : empty name for tag '%1'" ).arg( elem.tagName() );
}
+ readXmlItem( elem );
+
for ( QDomElement childElem = elem.firstChildElement(); !childElem.isNull(); childElem = childElem.nextSiblingElement() )
{
std::unique_ptr childItem = createChildItem( childElem );
@@ -192,6 +221,11 @@ QString QgsCustomization::QgsItem::readXml( const QDomElement &elem )
return QString();
}
+bool QgsCustomization::QgsItem::hasCapability( QgsCustomization::QgsItem::ItemCapability capability ) const
+{
+ return static_cast( capabilities() ) & static_cast( capability );
+}
+
std::unique_ptr QgsCustomization::QgsItem::createChildItem( const QDomElement & )
{
return nullptr;
@@ -209,6 +243,17 @@ void QgsCustomization::QgsItem::copyItemAttributes( const QgsCustomization::QgsI
}
}
+void QgsCustomization::QgsItem::writeXmlItem( QDomElement & ) const {
+};
+
+void QgsCustomization::QgsItem::readXmlItem( const QDomElement & ) {
+};
+
+QgsCustomization::QgsItem::ItemCapability QgsCustomization::QgsItem::capabilities() const
+{
+ return ItemCapability::None;
+}
+
////////////////
QgsCustomization::QgsActionItem::QgsActionItem( QgsCustomization::QgsItem *parent )
@@ -242,6 +287,19 @@ qsizetype QgsCustomization::QgsActionItem::qActionIndex() const
return mQActionIndex;
}
+QString QgsCustomization::QgsActionItem::path() const
+{
+ QString path = name();
+
+ QgsItem const *currentItem = this;
+ while ( ( currentItem = currentItem->parent() ) )
+ {
+ path.prepend( currentItem->name() + "/" );
+ }
+
+ return path;
+}
+
std::unique_ptr QgsCustomization::QgsActionItem::cloneActionItem( QgsCustomization::QgsItem *parent ) const
{
auto clone = std::make_unique( parent );
@@ -268,6 +326,66 @@ void QgsCustomization::QgsActionItem::copyItemAttributes( const QgsItem *other )
}
}
+QgsCustomization::QgsItem::ItemCapability QgsCustomization::QgsActionItem::capabilities() const
+{
+ return ItemCapability::Drag;
+}
+
+////////////////
+
+QgsCustomization::QgsActionRefItem::QgsActionRefItem( QgsItem *parent )
+ : QgsActionItem( parent ) {}
+
+QgsCustomization::QgsActionRefItem::QgsActionRefItem( const QString &name, const QString &title, const QString &path, QgsItem *parent )
+ : QgsActionItem( name, title, parent )
+ , mPath( path ) {}
+
+const QString &QgsCustomization::QgsActionRefItem::actionRefPath() const
+{
+ return mPath;
+}
+
+std::unique_ptr QgsCustomization::QgsActionRefItem::clone( QgsCustomization::QgsItem *parent ) const
+{
+ auto clone = std::make_unique( parent );
+ clone->copyItemAttributes( this );
+ return clone;
+}
+
+QString QgsCustomization::QgsActionRefItem::xmlTag() const
+{
+ return u"ActionRef"_s;
+};
+
+std::unique_ptr QgsCustomization::QgsActionRefItem::createChildItem( const QDomElement & )
+{
+ return nullptr;
+}
+
+void QgsCustomization::QgsActionRefItem::readXmlItem( const QDomElement &elem )
+{
+ mPath = elem.attribute( u"path"_s );
+};
+
+void QgsCustomization::QgsActionRefItem::writeXmlItem( QDomElement &elem ) const
+{
+ elem.setAttribute( u"path"_s, mPath );
+}
+
+QgsCustomization::QgsItem::ItemCapability QgsCustomization::QgsActionRefItem::capabilities() const
+{
+ return ItemCapability::Delete;
+};
+
+void QgsCustomization::QgsActionRefItem::copyItemAttributes( const QgsItem *other )
+{
+ QgsActionItem::copyItemAttributes( other );
+ if ( const QgsActionRefItem *action = dynamic_cast( other ) )
+ {
+ mPath = action->mPath;
+ }
+}
+
////////////////
QgsCustomization::QgsMenuItem::QgsMenuItem( QgsItem *parent )
@@ -299,6 +417,63 @@ std::unique_ptr QgsCustomization::QgsMenuItem::create
return nullptr;
}
+QgsCustomization::QgsItem::ItemCapability QgsCustomization::QgsMenuItem::capabilities() const
+{
+ return ItemCapability::None;
+}
+
+////////////////
+
+QgsCustomization::QgsUserMenuItem::QgsUserMenuItem( QgsItem *parent )
+ : QgsMenuItem( parent )
+{}
+
+QgsCustomization::QgsUserMenuItem::QgsUserMenuItem( const QString &name, const QString &title, QgsItem *parent )
+ : QgsMenuItem( name, title, parent )
+{}
+
+QgsCustomization::QgsItem::ItemCapability QgsCustomization::QgsUserMenuItem::capabilities() const
+{
+ return static_cast(
+ static_cast( ItemCapability::AddActionRefChild )
+ | static_cast( ItemCapability::AddUserMenuChild )
+ | static_cast( ItemCapability::Rename )
+ | static_cast( ItemCapability::Delete )
+ );
+}
+
+std::unique_ptr QgsCustomization::QgsUserMenuItem::clone( QgsCustomization::QgsItem *parent ) const
+{
+ auto clone = std::make_unique( parent );
+ clone->copyItemAttributes( this );
+ return clone;
+}
+
+QString QgsCustomization::QgsUserMenuItem::xmlTag() const
+{
+ return u"UserMenu"_s;
+}
+
+void QgsCustomization::QgsUserMenuItem::writeXmlItem( QDomElement &elem ) const
+{
+ elem.setAttribute( u"title"_s, title() );
+};
+
+void QgsCustomization::QgsUserMenuItem::readXmlItem( const QDomElement &elem )
+{
+ setTitle( elem.attribute( u"title"_s ) );
+};
+
+std::unique_ptr QgsCustomization::QgsUserMenuItem::createChildItem( const QDomElement &childElem )
+{
+ if ( childElem.tagName() == "ActionRef"_L1 )
+ return std::make_unique( this );
+ else if ( childElem.tagName() == "UserMenu"_L1 )
+ return std::make_unique( this );
+ else
+ return nullptr;
+}
+
////////////////
QgsCustomization::QgsToolBarItem::QgsToolBarItem( QgsItem *parent )
@@ -351,6 +526,55 @@ void QgsCustomization::QgsToolBarItem::copyItemAttributes( const QgsItem *other
////////////////
+QgsCustomization::QgsUserToolBarItem::QgsUserToolBarItem( QgsItem *parent )
+ : QgsToolBarItem( parent )
+{}
+
+QgsCustomization::QgsUserToolBarItem::QgsUserToolBarItem( const QString &name, const QString &title, QgsItem *parent )
+ : QgsToolBarItem( name, title, parent )
+{}
+
+std::unique_ptr QgsCustomization::QgsUserToolBarItem::clone( QgsCustomization::QgsItem *parent ) const
+{
+ auto clone = std::make_unique( parent );
+ clone->copyItemAttributes( this );
+ return clone;
+}
+
+QString QgsCustomization::QgsUserToolBarItem::xmlTag() const
+{
+ return u"UserToolBar"_s;
+}
+
+void QgsCustomization::QgsUserToolBarItem::writeXmlItem( QDomElement &elem ) const
+{
+ elem.setAttribute( u"title"_s, title() );
+};
+
+void QgsCustomization::QgsUserToolBarItem::readXmlItem( const QDomElement &elem )
+{
+ setTitle( elem.attribute( u"title"_s ) );
+};
+
+std::unique_ptr QgsCustomization::QgsUserToolBarItem::createChildItem( const QDomElement &childElem )
+{
+ if ( childElem.tagName() == "ActionRef"_L1 )
+ return std::make_unique( this );
+ else
+ return nullptr;
+}
+
+QgsCustomization::QgsItem::ItemCapability QgsCustomization::QgsUserToolBarItem::capabilities() const
+{
+ return static_cast(
+ static_cast( ItemCapability::AddActionRefChild )
+ | static_cast( ItemCapability::Rename )
+ | static_cast( ItemCapability::Delete )
+ );
+}
+
+////////////////
+
QgsCustomization::QgsToolBarsItem::QgsToolBarsItem()
: QgsItem()
{
@@ -374,10 +598,17 @@ std::unique_ptr QgsCustomization::QgsToolBarsItem::cr
{
if ( childElem.tagName() == "ToolBar"_L1 )
return std::make_unique( this );
+ else if ( childElem.tagName() == "UserToolBar"_L1 )
+ return std::make_unique( this );
else
return nullptr;
}
+QgsCustomization::QgsItem::ItemCapability QgsCustomization::QgsToolBarsItem::capabilities() const
+{
+ return ItemCapability::AddUserToolBarChild;
+}
+
////////////////
QgsCustomization::QgsMenusItem::QgsMenusItem()
@@ -403,10 +634,17 @@ std::unique_ptr QgsCustomization::QgsMenusItem::creat
{
if ( childElem.tagName() == "Menu"_L1 )
return std::make_unique( this );
+ else if ( childElem.tagName() == "UserMenu"_L1 )
+ return std::make_unique( this );
else
return nullptr;
}
+QgsCustomization::QgsItem::ItemCapability QgsCustomization::QgsMenusItem::capabilities() const
+{
+ return ItemCapability::AddUserMenuChild;
+}
+
////////////////
QgsCustomization::QgsDockItem::QgsDockItem( QgsItem *parent )
@@ -977,6 +1215,107 @@ QgsCustomization::QgsQActionsIterator::Iterator QgsCustomization::QgsQActionsIte
return Iterator( mWidget, mWidget->actions().count() );
}
+QWidget *QgsCustomization::findQWidget( const QString &path )
+{
+ QStringList pathElems = path.split( "/" );
+ if ( pathElems.isEmpty() )
+ return nullptr;
+
+ QgisApp *app = QgisApp::instance();
+ const QHash rootWidgets = { { "Menus", app->menuBar() }, { "ToolBars", app }, { "Docks", app }, { "StatusBarWidgets", app->statusBarIface() } };
+
+ const QString rootElem = pathElems.takeFirst();
+ QWidget *currentWidget = rootWidgets.value( rootElem );
+ if ( !currentWidget )
+ return nullptr;
+
+ for ( const QString &pathElem : pathElems )
+ {
+ if ( dynamic_cast( currentWidget )
+ || dynamic_cast( currentWidget )
+ || dynamic_cast( currentWidget ) )
+ {
+ QgsQActionsIterator actionsIterator( currentWidget );
+ currentWidget = nullptr;
+ for ( QgsQActionsIterator::Info it : actionsIterator )
+ {
+ if ( it.name == pathElem )
+ {
+ currentWidget = it.widget;
+ break;
+ }
+ }
+ }
+ else
+ {
+ QList children = currentWidget->children();
+ QList::const_iterator it = std::find_if( children.cbegin(), children.cend(), [&pathElem]( QObject *obj ) { return dynamic_cast( obj ) && obj->objectName() == pathElem; } );
+ currentWidget = it == children.cend() ? nullptr : dynamic_cast( *it );
+ }
+
+ if ( !currentWidget )
+ return nullptr;
+ }
+
+ return currentWidget;
+}
+
+QAction *QgsCustomization::findQAction( const QString &path )
+{
+ qsizetype lastSlashIndex = path.lastIndexOf( "/" );
+ if ( lastSlashIndex < 0 )
+ return nullptr;
+
+ QWidget *currentWidget = findQWidget( path.first( lastSlashIndex ) );
+ if ( !currentWidget )
+ return nullptr;
+
+ const QString actionName = path.mid( lastSlashIndex + 1 );
+
+ const QList actions = currentWidget->actions();
+ const QList::const_iterator actionIt = std::find_if( actions.cbegin(), actions.cend(), [&actionName]( QAction *action ) { return action->objectName() == actionName; } );
+ return actionIt != actions.cend() ? *actionIt : nullptr;
+}
+
+template
+void QgsCustomization::updateMenuActionVisibility( QgsCustomization::QgsItem *parentItem, WidgetType *parentWidget )
+{
+ // clear all user menu
+ const QList widgetActions = parentWidget->actions();
+ for ( QAction *action : widgetActions )
+ {
+ const QMenu *menu = action->menu();
+ if ( menu && menu->property( "__usermenu__" ).toBool() )
+ {
+ parentWidget->removeAction( action );
+ }
+ }
+
+ // update non-user menu visibility
+ updateActionVisibility( parentItem, parentWidget );
+
+ // add user menu
+ for ( const std::unique_ptr &childItem : parentItem->childItemList() )
+ {
+ if ( QgsCustomization::QgsUserMenuItem *userMenu = dynamic_cast( childItem.get() ) )
+ {
+ QMenu *menu = new QMenu( userMenu->title(), parentWidget );
+ menu->setProperty( "__usermenu__", true );
+ menu->setObjectName( userMenu->name() );
+ parentWidget->addMenu( menu );
+
+ updateMenuActionVisibility( userMenu, menu );
+ }
+ else if ( QgsCustomization::QgsActionRefItem *actionRef = dynamic_cast( childItem.get() ) )
+ {
+ if ( QAction *action = findQAction( actionRef->actionRefPath() ) )
+ {
+ parentWidget->addAction( action );
+ }
+ }
+ }
+}
+
void QgsCustomization::updateActionVisibility( QgsCustomization::QgsItem *item, QWidget *widget )
{
if ( !item || !widget )
@@ -994,7 +1333,10 @@ void QgsCustomization::updateActionVisibility( QgsCustomization::QgsItem *item,
widget->removeAction( it.action );
}
- updateActionVisibility( childItem, it.widget );
+ if ( QMenu *menu = dynamic_cast( it.widget ) )
+ updateMenuActionVisibility( childItem, menu );
+ else
+ updateActionVisibility( childItem, it.widget );
}
}
@@ -1033,7 +1375,7 @@ void QgsCustomization::applyToMenus() const
return;
QMenuBar *menuBar = mQgisApp->menuBar();
- updateActionVisibility( mMenus.get(), menuBar );
+ updateMenuActionVisibility( mMenus.get(), menuBar );
}
void QgsCustomization::applyToStatusBarWidgets() const
@@ -1061,17 +1403,49 @@ void QgsCustomization::applyToToolBars() const
if ( !mQgisApp )
return;
- const auto toolBars = mQgisApp->findChildren( QString(), Qt::FindDirectChildrenOnly );
- for ( QToolBar *tb : toolBars )
+ const auto toolBarWidgets = mQgisApp->findChildren( QString(), Qt::FindDirectChildrenOnly );
+ for ( QToolBar *tb : toolBarWidgets )
{
- const QString name = tb->objectName();
- if ( QgsToolBarItem *t = mToolBars->getChild( name ) )
+ if ( !tb )
+ continue;
+
+ if ( tb->property( "__usertoolbar__" ).toBool() )
+ {
+ // delete old toolbar, will recreate it later
+ QgisApp::instance()->removeToolBar( tb );
+ delete tb;
+ }
+ else if ( QgsToolBarItem *t = mToolBars->getChild( tb->objectName() ) )
{
tb->setVisible( t->wasVisible() && t->isVisible() );
tb->toggleViewAction()->setVisible( t->isVisible() );
updateActionVisibility( t, tb );
}
}
+
+ for ( const std::unique_ptr &childItem : toolBarsItem()->childItemList() )
+ {
+ if ( QgsCustomization::QgsUserToolBarItem *userToolBar = dynamic_cast( childItem.get() ) )
+ {
+ QToolBar *toolBar = new QToolBar( userToolBar->title(), QgisApp::instance() );
+ toolBar->setProperty( "__usertoolBar__", true );
+ toolBar->setObjectName( userToolBar->name() );
+ QgisApp::instance()->addToolBar( toolBar );
+
+ for ( const std::unique_ptr &actionRefItem : userToolBar->childItemList() )
+ {
+ if ( QgsCustomization::QgsActionRefItem *actionRef = dynamic_cast( actionRefItem.get() ) )
+ {
+ if ( QAction *action = findQAction( actionRef->actionRefPath() ) )
+ {
+ toolBar->addAction( action );
+ }
+ }
+ }
+
+ updateActionVisibility( userToolBar, toolBar );
+ }
+ }
}
@@ -1313,3 +1687,88 @@ void QgsCustomization::loadOldIniFile( const QString &filePath )
settings.endGroup();
}
+
+namespace
+{
+
+ int maxSuffixNum( const QgsCustomization::QgsItem *item, const QString &baseName )
+ {
+ int max = 0;
+
+ if ( item->name().startsWith( baseName ) )
+ {
+ bool ok = false;
+ const int suffixNum = item->name().mid( baseName.length() ).toInt( &ok );
+ if ( ok )
+ max = suffixNum;
+ }
+
+ for ( const std::unique_ptr &childItem : item->childItemList() )
+ {
+ const int childSuffixNum = maxSuffixNum( childItem.get(), baseName );
+ max = std::max( childSuffixNum, max );
+ }
+
+ return max;
+ }
+
+} //namespace
+
+QString QgsCustomization::uniqueItemName( const QString &baseName ) const
+{
+ // Now, we can only create only new child item for Menus and ToolBars
+ // We could have the same name in Menus and ToolBars but it's cleaner to have unique name within the 2
+
+ int suffixNum = maxSuffixNum( mMenus.get(), baseName );
+ suffixNum = std::max( suffixNum, maxSuffixNum( mToolBars.get(), baseName ) );
+
+ return QString( "%1%2" ).arg( baseName ).arg( ++suffixNum );
+}
+
+QString QgsCustomization::uniqueMenuName() const
+{
+ return uniqueItemName( u"UserMenu_"_s );
+}
+
+QString QgsCustomization::uniqueToolBarName() const
+{
+ return uniqueItemName( u"UserToolBar_"_s );
+}
+
+QString QgsCustomization::uniqueActionName( const QString &originalActionName ) const
+{
+ return uniqueItemName( u"ActionRef_"_s + originalActionName + "_" );
+}
+
+QgsCustomization::QgsItem *QgsCustomization::getItem( const QString &path ) const
+{
+ const QStringList pathElems = path.split( "/" );
+ if ( pathElems.isEmpty() )
+ return nullptr;
+
+ const QHash rootItems = {
+ { "Menus", menusItem() },
+ { "ToolBars", toolBarsItem() },
+ { "Docks", docksItem() },
+ { "BrowserItems", browserElementsItem() },
+ { "StatusBarWidgets", statusBarWidgetsItem() }
+ };
+
+ QgsCustomization::QgsItem *currentItem = nullptr;
+ for ( const QString &pathElem : pathElems )
+ {
+ if ( currentItem )
+ {
+ currentItem = currentItem->getChild( pathElem );
+ }
+ else
+ {
+ currentItem = rootItems.value( pathElem );
+ }
+
+ if ( !currentItem )
+ return nullptr;
+ }
+
+ return currentItem;
+}
diff --git a/src/app/qgscustomization.h b/src/app/qgscustomization.h
index 234fb1601b91..556e17ac69a1 100644
--- a/src/app/qgscustomization.h
+++ b/src/app/qgscustomization.h
@@ -3,7 +3,9 @@
-------------------
begin : 2011-04-01
copyright : (C) 2011 Radim Blazek
+ : (C) 2025 Julien Cabieces
email : radim dot blazek at gmail dot com
+ julien dot cabieces at oslandia dot com
***************************************************************************/
/***************************************************************************
@@ -155,6 +157,16 @@ class APP_EXPORT QgsCustomization
*/
void addChild( std::unique_ptr item );
+ /**
+ * Insert \a item at \a position
+ */
+ void insertChild( int position, std::unique_ptr item );
+
+ /**
+ * Delete item at \a position
+ */
+ void deleteChild( int position );
+
/**
* Return child item at \a index position, nullptr if index is outside the children list bounds
*/
@@ -223,6 +235,25 @@ class APP_EXPORT QgsCustomization
*/
virtual std::unique_ptr clone( QgsCustomization::QgsItem *parent = nullptr ) const = 0;
+ /**
+ * Item capability
+ */
+ enum class ItemCapability : int
+ {
+ None = 0, //! No capability
+ AddUserMenuChild = 1 << 0, //! Support adding UserMenu item as child
+ AddActionRefChild = 1 << 1, //! Support adding ActionRef as child
+ AddUserToolBarChild = 1 << 2, //! Support adding UserToolBar as child
+ Rename = 1 << 3, //! Support renaming
+ Delete = 1 << 4, //! Support delete
+ Drag = 1 << 5 //! Support dragging for later droping
+ };
+
+ /**
+ * Returns TRUE if \a capability is active
+ */
+ bool hasCapability( ItemCapability capability ) const;
+
protected:
/**
* Returns XML tag
@@ -239,6 +270,21 @@ class APP_EXPORT QgsCustomization
*/
virtual void copyItemAttributes( const QgsCustomization::QgsItem *other );
+ /**
+ * Write item content to XML element \a elem
+ */
+ virtual void writeXmlItem( QDomElement &elem ) const;
+
+ /**
+ * Read item content from XML element \a elem
+ */
+ virtual void readXmlItem( const QDomElement &elem );
+
+ /**
+ * Returns item capabilities
+ */
+ virtual ItemCapability capabilities() const;
+
QString mName;
private:
@@ -286,6 +332,11 @@ class APP_EXPORT QgsCustomization
*/
qsizetype qActionIndex() const;
+ /**
+ * Returns action path in the application
+ */
+ QString path() const;
+
/**
* Returns this item clone with \a parent as parent item
*/
@@ -297,12 +348,39 @@ class APP_EXPORT QgsCustomization
QString xmlTag() const override;
std::unique_ptr createChildItem( const QDomElement &childElem ) override;
void copyItemAttributes( const QgsItem *other ) override;
+ ItemCapability capabilities() const override;
private:
QAction *mQAction = nullptr;
qsizetype mQActionIndex = -1;
};
+ class QgsActionRefItem : public QgsActionItem
+ {
+ public:
+ QgsActionRefItem( QgsItem *parent );
+ QgsActionRefItem( const QString &name, const QString &title, const QString &path, QgsItem *parent );
+
+ /**
+ * Returns referenced action path. Path is a '/' separated list of
+ * items name representing the targeted item in its hierarchy
+ */
+ const QString &actionRefPath() const;
+
+ std::unique_ptr clone( QgsCustomization::QgsItem *parent = nullptr ) const override;
+
+ protected:
+ QString xmlTag() const override;
+ std::unique_ptr createChildItem( const QDomElement & ) override;
+ void readXmlItem( const QDomElement &elem ) override;
+ void writeXmlItem( QDomElement &elem ) const override;
+ ItemCapability capabilities() const override;
+ void copyItemAttributes( const QgsItem *other ) override;
+
+ private:
+ QString mPath;
+ };
+
/**
* Represent a Menu
* Inherits from Action because QMenu are stored within a QAction and we want to keep
@@ -316,6 +394,7 @@ class APP_EXPORT QgsCustomization
* \param parent parent Item
*/
QgsMenuItem( QgsItem *parent );
+
/**
* Constructor
* \param name name identifier
@@ -334,6 +413,35 @@ class APP_EXPORT QgsCustomization
protected:
QString xmlTag() const override;
std::unique_ptr createChildItem( const QDomElement &childElem ) override;
+ ItemCapability capabilities() const override;
+ };
+
+ class QgsUserMenuItem : public QgsMenuItem
+ {
+ public:
+ /**
+ * Constructor
+ * \param parent parent Item
+ */
+ QgsUserMenuItem( QgsItem *parent );
+
+ /**
+ * Constructor
+ * \param name name identifier
+ * \param title title
+ * \param parent parent Item
+ */
+ QgsUserMenuItem( const QString &name, const QString &title, QgsItem *parent );
+
+ ItemCapability capabilities() const override;
+
+ std::unique_ptr clone( QgsCustomization::QgsItem *parent = nullptr ) const override;
+
+ protected:
+ QString xmlTag() const override;
+ void writeXmlItem( QDomElement &elem ) const override;
+ void readXmlItem( const QDomElement &elem ) override;
+ std::unique_ptr createChildItem( const QDomElement &childElem ) override;
};
/**
@@ -385,6 +493,33 @@ class APP_EXPORT QgsCustomization
bool mWasVisible = false;
};
+ class QgsUserToolBarItem : public QgsToolBarItem
+ {
+ public:
+ /**
+ * Constructor
+ * \param parent parent Item
+ */
+ QgsUserToolBarItem( QgsItem *parent );
+
+ /**
+ * Constructor
+ * \param name name identifier
+ * \param title title
+ * \param parent parent Item
+ */
+ QgsUserToolBarItem( const QString &name, const QString &title, QgsItem *parent );
+
+ std::unique_ptr clone( QgsItem *parent = nullptr ) const override;
+
+ protected:
+ QString xmlTag() const override;
+ void writeXmlItem( QDomElement &elem ) const override;
+ void readXmlItem( const QDomElement &elem ) override;
+ std::unique_ptr createChildItem( const QDomElement &childElem ) override;
+ ItemCapability capabilities() const override;
+ };
+
/**
* Root item for all ToolBar item
*/
@@ -406,6 +541,7 @@ class APP_EXPORT QgsCustomization
protected:
QString xmlTag() const override;
std::unique_ptr createChildItem( const QDomElement &childElem ) override;
+ ItemCapability capabilities() const override;
};
/**
@@ -429,6 +565,7 @@ class APP_EXPORT QgsCustomization
protected:
QString xmlTag() const override;
std::unique_ptr createChildItem( const QDomElement &childElem ) override;
+ ItemCapability capabilities() const override;
};
/**
@@ -662,6 +799,39 @@ class APP_EXPORT QgsCustomization
*/
QString writeFile( const QString &filePath ) const;
+ /**
+ * Returns a menu unique name within the entire application
+ */
+ QString uniqueMenuName() const;
+
+ /**
+ * Returns a tool bar unique name within the entire application
+ */
+ QString uniqueToolBarName() const;
+
+ /**
+ * Returns an action unique name within the entire application
+ */
+ QString uniqueActionName( const QString &originalActionName ) const;
+
+ /**
+ * Returns customization item according to its \a path. \a path is a '/' separated list of
+ * items name representing the returned item in its hierarchy
+ * Returns nullptr if the item is not found or not the appropriate type
+ */
+ template
+ T *getItem( const QString &path ) const
+ {
+ return dynamic_cast( getItem( path ) );
+ }
+
+ /**
+ * Returns customization item according to its \a path. \a path is a '/' separated list of
+ * items name representing the returned item in its hierarchy
+ * Returns nullptr if the item is not found
+ */
+ QgsCustomization::QgsItem *getItem( const QString &path ) const;
+
private:
/**
* Add action items as children of \a item for each \a widget actions
@@ -777,10 +947,31 @@ class APP_EXPORT QgsCustomization
void loadOldIniFile( const QString &filePath );
/**
- * Update \a widget visibility based on \a item
+ * Update action \a widget visibility based on \a item
*/
static void updateActionVisibility( QgsCustomization::QgsItem *item, QWidget *widget );
+ /**
+ * Update menu \a widget visibility based on \a item
+ */
+ template
+ static void updateMenuActionVisibility( QgsCustomization::QgsItem *parentItem, WidgetType *parentWidget );
+
+ /**
+ * Returns QWidget corresponding to \a path. Path is a '/' separated list of
+ * items name representing the targeted item in its widget hierarchy
+ */
+ static QWidget *findQWidget( const QString &path );
+
+ /**
+ * Returns QAction corresponding to \a path. Path is a '/' separated list of
+ * items name representing the targeted item in its widget hierarchy
+ */
+ static QAction *findQAction( const QString &path );
+
+ QString uniqueItemName( const QString &baseName ) const;
+ QAction *findAction( const QString &path ) const;
+
std::unique_ptr mBrowserItems;
std::unique_ptr mDocks;
std::unique_ptr mMenus;
diff --git a/src/app/qgscustomizationdialog.cpp b/src/app/qgscustomizationdialog.cpp
index 990f348c92af..6a0845367173 100644
--- a/src/app/qgscustomizationdialog.cpp
+++ b/src/app/qgscustomizationdialog.cpp
@@ -27,6 +27,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -43,6 +44,8 @@ constexpr int TOOLBAR_COLUMN = 4;
const QgsSettingsEntryString *QgsCustomizationDialog::sSettingLastSaveDir = new QgsSettingsEntryString( u"last-save-directory"_s, sTreeCustomization, QDir::homePath(), u"Last directory used when saving a customization XML file"_s );
+#define ACTIONPATHS_MIMEDATA_NAME "application/qgis.customization.actionpaths"
+
QgsCustomizationDialog::QgsCustomizationModel::QgsCustomizationModel( QgisApp *qgisApp, Mode mode, QObject *parent )
: QAbstractItemModel( parent )
, mMode( mode )
@@ -86,15 +89,32 @@ QVariant QgsCustomizationDialog::QgsCustomizationModel::data( const QModelIndex
bool QgsCustomizationDialog::QgsCustomizationModel::setData( const QModelIndex &index, const QVariant &value, int role )
{
- if ( !index.isValid() || role != Qt::CheckStateRole )
+ if ( !index.isValid() || ( role != Qt::ItemDataRole::CheckStateRole && role != Qt::ItemDataRole::EditRole ) )
return false;
QgsCustomization::QgsItem *item = static_cast( index.internalPointer() );
- if ( item )
+ if ( !item )
+ return false;
+
+ switch ( role )
{
- item->setVisible( value.value() == Qt::CheckState::Checked );
- emit dataChanged( index, index, QList() << Qt::CheckStateRole );
- return true;
+ case Qt::ItemDataRole::CheckStateRole:
+ item->setVisible( value.value() == Qt::CheckState::Checked );
+ emit dataChanged( index, index, QList() << Qt::CheckStateRole );
+ return true;
+ break;
+
+ case Qt::ItemDataRole::EditRole:
+ if ( index.column() == 1 )
+ {
+ item->setTitle( value.toString() );
+ emit dataChanged( index, index, QList() << Qt::EditRole );
+ return true;
+ }
+ break;
+
+ default:
+ break;
}
return false;
@@ -106,10 +126,24 @@ Qt::ItemFlags QgsCustomizationDialog::QgsCustomizationModel::flags( const QModel
return Qt::ItemFlags();
Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+ QgsCustomization::QgsItem *item = index.parent().isValid() ? static_cast( index.internalPointer() ) : nullptr;
+ switch ( mMode )
+ {
+ case Mode::ActionSelector:
+ if ( item && item->hasCapability( QgsCustomization::QgsItem::ItemCapability::Drag ) )
+ flags |= Qt::ItemIsDragEnabled;
+ break;
- if ( mMode == Mode::ItemVisibility && index.parent().isValid() && index.column() == 0 )
- flags |= Qt::ItemIsUserCheckable;
+ case Mode::ItemVisibility:
+ // only root item (which have an invalid parent) are checkable
+ if ( index.parent().isValid() && index.column() == 0 )
+ flags |= Qt::ItemIsUserCheckable;
+
+ if ( item && item->hasCapability( QgsCustomization::QgsItem::ItemCapability::Rename ) && index.column() == 1 )
+ flags |= Qt::ItemIsEditable;
+ flags |= Qt::ItemIsDropEnabled;
+ }
return flags;
}
@@ -237,6 +271,179 @@ const std::unique_ptr &QgsCustomizationDialog::QgsCustomizatio
return mCustomization;
}
+QModelIndex QgsCustomizationDialog::QgsCustomizationModel::addUserItem( const QModelIndex &parent )
+{
+ QgsCustomization::QgsItem *item = parent.isValid() ? static_cast( parent.internalPointer() ) : nullptr;
+
+ if ( !item || ( !item->hasCapability( QgsCustomization::QgsItem::ItemCapability::AddUserMenuChild ) && !item->hasCapability( QgsCustomization::QgsItem::ItemCapability::AddUserToolBarChild ) ) )
+ return {};
+
+ const int nbChildren = static_cast( item->childItemList().size() );
+ beginInsertRows( parent, nbChildren, nbChildren );
+
+ if ( item->hasCapability( QgsCustomization::QgsItem::ItemCapability::AddUserMenuChild ) )
+ {
+ const QString name = mCustomization->uniqueMenuName();
+ item->addChild( std::make_unique( name, name, item ) );
+ }
+ else if ( item->hasCapability( QgsCustomization::QgsItem::ItemCapability::AddUserToolBarChild ) )
+ {
+ const QString name = mCustomization->uniqueToolBarName();
+ item->addChild( std::make_unique( name, name, item ) );
+ }
+ else
+ {
+ QgsDebugError( "Cannot add child to this item" );
+ return {};
+ }
+
+ endInsertRows();
+
+ return index( nbChildren, 0, parent );
+}
+
+void QgsCustomizationDialog::QgsCustomizationModel::deleteUserItems( const QModelIndexList &indexes )
+{
+ QHash toDelete;
+ bool allAcceptDelete = false;
+ for ( const QModelIndex &index : indexes )
+ {
+ QgsCustomization::QgsItem *item = index.isValid() ? static_cast( index.internalPointer() ) : nullptr;
+ allAcceptDelete = item && item->hasCapability( QgsCustomization::QgsItem::ItemCapability::Delete );
+ if ( !allAcceptDelete )
+ break;
+
+ toDelete.insert( item, index );
+ }
+
+ // Shall not happen, but better to check anyway
+ if ( !allAcceptDelete )
+ return;
+
+ // We need to remove all items whose parent is already in deleted list
+ std::function alreadyDeleted = [&alreadyDeleted, &toDelete]( QgsCustomization::QgsItem *item ) {
+ return item && ( toDelete.contains( item->parent() ) || alreadyDeleted( item->parent() ) );
+ };
+
+ QHash::iterator it = toDelete.begin();
+ while ( it != toDelete.end() )
+ {
+ // NOLINTBEGIN(bugprone-branch-clone)
+ if ( alreadyDeleted( it.key() ) )
+ {
+ it = toDelete.erase( it );
+ }
+ else
+ {
+ ++it;
+ }
+ // NOLINTEND(bugprone-branch-clone)
+ }
+
+ for ( QHash::const_iterator it = toDelete.cbegin(); it != toDelete.cend(); ++it )
+ {
+ QgsCustomization::QgsItem *item = it.key();
+ const int index = static_cast( item->parent() ? item->parent()->indexOf( item ) : -1 );
+ if ( index < 0 )
+ {
+ QgsDebugError( "Impossible to find item among parent's children" );
+ continue;
+ }
+
+ beginRemoveRows( parent( it.value() ), index, index );
+ item->parent()->deleteChild( index );
+ endRemoveRows();
+ }
+}
+
+QMimeData *QgsCustomizationDialog::QgsCustomizationModel::mimeData( const QModelIndexList &indexes ) const
+{
+ QMimeData *mimeData = new QMimeData();
+
+
+ QStringList strActionPaths;
+ QSet rows;
+ for ( const QModelIndex &index : indexes )
+ {
+ // there could be several indexes for each column representing the same row
+ if ( rows.contains( index.row() ) )
+ continue;
+
+ rows << index.row();
+
+ QgsCustomization::QgsItem *item = index.isValid() ? static_cast( index.internalPointer() ) : nullptr;
+ if ( QgsCustomization::QgsActionItem *action = dynamic_cast( item ) )
+ strActionPaths << action->path();
+ }
+
+ QByteArray actionPaths;
+ QDataStream dataStreamWrite( &actionPaths, QIODevice::WriteOnly );
+ dataStreamWrite << strActionPaths;
+
+ mimeData->setData( QStringLiteral( ACTIONPATHS_MIMEDATA_NAME ), actionPaths );
+
+ return mimeData;
+}
+
+bool QgsCustomizationDialog::QgsCustomizationModel::canDropMimeData( const QMimeData *data, Qt::DropAction action, int, int, const QModelIndex & ) const
+{
+ // QgsCustomization::Item *item = parent.isValid() ? static_cast( parent.internalPointer() ) : nullptr;
+ return ( action == Qt::DropAction::LinkAction || action == Qt::DropAction::MoveAction || action == Qt::DropAction::CopyAction )
+ // TODO Qt issue https://qt-project.atlassian.net/browse/QTBUG-76418?focusedCommentId=465643
+ // canDropMimeData() doesn't work if the result value differs from one index to another, specially
+ // when we start with a cannot-drop-item after we start dragging
+ // Try to see if we can workaround thin in dragEnterEvent
+ // uncomment the following lines when fixed
+ /* && item && item->hasCapability( QgsCustomization::Item::ItemCapability::UserMenuChild ) */
+ && data && data->hasFormat( QStringLiteral( ACTIONPATHS_MIMEDATA_NAME ) );
+}
+
+bool QgsCustomizationDialog::QgsCustomizationModel::dropMimeData( const QMimeData *data, Qt::DropAction action, int row, int, const QModelIndex &parent )
+{
+ if ( action == Qt::IgnoreAction )
+ return true;
+
+ QgsCustomization::QgsItem *item = parent.isValid() ? static_cast( parent.internalPointer() ) : nullptr;
+ if ( !item || !item->hasCapability( QgsCustomization::QgsItem::ItemCapability::AddActionRefChild ) || !data || !data->hasFormat( QStringLiteral( ACTIONPATHS_MIMEDATA_NAME ) ) )
+ return false;
+
+ QDataStream dataStreamRead( data->data( QStringLiteral( ACTIONPATHS_MIMEDATA_NAME ) ) );
+ QStringList actionPaths;
+ dataStreamRead >> actionPaths;
+
+ QList> actions; // name, path
+ for ( QString actionPath : actionPaths )
+ {
+ QgsCustomization::QgsActionItem *action = mCustomization->getItem( actionPath );
+ if ( !action )
+ {
+ QgsDebugError( u"Invalid action path '%1'"_s.arg( actionPath ) );
+ continue;
+ }
+
+ actions << QPair { action, actionPath };
+ }
+
+ if ( actions.isEmpty() )
+ return false;
+
+ if ( row == -1 )
+ row = 0; // if dropped directly onto group item, insert at first position
+
+ beginInsertRows( parent, row, row + static_cast( actions.count() ) - 1 );
+ for ( QPair actionAndPath : actions )
+ {
+ QgsCustomization::QgsActionItem *action = actionAndPath.first;
+ auto actionRef = std::make_unique( mCustomization->uniqueActionName( action->name() ), action->title(), actionAndPath.second, item );
+ actionRef->setVisible( action->isVisible() );
+ item->insertChild( row, std::move( actionRef ) );
+ }
+
+ endInsertRows();
+
+ return true;
+}
+
////////////////
QgsCustomizationDialog::QgsCustomizationDialog( QgisApp *qgisApp )
@@ -257,6 +464,9 @@ QgsCustomizationDialog::QgsCustomizationDialog( QgisApp *qgisApp )
connect( actionSelectAll, &QAction::triggered, this, &QgsCustomizationDialog::onSelectAll );
connect( mCustomizationEnabledCheckBox, &QCheckBox::toggled, this, &QgsCustomizationDialog::enableCustomization );
connect( actionCatch, &QAction::triggered, this, &QgsCustomizationDialog::onActionCatchToggled );
+ connect( mAddAction, &QAction::triggered, this, &QgsCustomizationDialog::addUserItem );
+ connect( mDeleteAction, &QAction::triggered, this, &QgsCustomizationDialog::deleteSelectedItems );
+ connect( mListActionsAction, &QAction::triggered, this, &QgsCustomizationDialog::updateSplitterSizes );
connect( buttonBox->button( QDialogButtonBox::Ok ), &QAbstractButton::clicked, this, &QgsCustomizationDialog::ok );
connect( buttonBox->button( QDialogButtonBox::Apply ), &QAbstractButton::clicked, this, &QgsCustomizationDialog::apply );
@@ -264,28 +474,53 @@ QgsCustomizationDialog::QgsCustomizationDialog( QgisApp *qgisApp )
connect( buttonBox->button( QDialogButtonBox::Reset ), &QAbstractButton::clicked, this, &QgsCustomizationDialog::reset );
connect( buttonBox->button( QDialogButtonBox::Help ), &QAbstractButton::clicked, this, &QgsCustomizationDialog::showHelp );
- mItemsVisibilityModel = new QgsCustomizationModel( mQgisApp, QgsCustomizationModel::Mode::ItemVisibility, this );
- QSortFilterProxyModel *proxyModel = new QSortFilterProxyModel( this );
- proxyModel->sort( 0 );
- proxyModel->setFilterKeyColumn( 0 );
- proxyModel->setFilterCaseSensitivity( Qt::CaseSensitivity::CaseInsensitive );
- proxyModel->setSortCaseSensitivity( Qt::CaseSensitivity::CaseInsensitive );
+ {
+ mActionsModel = new QgsCustomizationModel( mQgisApp, QgsCustomizationModel::Mode::ActionSelector, this );
+ QSortFilterProxyModel *proxyModel = new QSortFilterProxyModel( this );
+ proxyModel->sort( 0 );
+ proxyModel->setFilterKeyColumn( 0 );
+ proxyModel->setFilterCaseSensitivity( Qt::CaseSensitivity::CaseInsensitive );
+ proxyModel->setSortCaseSensitivity( Qt::CaseSensitivity::CaseInsensitive );
+
+ proxyModel->setRecursiveFilteringEnabled( true );
+ proxyModel->setSourceModel( mActionsModel );
+ mActionsTreeView->setModel( proxyModel );
+ mActionsTreeView->header()->resizeSection( 0, 250 );
+ connect( mFilterActionsLe, &QgsFilterLineEdit::valueChanged, proxyModel, &QSortFilterProxyModel::setFilterFixedString );
+ }
-#ifdef QGIS_DEBUG
- new QAbstractItemModelTester( mItemsVisibilityModel, QAbstractItemModelTester::FailureReportingMode::Fatal, this );
+ {
+ mItemsVisibilityModel = new QgsCustomizationModel( mQgisApp, QgsCustomizationModel::Mode::ItemVisibility, this );
+ QSortFilterProxyModel *proxyModel = new QSortFilterProxyModel( this );
+ proxyModel->setFilterKeyColumn( 0 );
+ proxyModel->setFilterCaseSensitivity( Qt::CaseSensitivity::CaseInsensitive );
+ proxyModel->setSortCaseSensitivity( Qt::CaseSensitivity::CaseInsensitive );
+
+#ifdef DEBUG_MODEL
+ new QAbstractItemModelTester( mItemsVisibilityModel, QAbstractItemModelTester::FailureReportingMode::Fatal, this );
#endif
- proxyModel->setRecursiveFilteringEnabled( true );
- proxyModel->setSourceModel( mItemsVisibilityModel );
- mTreeView->setModel( proxyModel );
- mTreeView->resizeColumnToContents( 0 );
- mTreeView->header()->resizeSection( 0, 250 );
- connect( mFilterLe, &QgsFilterLineEdit::valueChanged, proxyModel, &QSortFilterProxyModel::setFilterFixedString );
+ proxyModel->setRecursiveFilteringEnabled( true );
+ proxyModel->setSourceModel( mItemsVisibilityModel );
+ mTreeView->setModel( proxyModel );
+ mTreeView->resizeColumnToContents( 0 );
+ mTreeView->header()->resizeSection( 0, 250 );
+ mTreeView->setContextMenuPolicy( Qt::ContextMenuPolicy::ActionsContextMenu );
+ mTreeView->addActions( QList() << mDeleteAction << mAddAction );
+ connect( mFilterLe, &QgsFilterLineEdit::valueChanged, proxyModel, &QSortFilterProxyModel::setFilterFixedString );
+ connect( mTreeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &QgsCustomizationDialog::currentItemChanged );
+ connect( mTreeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &QgsCustomizationDialog::selectedItemsChanged );
+ }
+ mActionsTreeView->setEnabled( false );
+ mFilterActionsLe->setEnabled( false );
mTreeView->setEnabled( false );
toolBar->setEnabled( false );
mFilterLe->setEnabled( false );
mCustomizationEnabledCheckBox->setChecked( customization()->isEnabled() );
+ updateSplitterSizes();
+ currentItemChanged();
+ selectedItemsChanged();
}
void QgsCustomizationDialog::reset()
@@ -377,9 +612,11 @@ void QgsCustomizationDialog::onSelectAll( bool )
void QgsCustomizationDialog::enableCustomization( bool checked )
{
+ mActionsTreeView->setEnabled( checked );
mTreeView->setEnabled( checked );
toolBar->setEnabled( checked );
mFilterLe->setEnabled( checked );
+ mFilterActionsLe->setEnabled( checked );
if ( checked != customization()->isEnabled() )
customization()->setEnabled( checked );
@@ -390,6 +627,69 @@ void QgsCustomizationDialog::showHelp()
QgsHelp::openHelp( u"introduction/qgis_configuration.html#sec-customization"_s );
}
+void QgsCustomizationDialog::currentItemChanged()
+{
+ const QModelIndex index = treeViewModel()->mapToSource( mTreeView->currentIndex() );
+ QgsCustomization::QgsItem *item = index.isValid() ? static_cast( index.internalPointer() ) : nullptr;
+
+ const bool isEnabled = item && ( item->hasCapability( QgsCustomization::QgsItem::ItemCapability::AddUserMenuChild ) || item->hasCapability( QgsCustomization::QgsItem::ItemCapability::AddUserToolBarChild ) );
+ mAddAction->setEnabled( isEnabled );
+
+ QString tooltip = tr( "Add a user defined menu or toolbar" );
+ if ( !isEnabled )
+ tooltip += "
" + tr( "Current item doesn't accept user menu or toolbar" );
+
+ mAddAction->setToolTip( tooltip );
+}
+
+void QgsCustomizationDialog::selectedItemsChanged()
+{
+ bool allAcceptDelete = false;
+ for ( const QModelIndex &index : mTreeView->selectionModel()->selectedIndexes() )
+ {
+ const QModelIndex sourceIndex = treeViewModel()->mapToSource( index );
+ QgsCustomization::QgsItem *item = sourceIndex.isValid() ? static_cast( sourceIndex.internalPointer() ) : nullptr;
+ allAcceptDelete = item && item->hasCapability( QgsCustomization::QgsItem::ItemCapability::Delete );
+ if ( !allAcceptDelete )
+ break;
+ }
+
+ mDeleteAction->setEnabled( allAcceptDelete );
+
+ QString tooltip = tr( "Delete selected items" );
+ if ( !allAcceptDelete )
+ tooltip += "
" + tr( "Currently selected item are not all deletable" );
+
+ mDeleteAction->setToolTip( tooltip );
+}
+
+void QgsCustomizationDialog::addUserItem()
+{
+ const QModelIndex parentIndex = treeViewModel()->mapToSource( mTreeView->selectionModel()->currentIndex() );
+ const QModelIndex userItemIndex = mItemsVisibilityModel->addUserItem( parentIndex );
+ const QModelIndex viewUserItemIndex = treeViewModel()->mapFromSource( userItemIndex );
+ mTreeView->scrollTo( viewUserItemIndex );
+ mTreeView->setCurrentIndex( viewUserItemIndex );
+}
+
+void QgsCustomizationDialog::deleteSelectedItems()
+{
+ QModelIndexList sourceIndexes;
+ for ( const QModelIndex &index : mTreeView->selectionModel()->selectedIndexes() )
+ {
+ sourceIndexes << treeViewModel()->mapToSource( index );
+ }
+
+ mItemsVisibilityModel->deleteUserItems( sourceIndexes );
+}
+
+void QgsCustomizationDialog::updateSplitterSizes()
+{
+ // the splitter size values are arbitrary here, the only important thing is to set 0
+ // when the tool button is not checked
+ mSplitter->setSizes( QList() << 200 << ( mListActionsAction->isChecked() ? 200 : 0 ) );
+}
+
void QgsCustomizationDialog::onActionCatchToggled( bool triggered )
{
if ( triggered )
@@ -443,7 +743,7 @@ bool QgsCustomizationDialog::selectWidget( QWidget *widget )
if ( items.isEmpty() )
return false;
- const QModelIndex currentIndex = static_cast( mTreeView->model() )->mapFromSource( items.first() );
+ const QModelIndex currentIndex = treeViewModel()->mapFromSource( items.first() );
mTreeView->selectionModel()->setCurrentIndex( currentIndex, QItemSelectionModel::SelectCurrent );
raise();
@@ -467,7 +767,12 @@ void QgsCustomizationDialog::preNotify( QObject *receiver, QEvent *event, bool *
}
}
-const std::unique_ptr &QgsCustomizationDialog::customization() const
+QgsCustomization *QgsCustomizationDialog::customization() const
+{
+ return mItemsVisibilityModel->customization().get();
+}
+
+QSortFilterProxyModel *QgsCustomizationDialog::treeViewModel() const
{
- return mItemsVisibilityModel->customization();
+ return static_cast( mTreeView->model() );
}
diff --git a/src/app/qgscustomizationdialog.h b/src/app/qgscustomizationdialog.h
index 57f58ce2a030..0961354153ba 100644
--- a/src/app/qgscustomizationdialog.h
+++ b/src/app/qgscustomizationdialog.h
@@ -23,6 +23,7 @@
#include "qgssettingstree.h"
class QgisApp;
+class QSortFilterProxyModel;
/**
* \ingroup app
@@ -112,6 +113,31 @@ class APP_EXPORT QgsCustomizationDialog : public QMainWindow, private Ui::QgsCus
*/
void enableCustomization( bool checked );
+ /**
+ * Add a child item (UserMenu or UserToolBar) on current item in the item visibility treeview
+ */
+ void addUserItem();
+
+ /**
+ * Delete currently selected items in the item visibility treeview
+ */
+ void deleteSelectedItems();
+
+ /**
+ * Called whenever we need to update both splitter parts size according to "List actions" button
+ */
+ void updateSplitterSizes();
+
+ /**
+ * Called whenever current item from the item visibility treeview has changed
+ */
+ void currentItemChanged();
+
+ /**
+ * Called whenever selected items from the item visibility treeview has changed
+ */
+ void selectedItemsChanged();
+
private:
/**
* find QAction associated to \a toolbutton
@@ -123,7 +149,15 @@ class APP_EXPORT QgsCustomizationDialog : public QMainWindow, private Ui::QgsCus
*/
bool selectWidget( QWidget *widget );
- const std::unique_ptr &customization() const;
+ /**
+ * Returns current customization object
+ */
+ QgsCustomization *customization() const;
+
+ /**
+ * Returns items visibility tree view sort filter proxy model
+ */
+ QSortFilterProxyModel *treeViewModel() const;
/**
* \ingroup app
@@ -149,6 +183,20 @@ class APP_EXPORT QgsCustomizationDialog : public QMainWindow, private Ui::QgsCus
QModelIndex parent( const QModelIndex &index ) const override;
int rowCount( const QModelIndex &parent = {} ) const override;
int columnCount( const QModelIndex &parent = {} ) const override;
+ QMimeData *mimeData( const QModelIndexList &indexes ) const override;
+ bool canDropMimeData( const QMimeData *data, Qt::DropAction action, int, int, const QModelIndex & ) const override;
+ bool dropMimeData( const QMimeData *data, Qt::DropAction action, int row, int, const QModelIndex &parent ) override;
+
+ /**
+ * Add a user item (either a UserMenu or UserToolBar) on \a parent
+ * Returns created item model index. Invalid model index is returned if it was not possible
+ */
+ QModelIndex addUserItem( const QModelIndex &parent );
+
+ /**
+ * Delete user items at \a indexes location
+ */
+ void deleteUserItems( const QModelIndexList &indexes );
/**
* Initialize (or reinitialize if already initialized) model
@@ -187,6 +235,7 @@ class APP_EXPORT QgsCustomizationDialog : public QMainWindow, private Ui::QgsCus
QgisApp *mQgisApp = nullptr;
QgsCustomizationModel *mItemsVisibilityModel = nullptr;
+ QgsCustomizationModel *mActionsModel = nullptr;
friend class TestQgsCustomization;
};
diff --git a/src/ui/qgscustomizationdialogbase.ui b/src/ui/qgscustomizationdialogbase.ui
index 6b2c786466d2..ccc4a4ecb155 100644
--- a/src/ui/qgscustomizationdialogbase.ui
+++ b/src/ui/qgscustomizationdialogbase.ui
@@ -27,7 +27,7 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -40,9 +40,9 @@
-
-
+
- Qt::Horizontal
+ Qt::Orientation::Horizontal
@@ -51,15 +51,60 @@
Search…
-
+
true
-
+
+ QAbstractItemView::DragDropMode::DragDrop
+
+
+ Qt::DropAction::LinkAction
+
+
+ QAbstractItemView::SelectionMode::ExtendedSelection
+
- QAbstractItemView::SelectItems
+ QAbstractItemView::SelectionBehavior::SelectRows
+
+
+
+
+
+
+
+ -
+
+
+ Search…
+
+
+ true
+
+
+
+ -
+
+
+ Drag and drop existing actions onto user created menu or toolbar in the left panel
+
+
+ true
+
+
+ QAbstractItemView::DragDropMode::DragOnly
+
+
+ QAbstractItemView::SelectionMode::ExtendedSelection
+
+
+ QAbstractItemView::SelectionBehavior::SelectRows
+
+
+ false
@@ -70,10 +115,10 @@
-
- Qt::Horizontal
+ Qt::Orientation::Horizontal
- QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok|QDialogButtonBox::Reset
+ QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Help|QDialogButtonBox::StandardButton::Ok|QDialogButtonBox::StandardButton::Reset
@@ -104,6 +149,9 @@
+
+
+
@@ -171,6 +219,48 @@
Check All
+
+
+
+ :/images/themes/default/mActionDeleteSelected.svg:/images/themes/default/mActionDeleteSelected.svg
+
+
+ Delete user item
+
+
+ QAction::MenuRole::TextHeuristicRole
+
+
+
+
+
+ :/images/themes/default/mActionAdd.svg:/images/themes/default/mActionAdd.svg
+
+
+ Add user item
+
+
+ QAction::MenuRole::TextHeuristicRole
+
+
+
+
+ true
+
+
+
+ :/images/themes/default/mActionListActions.svg:/images/themes/default/mActionListActions.svg
+
+
+ Display action panel
+
+
+ <html><head/><body><p>Display action panel from which you can drag and drop existing actions onto user created menu or toolbar in the left panel</p></body></html>
+
+
+ QAction::MenuRole::NoRole
+
+
diff --git a/tests/src/app/testqgscustomization.cpp b/tests/src/app/testqgscustomization.cpp
index 5926ccce5727..4283cd258b3c 100644
--- a/tests/src/app/testqgscustomization.cpp
+++ b/tests/src/app/testqgscustomization.cpp
@@ -79,98 +79,17 @@ class TestQgsCustomization : public QgsTest
QgsCustomization::QgsItem *TestQgsCustomization::getItem( QgsCustomization *customization, const QString &path ) const
{
- QStringList pathElems = path.split( "/" );
- if ( pathElems.isEmpty() )
- return nullptr;
-
- const QHash rootItems = {
- { "Menus", customization->menusItem() },
- { "ToolBars", customization->toolBarsItem() },
- { "Docks", customization->docksItem() },
- { "BrowserItems", customization->browserElementsItem() },
- { "StatusBarWidgets", customization->statusBarWidgetsItem() }
- };
-
- QgsCustomization::QgsItem *currentItem = nullptr;
- for ( const QString &pathElem : pathElems )
- {
- if ( currentItem )
- {
- currentItem = currentItem->getChild( pathElem );
- }
- else
- {
- currentItem = rootItems.value( pathElem );
- }
-
- if ( !currentItem )
- return nullptr;
- }
-
- return currentItem;
+ return customization->getItem( path );
}
QWidget *TestQgsCustomization::findQWidget( const QString &path )
{
- QStringList pathElems = path.split( "/" );
- if ( pathElems.isEmpty() )
- return nullptr;
-
- const QHash rootWidgets = { { "Menus", QgisApp::instance()->menuBar() }, { "ToolBars", QgisApp::instance() }, { "Docks", QgisApp::instance() }, { "StatusBarWidgets", QgisApp::instance()->statusBarIface() } };
-
- const QString rootElem = pathElems.takeFirst();
- QWidget *currentWidget = rootWidgets.value( rootElem );
- if ( !currentWidget )
- return nullptr;
-
- for ( const QString &pathElem : pathElems )
- {
- if ( dynamic_cast( currentWidget )
- || dynamic_cast( currentWidget )
- || dynamic_cast( currentWidget ) )
- {
- for ( QgsCustomization::QgsQActionsIterator::Info it : QgsCustomization::QgsQActionsIterator( currentWidget ) )
- {
- if ( it.name == pathElem )
- {
- currentWidget = it.widget;
- break;
- }
- }
- }
- else
- {
- QList children = currentWidget->children();
- QList::const_iterator it = std::find_if( children.cbegin(), children.cend(), [&pathElem]( QObject *obj ) { return dynamic_cast( obj ) && obj->objectName() == pathElem; } );
- currentWidget = dynamic_cast( *it );
- }
-
- if ( !currentWidget )
- return nullptr;
- }
-
- return currentWidget;
+ return QgsCustomization::findQWidget( path );
}
QAction *TestQgsCustomization::findQAction( const QString &path )
{
- QStringList pathElems = path.split( "/" );
- if ( pathElems.isEmpty() )
- return nullptr;
-
- qsizetype lastSlashIndex = path.lastIndexOf( "/" );
- if ( lastSlashIndex < 0 )
- return nullptr;
-
- QWidget *currentWidget = findQWidget( path.first( lastSlashIndex ) );
- if ( !currentWidget )
- return nullptr;
-
- const QString actionName = path.mid( lastSlashIndex + 1 );
-
- const QList actions = currentWidget->actions();
- const QList::const_iterator actionIt = std::find_if( actions.cbegin(), actions.cend(), [&actionName]( QAction *action ) { return action->objectName() == actionName; } );
- return actionIt != actions.cend() ? *actionIt : nullptr;
+ return QgsCustomization::findQAction( path );
}
long long TestQgsCustomization::qactionPosition( const QString &path )
@@ -807,6 +726,25 @@ void TestQgsCustomization::testModel()
// the action is no longer visible
QVERIFY( !findQAction( "ToolBars/mLayerToolBar/mActionAddRasterLayer" ) );
+
+ // test add/delete
+ {
+ const QModelIndex menusIndex = model.index( 2, 0 );
+ QCOMPARE( model.data( menusIndex, Qt::ItemDataRole::DisplayRole ), u"Menus"_s );
+
+ const QModelIndex newItemIndex = model.addUserItem( menusIndex );
+ QCOMPARE( model.data( newItemIndex, Qt::ItemDataRole::DisplayRole ), u"UserMenu_1"_s );
+
+ model.apply();
+ QVERIFY( getItem( "Menus/UserMenu_1" ) );
+ QVERIFY( findQWidget( "Menus/UserMenu_1" ) );
+
+ model.deleteUserItems( QList() << newItemIndex );
+
+ model.apply();
+ QVERIFY( !getItem( "Menus/UserMenu_1" ) );
+ QVERIFY( !findQWidget( "Menus/UserMenu_1" ) );
+ }
}