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