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