1 /***************************************************************************
2     qgsdatetimeedit.cpp
3      --------------------------------------
4     Date                 : 08.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 <QAction>
17 #include <QCalendarWidget>
18 #include <QLineEdit>
19 #include <QMouseEvent>
20 #include <QStyle>
21 #include <QStyleOptionSpinBox>
22 
23 
24 #include "qgsdatetimeedit.h"
25 
26 #include "qgsapplication.h"
27 #include "qgslogger.h"
28 
29 
30 
QgsDateTimeEdit(QWidget * parent)31 QgsDateTimeEdit::QgsDateTimeEdit( QWidget *parent )
32 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
33   : QgsDateTimeEdit( QDateTime(), QVariant::DateTime, parent )
34 #else
35   : QgsDateTimeEdit( QDateTime(), QMetaType::QDateTime, parent )
36 #endif
37 {
38 
39 }
40 
41 ///@cond PRIVATE
42 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
QgsDateTimeEdit(const QVariant & var,QVariant::Type parserType,QWidget * parent)43 QgsDateTimeEdit::QgsDateTimeEdit( const QVariant &var, QVariant::Type parserType, QWidget *parent )
44   : QDateTimeEdit( var, parserType, parent )
45 #else
46 QgsDateTimeEdit::QgsDateTimeEdit( const QVariant & var, QMetaType::Type parserType, QWidget * parent )
47   : QDateTimeEdit( var, parserType, parent )
48 #endif
49   , mNullRepresentation( QgsApplication::nullRepresentation() )
50 {
51   const QIcon clearIcon = QgsApplication::getThemeIcon( "/mIconClearText.svg" );
52   mClearAction = new QAction( clearIcon, tr( "clear" ), this );
53   mClearAction->setCheckable( false );
54   lineEdit()->addAction( mClearAction, QLineEdit::TrailingPosition );
55   mClearAction->setVisible( mAllowNull );
56   connect( mClearAction, &QAction::triggered, this, &QgsDateTimeEdit::clear );
57 
58   connect( this, &QDateTimeEdit::dateTimeChanged, this, &QgsDateTimeEdit::changed );
59 
60   // enable calendar widget by default so it's already created
61   setCalendarPopup( true );
62 
63   setMinimumEditDateTime();
64 
65   // init with current time so mIsNull is properly initialized
66   QDateTimeEdit::setDateTime( QDateTime::currentDateTime() );
67 }
68 ///@endcond
69 
setAllowNull(bool allowNull)70 void QgsDateTimeEdit::setAllowNull( bool allowNull )
71 {
72   mAllowNull = allowNull;
73   mClearAction->setVisible( mAllowNull && ( !mIsNull || mIsEmpty ) );
74 }
75 
76 
clear()77 void QgsDateTimeEdit::clear()
78 {
79   if ( mAllowNull )
80   {
81     displayCurrentDate();
82 
83     // Check if it's really changed or crash, see GH #29937
84     if ( ! dateTime().isNull() )
85     {
86       changed( QDateTime() );
87     }
88 
89     // emit signal of QDateTime::dateTimeChanged with an invalid date
90     // anyway, using parent's signal should be avoided
91     // If you consequently connect parent's dateTimeChanged signal
92     // and call dateTime() afterwards there is no warranty to
93     // have a proper NULL value handling
94     disconnect( this, &QDateTimeEdit::dateTimeChanged, this, &QgsDateTimeEdit::changed );
95     emit dateTimeChanged( QDateTime() );
96     connect( this, &QDateTimeEdit::dateTimeChanged, this, &QgsDateTimeEdit::changed );
97   }
98 }
99 
setEmpty()100 void QgsDateTimeEdit::setEmpty()
101 {
102   mClearAction->setVisible( mAllowNull );
103   mIsEmpty = true;
104 }
105 
mousePressEvent(QMouseEvent * event)106 void QgsDateTimeEdit::mousePressEvent( QMouseEvent *event )
107 {
108   // catch mouse press on the button (when the current value is null)
109   // in non-calendar mode: modify the date  so it leads to showing current date (don't bother about time)
110   // in calendar mode: be sure NULL is displayed when needed and show page of current date in calendar widget
111 
112   bool updateCalendar = false;
113 
114   if ( mIsNull )
115   {
116     QStyle::SubControl control;
117     if ( calendarPopup() )
118     {
119       QStyleOptionComboBox optCombo;
120 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
121       optCombo.init( this );
122 #else
123       optCombo.initFrom( this );
124 #endif
125       optCombo.editable = true;
126       optCombo.subControls = QStyle::SC_All;
127       control = style()->hitTestComplexControl( QStyle::CC_ComboBox, &optCombo, event->pos(), this );
128 
129       if ( control == QStyle::SC_ComboBoxArrow && calendarWidget() )
130       {
131         mCurrentPressEvent = true;
132         // ensure the line edit still displays NULL
133         updateCalendar = true;
134         displayNull( updateCalendar );
135         mCurrentPressEvent = false;
136       }
137     }
138     else
139     {
140       QStyleOptionSpinBox opt;
141       this->initStyleOption( &opt );
142       control  = style()->hitTestComplexControl( QStyle::CC_SpinBox, &opt, event->pos(), this );
143 
144       if ( control == QStyle::SC_SpinBoxDown || control == QStyle::SC_SpinBoxUp )
145       {
146         mCurrentPressEvent = true;
147         disconnect( this, &QDateTimeEdit::dateTimeChanged, this, &QgsDateTimeEdit::changed );
148         resetBeforeChange( control == QStyle::SC_SpinBoxDown ? -1 : 1 );
149         connect( this, &QDateTimeEdit::dateTimeChanged, this, &QgsDateTimeEdit::changed );
150         mCurrentPressEvent = false;
151       }
152     }
153   }
154 
155   QDateTimeEdit::mousePressEvent( event );
156 
157   if ( updateCalendar )
158   {
159     // set calendar page to current date to avoid going to minimal date page when value is null
160     calendarWidget()->setCurrentPage( QDate::currentDate().year(), QDate::currentDate().month() );
161   }
162 }
163 
focusOutEvent(QFocusEvent * event)164 void QgsDateTimeEdit::focusOutEvent( QFocusEvent *event )
165 {
166   if ( mAllowNull && mIsNull && !mCurrentPressEvent )
167   {
168     QAbstractSpinBox::focusOutEvent( event );
169     if ( lineEdit()->text() != mNullRepresentation )
170     {
171       displayNull();
172     }
173     emit editingFinished();
174   }
175   else
176   {
177     QDateTimeEdit::focusOutEvent( event );
178   }
179 }
180 
focusInEvent(QFocusEvent * event)181 void QgsDateTimeEdit::focusInEvent( QFocusEvent *event )
182 {
183   if ( mAllowNull && mIsNull && !mCurrentPressEvent )
184   {
185     QAbstractSpinBox::focusInEvent( event );
186 
187     displayCurrentDate();
188   }
189   else
190   {
191     QDateTimeEdit::focusInEvent( event );
192   }
193 }
194 
wheelEvent(QWheelEvent * event)195 void QgsDateTimeEdit::wheelEvent( QWheelEvent *event )
196 {
197   // dateTime might have been set to minimum in calendar mode
198   if ( mAllowNull && mIsNull )
199   {
200     // convert angleDelta to approximate wheel "steps" -- angleDelta is in 1/8 degrees, and according
201     // to Qt docs most mice step in 15 degree increments
202     resetBeforeChange( -event->angleDelta().y() / ( 15 * 8 ) );
203   }
204   QDateTimeEdit::wheelEvent( event );
205 }
206 
showEvent(QShowEvent * event)207 void QgsDateTimeEdit::showEvent( QShowEvent *event )
208 {
209   QDateTimeEdit::showEvent( event );
210   if ( mAllowNull && mIsNull &&
211        lineEdit()->text() != mNullRepresentation )
212   {
213     displayNull();
214   }
215 }
216 
217 ///@cond PRIVATE
changed(const QVariant & dateTime)218 void QgsDateTimeEdit::changed( const QVariant &dateTime )
219 {
220   mIsEmpty = false;
221   const bool isNull = dateTime.isNull();
222   if ( isNull != mIsNull )
223   {
224     mIsNull = isNull;
225     if ( mIsNull )
226     {
227       if ( mOriginalStyleSheet.isNull() )
228       {
229         mOriginalStyleSheet = lineEdit()->styleSheet();
230       }
231       lineEdit()->setStyleSheet( QStringLiteral( "QLineEdit { font-style: italic; color: grey; }" ) );
232     }
233     else
234     {
235       lineEdit()->setStyleSheet( mOriginalStyleSheet );
236     }
237   }
238 
239   mClearAction->setVisible( mAllowNull && !mIsNull );
240   if ( !mBlockChangedSignal )
241     emitValueChanged( dateTime );
242 }
243 ///@endcond
244 
nullRepresentation() const245 QString QgsDateTimeEdit::nullRepresentation() const
246 {
247   return mNullRepresentation;
248 }
249 
setNullRepresentation(const QString & nullRepresentation)250 void QgsDateTimeEdit::setNullRepresentation( const QString &nullRepresentation )
251 {
252   mNullRepresentation = nullRepresentation;
253   if ( mIsNull )
254   {
255     lineEdit()->setText( mNullRepresentation );
256   }
257 }
258 
displayNull(bool updateCalendar)259 void QgsDateTimeEdit::displayNull( bool updateCalendar )
260 {
261   disconnect( this, &QDateTimeEdit::dateTimeChanged, this, &QgsDateTimeEdit::changed );
262   if ( updateCalendar )
263   {
264     // set current time to minimum date time to avoid having
265     // a date selected in calendar widget
266     QDateTimeEdit::setDateTime( minimumDateTime() );
267   }
268   lineEdit()->setCursorPosition( lineEdit()->text().length() );
269   lineEdit()->setText( mNullRepresentation );
270   connect( this, &QDateTimeEdit::dateTimeChanged, this, &QgsDateTimeEdit::changed );
271 }
272 
emitValueChanged(const QVariant & value)273 void QgsDateTimeEdit::emitValueChanged( const QVariant &value )
274 {
275   emit QgsDateTimeEdit::valueChanged( value.toDateTime() );
276 }
277 
isNull() const278 bool QgsDateTimeEdit::isNull() const
279 {
280   return mAllowNull && mIsNull;
281 }
282 
displayCurrentDate()283 void QgsDateTimeEdit::displayCurrentDate()
284 {
285   disconnect( this, &QDateTimeEdit::dateTimeChanged, this, &QgsDateTimeEdit::changed );
286   QDateTimeEdit::setDateTime( QDateTime::currentDateTime() );
287   connect( this, &QDateTimeEdit::dateTimeChanged, this, &QgsDateTimeEdit::changed );
288 }
289 
resetBeforeChange(int delta)290 void QgsDateTimeEdit::resetBeforeChange( int delta )
291 {
292   QDateTime dt = QDateTime::currentDateTime();
293   switch ( currentSection() )
294   {
295     case QDateTimeEdit::DaySection:
296       dt = dt.addDays( delta );
297       break;
298     case QDateTimeEdit::MonthSection:
299       dt = dt.addMonths( delta );
300       break;
301     case QDateTimeEdit::YearSection:
302       dt = dt.addYears( delta );
303       break;
304     default:
305       break;
306   }
307   if ( dt < minimumDateTime() )
308   {
309     dt = minimumDateTime();
310   }
311   else if ( dt > maximumDateTime() )
312   {
313     dt = maximumDateTime();
314   }
315   QDateTimeEdit::setDateTime( dt );
316 }
317 
setDateTime(const QDateTime & dateTime)318 void QgsDateTimeEdit::setDateTime( const QDateTime &dateTime )
319 {
320   mIsEmpty = false;
321 
322   // set an undefined date
323   if ( !dateTime.isValid() || dateTime.isNull() )
324   {
325     clear();
326     displayNull();
327   }
328   // Check if it's really changed or crash, see GH #29937
329   else if ( dateTime != QgsDateTimeEdit::dateTime() )
330   {
331     // changed emits a signal, so don't allow it to be emitted from setDateTime
332     mBlockChangedSignal++;
333     QDateTimeEdit::setDateTime( dateTime );
334     mBlockChangedSignal--;
335     changed( dateTime );
336   }
337 }
338 
dateTime() const339 QDateTime QgsDateTimeEdit::dateTime() const
340 {
341   if ( isNull() )
342   {
343     return QDateTime();
344   }
345   else
346   {
347     return QDateTimeEdit::dateTime();
348   }
349 }
350 
time() const351 QTime QgsDateTimeEdit::time() const
352 {
353   if ( isNull() )
354   {
355     return QTime();
356   }
357   else
358   {
359     return QDateTimeEdit::time();
360   }
361 }
362 
date() const363 QDate QgsDateTimeEdit::date() const
364 {
365   if ( isNull() )
366   {
367     return QDate();
368   }
369   else
370   {
371     return QDateTimeEdit::date();
372   }
373 }
374 
375 
376 //
377 // QgsTimeEdit
378 //
379 
QgsTimeEdit(QWidget * parent)380 QgsTimeEdit::QgsTimeEdit( QWidget *parent )
381 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
382   : QgsDateTimeEdit( QTime(), QVariant::Time, parent )
383 #else
384   : QgsDateTimeEdit( QTime(), QMetaType::QTime, parent )
385 #endif
386 {
387 
388 }
389 
setTime(const QTime & time)390 void QgsTimeEdit::setTime( const QTime &time )
391 {
392   mIsEmpty = false;
393 
394   // set an undefined date
395   if ( !time.isValid() || time.isNull() )
396   {
397     clear();
398     displayNull();
399   }
400   // Check if it's really changed or crash, see GH #29937
401   else if ( time != QgsTimeEdit::time() )
402   {
403     // changed emits a signal, so don't allow it to be emitted from setTime
404     mBlockChangedSignal++;
405     QDateTimeEdit::setTime( time );
406     mBlockChangedSignal--;
407     changed( time );
408   }
409 }
410 
emitValueChanged(const QVariant & value)411 void QgsTimeEdit::emitValueChanged( const QVariant &value )
412 {
413   emit timeValueChanged( value.toTime() );
414 }
415 
416 
417 //
418 // QgsDateEdit
419 //
420 
QgsDateEdit(QWidget * parent)421 QgsDateEdit::QgsDateEdit( QWidget *parent )
422 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
423   : QgsDateTimeEdit( QDate(), QVariant::Date, parent )
424 #else
425   : QgsDateTimeEdit( QDate(), QMetaType::QDate, parent )
426 #endif
427 {
428 
429 }
430 
setDate(const QDate & date)431 void QgsDateEdit::setDate( const QDate &date )
432 {
433   mIsEmpty = false;
434 
435   // set an undefined date
436   if ( !date.isValid() || date.isNull() )
437   {
438     clear();
439     displayNull();
440   }
441   // Check if it's really changed or crash, see GH #29937
442   else if ( date != QgsDateEdit::date() )
443   {
444     // changed emits a signal, so don't allow it to be emitted from setDate
445     mBlockChangedSignal++;
446     QDateTimeEdit::setDate( date );
447     mBlockChangedSignal--;
448     changed( date );
449   }
450 }
451 
emitValueChanged(const QVariant & value)452 void QgsDateEdit::emitValueChanged( const QVariant &value )
453 {
454   emit dateValueChanged( value.toDate() );
455 }
456