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" ) ); + } }