1 /***************************************************************************
2   qgsdatasourceselectdialog.cpp - QgsDataSourceSelectDialog
3 
4  ---------------------
5  begin                : 1.11.2018
6  copyright            : (C) 2018 by Alessandro Pasotti
7  email                : elpaso@itopen.it
8  ***************************************************************************
9  *                                                                         *
10  *   This program is free software; you can redistribute it and/or modify  *
11  *   it under the terms of the GNU General Public License as published by  *
12  *   the Free Software Foundation; either version 2 of the License, or     *
13  *   (at your option) any later version.                                   *
14  *                                                                         *
15  ***************************************************************************/
16 
17 #include "qgsdatasourceselectdialog.h"
18 
19 #include "qgis.h"
20 #include "qgsbrowsermodel.h"
21 #include "qgsgui.h"
22 #include "qgsguiutils.h"
23 #include "qgssettings.h"
24 #include "qgsnative.h"
25 
26 #include <QPushButton>
27 #include <QMenu>
28 #include <QDesktopServices>
29 #include <QDialogButtonBox>
30 
QgsDataSourceSelectWidget(QgsBrowserGuiModel * browserModel,bool setFilterByLayerType,QgsMapLayerType layerType,QWidget * parent)31 QgsDataSourceSelectWidget::QgsDataSourceSelectWidget(
32   QgsBrowserGuiModel *browserModel,
33   bool setFilterByLayerType,
34   QgsMapLayerType layerType,
35   QWidget *parent )
36   : QgsPanelWidget( parent )
37 {
38   if ( ! browserModel )
39   {
40     mBrowserModel = new QgsBrowserGuiModel( this );
41     mBrowserModel->initialize();
42   }
43   else
44   {
45     mBrowserModel = browserModel;
46     mBrowserModel->initialize();
47   }
48 
49   setupUi( this );
50 
51   mBrowserProxyModel.setBrowserModel( mBrowserModel );
52   mBrowserTreeView->setHeaderHidden( true );
53 
54   if ( setFilterByLayerType )
55   {
56     // This will also set the (proxy) model
57     setLayerTypeFilter( layerType );
58   }
59   else
60   {
61     mBrowserTreeView->setModel( &mBrowserProxyModel );
62     setValid( false );
63   }
64 
65   mBrowserTreeView->setBrowserModel( mBrowserModel );
66 
67   mWidgetFilter->hide();
68   mLeFilter->setPlaceholderText( tr( "Type here to filter visible items…" ) );
69   // icons from http://www.fatcow.com/free-icons License: CC Attribution 3.0
70 
71   QMenu *menu = new QMenu( this );
72   menu->setSeparatorsCollapsible( false );
73   mBtnFilterOptions->setMenu( menu );
74   QAction *action = new QAction( tr( "Case Sensitive" ), menu );
75   action->setData( "case" );
76   action->setCheckable( true );
77   action->setChecked( false );
78   connect( action, &QAction::toggled, this, &QgsDataSourceSelectWidget::setCaseSensitive );
79   menu->addAction( action );
80   QActionGroup *group = new QActionGroup( menu );
81   action = new QAction( tr( "Filter Pattern Syntax" ), group );
82   action->setSeparator( true );
83   menu->addAction( action );
84   action = new QAction( tr( "Normal" ), group );
85   action->setData( QgsBrowserProxyModel::Normal );
86   action->setCheckable( true );
87   action->setChecked( true );
88   menu->addAction( action );
89   action = new QAction( tr( "Wildcard(s)" ), group );
90   action->setData( QgsBrowserProxyModel::Wildcards );
91   action->setCheckable( true );
92   menu->addAction( action );
93   action = new QAction( tr( "Regular Expression" ), group );
94   action->setData( QgsBrowserProxyModel::RegularExpression );
95   action->setCheckable( true );
96   menu->addAction( action );
97 
98   connect( mActionRefresh, &QAction::triggered, this, [ = ] { refreshModel( QModelIndex() ); } );
99   connect( mBrowserTreeView, &QgsBrowserTreeView::clicked, this, &QgsDataSourceSelectWidget::onLayerSelected );
100   connect( mBrowserTreeView, &QgsBrowserTreeView::doubleClicked, this, &QgsDataSourceSelectWidget::itemDoubleClicked );
101   connect( mActionCollapse, &QAction::triggered, mBrowserTreeView, &QgsBrowserTreeView::collapseAll );
102   connect( mActionShowFilter, &QAction::triggered, this, &QgsDataSourceSelectWidget::showFilterWidget );
103   connect( mLeFilter, &QgsFilterLineEdit::returnPressed, this, &QgsDataSourceSelectWidget::setFilter );
104   connect( mLeFilter, &QgsFilterLineEdit::cleared, this, &QgsDataSourceSelectWidget::setFilter );
105   connect( mLeFilter, &QgsFilterLineEdit::textChanged, this, &QgsDataSourceSelectWidget::setFilter );
106   connect( group, &QActionGroup::triggered, this, &QgsDataSourceSelectWidget::setFilterSyntax );
107 
108   mBrowserToolbar->setIconSize( QgsGuiUtils::iconSize( true ) );
109 
110   if ( QgsSettings().value( QStringLiteral( "datasourceSelectFilterVisible" ), false, QgsSettings::Section::Gui ).toBool() )
111   {
112     mActionShowFilter->trigger();
113   }
114 }
115 
116 QgsDataSourceSelectWidget::~QgsDataSourceSelectWidget() = default;
117 
showEvent(QShowEvent * e)118 void QgsDataSourceSelectWidget::showEvent( QShowEvent *e )
119 {
120   QgsPanelWidget::showEvent( e );
121   QString lastSelectedPath( QgsSettings().value( QStringLiteral( "datasourceSelectLastSelectedItem" ),
122                             QString(), QgsSettings::Section::Gui ).toString() );
123   if ( ! lastSelectedPath.isEmpty() )
124   {
125     QModelIndexList items = mBrowserProxyModel.match(
126                               mBrowserProxyModel.index( 0, 0 ),
127                               QgsBrowserGuiModel::PathRole,
128                               QVariant::fromValue( lastSelectedPath ),
129                               1,
130                               Qt::MatchRecursive );
131     if ( items.count( ) > 0 )
132     {
133       QModelIndex expandIndex = items.at( 0 );
134       if ( expandIndex.isValid() )
135       {
136         mBrowserTreeView->scrollTo( expandIndex, QgsBrowserTreeView::ScrollHint::PositionAtTop );
137         mBrowserTreeView->expand( expandIndex );
138       }
139     }
140   }
141 }
142 
showFilterWidget(bool visible)143 void QgsDataSourceSelectWidget::showFilterWidget( bool visible )
144 {
145   QgsSettings().setValue( QStringLiteral( "datasourceSelectFilterVisible" ), visible, QgsSettings::Section::Gui );
146   mWidgetFilter->setVisible( visible );
147   if ( ! visible )
148   {
149     mLeFilter->setText( QString() );
150     setFilter();
151   }
152   else
153   {
154     mLeFilter->setFocus();
155   }
156 }
157 
setDescription(const QString & description)158 void QgsDataSourceSelectWidget::setDescription( const QString &description )
159 {
160   if ( !description.isEmpty() )
161   {
162     if ( !mDescriptionLabel )
163     {
164       mDescriptionLabel = new QLabel();
165       mDescriptionLabel->setWordWrap( true );
166       mDescriptionLabel->setMargin( 4 );
167       mDescriptionLabel->setTextInteractionFlags( Qt::TextBrowserInteraction );
168       connect( mDescriptionLabel, &QLabel::linkActivated, this, [ = ]( const QString & link )
169       {
170         QUrl url( link );
171         QFileInfo file( url.toLocalFile() );
172         if ( file.exists() && !file.isDir() )
173           QgsGui::instance()->nativePlatformInterface()->openFileExplorerAndSelectFile( url.toLocalFile() );
174         else
175           QDesktopServices::openUrl( url );
176       } );
177       verticalLayout->insertWidget( 1, mDescriptionLabel );
178     }
179     mDescriptionLabel->setText( description );
180   }
181   else
182   {
183     if ( mDescriptionLabel )
184     {
185       verticalLayout->removeWidget( mDescriptionLabel );
186       delete mDescriptionLabel;
187       mDescriptionLabel = nullptr;
188     }
189   }
190 }
191 
setFilter()192 void QgsDataSourceSelectWidget::setFilter()
193 {
194   QString filter = mLeFilter->text();
195   mBrowserProxyModel.setFilterString( filter );
196 }
197 
198 
refreshModel(const QModelIndex & index)199 void QgsDataSourceSelectWidget::refreshModel( const QModelIndex &index )
200 {
201 
202   QgsDataItem *item = mBrowserModel->dataItem( index );
203   if ( item )
204   {
205     QgsDebugMsgLevel( "path = " + item->path(), 2 );
206   }
207   else
208   {
209     QgsDebugMsg( QStringLiteral( "invalid item" ) );
210   }
211 
212   if ( item && ( item->capabilities2() & QgsDataItem::Fertile ) )
213   {
214     mBrowserModel->refresh( index );
215   }
216 
217   for ( int i = 0; i < mBrowserModel->rowCount( index ); i++ )
218   {
219     QModelIndex idx = mBrowserModel->index( i, 0, index );
220     QModelIndex proxyIdx = mBrowserProxyModel.mapFromSource( idx );
221     QgsDataItem *child = mBrowserModel->dataItem( idx );
222 
223     // Check also expanded descendants so that the whole expanded path does not get collapsed if one item is collapsed.
224     // Fast items (usually root items) are refreshed so that when collapsed, it is obvious they are if empty (no expand symbol).
225     if ( mBrowserTreeView->isExpanded( proxyIdx ) || mBrowserTreeView->hasExpandedDescendant( proxyIdx ) || ( child && child->capabilities2() & QgsDataItem::Fast ) )
226     {
227       refreshModel( idx );
228     }
229     else
230     {
231       if ( child && ( child->capabilities2() & QgsDataItem::Fertile ) )
232       {
233         child->depopulate();
234       }
235     }
236   }
237 }
238 
setValid(bool valid)239 void QgsDataSourceSelectWidget::setValid( bool valid )
240 {
241   const bool prev = mIsValid;
242   mIsValid = valid;
243   if ( prev != mIsValid )
244     emit validationChanged( mIsValid );
245 
246 }
247 
248 
setFilterSyntax(QAction * action)249 void QgsDataSourceSelectWidget::setFilterSyntax( QAction *action )
250 {
251   if ( !action )
252     return;
253   mBrowserProxyModel.setFilterSyntax( static_cast< QgsBrowserProxyModel::FilterSyntax >( action->data().toInt() ) );
254 }
255 
setCaseSensitive(bool caseSensitive)256 void QgsDataSourceSelectWidget::setCaseSensitive( bool caseSensitive )
257 {
258   mBrowserProxyModel.setFilterCaseSensitivity( caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive );
259 }
260 
setLayerTypeFilter(QgsMapLayerType layerType)261 void QgsDataSourceSelectWidget::setLayerTypeFilter( QgsMapLayerType layerType )
262 {
263   mBrowserProxyModel.setFilterByLayerType( true );
264   mBrowserProxyModel.setLayerType( layerType );
265   // reset model and button
266   mBrowserTreeView->setModel( &mBrowserProxyModel );
267   setValid( false );
268 }
269 
uri() const270 QgsMimeDataUtils::Uri QgsDataSourceSelectWidget::uri() const
271 {
272   return mUri;
273 }
274 
onLayerSelected(const QModelIndex & index)275 void QgsDataSourceSelectWidget::onLayerSelected( const QModelIndex &index )
276 {
277   bool isLayerCompatible = false;
278   mUri = QgsMimeDataUtils::Uri();
279   if ( index.isValid() )
280   {
281     const QgsDataItem *dataItem( mBrowserProxyModel.dataItem( index ) );
282     if ( dataItem )
283     {
284       const QgsLayerItem *layerItem = qobject_cast<const QgsLayerItem *>( dataItem );
285       if ( layerItem && ( ! mBrowserProxyModel.filterByLayerType() ||
286                           ( layerItem->mapLayerType() == mBrowserProxyModel.layerType() ) ) )
287       {
288         isLayerCompatible = true;
289         mUri = layerItem->mimeUri();
290         // Store last viewed item
291         QgsSettings().setValue( QStringLiteral( "datasourceSelectLastSelectedItem" ),  mBrowserProxyModel.data( index, QgsBrowserGuiModel::PathRole ).toString(), QgsSettings::Section::Gui );
292       }
293     }
294   }
295   setValid( isLayerCompatible );
296   emit selectionChanged();
297 }
298 
itemDoubleClicked(const QModelIndex & index)299 void QgsDataSourceSelectWidget::itemDoubleClicked( const QModelIndex &index )
300 {
301   onLayerSelected( index );
302   if ( mIsValid )
303     emit itemTriggered( uri() );
304 }
305 
306 //
307 // QgsDataSourceSelectDialog
308 //
309 
QgsDataSourceSelectDialog(QgsBrowserGuiModel * browserModel,bool setFilterByLayerType,QgsMapLayerType layerType,QWidget * parent)310 QgsDataSourceSelectDialog::QgsDataSourceSelectDialog( QgsBrowserGuiModel *browserModel, bool setFilterByLayerType, QgsMapLayerType layerType, QWidget *parent )
311   : QDialog( parent )
312 {
313   setWindowTitle( tr( "Select a Data Source" ) );
314   setObjectName( QStringLiteral( "QgsDataSourceSelectDialog" ) );
315   QgsGui::enableAutoGeometryRestore( this );
316 
317   mWidget = new QgsDataSourceSelectWidget( browserModel, setFilterByLayerType, layerType );
318 
319   QVBoxLayout *vl = new QVBoxLayout();
320   vl->addWidget( mWidget, 1 );
321   vl->setContentsMargins( 4, 4, 4, 4 );
322   QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel );
323   connect( buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
324   connect( buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
325   buttonBox->button( QDialogButtonBox::Ok )->setEnabled( false );
326   connect( mWidget, &QgsDataSourceSelectWidget::validationChanged, buttonBox->button( QDialogButtonBox::Ok ), &QWidget::setEnabled );
327   connect( mWidget, &QgsDataSourceSelectWidget::itemTriggered, this, &QDialog::accept );
328 
329   // pressing escape should reject the dialog
330   connect( mWidget, &QgsPanelWidget::panelAccepted, this, &QDialog::reject );
331 
332   vl->addWidget( buttonBox );
333   setLayout( vl );
334 }
335 
setLayerTypeFilter(QgsMapLayerType layerType)336 void QgsDataSourceSelectDialog::setLayerTypeFilter( QgsMapLayerType layerType )
337 {
338   mWidget->setLayerTypeFilter( layerType );
339 }
340 
setDescription(const QString & description)341 void QgsDataSourceSelectDialog::setDescription( const QString &description )
342 {
343   mWidget->setDescription( description );
344 }
345 
uri() const346 QgsMimeDataUtils::Uri QgsDataSourceSelectDialog::uri() const
347 {
348   return mWidget->uri();
349 }
350 
showFilterWidget(bool visible)351 void QgsDataSourceSelectDialog::showFilterWidget( bool visible )
352 {
353   mWidget->showFilterWidget( visible );
354 }
355 
setFilterSyntax(QAction * syntax)356 void QgsDataSourceSelectDialog::setFilterSyntax( QAction *syntax )
357 {
358   mWidget->setFilterSyntax( syntax );
359 }
360 
setCaseSensitive(bool caseSensitive)361 void QgsDataSourceSelectDialog::setCaseSensitive( bool caseSensitive )
362 {
363   mWidget->setCaseSensitive( caseSensitive );
364 }
365 
setFilter()366 void QgsDataSourceSelectDialog::setFilter()
367 {
368   mWidget->setFilter();
369 
370 }
371