1 /*
2   This file is part of libkdepim.
3 
4   Copyright (c) 2002 Cornelius Schumacher <schumacher@kde.org>
5   Copyright (c) 2002 David Jarvie <software@astrojar.org.uk>
6   Copyright (c) 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
7   Copyright (c) 2004 Tobias Koenig <tokoe@kde.org>
8 
9   This library is free software; you can redistribute it and/or
10   modify it under the terms of the GNU Library General Public
11   License as published by the Free Software Foundation; either
12 
13   This library is distributed in the hope that it will be useful,
14   Library General Public License for more details.
15 
16   You should have received a copy of the GNU Library General Public License
17   along with this library; see the file COPYING.LIB.  If not, write to
18   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19   Boston, MA 02110-1301, USA.
20 */
21 
22 // krazy:excludeall=qclasses as we want to subclass from QComboBox, not KComboBox
23 
24 #include "kdateedit.h"
25 
26 #include <klocalizedstring.h>
27 
28 #include <qabstractitemview.h>
29 #include <qapplication.h>
30 #include <qcompleter.h>
31 #include <qdesktopwidget.h>
32 #include <qevent.h>
33 #include <qlineedit.h>
34 #include <qscreen.h>
35 #include <qvalidator.h>
36 
37 #include "kdatevalidator.h"
38 
39 #define LOCALTEST
40 
41 using namespace KPIM;
42 
KDateEdit(QWidget * iParent)43 KDateEdit::KDateEdit(QWidget* iParent)
44     : QComboBox(iParent), mReadOnly(false)
45 {
46     // need at least one entry for popup to work
47     setMaxCount(1);
48     setEditable(true);
49 
50     // Check if we can use the QLocale::ShortFormat
51     mAlternativeDateFormatToUse = QLocale(LOCALTEST).dateFormat(QLocale::ShortFormat);
52     if (!mAlternativeDateFormatToUse.contains(QStringLiteral("yyyy"))) {
53         mAlternativeDateFormatToUse = mAlternativeDateFormatToUse.replace(QStringLiteral("yy"), QStringLiteral("yyyy"));
54     }
55 
56     mDate = QDate::currentDate();
57     QString today = QLocale(LOCALTEST).toString(mDate, mAlternativeDateFormatToUse);
58 
59     addItem(today);
60     setCurrentIndex(0);
61 
62     connect(lineEdit(), &QLineEdit::returnPressed, this, &KDateEdit::lineEnterPressed);
63     connect(this, &KDateEdit::editTextChanged, this, &KDateEdit::slotTextChanged);
64 
65     mPopup = new KDatePickerPopup(KDatePickerPopup::DatePicker | KDatePickerPopup::Words, QDate::currentDate(), this);
66     mPopup->hide();
67     mPopup->installEventFilter(this);
68 
69     connect(mPopup, &KDatePickerPopup::dateChanged, this, &KDateEdit::dateSelected);
70 
71     // handle keyword entry
72     setupKeywords();
73     lineEdit()->installEventFilter(this);
74 
75     auto newValidator = new KDateValidator(this);
76     newValidator->setKeywords(mKeywordMap.keys());
77     setValidator(newValidator);
78 
79     mTextChanged = false;
80 }
81 
82 KDateEdit::~KDateEdit()
83     = default;
84 
setDate(QDate iDate)85 void KDateEdit::setDate(QDate iDate)
86 {
87     assignDate(iDate);
88     updateView();
89 }
90 
date() const91 QDate KDateEdit::date() const
92 {
93     return mDate;
94 }
95 
setReadOnly(bool readOnly)96 void KDateEdit::setReadOnly(bool readOnly)
97 {
98     mReadOnly = readOnly;
99     lineEdit()->setReadOnly(readOnly);
100 }
101 
isReadOnly() const102 bool KDateEdit::isReadOnly() const
103 {
104     return mReadOnly;
105 }
106 
showPopup()107 void KDateEdit::showPopup()
108 {
109     if (mReadOnly) {
110         return;
111     }
112 
113     QRect desk = QGuiApplication::primaryScreen()->geometry();
114 
115     QPoint popupPoint = mapToGlobal(QPoint(0, 0));
116 
117     int dateFrameHeight = mPopup->sizeHint().height();
118     if (popupPoint.y() + height() + dateFrameHeight > desk.bottom()) {
119         popupPoint.setY(popupPoint.y() - dateFrameHeight);
120     } else {
121         popupPoint.setY(popupPoint.y() + height());
122     }
123 
124     int dateFrameWidth = mPopup->sizeHint().width();
125     if (popupPoint.x() + dateFrameWidth > desk.right()) {
126         popupPoint.setX(desk.right() - dateFrameWidth);
127     }
128 
129     if (popupPoint.x() < desk.left()) {
130         popupPoint.setX(desk.left());
131     }
132 
133     if (popupPoint.y() < desk.top()) {
134         popupPoint.setY(desk.top());
135     }
136 
137     if (mDate.isValid()) {
138         mPopup->setDate(mDate);
139     } else {
140         mPopup->setDate(QDate::currentDate());
141     }
142 
143     mPopup->popup(popupPoint);
144 
145     // The combo box is now shown pressed. Make it show not pressed again
146     // by causing its (invisible) list box to emit a 'selected' signal.
147     // First, ensure that the list box contains the date currently displayed.
148     QDate date2 = parseDate();
149     assignDate(date2);
150     updateView();
151 
152     // Now, simulate an Enter to unpress it
153     QAbstractItemView* lb = view();
154     if (lb != nullptr) {
155         lb->setCurrentIndex(lb->model()->index(0, 0));
156         QKeyEvent* keyEvent =
157             new QKeyEvent(QEvent::KeyPress, Qt::Key_Enter, Qt::NoModifier);
158         QApplication::postEvent(lb, keyEvent);
159     }
160 }
161 
dateSelected(QDate iDate)162 void KDateEdit::dateSelected(QDate iDate)
163 {
164     if (assignDate(iDate)) {
165         updateView();
166         emit dateChanged(iDate);
167         emit dateEntered(iDate);
168 
169         if (iDate.isValid()) {
170             mPopup->hide();
171         }
172     }
173 }
174 
lineEnterPressed()175 void KDateEdit::lineEnterPressed()
176 {
177     bool replaced = false;
178 
179     QDate date2 = parseDate(&replaced);
180 
181     if (assignDate(date2)) {
182         if (replaced) {
183             updateView();
184         }
185 
186         emit dateChanged(date2);
187         emit dateEntered(date2);
188     }
189 }
190 
parseDate(bool * replaced) const191 QDate KDateEdit::parseDate(bool* replaced) const
192 {
193     QString text = currentText();
194     QDate result;
195 
196     if (replaced != nullptr) {
197         (*replaced) = false;
198     }
199 
200     if (text.isEmpty()) {
201         result = QDate();
202     } else if (mKeywordMap.contains(text.toLower())) {
203         QDate today = QDate::currentDate();
204         int i = mKeywordMap[ text.toLower()];
205         if (i == 30) {
206             today = today.addMonths(1);
207         } else if (i >= 100) {
208             /* A day name has been entered. Convert to offset from today.
209              * This uses some math tricks to figure out the offset in days
210              * to the next date the given day of the week occurs. There
211              * are two cases, that the new day is >= the current day, which means
212              * the new day has not occurred yet or that the new day < the current day,
213              * which means the new day is already passed (so we need to find the
214              * day in the next week).
215              */
216             i -= 100;
217             int currentDay = today.dayOfWeek();
218             if (i >= currentDay) {
219                 i -= currentDay;
220             } else {
221                 i += 7 - currentDay;
222             }
223         }
224 
225         result = today.addDays(i);
226         if (replaced != nullptr) {
227             (*replaced) = true;
228         }
229     } else {
230         result = QLocale(LOCALTEST).toDate(text, mAlternativeDateFormatToUse);
231     }
232 
233     return result;
234 }
235 
focusOutEvent(QFocusEvent * e)236 void KDateEdit::focusOutEvent(QFocusEvent* e)
237 {
238     if (mTextChanged) {
239         lineEnterPressed();
240         mTextChanged = false;
241     }
242     QComboBox::focusOutEvent(e);
243 }
244 
keyPressEvent(QKeyEvent * e)245 void KDateEdit::keyPressEvent(QKeyEvent* e)
246 {
247     QDate date2;
248 
249     if (!mReadOnly) {
250         switch (e->key()) {
251         case Qt::Key_Up:
252             date2 = parseDate();
253             if (!date2.isValid()) {
254                 break;
255             }
256             if ((e->modifiers() & Qt::ControlModifier) != 0u) {  // Warning OSX: The KeypadModifier value will also be set when an arrow key is pressed as the arrow keys are considered part of the keypad.
257                 date2 = date2.addMonths(1);
258             } else {
259                 date2 = date2.addDays(1);
260             }
261             break;
262         case Qt::Key_Down:
263             date2 = parseDate();
264             if (!date2.isValid()) {
265                 break;
266             }
267             if ((e->modifiers() & Qt::ControlModifier) != 0u) {  // Warning OSX: The KeypadModifier value will also be set when an arrow key is pressed as the arrow keys are considered part of the keypad.
268                 date2 = date2.addMonths(-1);
269             } else {
270                 date2 = date2.addDays(-1);
271             }
272             break;
273         case Qt::Key_PageUp:
274             date2 = parseDate();
275             if (!date2.isValid()) {
276                 break;
277             }
278             date2 = date2.addMonths(1);
279             break;
280         case Qt::Key_PageDown:
281             date2 = parseDate();
282             if (!date2.isValid()) {
283                 break;
284             }
285             date2 = date2.addMonths(-1);
286             break;
287         case Qt::Key_Equal:
288             date2 = QDate::currentDate();
289             break;
290         default: {}
291         }
292 
293         if (date2.isValid() && assignDate(date2)) {
294             e->accept();
295             updateView();
296             emit dateChanged(date2);
297             emit dateEntered(date2);
298             return;
299         }
300     }
301 
302     QComboBox::keyPressEvent(e);
303 }
304 
eventFilter(QObject * iObject,QEvent * iEvent)305 bool KDateEdit::eventFilter(QObject* iObject, QEvent* iEvent)
306 {
307     if (iObject == lineEdit()) {
308         // We only process the focus out event if the text has changed
309         // since we got focus
310         if ((iEvent->type() == QEvent::FocusOut) && mTextChanged) {
311             lineEnterPressed();
312             mTextChanged = false;
313         } else if (iEvent->type() == QEvent::KeyPress) {
314             // Up and down arrow keys step the date
315             auto* keyEvent = dynamic_cast<QKeyEvent*>(iEvent);
316 
317             if (keyEvent && (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter)) {
318                 lineEnterPressed();
319                 return true;
320             }
321         }
322     }
323 
324     return QComboBox::eventFilter(iObject, iEvent);
325 }
326 
slotTextChanged(const QString &)327 void KDateEdit::slotTextChanged(const QString& /*unused*/)
328 {
329     QDate date2 = parseDate();
330 
331     if (assignDate(date2)) {
332         emit dateChanged(date2);
333     }
334 
335     mTextChanged = true;
336 }
337 
setupKeywords()338 void KDateEdit::setupKeywords()
339 {
340     // Create the keyword list. This will be used to match against when the user
341     // enters information.
342     mKeywordMap.insert(i18nc("the day after today", "tomorrow"), 1);
343     mKeywordMap.insert(i18nc("this day", "today"), 0);
344     mKeywordMap.insert(i18nc("the day before today", "yesterday"), -1);
345     mKeywordMap.insert(i18nc("the week after this week", "next week"), 7);
346     mKeywordMap.insert(i18nc("the month after this month", "next month"), 30);
347 
348     QString dayName;
349     for (int i = 1; i <= 7; ++i) {
350         dayName = QLocale().dayName(i).toLower();
351         mKeywordMap.insert(dayName, i + 100);
352     }
353 
354     auto comp = new QCompleter(mKeywordMap.keys(), this);
355     comp->setCaseSensitivity(Qt::CaseInsensitive);
356     comp->setCompletionMode(QCompleter::InlineCompletion);
357     setCompleter(comp);
358 }
359 
assignDate(QDate iDate)360 bool KDateEdit::assignDate(QDate iDate)
361 {
362     mDate = iDate;
363     mTextChanged = false;
364     return true;
365 }
366 
updateView()367 void KDateEdit::updateView()
368 {
369     QString dateString;
370     if (mDate.isValid()) {
371         dateString = QLocale(LOCALTEST).toString(mDate, mAlternativeDateFormatToUse);
372     }
373 
374     // We do not want to generate a signal here,
375     // since we explicitly setting the date
376     bool blocked = signalsBlocked();
377     blockSignals(true);
378     removeItem(0);
379     insertItem(0, dateString);
380     blockSignals(blocked);
381 }
382 
383 
384