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