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