QGIS API Documentation  2.99.0-Master (9caa722)
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  , mDialogTitle( QLatin1String( "" ) )
46  , mIconOnly( false )
47  , mSettings( settings )
48  , mDelSettings( false )
49 {
50 }
51 
53 {
54  if ( mInit )
55  {
56  mSettings->setValue( QStringLiteral( "/Windows/%1/geometry" ).arg( mOptsKey ), saveGeometry() );
57  mSettings->setValue( QStringLiteral( "/Windows/%1/splitState" ).arg( mOptsKey ), mOptSplitter->saveState() );
58  mSettings->setValue( QStringLiteral( "/Windows/%1/tab" ).arg( mOptsKey ), mOptStackedWidget->currentIndex() );
59  }
60 
61  if ( mDelSettings ) // local settings obj to delete
62  {
63  delete mSettings;
64  }
65 
66  mSettings = nullptr; // null the pointer (in case of outside settings obj)
67 }
68 
69 void QgsOptionsDialogBase::initOptionsBase( bool restoreUi, const QString &title )
70 {
71  // use pointer to app QgsSettings if no custom QgsSettings specified
72  // custom QgsSettings object may be from Python plugin
73  mDelSettings = false;
74 
75  if ( !mSettings )
76  {
77  mSettings = new QgsSettings();
78  mDelSettings = true; // only delete obj created by class
79  }
80 
81  // save dialog title so it can be used to be concatenated
82  // with category title in icon-only mode
83  if ( title.isEmpty() )
84  mDialogTitle = windowTitle();
85  else
86  mDialogTitle = title;
87 
88  // don't add to dialog margins
89  // redefine now, or those in inherited .ui file will be added
90  if ( layout() )
91  {
92  layout()->setContentsMargins( 0, 0, 0, 0 ); // Qt default spacing
93  }
94 
95  // start with copy of qgsoptionsdialog_template.ui to ensure existence of these objects
96  mOptListWidget = findChild<QListWidget *>( QStringLiteral( "mOptionsListWidget" ) );
97  QFrame *optionsFrame = findChild<QFrame *>( QStringLiteral( "mOptionsFrame" ) );
98  mOptStackedWidget = findChild<QStackedWidget *>( QStringLiteral( "mOptionsStackedWidget" ) );
99  mOptSplitter = findChild<QSplitter *>( QStringLiteral( "mOptionsSplitter" ) );
100  mOptButtonBox = findChild<QDialogButtonBox *>( QStringLiteral( "buttonBox" ) );
101  QFrame *buttonBoxFrame = findChild<QFrame *>( QStringLiteral( "mButtonBoxFrame" ) );
102  mSearchLineEdit = findChild<QgsFilterLineEdit *>( "mSearchLineEdit" );
103 
104  if ( !mOptListWidget || !mOptStackedWidget || !mOptSplitter || !optionsFrame )
105  {
106  return;
107  }
108 
109  int size = mSettings->value( QStringLiteral( "/IconSize" ), 24 ).toInt();
110  // buffer size to match displayed icon size in toolbars, and expected geometry restore
111  // newWidth (above) may need adjusted if you adjust iconBuffer here
112  int iconBuffer = 4;
113  mOptListWidget->setIconSize( QSize( size + iconBuffer, size + iconBuffer ) );
114  mOptListWidget->setFrameStyle( QFrame::NoFrame );
115 
116  optionsFrame->layout()->setContentsMargins( 0, 3, 3, 3 );
117  QVBoxLayout *layout = static_cast<QVBoxLayout *>( optionsFrame->layout() );
118 
119  if ( buttonBoxFrame )
120  {
121  buttonBoxFrame->layout()->setContentsMargins( 0, 0, 0, 0 );
122  layout->insertWidget( layout->count() + 1, buttonBoxFrame );
123  }
124  else if ( mOptButtonBox )
125  {
126  layout->insertWidget( layout->count() + 1, mOptButtonBox );
127  }
128 
129  if ( mOptButtonBox )
130  {
131  // enforce only one connection per signal, in case added in Qt Designer
132  disconnect( mOptButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
133  connect( mOptButtonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
134  disconnect( mOptButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
135  connect( mOptButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject );
136  }
137  connect( mOptSplitter, &QSplitter::splitterMoved, this, &QgsOptionsDialogBase::updateOptionsListVerticalTabs );
138  connect( mOptStackedWidget, &QStackedWidget::currentChanged, this, &QgsOptionsDialogBase::optionsStackedWidget_CurrentChanged );
139  connect( mOptStackedWidget, &QStackedWidget::widgetRemoved, this, &QgsOptionsDialogBase::optionsStackedWidget_WidgetRemoved );
140 
141  if ( mSearchLineEdit )
142  {
143  mSearchLineEdit->setShowSearchIcon( true );
144  connect( mSearchLineEdit, &QgsFilterLineEdit::textChanged, this, &QgsOptionsDialogBase::searchText );
145  }
146 
147  mInit = true;
148 
149  if ( restoreUi )
151 }
152 
154 {
155  if ( mDelSettings ) // local settings obj to delete
156  {
157  delete mSettings;
158  }
159 
160  mSettings = settings;
161  mDelSettings = false; // don't delete outside obj
162 }
163 
164 void QgsOptionsDialogBase::restoreOptionsBaseUi( const QString &title )
165 {
166  if ( !mInit )
167  {
168  return;
169  }
170 
171  if ( !title.isEmpty() )
172  {
173  mDialogTitle = title;
175  }
176 
177  // re-save original dialog title in case it was changed after dialog initialization
178  mDialogTitle = windowTitle();
179 
180  restoreGeometry( mSettings->value( QStringLiteral( "/Windows/%1/geometry" ).arg( mOptsKey ) ).toByteArray() );
181  // mOptListWidget width is fixed to take up less space in QtDesigner
182  // revert it now unless the splitter's state hasn't been saved yet
183  mOptListWidget->setMaximumWidth(
184  mSettings->value( QStringLiteral( "/Windows/%1/splitState" ).arg( mOptsKey ) ).isNull() ? 150 : 16777215 );
185  mOptSplitter->restoreState( mSettings->value( QStringLiteral( "/Windows/%1/splitState" ).arg( mOptsKey ) ).toByteArray() );
186  int curIndx = mSettings->value( QStringLiteral( "/Windows/%1/tab" ).arg( mOptsKey ), 0 ).toInt();
187 
188  // if the last used tab is out of range or not enabled display the first enabled one
189  if ( mOptStackedWidget->count() < ( curIndx + 1 )
190  || !mOptStackedWidget->widget( curIndx )->isEnabled() )
191  {
192  curIndx = 0;
193  for ( int i = 0; i < mOptStackedWidget->count(); i++ )
194  {
195  if ( mOptStackedWidget->widget( i )->isEnabled() )
196  {
197  curIndx = i;
198  break;
199  }
200  }
201  }
202 
203  if ( mOptStackedWidget->count() != 0 && mOptListWidget->count() != 0 )
204  {
205  mOptStackedWidget->setCurrentIndex( curIndx );
206  mOptListWidget->setCurrentRow( curIndx );
207  }
208 
209  // get rid of annoying outer focus rect on Mac
210  mOptListWidget->setAttribute( Qt::WA_MacShowFocusRect, false );
211 }
212 
213 void QgsOptionsDialogBase::searchText( const QString &text )
214 {
215  mSearchLineEdit->setMinimumWidth( text.isEmpty() ? 0 : 70 );
216 
217  if ( !mOptStackedWidget )
218  return;
219 
220  if ( mOptStackedWidget->isHidden() )
221  mOptStackedWidget->show();
222  if ( mOptButtonBox && mOptButtonBox->isHidden() )
223  mOptButtonBox->show();
224  // hide all page if text has to be search, show them all otherwise
225  for ( int r = 0; r < mOptListWidget->count(); ++r )
226  {
227  mOptListWidget->setRowHidden( r, !text.isEmpty() );
228  }
229 
230  for ( const QPair< QgsSearchHighlightOptionWidget *, int > &rsw : qgsAsConst( mRegisteredSearchWidgets ) )
231  {
232  rsw.first->reset();
233  if ( !text.isEmpty() && rsw.first->searchHighlight( text ) )
234  {
235  QgsDebugMsgLevel( QString( "Found %1 in %2 (tab: %3)" )
236  .arg( text )
237  .arg( rsw.first->isValid() ? rsw.first->widget()->objectName() : "no widget" )
238  .arg( mOptListWidget->item( rsw.second )->text() ), 4 );
239  mOptListWidget->setRowHidden( rsw.second, false );
240  }
241  }
242 
243  if ( mOptListWidget->isRowHidden( mOptStackedWidget->currentIndex() ) )
244  {
245  for ( int r = 0; r < mOptListWidget->count(); ++r )
246  {
247  if ( !mOptListWidget->isRowHidden( r ) )
248  {
249  mOptListWidget->setCurrentRow( r );
250  return;
251  }
252  }
253 
254  // if no page can be shown, hide stack widget
255  mOptStackedWidget->hide();
256  if ( mOptButtonBox )
257  mOptButtonBox->hide();
258  }
259 }
260 
262 {
263  mRegisteredSearchWidgets.clear();
264 
265  for ( int i = 0; i < mOptStackedWidget->count(); i++ )
266  {
267  Q_FOREACH ( QWidget *w, mOptStackedWidget->widget( i )->findChildren<QWidget *>() )
268  {
270  if ( shw->isValid() )
271  {
272  QgsDebugMsgLevel( QString( "Registering: %1" ).arg( w->objectName() ), 4 );
273  mRegisteredSearchWidgets.append( qMakePair( shw, i ) );
274  }
275  else
276  {
277  delete shw;
278  }
279  }
280  }
281 }
282 
283 void QgsOptionsDialogBase::showEvent( QShowEvent *e )
284 {
285  if ( mInit )
286  {
289  }
290  else
291  {
292  QTimer::singleShot( 0, this, SLOT( warnAboutMissingObjects() ) );
293  }
294 
295  if ( mSearchLineEdit )
296  {
298  }
299 
300  QDialog::showEvent( e );
301 }
302 
303 void QgsOptionsDialogBase::paintEvent( QPaintEvent *e )
304 {
305  if ( mInit )
306  QTimer::singleShot( 0, this, SLOT( updateOptionsListVerticalTabs() ) );
307 
308  QDialog::paintEvent( e );
309 }
310 
312 {
313  QListWidgetItem *curitem = mOptListWidget->currentItem();
314  if ( curitem )
315  {
316  setWindowTitle( QStringLiteral( "%1 | %2" ).arg( mDialogTitle, curitem->text() ) );
317  }
318  else
319  {
320  setWindowTitle( mDialogTitle );
321  }
322 }
323 
325 {
326  if ( !mInit )
327  return;
328 
329  if ( mOptListWidget->maximumWidth() != 16777215 )
330  mOptListWidget->setMaximumWidth( 16777215 );
331  // auto-resize splitter for vert scrollbar without covering icons in icon-only mode
332  // TODO: mOptListWidget has fixed 32px wide icons for now, allow user-defined
333  // Note: called on splitter resize and dialog paint event, so only update when necessary
334  int iconWidth = mOptListWidget->iconSize().width();
335  int snapToIconWidth = iconWidth + 32;
336 
337  QList<int> splitSizes = mOptSplitter->sizes();
338  mIconOnly = ( splitSizes.at( 0 ) <= snapToIconWidth );
339 
340  // iconBuffer (above) may need adjusted if you adjust iconWidth here
341  int newWidth = mOptListWidget->verticalScrollBar()->isVisible() ? iconWidth + 22 : iconWidth + 9;
342  bool diffWidth = mOptListWidget->minimumWidth() != newWidth;
343 
344  if ( diffWidth )
345  mOptListWidget->setMinimumWidth( newWidth );
346 
347  if ( mIconOnly && ( diffWidth || mOptListWidget->width() != newWidth ) )
348  {
349  splitSizes[1] = splitSizes.at( 1 ) - ( splitSizes.at( 0 ) - newWidth );
350  splitSizes[0] = newWidth;
351  mOptSplitter->setSizes( splitSizes );
352  }
353 
354  if ( mOptListWidget->wordWrap() && mIconOnly )
355  mOptListWidget->setWordWrap( false );
356  if ( !mOptListWidget->wordWrap() && !mIconOnly )
357  mOptListWidget->setWordWrap( true );
358 }
359 
361 {
362  mOptListWidget->blockSignals( true );
363  mOptListWidget->setCurrentRow( indx );
364  mOptListWidget->blockSignals( false );
365 
367 }
368 
370 {
371  // will need to take item first, if widgets are set for item in future
372  delete mOptListWidget->item( indx );
373 
374  QList<QPair< QgsSearchHighlightOptionWidget *, int > >::iterator it = mRegisteredSearchWidgets.begin();
375  while ( it != mRegisteredSearchWidgets.end() )
376  {
377  if ( ( *it ).second == indx )
378  it = mRegisteredSearchWidgets.erase( it );
379  else
380  ++it;
381  }
382 }
383 
385 {
386  QMessageBox::warning( nullptr, tr( "Missing objects" ),
387  tr( "Base options dialog could not be initialized.\n\n"
388  "Missing some of the .ui template objects:\n" )
389  + " mOptionsListWidget,\n mOptionsStackedWidget,\n mOptionsSplitter,\n mOptionsListFrame",
390  QMessageBox::Ok,
391  QMessageBox::Ok );
392 }
393 
394 
396  : QObject( widget )
397  , mWidget( widget )
398  , mStyleSheet( QString() )
399  , mValid( true )
400  , mChangedStyle( false )
401  , mText( [ = ]() {return QString();} )
402 {
403  if ( qobject_cast<QLabel *>( widget ) )
404  {
405  mStyleSheet = "QLabel { background-color: yellow; color: blue;}";
406  mText = [ = ]() {return qobject_cast<QLabel *>( mWidget )->text();};
407  }
408  else if ( qobject_cast<QCheckBox *>( widget ) )
409  {
410  mStyleSheet = "QCheckBox { background-color: yellow; color: blue;}";
411  mText = [ = ]() {return qobject_cast<QCheckBox *>( mWidget )->text();};
412  }
413  else if ( qobject_cast<QAbstractButton *>( widget ) )
414  {
415  mStyleSheet = "QAbstractButton { background-color: yellow; color: blue;}";
416  mText = [ = ]() {return qobject_cast<QAbstractButton *>( mWidget )->text();};
417  }
418  else if ( qobject_cast<QGroupBox *>( widget ) )
419  {
420  mStyleSheet = "QGroupBox::title { background-color: yellow; color: blue;}";
421  mText = [ = ]() {return qobject_cast<QGroupBox *>( mWidget )->title();};
422  }
423  else if ( qobject_cast<QTreeView *>( widget ) )
424  {
425  // TODO - style individual matching items
426  }
427  else
428  {
429  mValid = false;
430  }
431  if ( mValid )
432  {
433  mStyleSheet.prepend( "/*!search!*/" ).append( "/*!search!*/" );
434  QgsDebugMsgLevel( mStyleSheet, 4 );
435  connect( mWidget, &QWidget::destroyed, this, &QgsSearchHighlightOptionWidget::widgetDestroyed );
436  }
437 }
438 
439 bool QgsSearchHighlightOptionWidget::searchHighlight( const QString &searchText )
440 {
441  bool found = false;
442  if ( !mWidget )
443  return found;
444 
445  if ( !searchText.isEmpty() )
446  {
447  if ( QTreeView *tree = qobject_cast<QTreeView *>( mWidget ) )
448  {
449  QModelIndexList hits = tree->model()->match( tree->model()->index( 0, 0 ), Qt::DisplayRole, searchText, 1, Qt::MatchContains | Qt::MatchRecursive );
450  found = !hits.isEmpty();
451  }
452  else
453  {
454  QString origText = mText();
455  if ( origText.contains( searchText, Qt::CaseInsensitive ) )
456  {
457  found = true;
458  }
459  }
460  }
461 
462  if ( found && !mChangedStyle )
463  {
464  if ( !mWidget->isVisible() )
465  {
466  // show the widget to get initial stylesheet in case it's modified
467  mWidget->show();
468  }
469  mWidget->setStyleSheet( mWidget->styleSheet() + mStyleSheet );
470  mChangedStyle = true;
471  }
472 
473  return found;
474 }
475 
477 {
478  if ( mValid && mChangedStyle )
479  {
480  QString ss = mWidget->styleSheet();
481  ss.remove( mStyleSheet );
482  mWidget->setStyleSheet( ss );
483  mChangedStyle = false;
484  }
485 }
486 
487 void QgsSearchHighlightOptionWidget::widgetDestroyed()
488 {
489  mValid = false;
490 }
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:54
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
QWidget * widget()
return the widget
void optionsStackedWidget_WidgetRemoved(int indx)
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
void optionsStackedWidget_CurrentChanged(int indx)
Container for a widget to be used to search text in the option dialog If the widget type is handled...
QStackedWidget * mOptStackedWidget