QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
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
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
44QgsOptionsDialogBase::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
68void 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
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
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
277void 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
313void 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
328void 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
412void 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
516void 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{
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
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
704QStandardItem *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
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
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
858QgsOptionsProxyModel::QgsOptionsProxyModel( QObject *parent )
859 : QSortFilterProxyModel( parent )
860{
861 setDynamicSortFilter( true );
862}
863
864void QgsOptionsProxyModel::setPageHidden( int page, bool hidden )
865{
866 mHiddenPages[ page ] = hidden;
867 invalidateFilter();
868}
869
870QModelIndex 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
903int 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
935bool 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