1 /*
2  * Copyright 2000-2003  Michael Edwardes <mte@users.sourceforge.net>
3  * Copyright 2001       Felix Rodriguez <frodriguez@users.sourceforge.net>
4  * Copyright 2017-2018  Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License as
8  * published by the Free Software Foundation; either version 2 of
9  * the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include "kmymoneydateinput.h"
21 #include "kmymoneysettings.h"
22 
23 // ----------------------------------------------------------------------------
24 // QT Includes
25 
26 #include <QPoint>
27 #include <QApplication>
28 #include <QDesktopWidget>
29 #include <QTimer>
30 #include <QLabel>
31 #include <QKeyEvent>
32 #include <QEvent>
33 #include <QDateEdit>
34 #include <QLineEdit>
35 #include <QPushButton>
36 #include <QIcon>
37 #include <QVBoxLayout>
38 
39 // ----------------------------------------------------------------------------
40 // KDE Includes
41 
42 #include <KLocalizedString>
43 #include <KPassivePopup>
44 #include <KDatePicker>
45 
46 // ----------------------------------------------------------------------------
47 // Project Includes
48 
49 #include "icons/icons.h"
50 
51 using namespace Icons;
52 
53 namespace
54 {
55 const int DATE_POPUP_TIMEOUT = 1500;
56 const QDate INVALID_DATE = QDate(1800, 1, 1);
57 }
58 
OldDateEdit(const QDate & date,QWidget * parent)59 KMyMoney::OldDateEdit::OldDateEdit(const QDate& date, QWidget* parent)
60   : QDateEdit(date, parent)
61   , m_initialSection(QDateTimeEdit::DaySection)
62   , m_initStage(Created)
63 {
64 }
65 
keyPressEvent(QKeyEvent * k)66 void KMyMoney::OldDateEdit::keyPressEvent(QKeyEvent* k)
67 {
68   if ((lineEdit()->text().isEmpty() || lineEdit()->selectedText() == lineEdit()->text()) && QChar(k->key()).isDigit()) {
69     // the line edit is empty which means that the date was cleared
70     // or the whole text is selected and a digit character was entered
71     // (the same meaning as clearing the date) - in this case set the date
72     // to the current date and let the editor do the actual work
73     setDate(QDate::currentDate());
74     setSelectedSection(m_initialSection); // start as when focused in if the date was cleared
75   }
76   QDateEdit::keyPressEvent(k);
77 }
78 
focusInEvent(QFocusEvent * event)79 void KMyMoney::OldDateEdit::focusInEvent(QFocusEvent * event)
80 {
81   QDateEdit::focusInEvent(event);
82   setSelectedSection(m_initialSection);
83 }
84 
event(QEvent * e)85 bool KMyMoney::OldDateEdit::event(QEvent* e)
86 {
87   // make sure that we keep the current date setting of a KMyMoneyDateInput object
88   // across the QDateEdit::event(FocusOutEvent)
89   bool rc;
90 
91   KMyMoneyDateInput* p = dynamic_cast<KMyMoneyDateInput*>(parentWidget());
92   if (e->type() == QEvent::FocusOut && p) {
93     QDate d = p->date();
94     rc = QDateEdit::event(e);
95     if (d.isValid())
96       d = p->date();
97     p->loadDate(d);
98   } else {
99     rc = QDateEdit::event(e);
100   }
101   switch (m_initStage) {
102     case Created:
103       if (e->type() == QEvent::FocusIn) {
104         m_initStage = GotFocus;
105       }
106       break;
107     case GotFocus:
108       if (e->type() == QEvent::MouseButtonPress) {
109         // create a phony corresponding release event
110         QMouseEvent* mev = static_cast<QMouseEvent*>(e);
111         QMouseEvent release(QEvent::MouseButtonRelease,
112                             mev->localPos(),
113                             mev->windowPos(),
114                             mev->screenPos(),
115                             mev->button(),
116                             mev->buttons(),
117                             mev->modifiers(),
118                             mev->source());
119         QApplication::sendEvent(this, &release);
120         m_initStage = FirstMousePress;
121       }
122       break;
123     case FirstMousePress:
124       break;
125   }
126   return rc;
127 }
128 
focusNextPrevChild(bool next)129 bool KMyMoney::OldDateEdit::focusNextPrevChild(bool next)
130 {
131   Q_UNUSED(next)
132   return true;
133 }
134 
135 struct KMyMoneyDateInput::Private {
136   KMyMoney::OldDateEdit *m_dateEdit;
137   KDatePicker *m_datePicker;
138   QDate m_date;
139   QDate m_prevDate;
140   Qt::AlignmentFlag m_qtalignment;
141   QWidget *m_dateFrame;
142   QPushButton *m_dateButton;
143   KPassivePopup *m_datePopup;
144 };
145 
KMyMoneyDateInput(QWidget * parent,Qt::AlignmentFlag flags)146 KMyMoneyDateInput::KMyMoneyDateInput(QWidget *parent, Qt::AlignmentFlag flags)
147     : QWidget(parent), d(new Private)
148 {
149   d->m_qtalignment = flags;
150   d->m_date = QDate::currentDate();
151 
152   QHBoxLayout *dateInputLayout = new QHBoxLayout(this);
153   dateInputLayout->setSpacing(0);
154   dateInputLayout->setContentsMargins(0, 0, 0, 0);
155   d->m_dateEdit = new KMyMoney::OldDateEdit(d->m_date, this);
156   dateInputLayout->addWidget(d->m_dateEdit, 3);
157   setFocusProxy(d->m_dateEdit);
158   d->m_dateEdit->installEventFilter(this); // To get d->m_dateEdit's FocusIn/Out and some KeyPress events
159 
160   // we use INVALID_DATE as a special value for multi transaction editing
161   d->m_dateEdit->setMinimumDate(INVALID_DATE);
162   d->m_dateEdit->setSpecialValueText(QLatin1String(" "));
163 
164   d->m_datePopup = new KPassivePopup(d->m_dateEdit);
165   d->m_datePopup->setObjectName("datePopup");
166   d->m_datePopup->setTimeout(DATE_POPUP_TIMEOUT);
167   d->m_datePopup->setView(new QLabel(QLocale().toString(d->m_date), d->m_datePopup));
168 
169   d->m_dateFrame = new QWidget(this);
170   dateInputLayout->addWidget(d->m_dateFrame);
171   QVBoxLayout *dateFrameVBoxLayout = new QVBoxLayout(d->m_dateFrame);
172   dateFrameVBoxLayout->setMargin(0);
173   dateFrameVBoxLayout->setContentsMargins(0, 0, 0, 0);
174   d->m_dateFrame->setWindowFlags(Qt::Popup);
175   d->m_dateFrame->hide();
176 
177   d->m_dateEdit->setDisplayFormat(QLocale().dateFormat(QLocale::ShortFormat));
178   switch(KMyMoneySettings::initialDateFieldCursorPosition()) {
179     case KMyMoneySettings::Day:
180       d->m_dateEdit->setInitialSection(QDateTimeEdit::DaySection);
181       break;
182     case KMyMoneySettings::Month:
183       d->m_dateEdit->setInitialSection(QDateTimeEdit::MonthSection);
184       break;
185     case KMyMoneySettings::Year:
186       d->m_dateEdit->setInitialSection(QDateTimeEdit::YearSection);
187       break;
188   }
189 
190   d->m_datePicker = new KDatePicker(d->m_date, d->m_dateFrame);
191   dateFrameVBoxLayout->addWidget(d->m_datePicker);
192   // Let the date picker have a close button (Added in 3.1)
193   d->m_datePicker->setCloseButton(true);
194 
195   // the next line is a try to add an icon to the button
196   d->m_dateButton = new QPushButton(Icons::get(Icon::CalendarDay), QString(), this);
197   dateInputLayout->addWidget(d->m_dateButton);
198 
199   connect(d->m_dateButton, &QAbstractButton::clicked, this, &KMyMoneyDateInput::toggleDatePicker);
200   connect(d->m_dateEdit, &QDateTimeEdit::dateChanged, this, &KMyMoneyDateInput::slotDateChosenRef);
201   connect(d->m_datePicker, &KDatePicker::dateSelected, this, &KMyMoneyDateInput::slotDateChosen);
202   connect(d->m_datePicker, &KDatePicker::dateEntered, this, &KMyMoneyDateInput::slotDateChosen);
203   connect(d->m_datePicker, &KDatePicker::dateSelected, d->m_dateFrame, &QWidget::hide);
204 }
205 
markAsBadDate(bool bad,const QColor & color)206 void KMyMoneyDateInput::markAsBadDate(bool bad, const QColor& color)
207 {
208   // the next line knows a bit about the internals of QAbstractSpinBox
209   QLineEdit* le = d->m_dateEdit->findChild<QLineEdit *>(); //krazy:exclude=qclasses
210 
211   if (le) {
212     QPalette palette = this->palette();
213     le->setPalette(palette);
214     if (bad) {
215       palette.setColor(foregroundRole(), color);
216       le->setPalette(palette);
217     }
218   }
219 }
220 
showEvent(QShowEvent * event)221 void KMyMoneyDateInput::showEvent(QShowEvent* event)
222 {
223   // don't forget the standard behaviour  ;-)
224   QWidget::showEvent(event);
225 
226   // If the widget is shown, the size must be fixed a little later
227   // to be appropriate. I saw this in some other places and the only
228   // way to solve this problem is to postpone the setup of the size
229   // to the time when the widget is on the screen.
230   QTimer::singleShot(50, this, SLOT(fixSize()));
231 }
232 
fixSize()233 void KMyMoneyDateInput::fixSize()
234 {
235   // According to a hint in the documentation of KDatePicker::sizeHint()
236   // 28 pixels should be added in each direction to obtain a better
237   // display of the month button. I decided, (22,14) is good
238   // enough and save some space on the screen (ipwizard)
239   d->m_dateFrame->setFixedSize(d->m_datePicker->sizeHint() + QSize(22, 14));
240 }
241 
~KMyMoneyDateInput()242 KMyMoneyDateInput::~KMyMoneyDateInput()
243 {
244   delete d->m_dateFrame;
245   delete d->m_datePopup;
246   delete d;
247 }
248 
toggleDatePicker()249 void KMyMoneyDateInput::toggleDatePicker()
250 {
251   int w = d->m_dateFrame->width();
252   int h = d->m_dateFrame->height();
253 
254   if (d->m_dateFrame->isVisible()) {
255     d->m_dateFrame->hide();
256   } else {
257     QPoint tmpPoint = mapToGlobal(d->m_dateButton->geometry().bottomRight());
258 
259     // usually, the datepicker widget is shown underneath the d->m_dateEdit widget
260     // if it does not fit on the screen, we show it above this widget
261 
262     if (tmpPoint.y() + h > QApplication::desktop()->height()) {
263       tmpPoint.setY(tmpPoint.y() - h - d->m_dateButton->height());
264     }
265 
266     if ((d->m_qtalignment == Qt::AlignRight && tmpPoint.x() + w <= QApplication::desktop()->width())
267         || (tmpPoint.x() - w < 0)) {
268       d->m_dateFrame->setGeometry(tmpPoint.x() - width(), tmpPoint.y(), w, h);
269     } else {
270       tmpPoint.setX(tmpPoint.x() - w);
271       d->m_dateFrame->setGeometry(tmpPoint.x(), tmpPoint.y(), w, h);
272     }
273 
274     if (d->m_date.isValid() && d->m_date != INVALID_DATE) {
275       d->m_datePicker->setDate(d->m_date);
276     } else {
277       d->m_datePicker->setDate(QDate::currentDate());
278     }
279     d->m_dateFrame->show();
280   }
281 }
282 
283 
284 /** Overriding QWidget::keyPressEvent
285   *
286   * increments/decrements the date upon +/- or Up/Down key input
287   * sets the date to current date when the 'T' key is pressed
288   */
keyPressEvent(QKeyEvent * k)289 void KMyMoneyDateInput::keyPressEvent(QKeyEvent * k)
290 {
291   QKeySequence today(i18nc("Enter todays date into date input widget", "T"));
292 
293   auto adjustDateSection = [&](int offset) {
294     switch(d->m_dateEdit->currentSection()) {
295       case QDateTimeEdit::DaySection:
296         slotDateChosen(d->m_date.addDays(offset));
297         break;
298       case QDateTimeEdit::MonthSection:
299         slotDateChosen(d->m_date.addMonths(offset));
300         break;
301       case QDateTimeEdit::YearSection:
302         slotDateChosen(d->m_date.addYears(offset));
303         break;
304       default:
305         break;
306     }
307   };
308 
309   switch (k->key()) {
310     case Qt::Key_Equal:
311     case Qt::Key_Plus:
312       adjustDateSection(1);
313       k->accept();
314       break;
315 
316     case Qt::Key_Minus:
317       adjustDateSection(-1);
318       k->accept();
319       break;
320 
321     default:
322       if (today == QKeySequence(k->key()) || k->key() == Qt::Key_T) {
323         slotDateChosen(QDate::currentDate());
324         k->accept();
325       }
326       break;
327   }
328   k->ignore(); // signal that the key event was not handled
329 }
330 
331 /**
332   * This function receives all events that are sent to focusWidget().
333   * Some KeyPress events are intercepted and passed to keyPressEvent.
334   * Otherwise they would be consumed by QDateEdit.
335   */
eventFilter(QObject *,QEvent * e)336 bool KMyMoneyDateInput::eventFilter(QObject *, QEvent *e)
337 {
338   if (e->type() == QEvent::FocusIn) {
339 #ifndef Q_OS_MAC
340     d->m_datePopup->show(mapToGlobal(QPoint(0, height())));
341 #endif
342     // select the date section, but we need to delay it a bit
343   } else if (e->type() == QEvent::FocusOut) {
344 #ifndef Q_OS_MAC
345     d->m_datePopup->hide();
346 #endif
347   } else if (e->type() == QEvent::KeyPress) {
348     if (QKeyEvent *k = dynamic_cast<QKeyEvent*>(e)) {
349       keyPressEvent(k);
350       if (k->isAccepted())
351         return true; // signal that the key event was handled
352     }
353   }
354 
355   return false; // Don't filter the event
356 }
357 
slotDateChosenRef(const QDate & date)358 void KMyMoneyDateInput::slotDateChosenRef(const QDate& date)
359 {
360   if (date.isValid()) {
361     emit dateChanged(date);
362     d->m_date = date;
363 
364 #ifndef Q_OS_MAC
365     QLabel *lbl = static_cast<QLabel*>(d->m_datePopup->view());
366     lbl->setText(QLocale().toString(date));
367     lbl->adjustSize();
368     if (d->m_datePopup->isVisible() || hasFocus())
369       d->m_datePopup->show(mapToGlobal(QPoint(0, height()))); // Repaint
370 #endif
371   }
372 }
373 
slotDateChosen(QDate date)374 void KMyMoneyDateInput::slotDateChosen(QDate date)
375 {
376   if (date.isValid()) {
377     // the next line implies a call to slotDateChosenRef() above
378     d->m_dateEdit->setDate(date);
379   } else {
380     d->m_dateEdit->setDate(INVALID_DATE);
381   }
382 }
383 
date() const384 QDate KMyMoneyDateInput::date() const
385 {
386   QDate rc = d->m_dateEdit->date();
387   if (rc == INVALID_DATE)
388     rc = QDate();
389   return rc;
390 }
391 
setDate(QDate date)392 void KMyMoneyDateInput::setDate(QDate date)
393 {
394   slotDateChosen(date);
395 }
396 
loadDate(const QDate & date)397 void KMyMoneyDateInput::loadDate(const QDate& date)
398 {
399   d->m_date = d->m_prevDate = date;
400 
401   blockSignals(true);
402   slotDateChosen(date);
403   blockSignals(false);
404 }
405 
resetDate()406 void KMyMoneyDateInput::resetDate()
407 {
408   setDate(d->m_prevDate);
409 }
410 
setMaximumDate(const QDate & max)411 void KMyMoneyDateInput::setMaximumDate(const QDate& max)
412 {
413   d->m_dateEdit->setMaximumDate(max);
414 }
415 
focusWidget() const416 QWidget* KMyMoneyDateInput::focusWidget() const
417 {
418   QWidget* w = d->m_dateEdit;
419   while (w->focusProxy())
420     w = w->focusProxy();
421   return w;
422 }
423 /*
424 void KMyMoneyDateInput::setRange(const QDate & min, const QDate & max)
425 {
426   d->m_dateEdit->setDateRange(min, max);
427 }
428 */
429