QGIS API Documentation  3.37.0-Master (a5b4d9743e8)
qgsoptionsdialogbase.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsoptionsdialogbase.cpp - base vertical tabs option dialog
3 
4  ---------------------
5  begin : March 24, 2013
6  copyright : (C) 2013 by Larry Shaffer
7  email : larrys at dakcarto dot com
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 "qgsoptionsdialogbase.h"
18 
19 #include <QDialog>
20 #include <QDialogButtonBox>
21 #include <QLayout>
22 #include <QListWidget>
23 #include <QListWidgetItem>
24 #include <QMessageBox>
25 #include <QPainter>
26 #include <QScrollBar>
27 #include <QSplitter>
28 #include <QStackedWidget>
29 #include <QTimer>
30 #include <QStandardItem>
31 #include <QTreeView>
32 #include <QHeaderView>
33 #include <functional>
34 
35 #include "qgsfilterlineedit.h"
36 #include "qgslogger.h"
39 #include "qgsguiutils.h"
40 #include "qgsapplication.h"
41 #include "qgsvariantutils.h"
42 #include "qgsscrollarea.h"
43 
44 QgsOptionsDialogBase::QgsOptionsDialogBase( const QString &settingsKey, QWidget *parent, Qt::WindowFlags fl, QgsSettings *settings )
45  : QDialog( parent, fl )
46  , mOptsKey( settingsKey )
47  , mSettings( settings )
48 {
49 }
50 
52 {
53  if ( mInit )
54  {
55  mSettings->setValue( QStringLiteral( "/Windows/%1/geometry" ).arg( mOptsKey ), saveGeometry() );
56  mSettings->setValue( QStringLiteral( "/Windows/%1/splitState" ).arg( mOptsKey ), mOptSplitter->saveState() );
57  mSettings->setValue( QStringLiteral( "/Windows/%1/tab" ).arg( mOptsKey ), mOptStackedWidget->currentIndex() );
58  }
59 
60  if ( mDelSettings ) // local settings obj to delete
61  {
62  delete mSettings;
63  }
64 
65  mSettings = nullptr; // null the pointer (in case of outside settings obj)
66 }
67 
68 void QgsOptionsDialogBase::initOptionsBase( bool restoreUi, const QString &title )
69 {
70  // use pointer to app QgsSettings if no custom QgsSettings specified
71  // custom QgsSettings object may be from Python plugin
72  mDelSettings = false;
73 
74  if ( !mSettings )
75  {
76  mSettings = new QgsSettings();
77  mDelSettings = true; // only delete obj created by class
78  }
79 
80  // save dialog title so it can be used to be concatenated
81  // with category title in icon-only mode
82  if ( title.isEmpty() )
83  mDialogTitle = windowTitle();
84  else
85  mDialogTitle = title;
86 
87  // don't add to dialog margins
88  // redefine now, or those in inherited .ui file will be added
89  if ( auto *lLayout = layout() )
90  {
91  lLayout->setContentsMargins( 0, 0, 0, 0 ); // Qt default spacing
92  }
93 
94  // start with copy of qgsoptionsdialog_template.ui to ensure existence of these objects
95  mOptListWidget = findChild<QListWidget *>( QStringLiteral( "mOptionsListWidget" ) );
96  mOptTreeView = findChild<QTreeView *>( QStringLiteral( "mOptionsTreeView" ) );
97  if ( mOptTreeView )
98  {
99  mOptTreeModel = qobject_cast< QStandardItemModel * >( mOptTreeView->model() );
100  mTreeProxyModel = new QgsOptionsProxyModel( this );
101  mTreeProxyModel->setSourceModel( mOptTreeModel );
102  mOptTreeView->setModel( mTreeProxyModel );
103  mOptTreeView->expandAll();
104  }
105 
106  QFrame *optionsFrame = findChild<QFrame *>( QStringLiteral( "mOptionsFrame" ) );
107  mOptStackedWidget = findChild<QStackedWidget *>( QStringLiteral( "mOptionsStackedWidget" ) );
108  mOptSplitter = findChild<QSplitter *>( QStringLiteral( "mOptionsSplitter" ) );
109  mOptButtonBox = findChild<QDialogButtonBox *>( QStringLiteral( "buttonBox" ) );
110  QFrame *buttonBoxFrame = findChild<QFrame *>( QStringLiteral( "mButtonBoxFrame" ) );
111  mSearchLineEdit = findChild<QgsFilterLineEdit *>( QStringLiteral( "mSearchLineEdit" ) );
112 
113  if ( ( !mOptListWidget && !mOptTreeView ) || !mOptStackedWidget || !mOptSplitter || !optionsFrame )
114  {
115  return;
116  }
117 
118  QAbstractItemView *optView = mOptListWidget ? static_cast< QAbstractItemView * >( mOptListWidget ) : static_cast< QAbstractItemView * >( mOptTreeView );
119  int iconSize = 16;
120  if ( mOptListWidget )
121  {
122  int size = QgsGuiUtils::scaleIconSize( mSettings->value( QStringLiteral( "/IconSize" ), 24 ).toInt() );
123  // buffer size to match displayed icon size in toolbars, and expected geometry restore
124  // newWidth (above) may need adjusted if you adjust iconBuffer here
125  const int iconBuffer = QgsGuiUtils::scaleIconSize( 4 );
126  iconSize = size + iconBuffer;
127  }
128  else if ( mOptTreeView )
129  {
130  iconSize = QgsGuiUtils::scaleIconSize( mSettings->value( QStringLiteral( "/IconSize" ), 16 ).toInt() );
131  mOptTreeView->header()->setVisible( false );
132  }
133  optView->setIconSize( QSize( iconSize, iconSize ) );
134  optView->setFrameStyle( QFrame::NoFrame );
135 
136  const int frameMargin = QgsGuiUtils::scaleIconSize( 3 );
137  optionsFrame->layout()->setContentsMargins( 0, frameMargin, frameMargin, frameMargin );
138  QVBoxLayout *layout = static_cast<QVBoxLayout *>( optionsFrame->layout() );
139 
140  if ( buttonBoxFrame )
141  {
142  buttonBoxFrame->layout()->setContentsMargins( 0, 0, 0, 0 );
143  layout->insertWidget( layout->count(), buttonBoxFrame );
144  }
145  else if ( mOptButtonBox )
146  {
147  layout->insertWidget( layout->count(), mOptButtonBox );
148  }
149 
150  if ( mOptButtonBox )
151  {
152  // enforce only one connection per signal, in case added in Qt Designer
153  disconnect( mOptButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
154  connect( mOptButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
155  disconnect( mOptButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
156  connect( mOptButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
157  }
158  connect( mOptSplitter, &QSplitter::splitterMoved, this, &QgsOptionsDialogBase::updateOptionsListVerticalTabs );
159  connect( mOptStackedWidget, &QStackedWidget::currentChanged, this, &QgsOptionsDialogBase::optionsStackedWidget_CurrentChanged );
160  connect( mOptStackedWidget, &QStackedWidget::widgetRemoved, this, &QgsOptionsDialogBase::optionsStackedWidget_WidgetRemoved );
161 
162  if ( mOptTreeView )
163  {
164  // sync selection in tree view with current stacked widget index
165  connect( mOptTreeView->selectionModel(), &QItemSelectionModel::selectionChanged, mOptStackedWidget, [ = ]( const QItemSelection &, const QItemSelection & )
166  {
167  const QModelIndexList selected = mOptTreeView->selectionModel()->selectedIndexes();
168  if ( selected.isEmpty() )
169  return;
170 
171  const QModelIndex index = mTreeProxyModel->mapToSource( selected.at( 0 ) );
172 
173  if ( !mOptTreeModel || !mOptTreeModel->itemFromIndex( index )->isSelectable() )
174  return;
175 
176  mOptStackedWidget->setCurrentIndex( mTreeProxyModel->sourceIndexToPageNumber( index ) );
177  } );
178  }
179 
180  if ( mSearchLineEdit )
181  {
183  connect( mSearchLineEdit, &QgsFilterLineEdit::textChanged, this, &QgsOptionsDialogBase::searchText );
184  if ( mOptTreeView )
185  {
186  connect( mSearchLineEdit, &QgsFilterLineEdit::cleared, mOptTreeView, &QTreeView::expandAll );
187  }
188  }
189 
190  mInit = true;
191 
192  if ( restoreUi )
194 }
195 
197 {
198  if ( mDelSettings ) // local settings obj to delete
199  {
200  delete mSettings;
201  }
202 
203  mSettings = settings;
204  mDelSettings = false; // don't delete outside obj
205 }
206 
207 void QgsOptionsDialogBase::restoreOptionsBaseUi( const QString &title )
208 {
209  if ( !mInit )
210  {
211  return;
212  }
213 
214  if ( !title.isEmpty() )
215  {
216  mDialogTitle = title;
217  }
218  else
219  {
220  // re-save original dialog title in case it was changed after dialog initialization
221  mDialogTitle = windowTitle();
222  }
224 
225  restoreGeometry( mSettings->value( QStringLiteral( "/Windows/%1/geometry" ).arg( mOptsKey ) ).toByteArray() );
226  // mOptListWidget width is fixed to take up less space in QtDesigner
227  // revert it now unless the splitter's state hasn't been saved yet
228  QAbstractItemView *optView = mOptListWidget ? static_cast< QAbstractItemView * >( mOptListWidget ) : static_cast< QAbstractItemView * >( mOptTreeView );
229  if ( optView )
230  {
231  optView->setMaximumWidth(
232  QgsVariantUtils::isNull( mSettings->value( QStringLiteral( "/Windows/%1/splitState" ).arg( mOptsKey ) ) ) ? 150 : 16777215 );
233  // get rid of annoying outer focus rect on Mac
234  optView->setAttribute( Qt::WA_MacShowFocusRect, false );
235  }
236 
237  mOptSplitter->restoreState( mSettings->value( QStringLiteral( "/Windows/%1/splitState" ).arg( mOptsKey ) ).toByteArray() );
238 
239  restoreLastPage();
240 
241  // brute force approach to try to standardize page margins!
242  for ( int i = 0; i < mOptStackedWidget->count(); ++i )
243  {
244  if ( QLayout *l = mOptStackedWidget->widget( i )->layout() )
245  {
246  l->setContentsMargins( 0, 0, 0, 0 );
247  }
248  }
249 }
250 
252 {
253  int curIndx = mSettings->value( QStringLiteral( "/Windows/%1/tab" ).arg( mOptsKey ), 0 ).toInt();
254 
255  // if the last used tab is out of range or not enabled display the first enabled one
256  if ( mOptStackedWidget->count() < curIndx + 1
257  || !mOptStackedWidget->widget( curIndx )->isEnabled() )
258  {
259  curIndx = 0;
260  for ( int i = 0; i < mOptStackedWidget->count(); i++ )
261  {
262  if ( mOptStackedWidget->widget( i )->isEnabled() )
263  {
264  curIndx = i;
265  break;
266  }
267  }
268  }
269 
270  if ( mOptStackedWidget->count() == 0 )
271  return;
272 
273  mOptStackedWidget->setCurrentIndex( curIndx );
274  setListToItemAtIndex( curIndx );
275 }
276 
277 void QgsOptionsDialogBase::setListToItemAtIndex( int index )
278 {
279  if ( mOptListWidget && mOptListWidget->count() > index )
280  {
281  mOptListWidget->setCurrentRow( index );
282  }
283  else if ( mOptTreeView && mOptTreeModel )
284  {
285  mOptTreeView->setCurrentIndex( mTreeProxyModel->mapFromSource( mTreeProxyModel->pageNumberToSourceIndex( index ) ) );
286  }
287 }
288 
290 {
291  // Adjust size (GH issue #31449 and #32615)
292  // make the stacked widget size to the current page only
293  for ( int i = 0; i < mOptStackedWidget->count(); ++i )
294  {
295  // Set the size policy
296  QSizePolicy::Policy policy = QSizePolicy::Ignored;
297  if ( i == index )
298  {
299  policy = QSizePolicy::MinimumExpanding;
300  }
301 
302  // update the size policy
303  mOptStackedWidget->widget( i )->setSizePolicy( policy, policy );
304 
305  if ( i == index )
306  {
307  mOptStackedWidget->layout()->update();
308  }
309  }
310  mOptStackedWidget->adjustSize();
311 }
312 
313 void QgsOptionsDialogBase::setCurrentPage( const QString &page )
314 {
315  //find the page with a matching widget name
316  for ( int idx = 0; idx < mOptStackedWidget->count(); ++idx )
317  {
318  QWidget *currentPage = mOptStackedWidget->widget( idx );
319  if ( currentPage->objectName() == page )
320  {
321  //found the page, set it as current
322  mOptStackedWidget->setCurrentIndex( idx );
323  return;
324  }
325  }
326 }
327 
328 void QgsOptionsDialogBase::addPage( const QString &title, const QString &tooltip, const QIcon &icon, QWidget *widget, const QStringList &path, const QString &key )
329 {
330  int newPage = -1;
331 
332  if ( mOptListWidget )
333  {
334  QListWidgetItem *item = new QListWidgetItem();
335  item->setIcon( icon );
336  item->setText( title );
337  item->setToolTip( tooltip );
338  mOptListWidget->addItem( item );
339  }
340  else if ( mOptTreeModel )
341  {
342  QStandardItem *item = new QStandardItem( icon, title );
343  item->setToolTip( tooltip );
344  if ( !key.isEmpty() )
345  {
346  item->setData( key );
347  }
348 
349  QModelIndex parent;
350  QStandardItem *parentItem = nullptr;
351  if ( !path.empty() )
352  {
353  QStringList parents = path;
354  while ( !parents.empty() )
355  {
356  const QString parentPath = parents.takeFirst();
357 
358  QModelIndex thisParent;
359  for ( int row = 0; row < mOptTreeModel->rowCount( parent ); ++row )
360  {
361  const QModelIndex index = mOptTreeModel->index( row, 0, parent );
362  if ( index.data().toString().compare( parentPath, Qt::CaseInsensitive ) == 0
363  || index.data( Qt::UserRole + 1 ).toString().compare( parentPath, Qt::CaseInsensitive ) == 0 )
364  {
365  thisParent = index;
366  break;
367  }
368  }
369 
370  // add new child if required
371  if ( !thisParent.isValid() )
372  {
373  QStandardItem *newParentItem = new QStandardItem( parentPath );
374  newParentItem->setToolTip( parentPath );
375  newParentItem->setSelectable( false );
376  if ( parentItem )
377  parentItem->appendRow( newParentItem );
378  else
379  mOptTreeModel->appendRow( newParentItem );
380  parentItem = newParentItem;
381  }
382  else
383  {
384  parentItem = mOptTreeModel->itemFromIndex( thisParent );
385  }
386  parent = mOptTreeModel->indexFromItem( parentItem );
387  }
388  }
389 
390  if ( parentItem )
391  {
392  parentItem->appendRow( item );
393  const QModelIndex newIndex = mOptTreeModel->indexFromItem( item );
394  newPage = mTreeProxyModel->sourceIndexToPageNumber( newIndex );
395  }
396  else
397  mOptTreeModel->appendRow( item );
398  }
399 
400  QgsScrollArea *scrollArea = new QgsScrollArea();
401  scrollArea->setWidgetResizable( true );
402  scrollArea->setFrameShape( QFrame::NoFrame );
403  scrollArea->setObjectName( widget->objectName() );
404  scrollArea->setWidget( widget );
405 
406  if ( newPage < 0 )
407  mOptStackedWidget->addWidget( scrollArea );
408  else
409  mOptStackedWidget->insertWidget( newPage, scrollArea );
410 }
411 
412 void QgsOptionsDialogBase::insertPage( const QString &title, const QString &tooltip, const QIcon &icon, QWidget *widget, const QString &before, const QStringList &path, const QString &key )
413 {
414  //find the page with a matching widget name
415  for ( int page = 0; page < mOptStackedWidget->count(); ++page )
416  {
417  QWidget *currentPage = mOptStackedWidget->widget( page );
418  if ( currentPage->objectName() == before )
419  {
420  //found the "before" page
421 
422  if ( mOptListWidget )
423  {
424  QListWidgetItem *item = new QListWidgetItem();
425  item->setIcon( icon );
426  item->setText( title );
427  item->setToolTip( tooltip );
428  mOptListWidget->insertItem( page, item );
429  }
430  else if ( mOptTreeModel )
431  {
432  QModelIndex sourceIndexBefore = mTreeProxyModel->pageNumberToSourceIndex( page );
433  QList< QModelIndex > sourceBeforeIndices;
434  while ( sourceIndexBefore.parent().isValid() )
435  {
436  sourceBeforeIndices.insert( 0, sourceIndexBefore );
437  sourceIndexBefore = sourceIndexBefore.parent();
438  }
439  sourceBeforeIndices.insert( 0, sourceIndexBefore );
440 
441  QStringList parentPaths = path;
442 
443  QModelIndex parentIndex;
444  QStandardItem *parentItem = nullptr;
445  while ( !parentPaths.empty() )
446  {
447  QString thisPath = parentPaths.takeFirst();
448  QModelIndex sourceIndex = !sourceBeforeIndices.isEmpty() ? sourceBeforeIndices.takeFirst() : QModelIndex();
449 
450  if ( sourceIndex.data().toString().compare( thisPath, Qt::CaseInsensitive ) == 0
451  || sourceIndex.data( Qt::UserRole + 1 ).toString().compare( thisPath, Qt::CaseInsensitive ) == 0 )
452  {
453  parentIndex = sourceIndex;
454  parentItem = mOptTreeModel->itemFromIndex( parentIndex );
455  }
456  else
457  {
458  QStandardItem *newParentItem = new QStandardItem( thisPath );
459  newParentItem->setToolTip( thisPath );
460  newParentItem->setSelectable( false );
461  if ( sourceIndex.isValid() )
462  {
463  // insert in model before sourceIndex
464  if ( parentItem )
465  parentItem->insertRow( sourceIndex.row(), newParentItem );
466  else
467  mOptTreeModel->insertRow( sourceIndex.row(), newParentItem );
468  }
469  else
470  {
471  // append to end
472  if ( parentItem )
473  parentItem->appendRow( newParentItem );
474  else
475  mOptTreeModel->appendRow( newParentItem );
476  }
477  parentItem = newParentItem;
478  }
479  }
480 
481  QStandardItem *item = new QStandardItem( icon, title );
482  item->setToolTip( tooltip );
483  if ( !key.isEmpty() )
484  {
485  item->setData( key );
486  }
487  if ( parentItem )
488  {
489  if ( sourceBeforeIndices.empty() )
490  parentItem->appendRow( item );
491  else
492  {
493  parentItem->insertRow( sourceBeforeIndices.at( 0 ).row(), item );
494  }
495  }
496  else
497  {
498  mOptTreeModel->insertRow( sourceIndexBefore.row(), item );
499  }
500  }
501 
502  QgsScrollArea *scrollArea = new QgsScrollArea();
503  scrollArea->setWidgetResizable( true );
504  scrollArea->setFrameShape( QFrame::NoFrame );
505  scrollArea->setWidget( widget );
506  scrollArea->setObjectName( widget->objectName() );
507  mOptStackedWidget->insertWidget( page, scrollArea );
508  return;
509  }
510  }
511 
512  // no matching pages, so just add the page
513  addPage( title, tooltip, icon, widget, path );
514 }
515 
516 void QgsOptionsDialogBase::searchText( const QString &text )
517 {
518  const int minimumTextLength = 3;
519 
520  mSearchLineEdit->setMinimumWidth( text.isEmpty() ? 0 : 70 );
521 
522  if ( !mOptStackedWidget )
523  return;
524 
525  if ( mOptStackedWidget->isHidden() )
526  mOptStackedWidget->show();
527  if ( mOptButtonBox && mOptButtonBox->isHidden() )
528  mOptButtonBox->show();
529 
530  // hide all pages if text has to be search, show them all otherwise
531  if ( mOptListWidget )
532  {
533  for ( int r = 0; r < mOptStackedWidget->count(); ++r )
534  {
535  if ( mOptListWidget->item( r )->text().contains( text, Qt::CaseInsensitive ) )
536  {
537  mOptListWidget->setRowHidden( r, false );
538  }
539  else
540  {
541  mOptListWidget->setRowHidden( r, text.length() >= minimumTextLength );
542  }
543  }
544 
545  for ( const QPair< QgsOptionsDialogHighlightWidget *, int > &rsw : std::as_const( mRegisteredSearchWidgets ) )
546  {
547  if ( rsw.first->searchHighlight( text.length() >= minimumTextLength ? text : QString() ) )
548  {
549  mOptListWidget->setRowHidden( rsw.second, false );
550  }
551  }
552  }
553  else if ( mTreeProxyModel )
554  {
555  QMap< int, bool > hiddenPages;
556  for ( int r = 0; r < mOptStackedWidget->count(); ++r )
557  {
558  hiddenPages.insert( r, text.length() >= minimumTextLength );
559  }
560 
561  std::function<void( const QModelIndex & )> traverseModel;
562  // traverse through the model, showing pages which match by page name
563  traverseModel = [&]( const QModelIndex & parent )
564  {
565  for ( int row = 0; row < mOptTreeModel->rowCount( parent ); ++row )
566  {
567  const QModelIndex currentIndex = mOptTreeModel->index( row, 0, parent );
568  if ( currentIndex.data().toString().contains( text, Qt::CaseInsensitive ) )
569  {
570  hiddenPages.insert( mTreeProxyModel->sourceIndexToPageNumber( currentIndex ), false );
571  }
572  traverseModel( currentIndex );
573  }
574  };
575  traverseModel( QModelIndex() );
576 
577  for ( const QPair< QgsOptionsDialogHighlightWidget *, int > &rsw : std::as_const( mRegisteredSearchWidgets ) )
578  {
579  if ( rsw.first->searchHighlight( text.length() >= minimumTextLength ? text : QString() ) )
580  {
581  hiddenPages.insert( rsw.second, false );
582  }
583  }
584  for ( auto it = hiddenPages.constBegin(); it != hiddenPages.constEnd(); ++it )
585  {
586  mTreeProxyModel->setPageHidden( it.key(), it.value() );
587  }
588  }
589  if ( mOptTreeView && text.length() >= minimumTextLength )
590  {
591  // auto expand out any group with children matching the search term
592  mOptTreeView->expandAll();
593  }
594 
595  // if current item is hidden, move to first available...
596  if ( mOptListWidget && mOptListWidget->isRowHidden( mOptStackedWidget->currentIndex() ) )
597  {
598  for ( int r = 0; r < mOptListWidget->count(); ++r )
599  {
600  if ( !mOptListWidget->isRowHidden( r ) )
601  {
602  mOptListWidget->setCurrentRow( r );
603  return;
604  }
605  }
606 
607  // if no page can be shown, hide stack widget
608  mOptStackedWidget->hide();
609  if ( mOptButtonBox )
610  mOptButtonBox->hide();
611  }
612  else if ( mOptTreeView )
613  {
614  const QModelIndex currentSourceIndex = mTreeProxyModel->pageNumberToSourceIndex( mOptStackedWidget->currentIndex() );
615  if ( !mTreeProxyModel->filterAcceptsRow( currentSourceIndex.row(), currentSourceIndex.parent() ) )
616  {
617  std::function<QModelIndex( const QModelIndex & )> traverseModel;
618  traverseModel = [&]( const QModelIndex & parent ) -> QModelIndex
619  {
620  for ( int row = 0; row < mTreeProxyModel->rowCount(); ++row )
621  {
622  const QModelIndex proxyIndex = mTreeProxyModel->index( row, 0, parent );
623  const QModelIndex sourceIndex = mTreeProxyModel->mapToSource( proxyIndex );
624  if ( mOptTreeModel->itemFromIndex( sourceIndex )->isSelectable() )
625  {
626  return sourceIndex;
627  }
628  else
629  {
630  QModelIndex res = traverseModel( proxyIndex );
631  if ( res.isValid() )
632  return res;
633  }
634  }
635  return QModelIndex();
636  };
637 
638  const QModelIndex firstVisibleSourceIndex = traverseModel( QModelIndex() );
639 
640  if ( firstVisibleSourceIndex.isValid() )
641  {
642  mOptTreeView->setCurrentIndex( mTreeProxyModel->mapFromSource( firstVisibleSourceIndex ) );
643  }
644  else
645  {
646  // if no page can be shown, hide stack widget
647  mOptStackedWidget->hide();
648  if ( mOptButtonBox )
649  mOptButtonBox->hide();
650  }
651  }
652  else
653  {
654  // make sure item stays current
655  mOptTreeView->setCurrentIndex( mTreeProxyModel->mapFromSource( currentSourceIndex ) );
656  }
657  }
658 }
659 
661 {
662  mRegisteredSearchWidgets.clear();
663 
664  for ( int i = 0; i < mOptStackedWidget->count(); i++ )
665  {
666  const QList< QWidget * > widgets = mOptStackedWidget->widget( i )->findChildren<QWidget *>();
667  for ( QWidget *widget : widgets )
668  {
669  // see if the widget also inherits QgsOptionsDialogHighlightWidget
670  QgsOptionsDialogHighlightWidget *shw = dynamic_cast<QgsOptionsDialogHighlightWidget *>( widget );
671  if ( !shw )
672  {
673  // get custom highlight widget in user added pages
674  QHash<QWidget *, QgsOptionsDialogHighlightWidget *> customHighlightWidgets;
675  QgsOptionsPageWidget *opw = qobject_cast<QgsOptionsPageWidget *>( mOptStackedWidget->widget( i ) );
676  if ( opw )
677  {
678  customHighlightWidgets = opw->registeredHighlightWidgets();
679  }
680  // take custom if exists
681  if ( customHighlightWidgets.contains( widget ) )
682  {
683  shw = customHighlightWidgets.value( widget );
684  }
685  }
686  // try to construct one otherwise
687  if ( !shw || !shw->isValid() )
688  {
690  }
691  if ( shw && shw->isValid() )
692  {
693  QgsDebugMsgLevel( QStringLiteral( "Registering: %1" ).arg( widget->objectName() ), 4 );
694  mRegisteredSearchWidgets.append( qMakePair( shw, i ) );
695  }
696  else
697  {
698  delete shw;
699  }
700  }
701  }
702 }
703 
704 QStandardItem *QgsOptionsDialogBase::createItem( const QString &name, const QString &tooltip, const QString &icon )
705 {
706  QStandardItem *res = new QStandardItem( QgsApplication::getThemeIcon( icon ), name );
707  res->setToolTip( tooltip );
708  return res;
709 }
710 
711 void QgsOptionsDialogBase::showEvent( QShowEvent *e )
712 {
713  if ( mInit )
714  {
716  if ( mOptListWidget )
717  {
719  }
720  else if ( mOptTreeView )
721  {
722  optionsStackedWidget_CurrentChanged( mTreeProxyModel->sourceIndexToPageNumber( mTreeProxyModel->mapToSource( mOptTreeView->currentIndex() ) ) );
723  }
724  }
725  else
726  {
727  QTimer::singleShot( 0, this, &QgsOptionsDialogBase::warnAboutMissingObjects );
728  }
729 
730  if ( mSearchLineEdit )
731  {
733  }
734 
735  QDialog::showEvent( e );
736 }
737 
738 void QgsOptionsDialogBase::paintEvent( QPaintEvent *e )
739 {
740  if ( mInit )
741  QTimer::singleShot( 0, this, &QgsOptionsDialogBase::updateOptionsListVerticalTabs );
742 
743  QDialog::paintEvent( e );
744 }
745 
747 {
748  const QString itemText = mOptListWidget && mOptListWidget->currentItem() ? mOptListWidget->currentItem()->text()
749  : mOptTreeView && mOptTreeView->currentIndex().isValid() ? mOptTreeView->currentIndex().data( Qt::DisplayRole ).toString() : QString();
750  if ( !itemText.isEmpty() )
751  {
752  setWindowTitle( QStringLiteral( "%1 %2 %3" )
753  .arg( mDialogTitle )
754  .arg( QChar( 0x2014 ) ) // em-dash unicode
755  .arg( itemText ) );
756  }
757  else
758  {
759  setWindowTitle( mDialogTitle );
760  }
761 }
762 
764 {
765  if ( !mInit )
766  return;
767 
768  QAbstractItemView *optView = mOptListWidget ? static_cast< QAbstractItemView * >( mOptListWidget ) : static_cast< QAbstractItemView * >( mOptTreeView );
769  if ( optView )
770  {
771  if ( optView->maximumWidth() != 16777215 )
772  optView->setMaximumWidth( 16777215 );
773  // auto-resize splitter for vert scrollbar without covering icons in icon-only mode
774  // TODO: mOptListWidget has fixed 32px wide icons for now, allow user-defined
775  // Note: called on splitter resize and dialog paint event, so only update when necessary
776  int iconWidth = optView->iconSize().width();
777  int snapToIconWidth = iconWidth + 32;
778 
779  QList<int> splitSizes = mOptSplitter->sizes();
780  mIconOnly = ( splitSizes.at( 0 ) <= snapToIconWidth );
781 
782  // iconBuffer (above) may need adjusted if you adjust iconWidth here
783  int newWidth = optView->verticalScrollBar()->isVisible() ? iconWidth + 22 : iconWidth + 9;
784  bool diffWidth = optView->minimumWidth() != newWidth;
785 
786  if ( diffWidth )
787  optView->setMinimumWidth( newWidth );
788 
789  if ( mIconOnly && ( diffWidth || optView->width() != newWidth ) )
790  {
791  splitSizes[1] = splitSizes.at( 1 ) - ( splitSizes.at( 0 ) - newWidth );
792  splitSizes[0] = newWidth;
793  mOptSplitter->setSizes( splitSizes );
794  }
795 
796  if ( mOptListWidget )
797  {
798  if ( mOptListWidget->wordWrap() && mIconOnly )
799  mOptListWidget->setWordWrap( false );
800  if ( !mOptListWidget->wordWrap() && !mIconOnly )
801  mOptListWidget->setWordWrap( true );
802  }
803  }
804 }
805 
807 {
808  if ( mOptListWidget )
809  {
810  mOptListWidget->blockSignals( true );
811  mOptListWidget->setCurrentRow( index );
812  mOptListWidget->blockSignals( false );
813  }
814  else if ( mOptTreeView )
815  {
816  mOptTreeView->blockSignals( true );
817  mOptTreeView->setCurrentIndex( mTreeProxyModel->mapFromSource( mTreeProxyModel->pageNumberToSourceIndex( index ) ) );
818  mOptTreeView->blockSignals( false );
819  }
820 
822 }
823 
825 {
826  // will need to take item first, if widgets are set for item in future
827  if ( mOptListWidget )
828  {
829  delete mOptListWidget->item( index );
830  }
831  else if ( mOptTreeModel )
832  {
833  mOptTreeModel->removeRow( index );
834  }
835 
836  QList<QPair< QgsOptionsDialogHighlightWidget *, int > >::iterator it = mRegisteredSearchWidgets.begin();
837  while ( it != mRegisteredSearchWidgets.end() )
838  {
839  if ( ( *it ).second == index )
840  it = mRegisteredSearchWidgets.erase( it );
841  else
842  ++it;
843  }
844 }
845 
847 {
848  QMessageBox::warning( nullptr, tr( "Missing Objects" ),
849  tr( "Base options dialog could not be initialized.\n\n"
850  "Missing some of the .ui template objects:\n" )
851  + " mOptionsListWidget,\n mOptionsStackedWidget,\n mOptionsSplitter,\n mOptionsListFrame",
852  QMessageBox::Ok,
853  QMessageBox::Ok );
854 }
855 
856 
858 QgsOptionsProxyModel::QgsOptionsProxyModel( QObject *parent )
859  : QSortFilterProxyModel( parent )
860 {
861  setDynamicSortFilter( true );
862 }
863 
864 void QgsOptionsProxyModel::setPageHidden( int page, bool hidden )
865 {
866  mHiddenPages[ page ] = hidden;
867  invalidateFilter();
868 }
869 
870 QModelIndex QgsOptionsProxyModel::pageNumberToSourceIndex( int page ) const
871 {
872  QStandardItemModel *itemModel = qobject_cast< QStandardItemModel * >( sourceModel() );
873  if ( !itemModel )
874  return QModelIndex();
875 
876  int pagesRemaining = page;
877  std::function<QModelIndex( const QModelIndex & )> traversePages;
878 
879  // traverse through the model, counting all selectable items until we hit the desired page number
880  traversePages = [&]( const QModelIndex & parent ) -> QModelIndex
881  {
882  for ( int row = 0; row < itemModel->rowCount( parent ); ++row )
883  {
884  const QModelIndex currentIndex = itemModel->index( row, 0, parent );
885  if ( itemModel->itemFromIndex( currentIndex )->isSelectable() )
886  {
887  if ( pagesRemaining == 0 )
888  return currentIndex;
889 
890  else pagesRemaining--;
891  }
892 
893  const QModelIndex res = traversePages( currentIndex );
894  if ( res.isValid() )
895  return res;
896  }
897  return QModelIndex();
898  };
899 
900  return traversePages( QModelIndex() );
901 }
902 
903 int QgsOptionsProxyModel::sourceIndexToPageNumber( const QModelIndex &index ) const
904 {
905  QStandardItemModel *itemModel = qobject_cast< QStandardItemModel * >( sourceModel() );
906  if ( !itemModel )
907  return 0;
908 
909  int page = 0;
910 
911  std::function<int( const QModelIndex & )> traverseModel;
912 
913  // traverse through the model, counting all which correspond to pages till we hit the desired index
914  traverseModel = [&]( const QModelIndex & parent ) -> int
915  {
916  for ( int row = 0; row < itemModel->rowCount( parent ); ++row )
917  {
918  const QModelIndex currentIndex = itemModel->index( row, 0, parent );
919  if ( currentIndex == index )
920  return page;
921 
922  if ( itemModel->itemFromIndex( currentIndex )->isSelectable() )
923  page++;
924 
925  const int res = traverseModel( currentIndex );
926  if ( res >= 0 )
927  return res;
928  }
929  return -1;
930  };
931 
932  return traverseModel( QModelIndex() );
933 }
934 
935 bool QgsOptionsProxyModel::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const
936 {
937  QStandardItemModel *itemModel = qobject_cast< QStandardItemModel * >( sourceModel() );
938  if ( !itemModel )
939  return true;
940 
941  const QModelIndex sourceIndex = sourceModel()->index( source_row, 0, source_parent );
942 
943  const int pageNumber = sourceIndexToPageNumber( sourceIndex );
944  if ( !mHiddenPages.value( pageNumber, false ) )
945  return true;
946 
947  if ( sourceModel()->hasChildren( sourceIndex ) )
948  {
949  // this is a group -- show if any children are visible
950  for ( int row = 0; row < sourceModel()->rowCount( sourceIndex ); ++row )
951  {
952  if ( filterAcceptsRow( row, sourceIndex ) )
953  return true;
954  }
955  }
956  return false;
957 }
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
void setShowSearchIcon(bool visible)
Define if a search icon shall be shown on the left of the image when no text is entered.
void cleared()
Emitted when the widget is cleared.
QPointer< QgsSettings > mSettings
void resizeAlltabs(int index)
Resizes all tabs when the dialog is resized.
void paintEvent(QPaintEvent *e) override
void restoreLastPage()
Refocus the active tab from the last time the dialog was shown.
void searchText(const QString &text)
searchText searches for a text in all the pages of the stacked widget and highlight the results
void registerTextSearchWidgets()
register widgets in the dialog to search for text in it it is automatically called if a line edit has...
virtual void optionsStackedWidget_CurrentChanged(int index)
Select relevant tab on current page change.
QList< QPair< QgsOptionsDialogHighlightWidget *, int > > mRegisteredSearchWidgets
QgsOptionsDialogBase(const QString &settingsKey, QWidget *parent=nullptr, Qt::WindowFlags fl=Qt::WindowFlags(), QgsSettings *settings=nullptr)
Constructor.
QgsFilterLineEdit * mSearchLineEdit
void setSettings(QgsSettings *settings)
virtual void optionsStackedWidget_WidgetRemoved(int index)
Remove tab and unregister widgets on page remove.
QDialogButtonBox * mOptButtonBox
QgsOptionsProxyModel * mTreeProxyModel
void addPage(const QString &title, const QString &tooltip, const QIcon &icon, QWidget *widget, const QStringList &path=QStringList(), const QString &key=QString())
Adds a new page to the dialog pages.
QStandardItemModel * mOptTreeModel
QStandardItem * createItem(const QString &name, const QString &tooltip, const QString &icon)
Creates a new QStandardItem with the specified name, tooltip and icon.
virtual void updateOptionsListVerticalTabs()
Update tabs on the splitter move.
void restoreOptionsBaseUi(const QString &title=QString())
Restore the base ui.
QStackedWidget * mOptStackedWidget
void initOptionsBase(bool restoreUi=true, const QString &title=QString())
Set up the base ui connections for vertical tabs.
void showEvent(QShowEvent *e) override
void insertPage(const QString &title, const QString &tooltip, const QIcon &icon, QWidget *widget, const QString &before, const QStringList &path=QStringList(), const QString &key=QString())
Inserts a new page into the dialog pages.
void setCurrentPage(const QString &page)
Sets the dialog page (by object name) to show.
Container for a widget to be used to search text in the option dialog If the widget type is handled,...
static QgsOptionsDialogHighlightWidget * createWidget(QWidget *widget)
create a highlight widget implementation for the proper widget type.
bool isValid()
Returns if it valid: if the widget type is handled and if the widget is not still available.
Base class for widgets for pages included in the options dialog.
QHash< QWidget *, QgsOptionsDialogHighlightWidget * > registeredHighlightWidgets()
Returns the registered highlight widgets used to search and highlight text in options dialogs.
A QScrollArea subclass with improved scrolling behavior.
Definition: qgsscrollarea.h:41
This class is a composition of two QSettings instances:
Definition: qgssettings.h:64
static bool isNull(const QVariant &variant, bool silenceNullWarnings=false)
Returns true if the specified variant should be considered a NULL value.
bool restoreGeometry(QWidget *widget, const QString &keyName)
Restore the wigget geometry from settings.
QSize iconSize(bool dockableToolbar)
Returns the user-preferred size of a window's toolbar icons.
int scaleIconSize(int standardSize)
Scales an icon size to compensate for display pixel density, making the icon size hi-dpi friendly,...
void saveGeometry(QWidget *widget, const QString &keyName)
Save the wigget geometry into settings.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39