1 /***************************************************************************
2     qgsspinbox.cpp
3      --------------------------------------
4     Date                 : 09.2014
5     Copyright            : (C) 2014 Denis Rouzaud
6     Email                : denis.rouzaud@gmail.com
7  ***************************************************************************
8  *                                                                         *
9  *   This program is free software; you can redistribute it and/or modify  *
10  *   it under the terms of the GNU General Public License as published by  *
11  *   the Free Software Foundation; either version 2 of the License, or     *
12  *   (at your option) any later version.                                   *
13  *                                                                         *
14  ***************************************************************************/
15 
16 #include <QLineEdit>
17 #include <QMouseEvent>
18 #include <QSettings>
19 #include <QStyle>
20 
21 #include "qgsspinbox.h"
22 #include "qgsexpression.h"
23 #include "qgsapplication.h"
24 #include "qgslogger.h"
25 #include "qgsfilterlineedit.h"
26 
27 #define CLEAR_ICON_SIZE 16
28 
29 // This is required because private implementation of
30 // QAbstractSpinBoxPrivate checks for specialText emptiness
31 // and skips specialText handling if it's empty
32 #ifdef _MSC_VER
33 static QChar SPECIAL_TEXT_WHEN_EMPTY = QChar( 0x2063 );
34 #else
35 static constexpr QChar SPECIAL_TEXT_WHEN_EMPTY = QChar( 0x2063 );
36 #endif
37 
QgsSpinBox(QWidget * parent)38 QgsSpinBox::QgsSpinBox( QWidget *parent )
39   : QSpinBox( parent )
40 {
41   mLineEdit = new QgsSpinBoxLineEdit();
42   setLineEdit( mLineEdit );
43 
44   const QSize msz = minimumSizeHint();
45   setMinimumSize( msz.width() + CLEAR_ICON_SIZE + 9 + frameWidth() * 2 + 2,
46                   std::max( msz.height(), CLEAR_ICON_SIZE + frameWidth() * 2 + 2 ) );
47 
48   connect( mLineEdit, &QgsFilterLineEdit::cleared, this, &QgsSpinBox::clear );
49   connect( this, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsSpinBox::changed );
50 }
51 
setShowClearButton(const bool showClearButton)52 void QgsSpinBox::setShowClearButton( const bool showClearButton )
53 {
54   mShowClearButton = showClearButton;
55   mLineEdit->setShowClearButton( showClearButton );
56 }
57 
setExpressionsEnabled(const bool enabled)58 void QgsSpinBox::setExpressionsEnabled( const bool enabled )
59 {
60   mExpressionsEnabled = enabled;
61 }
62 
changeEvent(QEvent * event)63 void QgsSpinBox::changeEvent( QEvent *event )
64 {
65   QSpinBox::changeEvent( event );
66 
67   if ( event->type() == QEvent::FontChange )
68   {
69     lineEdit()->setFont( font() );
70   }
71 
72   mLineEdit->setShowClearButton( shouldShowClearForValue( value() ) );
73 }
74 
paintEvent(QPaintEvent * event)75 void QgsSpinBox::paintEvent( QPaintEvent *event )
76 {
77   mLineEdit->setShowClearButton( shouldShowClearForValue( value() ) );
78   QSpinBox::paintEvent( event );
79 }
80 
wheelEvent(QWheelEvent * event)81 void QgsSpinBox::wheelEvent( QWheelEvent *event )
82 {
83   const int step = singleStep();
84   if ( event->modifiers() & Qt::ControlModifier )
85   {
86     // ctrl modifier results in finer increments - 10% of usual step
87     int newStep = step / 10;
88     // step should be at least 1
89     newStep = std::max( newStep, 1 );
90 
91     setSingleStep( newStep );
92 
93     // clear control modifier before handing off event - Qt uses it for unwanted purposes
94     // (*increasing* step size, whereas QGIS UX convention is that control modifier
95     // results in finer changes!)
96     event->setModifiers( event->modifiers() & ~Qt::ControlModifier );
97   }
98   QSpinBox::wheelEvent( event );
99   setSingleStep( step );
100 }
101 
timerEvent(QTimerEvent * event)102 void QgsSpinBox::timerEvent( QTimerEvent *event )
103 {
104   // Process all events, which may include a mouse release event
105   // Only allow the timer to trigger additional value changes if the user
106   // has in fact held the mouse button, rather than the timer expiry
107   // simply appearing before the mouse release in the event queue
108   qApp->processEvents();
109   if ( QApplication::mouseButtons() & Qt::LeftButton )
110     QSpinBox::timerEvent( event );
111 }
112 
changed(int value)113 void QgsSpinBox::changed( int value )
114 {
115   mLineEdit->setShowClearButton( shouldShowClearForValue( value ) );
116 }
117 
clear()118 void QgsSpinBox::clear()
119 {
120   setValue( clearValue() );
121   if ( mLineEdit->isNull() )
122     mLineEdit->clear();
123 }
124 
setClearValue(int customValue,const QString & specialValueText)125 void QgsSpinBox::setClearValue( int customValue, const QString &specialValueText )
126 {
127   mClearValueMode = CustomValue;
128   mCustomClearValue = customValue;
129 
130   if ( !specialValueText.isEmpty() )
131   {
132     const int v = value();
133     clear();
134     setSpecialValueText( specialValueText );
135     setValue( v );
136   }
137 }
138 
setClearValueMode(QgsSpinBox::ClearValueMode mode,const QString & specialValueText)139 void QgsSpinBox::setClearValueMode( QgsSpinBox::ClearValueMode mode, const QString &specialValueText )
140 {
141   mClearValueMode = mode;
142   mCustomClearValue = 0;
143 
144   if ( !specialValueText.isEmpty() )
145   {
146     const int v = value();
147     clear();
148     setSpecialValueText( specialValueText );
149     setValue( v );
150   }
151 }
152 
clearValue() const153 int QgsSpinBox::clearValue() const
154 {
155   if ( mClearValueMode == MinimumValue )
156     return minimum();
157   else if ( mClearValueMode == MaximumValue )
158     return maximum();
159   else
160     return mCustomClearValue;
161 }
162 
setLineEditAlignment(Qt::Alignment alignment)163 void QgsSpinBox::setLineEditAlignment( Qt::Alignment alignment )
164 {
165   mLineEdit->setAlignment( alignment );
166 }
167 
setSpecialValueText(const QString & txt)168 void QgsSpinBox::setSpecialValueText( const QString &txt )
169 {
170   if ( txt.isEmpty() )
171   {
172     QSpinBox::setSpecialValueText( SPECIAL_TEXT_WHEN_EMPTY );
173     mLineEdit->setNullValue( SPECIAL_TEXT_WHEN_EMPTY );
174   }
175   else
176   {
177     QSpinBox::setSpecialValueText( txt );
178     mLineEdit->setNullValue( txt );
179   }
180 }
181 
valueFromText(const QString & text) const182 int QgsSpinBox::valueFromText( const QString &text ) const
183 {
184   if ( !mExpressionsEnabled )
185   {
186     return QSpinBox::valueFromText( text );
187   }
188 
189   const QString trimmedText = stripped( text );
190   if ( trimmedText.isEmpty() )
191   {
192     return mShowClearButton ? clearValue() : value();
193   }
194 
195   return std::round( QgsExpression::evaluateToDouble( trimmedText, value() ) );
196 }
197 
validate(QString & input,int & pos) const198 QValidator::State QgsSpinBox::validate( QString &input, int &pos ) const
199 {
200   if ( !mExpressionsEnabled )
201   {
202     const QValidator::State r = QSpinBox::validate( input, pos );
203     return r;
204   }
205 
206   return QValidator::Acceptable;
207 }
208 
frameWidth() const209 int QgsSpinBox::frameWidth() const
210 {
211   return style()->pixelMetric( QStyle::PM_DefaultFrameWidth );
212 }
213 
shouldShowClearForValue(const int value) const214 bool QgsSpinBox::shouldShowClearForValue( const int value ) const
215 {
216   if ( !mShowClearButton || !isEnabled() )
217   {
218     return false;
219   }
220   return value != clearValue();
221 }
222 
stripped(const QString & originalText) const223 QString QgsSpinBox::stripped( const QString &originalText ) const
224 {
225   //adapted from QAbstractSpinBoxPrivate::stripped
226   //trims whitespace, prefix and suffix from spin box text
227   QString text = originalText;
228   if ( specialValueText().isEmpty() || text != specialValueText() )
229   {
230     // Strip SPECIAL_TEXT_WHEN_EMPTY
231     if ( text.contains( SPECIAL_TEXT_WHEN_EMPTY ) )
232       text = text.replace( SPECIAL_TEXT_WHEN_EMPTY, QString() );
233     int from = 0;
234     int size = text.size();
235     bool changed = false;
236     if ( !prefix().isEmpty() && text.startsWith( prefix() ) )
237     {
238       from += prefix().size();
239       size -= from;
240       changed = true;
241     }
242     if ( !suffix().isEmpty() && text.endsWith( suffix() ) )
243     {
244       size -= suffix().size();
245       changed = true;
246     }
247     if ( changed )
248       text = text.mid( from, size );
249   }
250 
251   text = text.trimmed();
252 
253   return text;
254 }
255