QGIS API Documentation  3.20.0-Odense (decaadbb31)
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 
31 #include "qgsfilterlineedit.h"
32 #include "qgsmessagebaritem.h"
33 #include "qgslogger.h"
36 #include "qgsguiutils.h"
37 
38 QgsOptionsDialogBase::QgsOptionsDialogBase( const QString &settingsKey, QWidget *parent, Qt::WindowFlags fl, QgsSettings *settings )
39  : QDialog( parent, fl )
40  , mOptsKey( settingsKey )
41  , mInit( false )
42  , mIconOnly( false )
43  , mSettings( settings )
44  , mDelSettings( false )
45 {
46 }
47 
49 {
50  if ( mInit )
51  {
52  mSettings->setValue( QStringLiteral( "/Windows/%1/geometry" ).arg( mOptsKey ), saveGeometry() );
53  mSettings->setValue( QStringLiteral( "/Windows/%1/splitState" ).arg( mOptsKey ), mOptSplitter->saveState() );
54  mSettings->setValue( QStringLiteral( "/Windows/%1/tab" ).arg( mOptsKey ), mOptStackedWidget->currentIndex() );
55  }
56 
57  if ( mDelSettings ) // local settings obj to delete
58  {
59  delete mSettings;
60  }
61 
62  mSettings = nullptr; // null the pointer (in case of outside settings obj)
63 }
64 
65 void QgsOptionsDialogBase::initOptionsBase( bool restoreUi, const QString &title )
66 {
67  // use pointer to app QgsSettings if no custom QgsSettings specified
68  // custom QgsSettings object may be from Python plugin
69  mDelSettings = false;
70 
71  if ( !mSettings )
72  {
73  mSettings = new QgsSettings();
74  mDelSettings = true; // only delete obj created by class
75  }
76 
77  // save dialog title so it can be used to be concatenated
78  // with category title in icon-only mode
79  if ( title.isEmpty() )
80  mDialogTitle = windowTitle();
81  else
82  mDialogTitle = title;
83 
84  // don't add to dialog margins
85  // redefine now, or those in inherited .ui file will be added
86  if ( auto *lLayout = layout() )
87  {
88  lLayout->setContentsMargins( 0, 0, 0, 0 ); // Qt default spacing
89  }
90 
91  // start with copy of qgsoptionsdialog_template.ui to ensure existence of these objects
92  mOptListWidget = findChild<QListWidget *>( QStringLiteral( "mOptionsListWidget" ) );
93  QFrame *optionsFrame = findChild<QFrame *>( QStringLiteral( "mOptionsFrame" ) );
94  mOptStackedWidget = findChild<QStackedWidget *>( QStringLiteral( "mOptionsStackedWidget" ) );
95  mOptSplitter = findChild<QSplitter *>( QStringLiteral( "mOptionsSplitter" ) );
96  mOptButtonBox = findChild<QDialogButtonBox *>( QStringLiteral( "buttonBox" ) );
97  QFrame *buttonBoxFrame = findChild<QFrame *>( QStringLiteral( "mButtonBoxFrame" ) );
98  mSearchLineEdit = findChild<QgsFilterLineEdit *>( QStringLiteral( "mSearchLineEdit" ) );
99 
100  if ( !mOptListWidget || !mOptStackedWidget || !mOptSplitter || !optionsFrame )
101  {
102  return;
103  }
104 
105  int size = QgsGuiUtils::scaleIconSize( mSettings->value( QStringLiteral( "/IconSize" ), 24 ).toInt() );
106  // buffer size to match displayed icon size in toolbars, and expected geometry restore
107  // newWidth (above) may need adjusted if you adjust iconBuffer here
108  const int iconBuffer = QgsGuiUtils::scaleIconSize( 4 );
109  mOptListWidget->setIconSize( QSize( size + iconBuffer, size + iconBuffer ) );
110  mOptListWidget->setFrameStyle( QFrame::NoFrame );
111 
112  const int frameMargin = QgsGuiUtils::scaleIconSize( 3 );
113  optionsFrame->layout()->setContentsMargins( 0, frameMargin, frameMargin, frameMargin );
114  QVBoxLayout *layout = static_cast<QVBoxLayout *>( optionsFrame->layout() );
115 
116  if ( buttonBoxFrame )
117  {
118  buttonBoxFrame->layout()->setContentsMargins( 0, 0, 0, 0 );
119  layout->insertWidget( layout->count(), buttonBoxFrame );
120  }
121  else if ( mOptButtonBox )
122  {
123  layout->insertWidget( layout->count(), mOptButtonBox );
124  }
125 
126  if ( mOptButtonBox )
127  {
128  // enforce only one connection per signal, in case added in Qt Designer
129  disconnect( mOptButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
130  connect( mOptButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
131  disconnect( mOptButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
132  connect( mOptButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
133  }
134  connect( mOptSplitter, &QSplitter::splitterMoved, this, &QgsOptionsDialogBase::updateOptionsListVerticalTabs );
135  connect( mOptStackedWidget, &QStackedWidget::currentChanged, this, &QgsOptionsDialogBase::optionsStackedWidget_CurrentChanged );
136  connect( mOptStackedWidget, &QStackedWidget::widgetRemoved, this, &QgsOptionsDialogBase::optionsStackedWidget_WidgetRemoved );
137 
138  if ( mSearchLineEdit )
139  {
141  connect( mSearchLineEdit, &QgsFilterLineEdit::textChanged, this, &QgsOptionsDialogBase::searchText );
142  }
143 
144  mInit = true;
145 
146  if ( restoreUi )
148 }
149 
150 void QgsOptionsDialogBase::setSettings( QgsSettings *settings )
151 {
152  if ( mDelSettings ) // local settings obj to delete
153  {
154  delete mSettings;
155  }
156 
157  mSettings = settings;
158  mDelSettings = false; // don't delete outside obj
159 }
160 
161 void QgsOptionsDialogBase::restoreOptionsBaseUi( const QString &title )
162 {
163  if ( !mInit )
164  {
165  return;
166  }
167 
168  if ( !title.isEmpty() )
169  {
170  mDialogTitle = title;
171  }
172  else
173  {
174  // re-save original dialog title in case it was changed after dialog initialization
175  mDialogTitle = windowTitle();
176  }
178 
179  restoreGeometry( mSettings->value( QStringLiteral( "/Windows/%1/geometry" ).arg( mOptsKey ) ).toByteArray() );
180  // mOptListWidget width is fixed to take up less space in QtDesigner
181  // revert it now unless the splitter's state hasn't been saved yet
182  mOptListWidget->setMaximumWidth(
183  mSettings->value( QStringLiteral( "/Windows/%1/splitState" ).arg( mOptsKey ) ).isNull() ? 150 : 16777215 );
184  mOptSplitter->restoreState( mSettings->value( QStringLiteral( "/Windows/%1/splitState" ).arg( mOptsKey ) ).toByteArray() );
185 
186  restoreLastPage();
187 
188  // get rid of annoying outer focus rect on Mac
189  mOptListWidget->setAttribute( Qt::WA_MacShowFocusRect, false );
190 
191  // brute force approach to try to standardize page margins!
192  for ( int i = 0; i < mOptStackedWidget->count(); ++i )
193  {
194  if ( QLayout *l = mOptStackedWidget->widget( i )->layout() )
195  {
196  l->setContentsMargins( 0, 0, 0, 0 );
197  }
198  }
199 }
200 
202 {
203  int curIndx = mSettings->value( QStringLiteral( "/Windows/%1/tab" ).arg( mOptsKey ), 0 ).toInt();
204 
205  // if the last used tab is out of range or not enabled display the first enabled one
206  if ( mOptStackedWidget->count() < curIndx + 1
207  || !mOptStackedWidget->widget( curIndx )->isEnabled() )
208  {
209  curIndx = 0;
210  for ( int i = 0; i < mOptStackedWidget->count(); i++ )
211  {
212  if ( mOptStackedWidget->widget( i )->isEnabled() )
213  {
214  curIndx = i;
215  break;
216  }
217  }
218  }
219 
220  if ( mOptStackedWidget->count() != 0 && mOptListWidget->count() != 0 )
221  {
222  mOptStackedWidget->setCurrentIndex( curIndx );
223  mOptListWidget->setCurrentRow( curIndx );
224  }
225 }
226 
228 {
229  // Adjust size (GH issue #31449 and #32615)
230  // make the stacked widget size to the current page only
231  for ( int i = 0; i < mOptStackedWidget->count(); ++i )
232  {
233  // Set the size policy
234  QSizePolicy::Policy policy = QSizePolicy::Ignored;
235  if ( i == index )
236  {
237  policy = QSizePolicy::MinimumExpanding;
238  }
239 
240  // update the size policy
241  mOptStackedWidget->widget( i )->setSizePolicy( policy, policy );
242 
243  if ( i == index )
244  {
245  mOptStackedWidget->layout()->update();
246  }
247  }
248  mOptStackedWidget->adjustSize();
249 }
250 
251 void QgsOptionsDialogBase::setCurrentPage( const QString &page )
252 {
253  //find the page with a matching widget name
254  for ( int idx = 0; idx < mOptStackedWidget->count(); ++idx )
255  {
256  QWidget *currentPage = mOptStackedWidget->widget( idx );
257  if ( currentPage->objectName() == page )
258  {
259  //found the page, set it as current
260  mOptStackedWidget->setCurrentIndex( idx );
261  return;
262  }
263  }
264 }
265 
266 void QgsOptionsDialogBase::addPage( const QString &title, const QString &tooltip, const QIcon &icon, QWidget *widget )
267 {
268  QListWidgetItem *item = new QListWidgetItem();
269  item->setIcon( icon );
270  item->setText( title );
271  item->setToolTip( tooltip );
272 
273  mOptListWidget->addItem( item );
274  mOptStackedWidget->addWidget( widget );
275 }
276 
277 void QgsOptionsDialogBase::insertPage( const QString &title, const QString &tooltip, const QIcon &icon, QWidget *widget, const QString &before )
278 {
279  //find the page with a matching widget name
280  for ( int idx = 0; idx < mOptStackedWidget->count(); ++idx )
281  {
282  QWidget *currentPage = mOptStackedWidget->widget( idx );
283  if ( currentPage->objectName() == before )
284  {
285  //found the "before" page
286 
287  QListWidgetItem *item = new QListWidgetItem();
288  item->setIcon( icon );
289  item->setText( title );
290  item->setToolTip( tooltip );
291 
292  mOptListWidget->insertItem( idx, item );
293  mOptStackedWidget->insertWidget( idx, widget );
294  return;
295  }
296  }
297 
298  // no matching pages, so just add the page
299  addPage( title, tooltip, icon, widget );
300 }
301 
302 void QgsOptionsDialogBase::searchText( const QString &text )
303 {
304  const int minimumTextLength = 3;
305 
306  mSearchLineEdit->setMinimumWidth( text.isEmpty() ? 0 : 70 );
307 
308  if ( !mOptStackedWidget )
309  return;
310 
311  if ( mOptStackedWidget->isHidden() )
312  mOptStackedWidget->show();
313  if ( mOptButtonBox && mOptButtonBox->isHidden() )
314  mOptButtonBox->show();
315  // hide all page if text has to be search, show them all otherwise
316  for ( int r = 0; r < mOptListWidget->count(); ++r )
317  {
318  mOptListWidget->setRowHidden( r, text.length() >= minimumTextLength );
319  }
320 
321  for ( const QPair< QgsOptionsDialogHighlightWidget *, int > &rsw : std::as_const( mRegisteredSearchWidgets ) )
322  {
323  if ( rsw.first->searchHighlight( text.length() >= minimumTextLength ? text : QString() ) )
324  {
325  mOptListWidget->setRowHidden( rsw.second, false );
326  }
327  }
328 
329  if ( mOptListWidget->isRowHidden( mOptStackedWidget->currentIndex() ) )
330  {
331  for ( int r = 0; r < mOptListWidget->count(); ++r )
332  {
333  if ( !mOptListWidget->isRowHidden( r ) )
334  {
335  mOptListWidget->setCurrentRow( r );
336  return;
337  }
338  }
339 
340  // if no page can be shown, hide stack widget
341  mOptStackedWidget->hide();
342  if ( mOptButtonBox )
343  mOptButtonBox->hide();
344  }
345 }
346 
348 {
349  mRegisteredSearchWidgets.clear();
350 
351  for ( int i = 0; i < mOptStackedWidget->count(); i++ )
352  {
353  const auto constWidget = mOptStackedWidget->widget( i )->findChildren<QWidget *>();
354  for ( QWidget *w : constWidget )
355  {
356 
357  // get custom highlight widget in user added pages
358  QHash<QWidget *, QgsOptionsDialogHighlightWidget *> customHighlightWidgets;
359  QgsOptionsPageWidget *opw = qobject_cast<QgsOptionsPageWidget *>( mOptStackedWidget->widget( i ) );
360  if ( opw )
361  {
362  customHighlightWidgets = opw->registeredHighlightWidgets();
363  }
364  QgsOptionsDialogHighlightWidget *shw = nullptr;
365  // take custom if exists
366  if ( customHighlightWidgets.contains( w ) )
367  {
368  shw = customHighlightWidgets.value( w );
369  }
370  // try to construct one otherwise
371  if ( !shw || !shw->isValid() )
372  {
374  }
375  if ( shw && shw->isValid() )
376  {
377  QgsDebugMsgLevel( QStringLiteral( "Registering: %1" ).arg( w->objectName() ), 4 );
378  mRegisteredSearchWidgets.append( qMakePair( shw, i ) );
379  }
380  else
381  {
382  delete shw;
383  }
384  }
385  }
386 }
387 
388 void QgsOptionsDialogBase::showEvent( QShowEvent *e )
389 {
390  if ( mInit )
391  {
394  }
395  else
396  {
397  QTimer::singleShot( 0, this, SLOT( warnAboutMissingObjects() ) );
398  }
399 
400  if ( mSearchLineEdit )
401  {
403  }
404 
405  QDialog::showEvent( e );
406 }
407 
408 void QgsOptionsDialogBase::paintEvent( QPaintEvent *e )
409 {
410  if ( mInit )
411  QTimer::singleShot( 0, this, SLOT( updateOptionsListVerticalTabs() ) );
412 
413  QDialog::paintEvent( e );
414 }
415 
417 {
418  QListWidgetItem *curitem = mOptListWidget->currentItem();
419  if ( curitem )
420  {
421  setWindowTitle( QStringLiteral( "%1 %2 %3" )
422  .arg( mDialogTitle )
423  .arg( QChar( 0x2014 ) ) // em-dash unicode
424  .arg( curitem->text() ) );
425  }
426  else
427  {
428  setWindowTitle( mDialogTitle );
429  }
430 }
431 
433 {
434  if ( !mInit )
435  return;
436 
437  if ( mOptListWidget->maximumWidth() != 16777215 )
438  mOptListWidget->setMaximumWidth( 16777215 );
439  // auto-resize splitter for vert scrollbar without covering icons in icon-only mode
440  // TODO: mOptListWidget has fixed 32px wide icons for now, allow user-defined
441  // Note: called on splitter resize and dialog paint event, so only update when necessary
442  int iconWidth = mOptListWidget->iconSize().width();
443  int snapToIconWidth = iconWidth + 32;
444 
445  QList<int> splitSizes = mOptSplitter->sizes();
446  mIconOnly = ( splitSizes.at( 0 ) <= snapToIconWidth );
447 
448  // iconBuffer (above) may need adjusted if you adjust iconWidth here
449  int newWidth = mOptListWidget->verticalScrollBar()->isVisible() ? iconWidth + 22 : iconWidth + 9;
450  bool diffWidth = mOptListWidget->minimumWidth() != newWidth;
451 
452  if ( diffWidth )
453  mOptListWidget->setMinimumWidth( newWidth );
454 
455  if ( mIconOnly && ( diffWidth || mOptListWidget->width() != newWidth ) )
456  {
457  splitSizes[1] = splitSizes.at( 1 ) - ( splitSizes.at( 0 ) - newWidth );
458  splitSizes[0] = newWidth;
459  mOptSplitter->setSizes( splitSizes );
460  }
461 
462  if ( mOptListWidget->wordWrap() && mIconOnly )
463  mOptListWidget->setWordWrap( false );
464  if ( !mOptListWidget->wordWrap() && !mIconOnly )
465  mOptListWidget->setWordWrap( true );
466 }
467 
469 {
470  mOptListWidget->blockSignals( true );
471  mOptListWidget->setCurrentRow( index );
472  mOptListWidget->blockSignals( false );
473 
475 }
476 
478 {
479  // will need to take item first, if widgets are set for item in future
480  delete mOptListWidget->item( index );
481 
482  QList<QPair< QgsOptionsDialogHighlightWidget *, int > >::iterator it = mRegisteredSearchWidgets.begin();
483  while ( it != mRegisteredSearchWidgets.end() )
484  {
485  if ( ( *it ).second == index )
486  it = mRegisteredSearchWidgets.erase( it );
487  else
488  ++it;
489  }
490 }
491 
493 {
494  QMessageBox::warning( nullptr, tr( "Missing Objects" ),
495  tr( "Base options dialog could not be initialized.\n\n"
496  "Missing some of the .ui template objects:\n" )
497  + " mOptionsListWidget,\n mOptionsStackedWidget,\n mOptionsSplitter,\n mOptionsListFrame",
498  QMessageBox::Ok,
499  QMessageBox::Ok );
500 }
501 
void setShowSearchIcon(bool visible)
Define if a search icon shall be shown on the left of the image when no text is entered.
QPointer< QgsSettings > mSettings
void resizeAlltabs(int index)
Resizes all tabs when the dialog is resized.
void insertPage(const QString &title, const QString &tooltip, const QIcon &icon, QWidget *widget, const QString &before)
Inserts a new page into the dialog pages.
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 addPage(const QString &title, const QString &tooltip, const QIcon &icon, QWidget *widget)
Adds a new page to the dialog pages.
void setSettings(QgsSettings *settings)
virtual void optionsStackedWidget_WidgetRemoved(int index)
Remove tab and unregister widgets on page remove.
QDialogButtonBox * mOptButtonBox
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.
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