1 /***************************************************************************
2     qgsbrowserwidget.cpp
3     ---------------------
4     begin                : July 2011
5     copyright            : (C) 2011 by Martin Dobias
6     email                : wonder dot sk at gmail dot com
7  ***************************************************************************
8  *                                                                         *
9  *   This program is free software; you can redistribute it and/or modify  *
10  *   it under the terms of the GNU General Public License as published by  *
11  *   the Free Software Foundation; either version 2 of the License, or     *
12  *   (at your option) any later version.                                   *
13  *                                                                         *
14  ***************************************************************************/
15 #include "qgsbrowserwidget.h"
16 
17 #include <QAbstractTextDocumentLayout>
18 #include <QHeaderView>
19 #include <QTreeView>
20 #include <QMenu>
21 #include <QToolButton>
22 #include <QFileDialog>
23 #include <QPlainTextDocumentLayout>
24 #include <QSortFilterProxyModel>
25 #include <QActionGroup>
26 
27 #include "qgsbrowserguimodel.h"
28 #include "qgsbrowsertreeview.h"
29 #include "qgslogger.h"
30 #include "qgsrasterlayer.h"
31 #include "qgsvectorlayer.h"
32 #include "qgsproject.h"
33 #include "qgssettings.h"
34 #include "qgsnewnamedialog.h"
35 #include "qgsbrowserproxymodel.h"
36 #include "qgsgui.h"
37 #include "qgswindowmanagerinterface.h"
38 #include "qgsnative.h"
39 #include "qgsdataitemguiproviderregistry.h"
40 #include "qgsdataitemguiprovider.h"
41 #include "qgsdirectoryitem.h"
42 #include "qgslayeritem.h"
43 #include "qgsprojectitem.h"
44 #include "qgsbrowserdockwidget_p.h"
45 
46 // browser layer properties dialog
47 #include "qgsapplication.h"
48 #include "qgsmapcanvas.h"
49 
50 #include <QDragEnterEvent>
51 #include <functional>
52 
QgsBrowserWidget(QgsBrowserGuiModel * browserModel,QWidget * parent)53 QgsBrowserWidget::QgsBrowserWidget( QgsBrowserGuiModel *browserModel, QWidget *parent )
54   : QgsPanelWidget( parent )
55   , mModel( browserModel )
56 {
57   setupUi( this );
58 
59   layout()->setContentsMargins( 0, 0, 0, 0 );
60   qgis::down_cast< QVBoxLayout * >( layout() )->setSpacing( 0 );
61 
62   mBrowserView = new QgsDockBrowserTreeView( this );
63   mLayoutBrowser->addWidget( mBrowserView );
64 
65   mWidgetFilter->hide();
66   mLeFilter->setPlaceholderText( tr( "Type here to filter visible items…" ) );
67   // icons from http://www.fatcow.com/free-icons License: CC Attribution 3.0
68 
69   QMenu *menu = new QMenu( this );
70   menu->setSeparatorsCollapsible( false );
71   mBtnFilterOptions->setMenu( menu );
72   QAction *action = new QAction( tr( "Case Sensitive" ), menu );
73   action->setData( "case" );
74   action->setCheckable( true );
75   action->setChecked( false );
76   connect( action, &QAction::toggled, this, &QgsBrowserWidget::setCaseSensitive );
77   menu->addAction( action );
78   QActionGroup *group = new QActionGroup( menu );
79   action = new QAction( tr( "Filter Pattern Syntax" ), group );
80   action->setSeparator( true );
81   menu->addAction( action );
82   action = new QAction( tr( "Normal" ), group );
83   action->setData( QgsBrowserProxyModel::Normal );
84   action->setCheckable( true );
85   action->setChecked( true );
86   menu->addAction( action );
87   action = new QAction( tr( "Wildcard(s)" ), group );
88   action->setData( QgsBrowserProxyModel::Wildcards );
89   action->setCheckable( true );
90   menu->addAction( action );
91   action = new QAction( tr( "Regular Expression" ), group );
92   action->setData( QgsBrowserProxyModel::RegularExpression );
93   action->setCheckable( true );
94   menu->addAction( action );
95 
96   mBrowserView->setExpandsOnDoubleClick( false );
97 
98   connect( mActionRefresh, &QAction::triggered, this, &QgsBrowserWidget::refresh );
99   connect( mActionAddLayers, &QAction::triggered, this, &QgsBrowserWidget::addSelectedLayers );
100   connect( mActionCollapse, &QAction::triggered, mBrowserView, &QgsDockBrowserTreeView::collapseAll );
101   connect( mActionShowFilter, &QAction::triggered, this, &QgsBrowserWidget::showFilterWidget );
102   connect( mActionPropertiesWidget, &QAction::triggered, this, &QgsBrowserWidget::enablePropertiesWidget );
103   connect( mLeFilter, &QgsFilterLineEdit::returnPressed, this, &QgsBrowserWidget::setFilter );
104   connect( mLeFilter, &QgsFilterLineEdit::cleared, this, &QgsBrowserWidget::setFilter );
105   connect( mLeFilter, &QgsFilterLineEdit::textChanged, this, &QgsBrowserWidget::setFilter );
106   connect( group, &QActionGroup::triggered, this, &QgsBrowserWidget::setFilterSyntax );
107   connect( mBrowserView, &QgsDockBrowserTreeView::customContextMenuRequested, this, &QgsBrowserWidget::showContextMenu );
108   connect( mBrowserView, &QgsDockBrowserTreeView::doubleClicked, this, &QgsBrowserWidget::itemDoubleClicked );
109   connect( mSplitter, &QSplitter::splitterMoved, this, &QgsBrowserWidget::splitterMoved );
110 
111   connect( QgsGui::instance(), &QgsGui::optionsChanged, this, &QgsBrowserWidget::onOptionsChanged );
112 }
113 
~QgsBrowserWidget()114 QgsBrowserWidget::~QgsBrowserWidget()
115 {
116   QgsSettings settings;
117   settings.setValue( settingsSection() + "/propertiesWidgetEnabled", mPropertiesWidgetEnabled );
118   //settings.setValue(settingsSection() + "/propertiesWidgetHeight", mPropertiesWidget->size().height() );
119   settings.setValue( settingsSection() + "/propertiesWidgetHeight", mPropertiesWidgetHeight );
120 }
121 
showEvent(QShowEvent * e)122 void QgsBrowserWidget::showEvent( QShowEvent *e )
123 {
124   // delayed initialization of the model
125   if ( !mModel->initialized( ) )
126   {
127     mModel->initialize();
128   }
129   if ( ! mProxyModel )
130   {
131     mProxyModel = new QgsBrowserProxyModel( this );
132     mProxyModel->setBrowserModel( mModel );
133     mProxyModel->setHiddenDataItemProviderKeyFilter( mDisabledDataItemsKeys );
134     mBrowserView->setSettingsSection( objectName().toLower() ); // to distinguish 2 or more instances of the browser
135     mBrowserView->setBrowserModel( mModel );
136     mBrowserView->setModel( mProxyModel );
137     mBrowserView->setSortingEnabled( true );
138     mBrowserView->sortByColumn( 0, Qt::AscendingOrder );
139     // provide a horizontal scroll bar instead of using ellipse (...) for longer items
140     mBrowserView->setTextElideMode( Qt::ElideNone );
141     mBrowserView->header()->setSectionResizeMode( 0, QHeaderView::ResizeToContents );
142     mBrowserView->header()->setStretchLastSection( false );
143 
144     // selectionModel is created when model is set on tree
145     connect( mBrowserView->selectionModel(), &QItemSelectionModel::selectionChanged,
146              this, &QgsBrowserWidget::selectionChanged );
147 
148     // Forward the model changed signals to the widget
149     connect( mModel, &QgsBrowserModel::connectionsChanged,
150              this, &QgsBrowserWidget::connectionsChanged );
151 
152 
153     // objectName used by settingsSection() is not yet set in constructor
154     QgsSettings settings;
155     mPropertiesWidgetEnabled = settings.value( settingsSection() + "/propertiesWidgetEnabled", false ).toBool();
156     mActionPropertiesWidget->setChecked( mPropertiesWidgetEnabled );
157     mPropertiesWidget->setVisible( false ); // false until item is selected
158 
159     mPropertiesWidgetHeight = settings.value( settingsSection() + "/propertiesWidgetHeight" ).toFloat();
160     QList<int> sizes = mSplitter->sizes();
161     int total = sizes.value( 0 ) + sizes.value( 1 );
162     int height = static_cast<int>( total * mPropertiesWidgetHeight );
163     sizes.clear();
164     sizes << total - height << height;
165     mSplitter->setSizes( sizes );
166   }
167 
168   QWidget::showEvent( e );
169 }
170 
itemDoubleClicked(const QModelIndex & index)171 void QgsBrowserWidget::itemDoubleClicked( const QModelIndex &index )
172 {
173   QgsDataItem *item = mModel->dataItem( mProxyModel->mapToSource( index ) );
174   if ( !item )
175     return;
176 
177   QgsDataItemGuiContext context = createContext();
178 
179   const QList< QgsDataItemGuiProvider * > providers = QgsGui::instance()->dataItemGuiProviderRegistry()->providers();
180   for ( QgsDataItemGuiProvider *provider : providers )
181   {
182     if ( provider->handleDoubleClick( item, context ) )
183       return;
184   }
185 
186   // if no providers overrode the double-click handling for this item, we give the item itself a chance
187   if ( !item->handleDoubleClick() )
188   {
189     // double-click not handled by browser model, so use as default view expand behavior
190     if ( mBrowserView->isExpanded( index ) )
191       mBrowserView->collapse( index );
192     else
193       mBrowserView->expand( index );
194   }
195 }
196 
onOptionsChanged()197 void QgsBrowserWidget::onOptionsChanged()
198 {
199   std::function< void( const QModelIndex &index ) > updateItem;
200   updateItem = [this, &updateItem]( const QModelIndex & index )
201   {
202     if ( QgsDirectoryItem *dirItem = qobject_cast< QgsDirectoryItem * >( mModel->dataItem( index ) ) )
203     {
204       dirItem->reevaluateMonitoring();
205     }
206 
207     const int rowCount = mModel->rowCount( index );
208     for ( int i = 0; i < rowCount; ++i )
209     {
210       const QModelIndex child = mModel->index( i, 0, index );
211       updateItem( child );
212     }
213   };
214 
215   for ( int i = 0; i < mModel->rowCount(); ++i )
216   {
217     updateItem( mModel->index( i, 0 ) );
218   }
219 }
220 
showContextMenu(QPoint pt)221 void QgsBrowserWidget::showContextMenu( QPoint pt )
222 {
223   QModelIndex index = mProxyModel->mapToSource( mBrowserView->indexAt( pt ) );
224   QgsDataItem *item = mModel->dataItem( index );
225   if ( !item )
226     return;
227 
228   const QModelIndexList selection = mBrowserView->selectionModel()->selectedIndexes();
229   QList< QgsDataItem * > selectedItems;
230   selectedItems.reserve( selection.size() );
231   for ( const QModelIndex &selectedIndex : selection )
232   {
233     QgsDataItem *selectedItem = mProxyModel->dataItem( selectedIndex );
234     if ( selectedItem )
235       selectedItems << selectedItem;
236   }
237 
238   QMenu *menu = new QMenu( this );
239 
240   const QList<QMenu *> menus = item->menus( menu );
241   QList<QAction *> actions = item->actions( menu );
242 
243   if ( !menus.isEmpty() )
244   {
245     for ( QMenu *mn : menus )
246     {
247       menu->addMenu( mn );
248     }
249   }
250 
251   if ( !actions.isEmpty() )
252   {
253     if ( !menu->actions().isEmpty() )
254       menu->addSeparator();
255     // add action to the menu
256     menu->addActions( actions );
257   }
258 
259   QgsDataItemGuiContext context = createContext();
260 
261   QList< QgsDataItemGuiProvider * > providers = QgsGui::instance()->dataItemGuiProviderRegistry()->providers();
262   std::sort( providers.begin(), providers.end(), []( QgsDataItemGuiProvider * a, QgsDataItemGuiProvider * b )
263   {
264     return a->precedenceWhenPopulatingMenus() < b->precedenceWhenPopulatingMenus();
265   } );
266   for ( QgsDataItemGuiProvider *provider : std::as_const( providers ) )
267   {
268     provider->populateContextMenu( item, menu, selectedItems, context );
269   }
270 
271   if ( menu->actions().isEmpty() )
272   {
273     delete menu;
274     return;
275   }
276 
277   menu->popup( mBrowserView->mapToGlobal( pt ) );
278 }
279 
setMessageBar(QgsMessageBar * bar)280 void QgsBrowserWidget::setMessageBar( QgsMessageBar *bar )
281 {
282   mMessageBar = bar;
283   mModel->setMessageBar( bar );
284 }
285 
messageBar()286 QgsMessageBar *QgsBrowserWidget::messageBar()
287 {
288   return mMessageBar;
289 }
290 
setDisabledDataItemsKeys(const QStringList & filter)291 void QgsBrowserWidget::setDisabledDataItemsKeys( const QStringList &filter )
292 {
293   mDisabledDataItemsKeys = filter;
294 
295   if ( !mProxyModel )
296     return;
297 
298   mProxyModel->setHiddenDataItemProviderKeyFilter( mDisabledDataItemsKeys );
299 }
300 
refresh()301 void QgsBrowserWidget::refresh()
302 {
303   refreshModel( QModelIndex() );
304 }
305 
refreshModel(const QModelIndex & index)306 void QgsBrowserWidget::refreshModel( const QModelIndex &index )
307 {
308   if ( mModel && mProxyModel )
309   {
310     QgsDataItem *item = mModel->dataItem( index );
311     if ( item )
312     {
313       QgsDebugMsgLevel( "path = " + item->path(), 4 );
314     }
315     else
316     {
317       QgsDebugMsgLevel( QStringLiteral( "invalid item" ), 4 );
318     }
319 
320     if ( item && ( item->capabilities2() & Qgis::BrowserItemCapability::Fertile ) )
321     {
322       mModel->refresh( index );
323     }
324 
325     for ( int i = 0; i < mModel->rowCount( index ); i++ )
326     {
327       QModelIndex idx = mModel->index( i, 0, index );
328       QModelIndex proxyIdx = mProxyModel->mapFromSource( idx );
329       QgsDataItem *child = mModel->dataItem( idx );
330 
331       // Check also expanded descendants so that the whole expanded path does not get collapsed if one item is collapsed.
332       // Fast items (usually root items) are refreshed so that when collapsed, it is obvious they are if empty (no expand symbol).
333       if ( mBrowserView->isExpanded( proxyIdx ) || mBrowserView->hasExpandedDescendant( proxyIdx ) || ( child && child->capabilities2() & Qgis::BrowserItemCapability::Fast ) )
334       {
335         refreshModel( idx );
336       }
337       else
338       {
339         if ( child && ( child->capabilities2() & Qgis::BrowserItemCapability::Fertile ) )
340         {
341           child->depopulate();
342         }
343       }
344     }
345   }
346 }
347 
addLayer(QgsLayerItem * layerItem)348 void QgsBrowserWidget::addLayer( QgsLayerItem *layerItem )
349 {
350   if ( !layerItem )
351     return;
352 
353   emit handleDropUriList( layerItem->mimeUris() );
354 }
355 
addSelectedLayers()356 void QgsBrowserWidget::addSelectedLayers()
357 {
358   QApplication::setOverrideCursor( Qt::WaitCursor );
359 
360   // get a sorted list of selected indexes
361   QModelIndexList list = mBrowserView->selectionModel()->selectedIndexes();
362   std::sort( list.begin(), list.end() );
363 
364   // If any of the layer items are QGIS we just open and exit the loop
365   const auto constList = list;
366   for ( const QModelIndex &index : constList )
367   {
368     QgsDataItem *item = mModel->dataItem( mProxyModel->mapToSource( index ) );
369     if ( item && item->type() == Qgis::BrowserItemType::Project )
370     {
371       QgsProjectItem *projectItem = qobject_cast<QgsProjectItem *>( item );
372       if ( projectItem )
373         emit openFile( projectItem->path(), QStringLiteral( "project" ) );
374 
375       QApplication::restoreOverrideCursor();
376       return;
377     }
378   }
379 
380   // add items in reverse order so they are in correct order in the layers dock
381   for ( int i = list.size() - 1; i >= 0; i-- )
382   {
383     QgsDataItem *item = mModel->dataItem( mProxyModel->mapToSource( list[i] ) );
384     if ( item && item->type() == Qgis::BrowserItemType::Layer )
385     {
386       QgsLayerItem *layerItem = qobject_cast<QgsLayerItem *>( item );
387       if ( layerItem )
388         addLayer( layerItem );
389     }
390   }
391 
392   QApplication::restoreOverrideCursor();
393 }
394 
hideItem()395 void QgsBrowserWidget::hideItem()
396 {
397   QModelIndex index = mProxyModel->mapToSource( mBrowserView->currentIndex() );
398   QgsDataItem *item = mModel->dataItem( index );
399   if ( ! item )
400     return;
401 
402   if ( item->type() == Qgis::BrowserItemType::Directory )
403   {
404     mModel->hidePath( item );
405   }
406 }
407 
showProperties()408 void QgsBrowserWidget::showProperties()
409 {
410   QModelIndex index = mProxyModel->mapToSource( mBrowserView->currentIndex() );
411   QgsDataItem *item = mModel->dataItem( index );
412   if ( ! item )
413     return;
414 
415   if ( item->type() == Qgis::BrowserItemType::Layer || item->type() == Qgis::BrowserItemType::Directory )
416   {
417     QgsBrowserPropertiesDialog *dialog = new QgsBrowserPropertiesDialog( settingsSection(), this );
418     dialog->setAttribute( Qt::WA_DeleteOnClose );
419     dialog->setItem( item, createContext() );
420     dialog->show();
421   }
422 }
423 
showFilterWidget(bool visible)424 void QgsBrowserWidget::showFilterWidget( bool visible )
425 {
426   mWidgetFilter->setVisible( visible );
427   if ( ! visible )
428   {
429     mLeFilter->setText( QString() );
430     setFilter();
431   }
432   else
433   {
434     mLeFilter->setFocus();
435   }
436 }
437 
setFilter()438 void QgsBrowserWidget::setFilter()
439 {
440   QString filter = mLeFilter->text();
441   if ( mProxyModel )
442     mProxyModel->setFilterString( filter );
443 }
444 
updateProjectHome()445 void QgsBrowserWidget::updateProjectHome()
446 {
447   if ( mModel )
448     mModel->updateProjectHome();
449 }
450 
setFilterSyntax(QAction * action)451 void QgsBrowserWidget::setFilterSyntax( QAction *action )
452 {
453   if ( !action || ! mProxyModel )
454     return;
455 
456   mProxyModel->setFilterSyntax( static_cast< QgsBrowserProxyModel::FilterSyntax >( action->data().toInt() ) );
457 }
458 
setCaseSensitive(bool caseSensitive)459 void QgsBrowserWidget::setCaseSensitive( bool caseSensitive )
460 {
461   if ( ! mProxyModel )
462     return;
463   mProxyModel->setFilterCaseSensitivity( caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive );
464 }
465 
selectedItemsCount()466 int QgsBrowserWidget::selectedItemsCount()
467 {
468   QItemSelectionModel *selectionModel = mBrowserView->selectionModel();
469   if ( selectionModel )
470   {
471     return selectionModel->selectedIndexes().size();
472   }
473   return 0;
474 }
475 
createContext()476 QgsDataItemGuiContext QgsBrowserWidget::createContext()
477 {
478   QgsDataItemGuiContext context;
479   context.setMessageBar( mMessageBar );
480   return context;
481 }
482 
selectionChanged(const QItemSelection & selected,const QItemSelection & deselected)483 void QgsBrowserWidget::selectionChanged( const QItemSelection &selected, const QItemSelection &deselected )
484 {
485   Q_UNUSED( selected )
486   Q_UNUSED( deselected )
487   if ( mPropertiesWidgetEnabled )
488   {
489     setPropertiesWidget();
490   }
491 }
492 
clearPropertiesWidget()493 void QgsBrowserWidget::clearPropertiesWidget()
494 {
495   while ( mPropertiesLayout->count() > 0 )
496   {
497     delete mPropertiesLayout->itemAt( 0 )->widget();
498   }
499   mPropertiesWidget->setVisible( false );
500 }
501 
setPropertiesWidget()502 void QgsBrowserWidget::setPropertiesWidget()
503 {
504   clearPropertiesWidget();
505   QItemSelectionModel *selectionModel = mBrowserView->selectionModel();
506   if ( selectionModel )
507   {
508     QModelIndexList indexes = selectionModel->selectedIndexes();
509     if ( indexes.size() == 1 )
510     {
511       QModelIndex index = mProxyModel->mapToSource( indexes.value( 0 ) );
512       QgsDataItem *item = mModel->dataItem( index );
513       QgsDataItemGuiContext context = createContext();
514       QgsBrowserPropertiesWidget *propertiesWidget = QgsBrowserPropertiesWidget::createWidget( item, context, mPropertiesWidget );
515       if ( propertiesWidget )
516       {
517         propertiesWidget->setCondensedMode( true );
518         mPropertiesLayout->addWidget( propertiesWidget );
519       }
520     }
521   }
522   mPropertiesWidget->setVisible( mPropertiesLayout->count() > 0 );
523 }
524 
enablePropertiesWidget(bool enable)525 void QgsBrowserWidget::enablePropertiesWidget( bool enable )
526 {
527   mPropertiesWidgetEnabled = enable;
528   if ( enable && selectedItemsCount() == 1 )
529   {
530     setPropertiesWidget();
531   }
532   else
533   {
534     clearPropertiesWidget();
535   }
536 }
537 
setActiveIndex(const QModelIndex & index)538 void QgsBrowserWidget::setActiveIndex( const QModelIndex &index )
539 {
540   if ( index.isValid() )
541   {
542     QModelIndex proxyIndex = mProxyModel->mapFromSource( index );
543     mBrowserView->expand( proxyIndex );
544     mBrowserView->setCurrentIndex( proxyIndex );
545   }
546 }
547 
splitterMoved()548 void QgsBrowserWidget::splitterMoved()
549 {
550   QList<int> sizes = mSplitter->sizes();
551   float total = sizes.value( 0 ) + sizes.value( 1 );
552   mPropertiesWidgetHeight = total > 0 ? sizes.value( 1 ) / total : 0;
553 }
554