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