/*************************************************************************** qgsbrowsermodel.cpp --------------------- begin : July 2011 copyright : (C) 2011 by Martin Dobias email : wonder dot sk at gmail dot com *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include #include #include #include #include #include "qgis.h" #include "qgsapplication.h" #include "qgsdataitemprovider.h" #include "qgsdataitemproviderregistry.h" #include "qgsdataprovider.h" #include "qgsmimedatautils.h" #include "qgslogger.h" #include "qgsproviderregistry.h" #include "qgsbrowsermodel.h" #include "qgsproject.h" #include "qgssettings.h" #define PROJECT_HOME_PREFIX "project:" #define HOME_PREFIX "home:" QgsBrowserWatcher::QgsBrowserWatcher( QgsDataItem *item ) : QFutureWatcher( nullptr ) , mItem( item ) { } // sort function for QList, e.g. sorted/grouped provider listings static bool cmpByDataItemName_( QgsDataItem *a, QgsDataItem *b ) { return QString::localeAwareCompare( a->name(), b->name() ) < 0; } QgsBrowserModel::QgsBrowserModel( QObject *parent ) : QAbstractItemModel( parent ) { } QgsBrowserModel::~QgsBrowserModel() { removeRootItems(); } void QgsBrowserModel::updateProjectHome() { QString home = QgsProject::instance()->homePath(); if ( mProjectHome && mProjectHome->path().mid( QStringLiteral( PROJECT_HOME_PREFIX ).length() ) == home ) return; int idx = mRootItems.indexOf( mProjectHome ); // using layoutAboutToBeChanged() was messing expanded items if ( idx >= 0 ) { beginRemoveRows( QModelIndex(), idx, idx ); mRootItems.remove( idx ); endRemoveRows(); } delete mProjectHome; mProjectHome = home.isNull() ? nullptr : new QgsProjectHomeItem( nullptr, tr( "Project Home" ), home, QStringLiteral( PROJECT_HOME_PREFIX ) + home ); if ( mProjectHome ) { setupItemConnections( mProjectHome ); beginInsertRows( QModelIndex(), 0, 0 ); mRootItems.insert( 0, mProjectHome ); endInsertRows(); } } void QgsBrowserModel::addRootItems() { updateProjectHome(); // give the home directory a prominent third place QgsDirectoryItem *item = new QgsDirectoryItem( nullptr, tr( "Home" ), QDir::homePath(), QStringLiteral( HOME_PREFIX ) + QDir::homePath(), QStringLiteral( "special:Home" ) ); item->setSortKey( QStringLiteral( " 2" ) ); setupItemConnections( item ); mRootItems << item; // add favorite directories mFavorites = new QgsFavoritesItem( nullptr, tr( "Favorites" ) ); if ( mFavorites ) { setupItemConnections( mFavorites ); mRootItems << mFavorites; } // add drives const auto drives { QDir::drives() }; for ( const QFileInfo &drive : drives ) { const QString path = drive.absolutePath(); if ( QgsDirectoryItem::hiddenPath( path ) ) continue; QgsDirectoryItem *item = new QgsDirectoryItem( nullptr, path, QString(), path, QStringLiteral( "special:Drives" ) ); item->setSortKey( QStringLiteral( " 3 %1" ).arg( path ) ); mDriveItems.insert( path, item ); setupItemConnections( item ); mRootItems << item; } #ifdef Q_OS_MAC QString path = QString( "/Volumes" ); QgsDirectoryItem *vols = new QgsDirectoryItem( nullptr, path, QString(), path, QStringLiteral( "special:Volumes" ) ); mRootItems << vols; #endif // container for displaying providers as sorted groups (by QgsDataProvider::DataCapability enum) QMap providerMap; const auto constProviders = QgsApplication::dataItemProviderRegistry()->providers(); for ( QgsDataItemProvider *pr : constProviders ) { int capabilities = pr->capabilities(); if ( capabilities == QgsDataProvider::NoDataCapabilities ) { QgsDebugMsgLevel( pr->name() + " does not have any dataCapabilities", 4 ); continue; } QgsDataItem *item = pr->createDataItem( QString(), nullptr ); // empty path -> top level if ( item ) { // make sure the top level key is set always item->setProviderKey( pr->name() ); // Forward the signal from the root items to the model (and then to the app) connect( item, &QgsDataItem::connectionsChanged, this, &QgsBrowserModel::connectionsChanged ); QgsDebugMsgLevel( "Add new top level item : " + item->name(), 4 ); setupItemConnections( item ); providerMap.insertMulti( capabilities, item ); } } // add as sorted groups by QgsDataProvider::DataCapability enum const auto constUniqueKeys = providerMap.uniqueKeys(); for ( int key : constUniqueKeys ) { QList providerGroup = providerMap.values( key ); if ( providerGroup.size() > 1 ) { std::sort( providerGroup.begin(), providerGroup.end(), cmpByDataItemName_ ); } const auto constProviderGroup = providerGroup; for ( QgsDataItem *ditem : constProviderGroup ) { mRootItems << ditem; } } } void QgsBrowserModel::removeRootItems() { const auto constMRootItems = mRootItems; for ( QgsDataItem *item : constMRootItems ) { delete item; } mRootItems.clear(); mDriveItems.clear(); } QMap QgsBrowserModel::driveItems() const { return mDriveItems; } void QgsBrowserModel::initialize() { if ( ! mInitialized ) { connect( QgsProject::instance(), &QgsProject::homePathChanged, this, &QgsBrowserModel::updateProjectHome ); addRootItems(); mInitialized = true; } } Qt::ItemFlags QgsBrowserModel::flags( const QModelIndex &index ) const { if ( !index.isValid() ) return Qt::ItemFlags(); Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable; QgsDataItem *ptr = dataItem( index ); if ( !ptr ) { QgsDebugMsgLevel( QStringLiteral( "FLAGS PROBLEM!" ), 4 ); return Qt::ItemFlags(); } if ( ptr->hasDragEnabled() ) flags |= Qt::ItemIsDragEnabled; Q_NOWARN_DEPRECATED_PUSH if ( ptr->acceptDrop() ) flags |= Qt::ItemIsDropEnabled; Q_NOWARN_DEPRECATED_POP if ( ptr->capabilities2() & QgsDataItem::Rename ) flags |= Qt::ItemIsEditable; return flags; } QVariant QgsBrowserModel::data( const QModelIndex &index, int role ) const { if ( !index.isValid() ) return QVariant(); QgsDataItem *item = dataItem( index ); if ( !item ) { return QVariant(); } else if ( role == Qt::DisplayRole || role == Qt::EditRole ) { return item->name(); } else if ( role == QgsBrowserModel::SortRole ) { return item->sortKey(); } else if ( role == Qt::ToolTipRole ) { return item->toolTip(); } else if ( role == Qt::DecorationRole && index.column() == 0 ) { return item->icon(); } else if ( role == QgsBrowserModel::PathRole ) { return item->path(); } else if ( role == QgsBrowserModel::CommentRole ) { if ( item->type() == QgsDataItem::Layer ) { QgsLayerItem *lyrItem = qobject_cast( item ); return lyrItem->comments(); } return QVariant(); } else if ( role == QgsBrowserModel::ProviderKeyRole ) { return item->providerKey(); } else { // unsupported role return QVariant(); } } bool QgsBrowserModel::setData( const QModelIndex &index, const QVariant &value, int role ) { if ( !index.isValid() ) return false; QgsDataItem *item = dataItem( index ); if ( !item ) { return false; } if ( !( item->capabilities2() & QgsDataItem::Rename ) ) return false; switch ( role ) { case Qt::EditRole: { Q_NOWARN_DEPRECATED_PUSH return item->rename( value.toString() ); Q_NOWARN_DEPRECATED_POP } } return false; } QVariant QgsBrowserModel::headerData( int section, Qt::Orientation orientation, int role ) const { Q_UNUSED( section ) if ( orientation == Qt::Horizontal && role == Qt::DisplayRole ) { return QVariant( "header" ); } return QVariant(); } int QgsBrowserModel::rowCount( const QModelIndex &parent ) const { //QgsDebugMsg(QString("isValid = %1 row = %2 column = %3").arg(parent.isValid()).arg(parent.row()).arg(parent.column())); if ( !parent.isValid() ) { // root item: its children are top level items return mRootItems.count(); // mRoot } else { // ordinary item: number of its children QgsDataItem *item = dataItem( parent ); //if ( item ) QgsDebugMsg(QString("path = %1 rowCount = %2").arg(item->path()).arg(item->rowCount()) ); return item ? item->rowCount() : 0; } } bool QgsBrowserModel::hasChildren( const QModelIndex &parent ) const { if ( !parent.isValid() ) return !mRootItems.isEmpty(); // root item: its children are top level items QgsDataItem *item = dataItem( parent ); return item && item->hasChildren(); } int QgsBrowserModel::columnCount( const QModelIndex &parent ) const { Q_UNUSED( parent ) return 1; } QModelIndex QgsBrowserModel::findPath( const QString &path, Qt::MatchFlag matchFlag ) { return findPath( this, path, matchFlag ); } QModelIndex QgsBrowserModel::findPath( QAbstractItemModel *model, const QString &path, Qt::MatchFlag matchFlag ) { if ( !model ) return QModelIndex(); QModelIndex index; // starting from root bool foundChild = true; while ( foundChild ) { foundChild = false; // assume that the next child item will not be found for ( int i = 0; i < model->rowCount( index ); i++ ) { QModelIndex idx = model->index( i, 0, index ); QString itemPath = model->data( idx, PathRole ).toString(); if ( itemPath == path ) { QgsDebugMsgLevel( "Arrived " + itemPath, 4 ); return idx; // we have found the item we have been looking for } // paths are slash separated identifier if ( path.startsWith( itemPath + '/' ) ) { foundChild = true; index = idx; break; } } } if ( matchFlag == Qt::MatchStartsWith ) return index; QgsDebugMsgLevel( QStringLiteral( "path not found" ), 4 ); return QModelIndex(); // not found } QModelIndex QgsBrowserModel::findUri( const QString &uri, QModelIndex index ) { for ( int i = 0; i < this->rowCount( index ); i++ ) { QModelIndex idx = this->index( i, 0, index ); if ( qobject_cast( dataItem( idx ) ) ) { QString itemUri = qobject_cast( dataItem( idx ) )->uri(); if ( itemUri == uri ) { QgsDebugMsgLevel( "Arrived " + itemUri, 4 ); return idx; // we have found the item we have been looking for } } QModelIndex childIdx = findUri( uri, idx ); if ( childIdx.isValid() ) return childIdx; } return QModelIndex(); } void QgsBrowserModel::connectItem( QgsDataItem * ) { // deprecated, no use } void QgsBrowserModel::reload() { // TODO: put items creating currently children in threads to deleteLater (does not seem urget because reload() is not used in QGIS) beginResetModel(); removeRootItems(); addRootItems(); endResetModel(); } void QgsBrowserModel::refreshDrives() { const QList< QFileInfo > drives = QDir::drives(); // remove any removed drives const QStringList existingDrives = mDriveItems.keys(); for ( const QString &drivePath : existingDrives ) { bool stillExists = false; for ( const QFileInfo &drive : drives ) { if ( drivePath == drive.absolutePath() ) { stillExists = true; break; } } if ( stillExists ) continue; // drive has been removed, remove corresponding item if ( QgsDirectoryItem *driveItem = mDriveItems.value( drivePath ) ) removeRootItem( driveItem ); } for ( const QFileInfo &drive : drives ) { const QString path = drive.absolutePath(); if ( QgsDirectoryItem::hiddenPath( path ) ) continue; // does an item for this drive already exist? if ( !mDriveItems.contains( path ) ) { QgsDirectoryItem *item = new QgsDirectoryItem( nullptr, path, path ); item->setSortKey( QStringLiteral( " 3 %1" ).arg( path ) ); mDriveItems.insert( path, item ); setupItemConnections( item ); beginInsertRows( QModelIndex(), mRootItems.count(), mRootItems.count() ); mRootItems << item; endInsertRows(); } } } QModelIndex QgsBrowserModel::index( int row, int column, const QModelIndex &parent ) const { if ( column < 0 || column >= columnCount() || row < 0 ) return QModelIndex(); QgsDataItem *p = dataItem( parent ); const QVector &items = p ? p->children() : mRootItems; QgsDataItem *item = items.value( row, nullptr ); return item ? createIndex( row, column, item ) : QModelIndex(); } QModelIndex QgsBrowserModel::parent( const QModelIndex &index ) const { QgsDataItem *item = dataItem( index ); if ( !item ) return QModelIndex(); return findItem( item->parent() ); } QModelIndex QgsBrowserModel::findItem( QgsDataItem *item, QgsDataItem *parent ) const { const QVector &items = parent ? parent->children() : mRootItems; for ( int i = 0; i < items.size(); i++ ) { if ( items[i] == item ) return createIndex( i, 0, item ); QModelIndex childIndex = findItem( item, items[i] ); if ( childIndex.isValid() ) return childIndex; } return QModelIndex(); } void QgsBrowserModel::beginInsertItems( QgsDataItem *parent, int first, int last ) { QgsDebugMsgLevel( "parent mPath = " + parent->path(), 3 ); QModelIndex idx = findItem( parent ); if ( !idx.isValid() ) return; QgsDebugMsgLevel( QStringLiteral( "valid" ), 3 ); beginInsertRows( idx, first, last ); QgsDebugMsgLevel( QStringLiteral( "end" ), 3 ); } void QgsBrowserModel::endInsertItems() { QgsDebugMsgLevel( QStringLiteral( "Entered" ), 3 ); endInsertRows(); } void QgsBrowserModel::beginRemoveItems( QgsDataItem *parent, int first, int last ) { QgsDebugMsgLevel( "parent mPath = " + parent->path(), 3 ); QModelIndex idx = findItem( parent ); if ( !idx.isValid() ) return; beginRemoveRows( idx, first, last ); } void QgsBrowserModel::endRemoveItems() { QgsDebugMsgLevel( QStringLiteral( "Entered" ), 3 ); endRemoveRows(); } void QgsBrowserModel::itemDataChanged( QgsDataItem *item ) { QgsDebugMsgLevel( QStringLiteral( "Entered" ), 3 ); QModelIndex idx = findItem( item ); if ( !idx.isValid() ) return; emit dataChanged( idx, idx ); } void QgsBrowserModel::itemStateChanged( QgsDataItem *item, QgsDataItem::State oldState ) { if ( !item ) return; QModelIndex idx = findItem( item ); if ( !idx.isValid() ) return; QgsDebugMsgLevel( QStringLiteral( "item %1 state changed %2 -> %3" ).arg( item->path() ).arg( oldState ).arg( item->state() ), 4 ); emit stateChanged( idx, oldState ); } void QgsBrowserModel::setupItemConnections( QgsDataItem *item ) { connect( item, &QgsDataItem::beginInsertItems, this, &QgsBrowserModel::beginInsertItems ); connect( item, &QgsDataItem::endInsertItems, this, &QgsBrowserModel::endInsertItems ); connect( item, &QgsDataItem::beginRemoveItems, this, &QgsBrowserModel::beginRemoveItems ); connect( item, &QgsDataItem::endRemoveItems, this, &QgsBrowserModel::endRemoveItems ); connect( item, &QgsDataItem::dataChanged, this, &QgsBrowserModel::itemDataChanged ); connect( item, &QgsDataItem::stateChanged, this, &QgsBrowserModel::itemStateChanged ); // if it's a collection item, also forwards connectionsChanged QgsDataCollectionItem *collectionItem = qobject_cast( item ); if ( collectionItem ) connect( collectionItem, &QgsDataCollectionItem::connectionsChanged, this, &QgsBrowserModel::connectionsChanged ); } QStringList QgsBrowserModel::mimeTypes() const { QStringList types; // In theory the mime type convention is: application/x-vnd... // but it seems a bit over formalized. Would be an application/x-qgis-uri better? types << QStringLiteral( "application/x-vnd.qgis.qgis.uri" ); return types; } QMimeData *QgsBrowserModel::mimeData( const QModelIndexList &indexes ) const { QgsMimeDataUtils::UriList lst; const auto constIndexes = indexes; for ( const QModelIndex &index : constIndexes ) { if ( index.isValid() ) { QgsDataItem *ptr = reinterpret_cast< QgsDataItem * >( index.internalPointer() ); QgsMimeDataUtils::Uri uri = ptr->mimeUri(); if ( uri.isValid() ) lst.append( uri ); } } return QgsMimeDataUtils::encodeUriList( lst ); } bool QgsBrowserModel::dropMimeData( const QMimeData *data, Qt::DropAction action, int, int, const QModelIndex &parent ) { QgsDataItem *destItem = dataItem( parent ); if ( !destItem ) { QgsDebugMsgLevel( QStringLiteral( "DROP PROBLEM!" ), 4 ); return false; } Q_NOWARN_DEPRECATED_PUSH return destItem->handleDrop( data, action ); Q_NOWARN_DEPRECATED_POP } QgsDataItem *QgsBrowserModel::dataItem( const QModelIndex &idx ) const { void *v = idx.internalPointer(); QgsDataItem *d = reinterpret_cast( v ); Q_ASSERT( !v || d ); return d; } bool QgsBrowserModel::canFetchMore( const QModelIndex &parent ) const { QgsDataItem *item = dataItem( parent ); // if ( item ) // QgsDebugMsg( QStringLiteral( "path = %1 canFetchMore = %2" ).arg( item->path() ).arg( item && ! item->isPopulated() ) ); return ( item && item->state() == QgsDataItem::NotPopulated ); } void QgsBrowserModel::fetchMore( const QModelIndex &parent ) { QgsDataItem *item = dataItem( parent ); if ( !item || item->state() == QgsDataItem::Populating || item->state() == QgsDataItem::Populated ) return; QgsDebugMsgLevel( "path = " + item->path(), 4 ); item->populate(); } /* Refresh dir path */ void QgsBrowserModel::refresh( const QString &path ) { QModelIndex index = findPath( path ); refresh( index ); } /* Refresh item */ void QgsBrowserModel::refresh( const QModelIndex &index ) { QgsDataItem *item = dataItem( index ); if ( !item || item->state() == QgsDataItem::Populating ) return; QgsDebugMsgLevel( "Refresh " + item->path(), 4 ); item->refresh(); } void QgsBrowserModel::addFavoriteDirectory( const QString &directory, const QString &name ) { Q_ASSERT( mFavorites ); mFavorites->addDirectory( directory, name ); } void QgsBrowserModel::removeFavorite( const QModelIndex &index ) { QgsDirectoryItem *item = qobject_cast( dataItem( index ) ); if ( !item ) return; mFavorites->removeDirectory( item ); } void QgsBrowserModel::removeFavorite( QgsFavoriteItem *favorite ) { if ( !favorite ) return; mFavorites->removeDirectory( favorite ); } void QgsBrowserModel::hidePath( QgsDataItem *item ) { QgsSettings settings; QStringList hiddenItems = settings.value( QStringLiteral( "browser/hiddenPaths" ), QStringList() ).toStringList(); int idx = hiddenItems.indexOf( item->path() ); if ( idx != -1 ) { hiddenItems.removeAt( idx ); } else { hiddenItems << item->path(); } settings.setValue( QStringLiteral( "browser/hiddenPaths" ), hiddenItems ); if ( item->parent() ) { item->parent()->deleteChildItem( item ); } else { removeRootItem( item ); } } void QgsBrowserModel::removeRootItem( QgsDataItem *item ) { int i = mRootItems.indexOf( item ); beginRemoveRows( QModelIndex(), i, i ); mRootItems.remove( i ); QgsDirectoryItem *dirItem = qobject_cast< QgsDirectoryItem * >( item ); if ( !mDriveItems.key( dirItem ).isEmpty() ) { mDriveItems.remove( mDriveItems.key( dirItem ) ); } item->deleteLater(); endRemoveRows(); }