1 /***************************************************************************
2                         qgsinbuiltdataitemproviders.cpp
3                         ----------------------------
4    begin                : October 2018
5    copyright            : (C) 2018 by Nyall Dawson
6    email                : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8 
9 /***************************************************************************
10  *                                                                         *
11  *   This program is free software; you can redistribute it and/or modify  *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  ***************************************************************************/
17 
18 
19 #include "qgsinbuiltdataitemproviders.h"
20 #include "qgsdataitem.h"
21 #include "qgsdataitemguiproviderregistry.h"
22 #include "qgssettings.h"
23 #include "qgsgui.h"
24 #include "qgsnative.h"
25 #include "qgisapp.h"
26 #include "qgsmessagebar.h"
27 #include "qgsmessagelog.h"
28 #include "qgsnewnamedialog.h"
29 #include "qgsbrowserguimodel.h"
30 #include "qgsbrowserdockwidget_p.h"
31 #include "qgswindowmanagerinterface.h"
32 #include "qgsrasterlayer.h"
33 #include "qgsnewvectorlayerdialog.h"
34 #include "qgsnewgeopackagelayerdialog.h"
35 #include "qgsfileutils.h"
36 #include "qgsapplication.h"
37 #include "processing/qgsprojectstylealgorithms.h"
38 #include "qgsstylemanagerdialog.h"
39 #include "qgsproviderregistry.h"
40 #include "qgsaddattrdialog.h"
41 #include "qgsabstractdatabaseproviderconnection.h"
42 #include "qgsprovidermetadata.h"
43 #include "qgsnewvectortabledialog.h"
44 #include "qgsdataitemproviderregistry.h"
45 
46 #include <QFileInfo>
47 #include <QMenu>
48 #include <QInputDialog>
49 #include <QDesktopServices>
50 #include <QFileDialog>
51 #include <QMessageBox>
52 
name()53 QString QgsAppDirectoryItemGuiProvider::name()
54 {
55   return QStringLiteral( "directory_items" );
56 }
57 
populateContextMenu(QgsDataItem * item,QMenu * menu,const QList<QgsDataItem * > &,QgsDataItemGuiContext context)58 void QgsAppDirectoryItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu *menu, const QList<QgsDataItem *> &, QgsDataItemGuiContext context )
59 {
60   if ( item->type() != QgsDataItem::Directory )
61     return;
62 
63   QgsDirectoryItem *directoryItem = qobject_cast< QgsDirectoryItem * >( item );
64 
65   QgsSettings settings;
66 
67   QMenu *newMenu = new QMenu( tr( "New" ), menu );
68 
69   QAction *createFolder = new QAction( QgsApplication::getThemeIcon( QStringLiteral( "mActionNewFolder.svg" ) ), tr( "Directory…" ), menu );
70   connect( createFolder, &QAction::triggered, this, [ = ]
71   {
72     bool ok = false;
73 
74     const QString name = QInputDialog::getText( QgisApp::instance(), tr( "Create Directory" ), tr( "Directory name" ), QLineEdit::Normal, QString(), &ok );
75     if ( ok && !name.isEmpty() )
76     {
77       QDir dir( directoryItem->dirPath() );
78       if ( QFileInfo::exists( dir.absoluteFilePath( name ) ) )
79       {
80         notify( tr( "Create Directory" ), tr( "The path “%1” already exists." ).arg( QDir::toNativeSeparators( dir.absoluteFilePath( name ) ) ), context, Qgis::MessageLevel::Warning );
81       }
82       else if ( !dir.mkdir( name ) )
83       {
84         notify( tr( "Create Directory" ), tr( "Could not create directory “%1”." ).arg( QDir::toNativeSeparators( dir.absoluteFilePath( name ) ) ), context, Qgis::MessageLevel::Critical );
85       }
86       else
87       {
88         directoryItem->refresh();
89       }
90     }
91   } );
92   newMenu->addAction( createFolder );
93 
94   QAction *createGpkg = new QAction( tr( "GeoPackage…" ), newMenu );
95   createGpkg->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionNewGeoPackageLayer.svg" ) ) );
96   connect( createGpkg, &QAction::triggered, this, [ = ]
97   {
98     QgsNewGeoPackageLayerDialog dialog( QgisApp::instance() );
99     QDir dir( directoryItem->dirPath() );
100     dialog.setDatabasePath( dir.filePath( QStringLiteral( "new_geopackage" ) ) );
101     dialog.setCrs( QgsProject::instance()->defaultCrsForNewLayers() );
102     dialog.setAddToProject( false );
103     if ( dialog.exec() )
104     {
105       QString file = dialog.databasePath();
106       file = QgsFileUtils::ensureFileNameHasExtension( file, QStringList() << QStringLiteral( "gpkg" ) );
107       context.messageBar()->pushSuccess( tr( "New GeoPackage" ), tr( "Created <a href=\"%1\">%2</a>" ).arg(
108                                            QUrl::fromLocalFile( file ).toString(), QDir::toNativeSeparators( file ) ) );
109       item->refresh();
110     }
111   } );
112   newMenu->addAction( createGpkg );
113 
114   QAction *createShp = new QAction( tr( "ShapeFile…" ), newMenu );
115   createShp->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionNewVectorLayer.svg" ) ) );
116   connect( createShp, &QAction::triggered, this, [ = ]
117   {
118     QString enc;
119     QDir dir( directoryItem->dirPath() );
120     QString error;
121     const QString newFile = QgsNewVectorLayerDialog::execAndCreateLayer( error, QgisApp::instance(), dir.filePath( QStringLiteral( "new_layer.shp" ) ), &enc, QgsProject::instance()->defaultCrsForNewLayers() );
122     if ( !newFile.isEmpty() )
123     {
124       context.messageBar()->pushSuccess( tr( "New ShapeFile" ), tr( "Created <a href=\"%1\">%2</a>" ).arg(
125                                            QUrl::fromLocalFile( newFile ).toString(), QDir::toNativeSeparators( newFile ) ) );
126       item->refresh();
127     }
128     else if ( !error.isEmpty() )
129     {
130       context.messageBar()->pushCritical( tr( "New ShapeFile" ), tr( "Layer creation failed: %1" ).arg( error ) );
131     }
132   } );
133   newMenu->addAction( createShp );
134 
135   menu->addMenu( newMenu );
136 
137   menu->addSeparator();
138 
139   bool inFavDirs = item->parent() && item->parent()->type() == QgsDataItem::Favorites;
140   if ( item->parent() && !inFavDirs )
141   {
142     // only non-root directories can be added as favorites
143     QAction *addAsFavorite = new QAction( tr( "Add as a Favorite" ), menu );
144     addAsFavorite->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconFavorites.svg" ) ) );
145     menu->addAction( addAsFavorite );
146     connect( addAsFavorite, &QAction::triggered, this, [ = ]
147     {
148       addFavorite( directoryItem );
149     } );
150   }
151   else if ( inFavDirs )
152   {
153     if ( QgsFavoriteItem *favoriteItem = qobject_cast< QgsFavoriteItem * >( item ) )
154     {
155       QAction *actionRename = new QAction( tr( "Rename Favorite…" ), menu );
156       connect( actionRename, &QAction::triggered, this, [ = ]
157       {
158         renameFavorite( favoriteItem );
159       } );
160       menu->addAction( actionRename );
161       QAction *removeFavoriteAction = new QAction( tr( "Remove Favorite" ), menu );
162       connect( removeFavoriteAction, &QAction::triggered, this, [ = ]
163       {
164         removeFavorite( favoriteItem );
165       } );
166       menu->addAction( removeFavoriteAction );
167       menu->addSeparator();
168     }
169   }
170   QAction *hideAction = new QAction( tr( "Hide from Browser" ), menu );
171   connect( hideAction, &QAction::triggered, this, [ = ]
172   {
173     hideDirectory( directoryItem );
174   } );
175   menu->addAction( hideAction );
176 
177   QMenu *hiddenMenu = new QMenu( tr( "Hidden Items" ), menu );
178   int count = 0;
179   const QStringList hiddenPathList = settings.value( QStringLiteral( "/browser/hiddenPaths" ) ).toStringList();
180   static int MAX_HIDDEN_ENTRIES = 5;
181   for ( const QString &path : hiddenPathList )
182   {
183     QAction *action = new QAction( QDir::toNativeSeparators( path ), hiddenMenu );
184     connect( action, &QAction::triggered, this, [ = ]
185     {
186       QgsSettings s;
187       QStringList pathsList = s.value( QStringLiteral( "/browser/hiddenPaths" ) ).toStringList();
188       pathsList.removeAll( path );
189       s.setValue( QStringLiteral( "/browser/hiddenPaths" ), pathsList );
190 
191       // get parent path and refresh corresponding node
192       int idx = path.lastIndexOf( QLatin1Char( '/' ) );
193       if ( idx != -1 && path.count( QStringLiteral( "/" ) ) > 1 )
194       {
195         QString parentPath = path.left( idx );
196         QgisApp::instance()->browserModel()->refresh( parentPath );
197       }
198       else
199       {
200         // top-level (drive or root) node
201         QgisApp::instance()->browserModel()->refreshDrives();
202       }
203     } );
204     hiddenMenu->addAction( action );
205     count += 1;
206     if ( count == MAX_HIDDEN_ENTRIES )
207     {
208       break;
209     }
210   }
211 
212   if ( hiddenPathList.size() > MAX_HIDDEN_ENTRIES )
213   {
214     hiddenMenu->addSeparator();
215 
216     QAction *moreAction = new QAction( tr( "Show More…" ), hiddenMenu );
217     connect( moreAction, &QAction::triggered, this, [ = ]
218     {
219       QgisApp::instance()->showOptionsDialog( QgisApp::instance(), QStringLiteral( "mOptionsPageDataSources" ) );
220     } );
221     hiddenMenu->addAction( moreAction );
222   }
223   if ( count > 0 )
224   {
225     menu->addMenu( hiddenMenu );
226   }
227 
228   QAction *fastScanAction = new QAction( tr( "Fast Scan this Directory" ), menu );
229   connect( fastScanAction, &QAction::triggered, this, [ = ]
230   {
231     toggleFastScan( directoryItem );
232   } );
233   menu->addAction( fastScanAction );
234   fastScanAction->setCheckable( true );
235   fastScanAction->setChecked( settings.value( QStringLiteral( "qgis/scanItemsFastScanUris" ),
236                               QStringList() ).toStringList().contains( item->path() ) );
237 
238   menu->addSeparator();
239 
240   QAction *openFolder = new QAction( QgsApplication::getThemeIcon( QStringLiteral( "mIconFolder.svg" ) ), tr( "Open Directory…" ), menu );
241   connect( openFolder, &QAction::triggered, this, [ = ]
242   {
243     QDesktopServices::openUrl( QUrl::fromLocalFile( directoryItem->dirPath() ) );
244   } );
245   menu->addAction( openFolder );
246 
247   if ( QgsGui::instance()->nativePlatformInterface()->capabilities() & QgsNative::NativeOpenTerminalAtPath )
248   {
249     QAction *openTerminal = new QAction( QgsApplication::getThemeIcon( QStringLiteral( "mActionTerminal.svg" ) ), tr( "Open in Terminal…" ), menu );
250     connect( openTerminal, &QAction::triggered, this, [ = ]
251     {
252       QgsGui::instance()->nativePlatformInterface()->openTerminalAtPath( directoryItem->dirPath() );
253     } );
254     menu->addAction( openTerminal );
255     menu->addSeparator();
256   }
257 
258   QAction *propertiesAction = new QAction( tr( "Properties…" ), menu );
259   connect( propertiesAction, &QAction::triggered, this, [ = ]
260   {
261     showProperties( directoryItem, context );
262   } );
263   menu->addAction( propertiesAction );
264 
265   if ( QgsGui::nativePlatformInterface()->capabilities() & QgsNative::NativeFilePropertiesDialog )
266   {
267     if ( QgsDirectoryItem *dirItem = qobject_cast< QgsDirectoryItem * >( item ) )
268     {
269       QAction *action = menu->addAction( tr( "Directory Properties…" ) );
270       connect( action, &QAction::triggered, dirItem, [ dirItem ]
271       {
272         QgsGui::nativePlatformInterface()->showFileProperties( dirItem->dirPath() );
273       } );
274     }
275   }
276 }
277 
addFavorite(QgsDirectoryItem * item)278 void QgsAppDirectoryItemGuiProvider::addFavorite( QgsDirectoryItem *item )
279 {
280   if ( !item )
281     return;
282 
283   QgisApp::instance()->browserModel()->addFavoriteDirectory( item->dirPath() );
284 }
285 
removeFavorite(QgsFavoriteItem * favorite)286 void QgsAppDirectoryItemGuiProvider::removeFavorite( QgsFavoriteItem *favorite )
287 {
288   QgisApp::instance()->browserModel()->removeFavorite( favorite );
289 }
290 
renameFavorite(QgsFavoriteItem * favorite)291 void QgsAppDirectoryItemGuiProvider::renameFavorite( QgsFavoriteItem *favorite )
292 {
293   QgsNewNameDialog dlg( tr( "favorite “%1”" ).arg( favorite->name() ), favorite->name() );
294   dlg.setWindowTitle( tr( "Rename Favorite" ) );
295   if ( dlg.exec() != QDialog::Accepted || dlg.name() == favorite->name() )
296     return;
297 
298   favorite->rename( dlg.name() );
299 }
300 
hideDirectory(QgsDirectoryItem * item)301 void QgsAppDirectoryItemGuiProvider::hideDirectory( QgsDirectoryItem *item )
302 {
303   if ( ! item )
304     return;
305 
306   QgisApp::instance()->browserModel()->hidePath( item );
307 }
308 
toggleFastScan(QgsDirectoryItem * item)309 void QgsAppDirectoryItemGuiProvider::toggleFastScan( QgsDirectoryItem *item )
310 {
311   QgsSettings settings;
312   QStringList fastScanDirs = settings.value( QStringLiteral( "qgis/scanItemsFastScanUris" ),
313                              QStringList() ).toStringList();
314   int idx = fastScanDirs.indexOf( item->path() );
315   if ( idx != -1 )
316   {
317     fastScanDirs.removeAt( idx );
318   }
319   else
320   {
321     fastScanDirs << item->path();
322   }
323   settings.setValue( QStringLiteral( "qgis/scanItemsFastScanUris" ), fastScanDirs );
324 }
325 
showProperties(QgsDirectoryItem * item,QgsDataItemGuiContext context)326 void QgsAppDirectoryItemGuiProvider::showProperties( QgsDirectoryItem *item, QgsDataItemGuiContext context )
327 {
328   if ( ! item )
329     return;
330 
331   QgsBrowserPropertiesDialog *dialog = new QgsBrowserPropertiesDialog( QStringLiteral( "browser" ), QgisApp::instance() );
332   dialog->setAttribute( Qt::WA_DeleteOnClose );
333 
334   dialog->setItem( item, context );
335   dialog->show();
336 }
337 
338 
339 //
340 // QgsProjectHomeItemGuiProvider
341 //
342 
name()343 QString QgsProjectHomeItemGuiProvider::name()
344 {
345   return QStringLiteral( "project_home_item" );
346 }
347 
populateContextMenu(QgsDataItem * item,QMenu * menu,const QList<QgsDataItem * > &,QgsDataItemGuiContext)348 void QgsProjectHomeItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu *menu, const QList<QgsDataItem *> &, QgsDataItemGuiContext )
349 {
350   if ( !qobject_cast< QgsProjectHomeItem * >( item ) )
351     return;
352 
353   if ( !menu->actions().empty() )
354     menu->insertSeparator( menu->actions().at( 0 ) );
355 
356   QAction *setHome = new QAction( tr( "Set Project Home…" ), menu );
357   connect( setHome, &QAction::triggered, this, [ = ]
358   {
359     QString oldHome = QgsProject::instance()->homePath();
360     QString newPath = QFileDialog::getExistingDirectory( QgisApp::instance(), tr( "Select Project Home Directory" ), oldHome );
361     if ( !newPath.isEmpty() )
362     {
363       QgsProject::instance()->setPresetHomePath( newPath );
364     }
365   } );
366 
367   // ensure item is the first one shown
368   if ( !menu->actions().empty() )
369     menu->insertAction( menu->actions().at( 0 ), setHome );
370   else
371     menu->addAction( setHome );
372 }
373 
374 //
375 // QgsFavoritesItemGuiProvider
376 //
377 
name()378 QString QgsFavoritesItemGuiProvider::name()
379 {
380   return QStringLiteral( "favorites_item" );
381 }
382 
populateContextMenu(QgsDataItem * item,QMenu * menu,const QList<QgsDataItem * > &,QgsDataItemGuiContext)383 void QgsFavoritesItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu *menu, const QList<QgsDataItem *> &, QgsDataItemGuiContext )
384 {
385   if ( item->type() != QgsDataItem::Favorites )
386     return;
387 
388   QAction *addAction = new QAction( tr( "Add a Directory…" ), menu );
389   connect( addAction, &QAction::triggered, this, [ = ]
390   {
391     QString directory = QFileDialog::getExistingDirectory( QgisApp::instance(), tr( "Add Directory to Favorites" ) );
392     if ( !directory.isEmpty() )
393     {
394       QgisApp::instance()->browserModel()->addFavoriteDirectory( directory );
395     }
396   } );
397   menu->addAction( addAction );
398 }
399 
400 //
401 // QgsLayerItemGuiProvider
402 //
403 
name()404 QString QgsLayerItemGuiProvider::name()
405 {
406   return QStringLiteral( "layer_item" );
407 }
408 
populateContextMenu(QgsDataItem * item,QMenu * menu,const QList<QgsDataItem * > & selectedItems,QgsDataItemGuiContext context)409 void QgsLayerItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu *menu, const QList<QgsDataItem *> &selectedItems, QgsDataItemGuiContext context )
410 {
411   if ( item->type() != QgsDataItem::Layer )
412     return;
413 
414   QgsLayerItem *layerItem = qobject_cast<QgsLayerItem *>( item );
415 
416   if ( layerItem )
417   {
418     // Check for certain file items
419     QVariantMap parts = QgsProviderRegistry::instance()->decodeUri( layerItem->providerKey(), layerItem->uri() );
420     const QString filename = parts.value( QStringLiteral( "path" ) ).toString();
421     if ( !filename.isEmpty() )
422     {
423       QFileInfo fi( filename );
424 
425       const static QList< std::pair< QString, QString > > sStandardFileTypes =
426       {
427         { QStringLiteral( "pdf" ), QObject::tr( "Document" )},
428         { QStringLiteral( "xls" ), QObject::tr( "Spreadsheet" )},
429         { QStringLiteral( "xlsx" ), QObject::tr( "Spreadsheet" )},
430         { QStringLiteral( "ods" ), QObject::tr( "Spreadsheet" )},
431         { QStringLiteral( "csv" ), QObject::tr( "CSV File" )},
432         { QStringLiteral( "txt" ), QObject::tr( "Text File" )},
433         { QStringLiteral( "png" ), QObject::tr( "PNG Image" )},
434         { QStringLiteral( "jpg" ), QObject::tr( "JPEG Image" )},
435         { QStringLiteral( "jpeg" ), QObject::tr( "JPEG Image" )},
436         { QStringLiteral( "tif" ), QObject::tr( "TIFF Image" )},
437         { QStringLiteral( "tiff" ), QObject::tr( "TIFF Image" )},
438         { QStringLiteral( "svg" ), QObject::tr( "SVG File" )}
439       };
440       for ( const auto &it : sStandardFileTypes )
441       {
442         const QString ext = it.first;
443         const QString name = it.second;
444         if ( fi.suffix().compare( ext, Qt::CaseInsensitive ) == 0 )
445         {
446           // pdf file
447           QAction *viewAction = new QAction( tr( "Open %1 Externally…" ).arg( name ), menu );
448           connect( viewAction, &QAction::triggered, this, [ = ]
449           {
450             QDesktopServices::openUrl( QUrl::fromLocalFile( filename ) );
451           } );
452 
453           // we want this action to be at the top
454           QAction *beforeAction = menu->actions().value( 0 );
455           if ( beforeAction )
456           {
457             menu->insertAction( beforeAction, viewAction );
458             menu->insertSeparator( beforeAction );
459           }
460           else
461           {
462             menu->addAction( viewAction );
463             menu->addSeparator();
464           }
465           // will only find one!
466           break;
467         }
468       }
469     }
470   }
471 
472   if ( layerItem && ( layerItem->mapLayerType() == QgsMapLayerType::VectorLayer ||
473                       layerItem->mapLayerType() == QgsMapLayerType::RasterLayer ) )
474   {
475     QMenu *exportMenu = new QMenu( tr( "Export Layer" ), menu );
476     menu->addMenu( exportMenu );
477     QAction *toFileAction = new QAction( tr( "To File…" ), exportMenu );
478     exportMenu->addAction( toFileAction );
479     connect( toFileAction, &QAction::triggered, layerItem, [ layerItem ]
480     {
481       switch ( layerItem->mapLayerType() )
482       {
483         case QgsMapLayerType::VectorLayer:
484         {
485           const QgsVectorLayer::LayerOptions options { QgsProject::instance()->transformContext() };
486           std::unique_ptr<QgsVectorLayer> layer( new QgsVectorLayer( layerItem->uri(), layerItem->name(), layerItem->providerKey(), options ) );
487           if ( layer && layer->isValid() )
488           {
489             QgisApp::instance()->saveAsFile( layer.get(), false, false );
490           }
491           break;
492         }
493 
494         case QgsMapLayerType::RasterLayer:
495         {
496           std::unique_ptr<QgsRasterLayer> layer( new QgsRasterLayer( layerItem->uri(), layerItem->name(), layerItem->providerKey() ) );
497           if ( layer && layer->isValid() )
498           {
499             QgisApp::instance()->saveAsFile( layer.get(), false, false );
500           }
501           break;
502         }
503 
504         case QgsMapLayerType::PluginLayer:
505         case QgsMapLayerType::AnnotationLayer:
506         case QgsMapLayerType::MeshLayer:
507         case QgsMapLayerType::VectorTileLayer:
508           break;
509       }
510     } );
511   }
512 
513   const QString addText = selectedItems.count() == 1 ? tr( "Add Layer to Project" )
514                           : tr( "Add Selected Layers to Project" );
515   QAction *addAction = new QAction( addText, menu );
516   connect( addAction, &QAction::triggered, this, [ = ]
517   {
518     addLayersFromItems( selectedItems );
519   } );
520   menu->addAction( addAction );
521 
522   if ( item->capabilities2() & QgsDataItem::Delete )
523   {
524     QStringList selectedDeletableItemPaths;
525     for ( QgsDataItem *selectedItem : selectedItems )
526     {
527       if ( qobject_cast<QgsLayerItem *>( selectedItem ) && ( selectedItem->capabilities2() & QgsDataItem::Delete ) )
528         selectedDeletableItemPaths.append( qobject_cast<QgsLayerItem *>( selectedItem )->uri() );
529     }
530 
531     const QString deleteText = selectedDeletableItemPaths.count() == 1 ? tr( "Delete Layer…" )
532                                : tr( "Delete Selected Layers…" );
533     QAction *deleteAction = new QAction( deleteText, menu );
534     connect( deleteAction, &QAction::triggered, this, [ = ]
535     {
536       deleteLayers( selectedDeletableItemPaths, context );
537     } );
538     menu->addAction( deleteAction );
539   }
540 
541   QAction *propertiesAction = new QAction( tr( "Layer Properties…" ), menu );
542   connect( propertiesAction, &QAction::triggered, this, [ = ]
543   {
544     showPropertiesForItem( layerItem, context );
545   } );
546   menu->addAction( propertiesAction );
547 
548   if ( QgsGui::nativePlatformInterface()->capabilities() & QgsNative::NativeFilePropertiesDialog )
549   {
550     if ( QFileInfo::exists( item->path() ) )
551     {
552       QAction *action = menu->addAction( tr( "File Properties…" ) );
553       connect( action, &QAction::triggered, this, [ = ]
554       {
555         QgsGui::nativePlatformInterface()->showFileProperties( item->path() );
556       } );
557     }
558   }
559 }
560 
handleDoubleClick(QgsDataItem * item,QgsDataItemGuiContext)561 bool QgsLayerItemGuiProvider::handleDoubleClick( QgsDataItem *item, QgsDataItemGuiContext )
562 {
563   if ( !item || item->type() != QgsDataItem::Layer )
564     return false;
565 
566   if ( QgsLayerItem *layerItem = qobject_cast<QgsLayerItem *>( item ) )
567   {
568     const QgsMimeDataUtils::UriList layerUriList = QgsMimeDataUtils::UriList() << layerItem->mimeUri();
569     QgisApp::instance()->handleDropUriList( layerUriList );
570     return true;
571   }
572   else
573   {
574     return false;
575   }
576 }
577 
addLayersFromItems(const QList<QgsDataItem * > & items)578 void QgsLayerItemGuiProvider::addLayersFromItems( const QList<QgsDataItem *> &items )
579 {
580   QgsTemporaryCursorOverride cursor( Qt::WaitCursor );
581 
582   // If any of the layer items are QGIS we just open and exit the loop
583   // TODO - maybe this logic is wrong?
584   for ( const QgsDataItem *item : items )
585   {
586     if ( item && item->type() == QgsDataItem::Project )
587     {
588       if ( const QgsProjectItem *projectItem = qobject_cast<const QgsProjectItem *>( item ) )
589         QgisApp::instance()->openProject( projectItem->path() );
590 
591       return;
592     }
593   }
594 
595   QgsMimeDataUtils::UriList layerUriList;
596   layerUriList.reserve( items.size() );
597   // add items in reverse order so they are in correct order in the layers dock
598   for ( int i = items.size() - 1; i >= 0; i-- )
599   {
600     QgsDataItem *item = items.at( i );
601     if ( item && item->type() == QgsDataItem::Layer )
602     {
603       if ( QgsLayerItem *layerItem = qobject_cast<QgsLayerItem *>( item ) )
604         layerUriList << layerItem->mimeUri();
605     }
606   }
607   if ( !layerUriList.isEmpty() )
608     QgisApp::instance()->handleDropUriList( layerUriList );
609 }
610 
deleteLayers(const QStringList & itemPaths,QgsDataItemGuiContext context)611 void QgsLayerItemGuiProvider::deleteLayers( const QStringList &itemPaths, QgsDataItemGuiContext context )
612 {
613   for ( const QString &itemPath : itemPaths )
614   {
615     //get the item from browserModel by its path
616     QgsLayerItem *item = qobject_cast<QgsLayerItem *>( QgisApp::instance()->browserModel()->dataItem( QgisApp::instance()->browserModel()->findUri( itemPath ) ) );
617     if ( !item )
618     {
619       QgsMessageLog::logMessage( tr( "Item with path %1 no longer exists." ).arg( itemPath ) );
620       return;
621     }
622 
623     // first try to use the new API - through QgsDataItemGuiProvider. If that fails, try to use the legacy API...
624     bool usedNewApi = false;
625     const QList<QgsDataItemGuiProvider *> providers = QgsGui::dataItemGuiProviderRegistry()->providers();
626     for ( QgsDataItemGuiProvider *provider : providers )
627     {
628       if ( provider->deleteLayer( item, context ) )
629       {
630         usedNewApi = true;
631         break;
632       }
633     }
634 
635     if ( !usedNewApi )
636     {
637       Q_NOWARN_DEPRECATED_PUSH
638       bool res = item->deleteLayer();
639       Q_NOWARN_DEPRECATED_POP
640 
641       if ( !res )
642         notify( tr( "Delete Layer" ), tr( "Item Layer %1 cannot be deleted." ).arg( item->name() ), context, Qgis::MessageLevel::Warning );
643     }
644   }
645 }
646 
showPropertiesForItem(QgsLayerItem * item,QgsDataItemGuiContext context)647 void QgsLayerItemGuiProvider::showPropertiesForItem( QgsLayerItem *item, QgsDataItemGuiContext context )
648 {
649   if ( ! item )
650     return;
651 
652   QgsBrowserPropertiesDialog *dialog = new QgsBrowserPropertiesDialog( QStringLiteral( "browser" ), QgisApp::instance() );
653   dialog->setAttribute( Qt::WA_DeleteOnClose );
654   dialog->setItem( item, context );
655   dialog->show();
656 }
657 
658 //
659 // QgsProjectItemGuiProvider
660 //
661 
name()662 QString QgsProjectItemGuiProvider::name()
663 {
664   return QStringLiteral( "project_items" );
665 }
666 
populateContextMenu(QgsDataItem * item,QMenu * menu,const QList<QgsDataItem * > &,QgsDataItemGuiContext context)667 void QgsProjectItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu *menu, const QList<QgsDataItem *> &, QgsDataItemGuiContext context )
668 {
669   if ( !item || item->type() != QgsDataItem::Project )
670     return;
671 
672   if ( QgsProjectItem *projectItem = qobject_cast<QgsProjectItem *>( item ) )
673   {
674     QAction *openAction = new QAction( tr( "Open Project" ), menu );
675     const QString projectPath = projectItem->path();
676     connect( openAction, &QAction::triggered, this, [projectPath]
677     {
678       QgisApp::instance()->openProject( projectPath );
679     } );
680     menu->addAction( openAction );
681 
682     QAction *extractAction = new QAction( tr( "Extract Symbols…" ), menu );
683     connect( extractAction, &QAction::triggered, this, [projectPath, context]
684     {
685       QgsStyle style;
686       style.createMemoryDatabase();
687 
688       QgsSaveToStyleVisitor visitor( &style );
689 
690       QgsProject p;
691       QgsTemporaryCursorOverride override( Qt::WaitCursor );
692       if ( p.read( projectPath, QgsProject::ReadFlag::FlagDontResolveLayers | QgsProject::ReadFlag::FlagDontStoreOriginalStyles ) )
693       {
694         p.accept( &visitor );
695         override.release();
696         QgsStyleManagerDialog dlg( &style, QgisApp::instance(), Qt::WindowFlags(), true );
697         dlg.setFavoritesGroupVisible( false );
698         dlg.setSmartGroupsVisible( false );
699         QFileInfo fi( projectPath );
700         dlg.setBaseStyleName( fi.baseName() );
701         dlg.exec();
702       }
703       else if ( context.messageBar() )
704       {
705         context.messageBar()->pushWarning( tr( "Extract Symbols" ), tr( "Could not read project file" ) );
706       }
707     } );
708     menu->addAction( extractAction );
709 
710     if ( QgsGui::nativePlatformInterface()->capabilities() & QgsNative::NativeFilePropertiesDialog )
711     {
712       QAction *action = menu->addAction( tr( "File Properties…" ) );
713       connect( action, &QAction::triggered, this, [projectPath]
714       {
715         QgsGui::nativePlatformInterface()->showFileProperties( projectPath );
716       } );
717     }
718   }
719 }
720 
handleDoubleClick(QgsDataItem * item,QgsDataItemGuiContext)721 bool QgsProjectItemGuiProvider::handleDoubleClick( QgsDataItem *item, QgsDataItemGuiContext )
722 {
723   if ( !item || item->type() != QgsDataItem::Project )
724     return false;
725 
726   if ( QgsProjectItem *projectItem = qobject_cast<QgsProjectItem *>( item ) )
727   {
728     QgisApp::instance()->openProject( projectItem->path() );
729     return true;
730   }
731   else
732   {
733     return false;
734   }
735 }
736 
name()737 QString QgsFieldsItemGuiProvider::name()
738 {
739   return QStringLiteral( "fields_item" );
740 }
741 
populateContextMenu(QgsDataItem * item,QMenu * menu,const QList<QgsDataItem * > & selectedItems,QgsDataItemGuiContext context)742 void QgsFieldsItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu *menu, const QList<QgsDataItem *> &selectedItems, QgsDataItemGuiContext context )
743 {
744   Q_UNUSED( selectedItems )
745 
746   if ( !item || item->type() != QgsDataItem::Type::Fields )
747     return;
748 
749 
750   if ( QgsFieldsItem *fieldsItem = qobject_cast<QgsFieldsItem *>( item ) )
751   {
752     QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( fieldsItem->providerKey() ) };
753     if ( md )
754     {
755       std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn { static_cast<QgsAbstractDatabaseProviderConnection *>( md->createConnection( fieldsItem->connectionUri(), {} ) ) };
756       // Check if it is supported
757       if ( conn && conn->capabilities().testFlag( QgsAbstractDatabaseProviderConnection::Capability::AddField ) )
758       {
759         QAction *addColumnAction = new QAction( tr( "Add New Field…" ), menu );
760         QPointer<QgsDataItem>itemPtr { item };
761         const QString itemName { item->name() };
762 
763         connect( addColumnAction, &QAction::triggered, fieldsItem, [ md, fieldsItem, context, itemPtr, menu ]
764         {
765           std::unique_ptr<QgsVectorLayer> layer { fieldsItem->layer() };
766           if ( layer )
767           {
768             QgsAddAttrDialog dialog( layer.get(), menu );
769             if ( dialog.exec() == QDialog::Accepted )
770             {
771               std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn2 { static_cast<QgsAbstractDatabaseProviderConnection *>( md->createConnection( fieldsItem->connectionUri(), {} ) ) };
772               try
773               {
774                 conn2->addField( dialog.field(), fieldsItem->schema(), fieldsItem->tableName() );
775                 if ( itemPtr )
776                   itemPtr->refresh();
777               }
778               catch ( const QgsProviderConnectionException &ex )
779               {
780                 notify( tr( "New Field" ), tr( "Failed to add the new field to '%1': %2" ).arg( fieldsItem->tableName(), ex.what() ), context, Qgis::MessageLevel::Critical );
781               }
782             }
783           }
784           else
785           {
786             notify( tr( "New Field" ), tr( "Failed to load layer '%1'. Check application logs and user permissions." ).arg( fieldsItem->tableName() ), context, Qgis::MessageLevel::Critical );
787           }
788         } );
789         menu->addAction( addColumnAction );
790       }
791     }
792   }
793 }
794 
name()795 QString QgsFieldItemGuiProvider::name()
796 {
797   return QStringLiteral( "field_item" );
798 }
799 
populateContextMenu(QgsDataItem * item,QMenu * menu,const QList<QgsDataItem * > & selectedItems,QgsDataItemGuiContext context)800 void QgsFieldItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu *menu, const QList<QgsDataItem *> &selectedItems, QgsDataItemGuiContext context )
801 {
802   Q_UNUSED( selectedItems )
803 
804   if ( !item || item->type() != QgsDataItem::Type::Field )
805     return;
806 
807   if ( QgsFieldItem *fieldItem = qobject_cast<QgsFieldItem *>( item ) )
808   {
809     // Retrieve the connection from the parent
810     QgsFieldsItem *fieldsItem { static_cast<QgsFieldsItem *>( fieldItem->parent() ) };
811     if ( fieldsItem )
812     {
813       // Check if it is supported
814       QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( fieldsItem->providerKey() ) };
815       if ( md )
816       {
817         std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn { static_cast<QgsAbstractDatabaseProviderConnection *>( md->createConnection( fieldsItem->connectionUri(), {} ) ) };
818         if ( conn && conn->capabilities().testFlag( QgsAbstractDatabaseProviderConnection::Capability::DeleteField ) )
819         {
820           QAction *deleteFieldAction = new QAction( tr( "Delete Field…" ), menu );
821           const bool supportsCascade { conn->capabilities().testFlag( QgsAbstractDatabaseProviderConnection::Capability::DeleteFieldCascade ) };
822           const QString itemName { item->name() };
823 
824           connect( deleteFieldAction, &QAction::triggered, fieldsItem, [ md, fieldsItem, itemName, context, supportsCascade ]
825           {
826             // Confirmation dialog
827             QString message {  tr( "Delete '%1' permanently?" ).arg( itemName ) };
828             if ( fieldsItem->tableProperty() && fieldsItem->tableProperty()->primaryKeyColumns().contains( itemName ) )
829             {
830               message.append( tr( "\nThis field is part of a primary key, its removal may make the table unusable by QGIS!" ) );
831             }
832             if ( fieldsItem->tableProperty() && fieldsItem->tableProperty()->geometryColumn() == itemName )
833             {
834               message.append( tr( "\nThis field is a geometry column, its removal may make the table unusable by QGIS!" ) );
835             }
836             QMessageBox msgbox{QMessageBox::Icon::Question, tr( "Delete Field" ), message, QMessageBox::Ok | QMessageBox::Cancel };
837             QCheckBox *cb = new QCheckBox( tr( "Delete all related objects (CASCADE)?" ) );
838             msgbox.setCheckBox( cb );
839             msgbox.setDefaultButton( QMessageBox::Cancel );
840 
841             if ( ! supportsCascade )
842             {
843               cb->hide();
844             }
845 
846             if ( msgbox.exec() == QMessageBox::Ok )
847             {
848               std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn2 { static_cast<QgsAbstractDatabaseProviderConnection *>( md->createConnection( fieldsItem->connectionUri(), {} ) ) };
849               try
850               {
851                 conn2->deleteField( itemName, fieldsItem->schema(), fieldsItem->tableName(), supportsCascade && cb->isChecked() );
852                 fieldsItem->refresh();
853               }
854               catch ( const QgsProviderConnectionException &ex )
855               {
856                 notify( tr( "Delete Field" ), tr( "Failed to delete field '%1': %2" ).arg( itemName, ex.what() ), context, Qgis::MessageLevel::Critical );
857               }
858             }
859           } );
860           menu->addAction( deleteFieldAction );
861         }
862       }
863     }
864     else
865     {
866       // This should never happen!
867       QgsDebugMsg( QStringLiteral( "Error getting parent fields for %1" ).arg( item->name() ) );
868     }
869   }
870 }
871 
name()872 QString QgsDatabaseItemGuiProvider::name()
873 {
874   return QStringLiteral( "database" );
875 }
876 
populateContextMenu(QgsDataItem * item,QMenu * menu,const QList<QgsDataItem * > & selectedItems,QgsDataItemGuiContext context)877 void QgsDatabaseItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu *menu, const QList<QgsDataItem *> &selectedItems, QgsDataItemGuiContext context )
878 {
879   Q_UNUSED( selectedItems )
880 
881   // Add create new table for collection items but not not if it is a root item
882   if ( ! qobject_cast<QgsConnectionsRootItem *>( item ) )
883   {
884     if ( QgsDataCollectionItem * collectionItem { qobject_cast<QgsDataCollectionItem *>( item ) } )
885     {
886       std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn( item->databaseConnection() );
887 
888       if ( conn && conn->capabilities().testFlag( QgsAbstractDatabaseProviderConnection::Capability::CreateVectorTable ) )
889       {
890 
891         QAction *newTableAction = new QAction( QObject::tr( "New Table…" ), menu );
892 
893         QObject::connect( newTableAction, &QAction::triggered, collectionItem, [ collectionItem, context]
894         {
895 
896           std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn2( collectionItem->databaseConnection() );
897           // This should never happen but let's play safe
898           if ( ! conn2 )
899           {
900             QgsMessageLog::logMessage( tr( "Connection to the database (%1) was lost." ).arg( collectionItem->name() ) );
901             return;
902           }
903 
904           QgsNewVectorTableDialog dlg { conn2.get(), nullptr };
905           dlg.setCrs( QgsProject::instance()->defaultCrsForNewLayers() );
906 
907           const bool isSchema { qobject_cast<QgsDatabaseSchemaItem *>( collectionItem ) != nullptr };
908 
909           if ( isSchema )
910           {
911             dlg.setSchemaName( collectionItem->name() );
912           }
913 
914           if ( dlg.exec() == QgsNewVectorTableDialog::DialogCode::Accepted )
915           {
916             const QgsFields fields { dlg.fields() };
917             const QString tableName { dlg.tableName() };
918             const QString schemaName { dlg.schemaName() };
919             const QString geometryColumn { dlg.geometryColumnName() };
920             const QgsWkbTypes::Type geometryType { dlg.geometryType() };
921             const bool createSpatialIndex { dlg.createSpatialIndex() &&
922                                             geometryType != QgsWkbTypes::NoGeometry &&
923                                             geometryType != QgsWkbTypes::Unknown };
924             const QgsCoordinateReferenceSystem crs { dlg.crs( ) };
925             // This flag tells to the provider that field types do not need conversion
926             // also prevents  GDAL to create a spatial index by default for GPKG, we are
927             // going to create it afterwards in a unified manner for all providers.
928             QMap<QString, QVariant> options { { QStringLiteral( "skipConvertFields" ), true },
929               { QStringLiteral( "layerOptions" ), QStringLiteral( "SPATIAL_INDEX=NO" ) } };
930 
931             if ( ! geometryColumn.isEmpty() )
932             {
933               options[ QStringLiteral( "geometryColumn" ) ] = geometryColumn;
934             }
935 
936             try
937             {
938               conn2->createVectorTable( schemaName, tableName, fields, geometryType, crs, true, &options );
939               if ( createSpatialIndex && conn2->capabilities().testFlag( QgsAbstractDatabaseProviderConnection::Capability::CreateSpatialIndex ) )
940               {
941                 try
942                 {
943                   conn2->createSpatialIndex( schemaName, tableName );
944                 }
945                 catch ( QgsProviderConnectionException &ex )
946                 {
947                   notify( QObject::tr( "Create Spatial Index" ), QObject::tr( "Could not create spatial index for table '%1':%2." ).arg( tableName, ex.what() ), context, Qgis::MessageLevel::Warning );
948                 }
949               }
950               // Ok, here is the trick: we cannot refresh the connection item because the refresh is not
951               // recursive.
952               // So, we check if the item is a schema or not, if it's not it means we initiated the new table from
953               // the parent connection item, hence we search for the schema item and refresh it instead of refreshing
954               // the connection item (the parent) with no effects.
955               if ( ! isSchema && conn2->capabilities().testFlag( QgsAbstractDatabaseProviderConnection::Capability::Schemas ) )
956               {
957                 const auto constChildren { collectionItem->children() };
958                 for ( const auto &c : constChildren )
959                 {
960                   if ( c->name() == schemaName )
961                   {
962                     c->refresh();
963                   }
964                 }
965               }
966               else
967               {
968                 collectionItem->refresh( );
969               }
970               notify( QObject::tr( "New Table Created" ), QObject::tr( "Table '%1' was created successfully." ).arg( tableName ), context, Qgis::MessageLevel::Success );
971             }
972             catch ( QgsProviderConnectionException &ex )
973             {
974               notify( QObject::tr( "New Table Creation Error" ), QObject::tr( "Error creating new table '%1': %2" ).arg( tableName, ex.what() ), context, Qgis::MessageLevel::Critical );
975             }
976 
977           }
978         } );
979         menu->addAction( newTableAction );
980       }
981     }
982   }
983 }
984 
985 
986