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