QGIS API Documentation  2.99.0-Master (cd0ba91)
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 <QCheckBox>
20 #include <QDialog>
21 #include <QDialogButtonBox>
22 #include <QEvent>
23 #include <QGroupBox>
24 #include <QLabel>
25 #include <QLayout>
26 #include <QListWidget>
27 #include <QListWidgetItem>
28 #include <QMessageBox>
29 #include <QPainter>
30 #include <QScrollBar>
31 #include <QSplitter>
32 #include <QStackedWidget>
33 #include <QTimer>
34 #include <QTreeView>
35 #include <QAbstractItemModel>
36 
37 #include "qgsfilterlineedit.h"
38 
39 #include "qgslogger.h"
40 
41 QgsOptionsDialogBase::QgsOptionsDialogBase( const QString &settingsKey, QWidget *parent, Qt::WindowFlags fl, QgsSettings *settings )
42  : QDialog( parent, fl )
43  , mOptsKey( settingsKey )
44  , mInit( false )
45  , mIconOnly( false )
46  , mSettings( settings )
47  , mDelSettings( false )
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 ( layout() )
90  {
91  layout()->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  QFrame *optionsFrame = findChild<QFrame *>( QStringLiteral( "mOptionsFrame" ) );
97  mOptStackedWidget = findChild<QStackedWidget *>( QStringLiteral( "mOptionsStackedWidget" ) );
98  mOptSplitter = findChild<QSplitter *>( QStringLiteral( "mOptionsSplitter" ) );
99  mOptButtonBox = findChild<QDialogButtonBox *>( QStringLiteral( "buttonBox" ) );
100  QFrame *buttonBoxFrame = findChild<QFrame *>( QStringLiteral( "mButtonBoxFrame" ) );
101  mSearchLineEdit = findChild<QgsFilterLineEdit *>( QStringLiteral( "mSearchLineEdit" ) );
102 
103  if ( !mOptListWidget || !mOptStackedWidget || !mOptSplitter || !optionsFrame )
104  {
105  return;
106  }
107 
108  int size = mSettings->value( QStringLiteral( "/IconSize" ), 24 ).toInt();
109  // buffer size to match displayed icon size in toolbars, and expected geometry restore
110  // newWidth (above) may need adjusted if you adjust iconBuffer here
111  int iconBuffer = 4;
112  mOptListWidget->setIconSize( QSize( size + iconBuffer, size + iconBuffer ) );
113  mOptListWidget->setFrameStyle( QFrame::NoFrame );
114 
115  optionsFrame->layout()->setContentsMargins( 0, 3, 3, 3 );
116  QVBoxLayout *layout = static_cast<QVBoxLayout *>( optionsFrame->layout() );
117 
118  if ( buttonBoxFrame )
119  {
120  buttonBoxFrame->layout()->setContentsMargins( 0, 0, 0, 0 );
121  layout->insertWidget( layout->count() + 1, buttonBoxFrame );
122  }
123  else if ( mOptButtonBox )
124  {
125  layout->insertWidget( layout->count() + 1, mOptButtonBox );
126  }
127 
128  if ( mOptButtonBox )
129  {
130  // enforce only one connection per signal, in case added in Qt Designer
131  disconnect( mOptButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
132  connect( mOptButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
133  disconnect( mOptButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
134  connect( mOptButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
135  }
136  connect( mOptSplitter, &QSplitter::splitterMoved, this, &QgsOptionsDialogBase::updateOptionsListVerticalTabs );
137  connect( mOptStackedWidget, &QStackedWidget::currentChanged, this, &QgsOptionsDialogBase::optionsStackedWidget_CurrentChanged );
138  connect( mOptStackedWidget, &QStackedWidget::widgetRemoved, this, &QgsOptionsDialogBase::optionsStackedWidget_WidgetRemoved );
139 
140  if ( mSearchLineEdit )
141  {
142  mSearchLineEdit->setShowSearchIcon( true );
143  connect( mSearchLineEdit, &QgsFilterLineEdit::textChanged, this, &QgsOptionsDialogBase::searchText );
144  }
145 
146  mInit = true;
147 
148  if ( restoreUi )
150 }
151 
153 {
154  if ( mDelSettings ) // local settings obj to delete
155  {
156  delete mSettings;
157  }
158 
159  mSettings = settings;
160  mDelSettings = false; // don't delete outside obj
161 }
162 
163 void QgsOptionsDialogBase::restoreOptionsBaseUi( const QString &title )
164 {
165  if ( !mInit )
166  {
167  return;
168  }
169 
170  if ( !title.isEmpty() )
171  {
172  mDialogTitle = title;
174  }
175 
176  // re-save original dialog title in case it was changed after dialog initialization
177  mDialogTitle = windowTitle();
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  int curIndx = mSettings->value( QStringLiteral( "/Windows/%1/tab" ).arg( mOptsKey ), 0 ).toInt();
186 
187  // if the last used tab is out of range or not enabled display the first enabled one
188  if ( mOptStackedWidget->count() < ( curIndx + 1 )
189  || !mOptStackedWidget->widget( curIndx )->isEnabled() )
190  {
191  curIndx = 0;
192  for ( int i = 0; i < mOptStackedWidget->count(); i++ )
193  {
194  if ( mOptStackedWidget->widget( i )->isEnabled() )
195  {
196  curIndx = i;
197  break;
198  }
199  }
200  }
201 
202  if ( mOptStackedWidget->count() != 0 && mOptListWidget->count() != 0 )
203  {
204  mOptStackedWidget->setCurrentIndex( curIndx );
205  mOptListWidget->setCurrentRow( curIndx );
206  }
207 
208  // get rid of annoying outer focus rect on Mac
209  mOptListWidget->setAttribute( Qt::WA_MacShowFocusRect, false );
210 }
211 
212 void QgsOptionsDialogBase::searchText( const QString &text )
213 {
214  mSearchLineEdit->setMinimumWidth( text.isEmpty() ? 0 : 70 );
215 
216  if ( !mOptStackedWidget )
217  return;
218 
219  if ( mOptStackedWidget->isHidden() )
220  mOptStackedWidget->show();
221  if ( mOptButtonBox && mOptButtonBox->isHidden() )
222  mOptButtonBox->show();
223  // hide all page if text has to be search, show them all otherwise
224  for ( int r = 0; r < mOptListWidget->count(); ++r )
225  {
226  mOptListWidget->setRowHidden( r, !text.isEmpty() );
227  }
228 
229  for ( const QPair< QgsSearchHighlightOptionWidget *, int > &rsw : qgis::as_const( mRegisteredSearchWidgets ) )
230  {
231  rsw.first->reset();
232  if ( !text.isEmpty() && rsw.first->searchHighlight( text ) )
233  {
234  QgsDebugMsgLevel( QString( "Found %1 in %2 (tab: %3)" )
235  .arg( text )
236  .arg( rsw.first->isValid() ? rsw.first->widget()->objectName() : "no widget" )
237  .arg( mOptListWidget->item( rsw.second )->text() ), 4 );
238  mOptListWidget->setRowHidden( rsw.second, false );
239  }
240  }
241 
242  if ( mOptListWidget->isRowHidden( mOptStackedWidget->currentIndex() ) )
243  {
244  for ( int r = 0; r < mOptListWidget->count(); ++r )
245  {
246  if ( !mOptListWidget->isRowHidden( r ) )
247  {
248  mOptListWidget->setCurrentRow( r );
249  return;
250  }
251  }
252 
253  // if no page can be shown, hide stack widget
254  mOptStackedWidget->hide();
255  if ( mOptButtonBox )
256  mOptButtonBox->hide();
257  }
258 }
259 
261 {
262  mRegisteredSearchWidgets.clear();
263 
264  for ( int i = 0; i < mOptStackedWidget->count(); i++ )
265  {
266  Q_FOREACH ( QWidget *w, mOptStackedWidget->widget( i )->findChildren<QWidget *>() )
267  {
269  if ( shw->isValid() )
270  {
271  QgsDebugMsgLevel( QString( "Registering: %1" ).arg( w->objectName() ), 4 );
272  mRegisteredSearchWidgets.append( qMakePair( shw, i ) );
273  }
274  else
275  {
276  delete shw;
277  }
278  }
279  }
280 }
281 
282 void QgsOptionsDialogBase::showEvent( QShowEvent *e )
283 {
284  if ( mInit )
285  {
288  }
289  else
290  {
291  QTimer::singleShot( 0, this, SLOT( warnAboutMissingObjects() ) );
292  }
293 
294  if ( mSearchLineEdit )
295  {
297  }
298 
299  QDialog::showEvent( e );
300 }
301 
302 void QgsOptionsDialogBase::paintEvent( QPaintEvent *e )
303 {
304  if ( mInit )
305  QTimer::singleShot( 0, this, SLOT( updateOptionsListVerticalTabs() ) );
306 
307  QDialog::paintEvent( e );
308 }
309 
311 {
312  QListWidgetItem *curitem = mOptListWidget->currentItem();
313  if ( curitem )
314  {
315  setWindowTitle( QStringLiteral( "%1 | %2" ).arg( mDialogTitle, curitem->text() ) );
316  }
317  else
318  {
319  setWindowTitle( mDialogTitle );
320  }
321 }
322 
324 {
325  if ( !mInit )
326  return;
327 
328  if ( mOptListWidget->maximumWidth() != 16777215 )
329  mOptListWidget->setMaximumWidth( 16777215 );
330  // auto-resize splitter for vert scrollbar without covering icons in icon-only mode
331  // TODO: mOptListWidget has fixed 32px wide icons for now, allow user-defined
332  // Note: called on splitter resize and dialog paint event, so only update when necessary
333  int iconWidth = mOptListWidget->iconSize().width();
334  int snapToIconWidth = iconWidth + 32;
335 
336  QList<int> splitSizes = mOptSplitter->sizes();
337  mIconOnly = ( splitSizes.at( 0 ) <= snapToIconWidth );
338 
339  // iconBuffer (above) may need adjusted if you adjust iconWidth here
340  int newWidth = mOptListWidget->verticalScrollBar()->isVisible() ? iconWidth + 22 : iconWidth + 9;
341  bool diffWidth = mOptListWidget->minimumWidth() != newWidth;
342 
343  if ( diffWidth )
344  mOptListWidget->setMinimumWidth( newWidth );
345 
346  if ( mIconOnly && ( diffWidth || mOptListWidget->width() != newWidth ) )
347  {
348  splitSizes[1] = splitSizes.at( 1 ) - ( splitSizes.at( 0 ) - newWidth );
349  splitSizes[0] = newWidth;
350  mOptSplitter->setSizes( splitSizes );
351  }
352 
353  if ( mOptListWidget->wordWrap() && mIconOnly )
354  mOptListWidget->setWordWrap( false );
355  if ( !mOptListWidget->wordWrap() && !mIconOnly )
356  mOptListWidget->setWordWrap( true );
357 }
358 
360 {
361  mOptListWidget->blockSignals( true );
362  mOptListWidget->setCurrentRow( index );
363  mOptListWidget->blockSignals( false );
364 
366 }
367 
369 {
370  // will need to take item first, if widgets are set for item in future
371  delete mOptListWidget->item( index );
372 
373  QList<QPair< QgsSearchHighlightOptionWidget *, int > >::iterator it = mRegisteredSearchWidgets.begin();
374  while ( it != mRegisteredSearchWidgets.end() )
375  {
376  if ( ( *it ).second == index )
377  it = mRegisteredSearchWidgets.erase( it );
378  else
379  ++it;
380  }
381 }
382 
384 {
385  QMessageBox::warning( nullptr, tr( "Missing objects" ),
386  tr( "Base options dialog could not be initialized.\n\n"
387  "Missing some of the .ui template objects:\n" )
388  + " mOptionsListWidget,\n mOptionsStackedWidget,\n mOptionsSplitter,\n mOptionsListFrame",
389  QMessageBox::Ok,
390  QMessageBox::Ok );
391 }
392 
393 
395  : QObject( widget )
396  , mWidget( widget )
397  , mText( [ = ]() {return QString();} )
398 {
399  if ( qobject_cast<QLabel *>( widget ) )
400  {
401  mStyleSheet = QStringLiteral( "QLabel { background-color: yellow; color: blue;}" );
402  mText = [ = ]() {return qobject_cast<QLabel *>( mWidget )->text();};
403  }
404  else if ( qobject_cast<QCheckBox *>( widget ) )
405  {
406  mStyleSheet = QStringLiteral( "QCheckBox { background-color: yellow; color: blue;}" );
407  mText = [ = ]() {return qobject_cast<QCheckBox *>( mWidget )->text();};
408  }
409  else if ( qobject_cast<QAbstractButton *>( widget ) )
410  {
411  mStyleSheet = QStringLiteral( "QAbstractButton { background-color: yellow; color: blue;}" );
412  mText = [ = ]() {return qobject_cast<QAbstractButton *>( mWidget )->text();};
413  }
414  else if ( qobject_cast<QGroupBox *>( widget ) )
415  {
416  mStyleSheet = QStringLiteral( "QGroupBox::title { background-color: yellow; color: blue;}" );
417  mText = [ = ]() {return qobject_cast<QGroupBox *>( mWidget )->title();};
418  }
419  else if ( qobject_cast<QTreeView *>( widget ) )
420  {
421  // TODO - style individual matching items
422  }
423  else
424  {
425  mValid = false;
426  }
427  if ( mValid )
428  {
429  mStyleSheet.prepend( "/*!search!*/" ).append( "/*!search!*/" );
430  QgsDebugMsgLevel( mStyleSheet, 4 );
431  connect( mWidget, &QWidget::destroyed, this, &QgsSearchHighlightOptionWidget::widgetDestroyed );
432  }
433 }
434 
435 bool QgsSearchHighlightOptionWidget::searchHighlight( const QString &searchText )
436 {
437  bool found = false;
438  if ( !mWidget )
439  return found;
440 
441  if ( !searchText.isEmpty() )
442  {
443  if ( QTreeView *tree = qobject_cast<QTreeView *>( mWidget ) )
444  {
445  QModelIndexList hits = tree->model()->match( tree->model()->index( 0, 0 ), Qt::DisplayRole, searchText, 1, Qt::MatchContains | Qt::MatchRecursive );
446  found = !hits.isEmpty();
447  }
448  else
449  {
450  QString origText = mText();
451  if ( origText.contains( searchText, Qt::CaseInsensitive ) )
452  {
453  found = true;
454  }
455  }
456  }
457 
458  if ( found && !mChangedStyle )
459  {
460  if ( !mWidget->isVisible() )
461  {
462  // show the widget to get initial stylesheet in case it's modified
463  mWidget->show();
464  }
465  mWidget->setStyleSheet( mWidget->styleSheet() + mStyleSheet );
466  mChangedStyle = true;
467  }
468 
469  return found;
470 }
471 
473 {
474  if ( mValid && mChangedStyle )
475  {
476  QString ss = mWidget->styleSheet();
477  ss.remove( mStyleSheet );
478  mWidget->setStyleSheet( ss );
479  mChangedStyle = false;
480  }
481 }
482 
483 void QgsSearchHighlightOptionWidget::widgetDestroyed()
484 {
485  mValid = false;
486 }
QgsOptionsDialogBase(const QString &settingsKey, QWidget *parent=nullptr, Qt::WindowFlags fl=0, QgsSettings *settings=nullptr)
Constructor.
This class is a composition of two QSettings instances:
Definition: qgssettings.h:55
bool isValid()
Returns if it valid: if the widget type is handled and if the widget is not still available...
void initOptionsBase(bool restoreUi=true, const QString &title=QString())
Set up the base ui connections for vertical tabs.
void paintEvent(QPaintEvent *e) override
void restoreOptionsBaseUi(const QString &title=QString())
Restore the base ui.
void reset()
reset the style to the original state
QgsFilterLineEdit * mSearchLineEdit
QgsSearchHighlightOptionWidget(QWidget *widget=0)
Constructor.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:38
void searchText(const QString &text)
searchText searches for a text in all the pages of the stacked widget and highlight the results ...
QList< QPair< QgsSearchHighlightOptionWidget *, int > > mRegisteredSearchWidgets
virtual void optionsStackedWidget_WidgetRemoved(int index)
Remove tab and unregister widgets on page remove.
QWidget * widget()
return the widget
bool searchHighlight(const QString &searchText)
search for a text pattern and highlight the widget if the text is found
void showEvent(QShowEvent *e) override
void registerTextSearchWidgets()
register widgets in the dialog to search for text in it it is automatically called if a line edit has...
QDialogButtonBox * mOptButtonBox
void setSettings(QgsSettings *settings)
QPointer< QgsSettings > mSettings
virtual void updateOptionsListVerticalTabs()
Update tabs on the splitter move.
Container for a widget to be used to search text in the option dialog If the widget type is handled...
QStackedWidget * mOptStackedWidget
virtual void optionsStackedWidget_CurrentChanged(int index)
Select relevant tab on current page change.