File indexing completed on 2025-01-05 03:51:06
0001 /* ============================================================ 0002 * 0003 * This file is a part of digiKam project 0004 * https://www.digikam.org 0005 * 0006 * Date : 2002-01-10 0007 * Description : a combo box to list date. 0008 * this widget come from libkdepim. 0009 * 0010 * SPDX-FileCopyrightText: 2011-2024 by Gilles Caulier <caulier dot gilles at gmail dot com> 0011 * SPDX-FileCopyrightText: 2002 by Cornelius Schumacher <schumacher at kde dot org> 0012 * SPDX-FileCopyrightText: 2003-2004 by Reinhold Kainhofer <reinhold at kainhofer dot com> 0013 * SPDX-FileCopyrightText: 2004 by Tobias Koenig <tokoe at kde dot org> 0014 * 0015 * SPDX-License-Identifier: GPL-2.0-or-later 0016 * 0017 * ============================================================ */ 0018 0019 #include "ddateedit.h" 0020 0021 // Qt includes 0022 0023 #include <QAbstractItemView> 0024 #include <QApplication> 0025 #include <QKeyEvent> 0026 #include <QLineEdit> 0027 #include <QValidator> 0028 #include <QWindow> 0029 #include <QScreen> 0030 #include <QLocale> 0031 0032 // KDE includes 0033 0034 #include <klocalizedstring.h> 0035 0036 // Local includes 0037 0038 #include "ddatepickerpopup.h" 0039 0040 namespace Digikam 0041 { 0042 0043 class Q_DECL_HIDDEN DateValidator : public QValidator 0044 { 0045 Q_OBJECT 0046 0047 public: 0048 0049 DateValidator(const QStringList& keywords, const QString& dateFormat, QWidget* const parent) 0050 : QValidator (parent), 0051 mKeywords (keywords), 0052 mDateFormat(dateFormat) 0053 { 0054 } 0055 0056 State validate(QString& str, int&) const override 0057 { 0058 int length = str.length(); 0059 0060 // empty string is intermediate so one can clear the edit line and start from scratch 0061 0062 if (length <= 0) 0063 { 0064 return Intermediate; 0065 } 0066 0067 if (mKeywords.contains(str.toLower())) 0068 { 0069 return Acceptable; 0070 } 0071 0072 bool ok = QDate::fromString(str, mDateFormat).isValid(); 0073 0074 if (ok) 0075 { 0076 return Acceptable; 0077 } 0078 else 0079 { 0080 return Intermediate; 0081 } 0082 } 0083 0084 private: 0085 0086 QStringList mKeywords; 0087 QString mDateFormat; 0088 }; 0089 0090 // ----------------------------------------------------------------------------------- 0091 0092 class Q_DECL_HIDDEN DDateEdit::Private 0093 { 0094 public: 0095 0096 explicit Private() 0097 : readOnly (false), 0098 textChanged (false), 0099 discardNextMousePress(false), 0100 popup (nullptr) 0101 { 0102 } 0103 0104 bool readOnly; 0105 bool textChanged; 0106 bool discardNextMousePress; 0107 0108 QDate date; 0109 QString dateFormat; 0110 0111 QMap<QString, int> keywordMap; 0112 0113 DDatePickerPopup* popup; 0114 }; 0115 0116 DDateEdit::DDateEdit(QWidget* const parent, const QString& name) 0117 : QComboBox(parent), 0118 d (new Private) 0119 { 0120 setObjectName(name); 0121 0122 // need at least one entry for popup to work 0123 0124 setMaxCount(1); 0125 setEditable(true); 0126 0127 d->date = QDate::currentDate(); 0128 0129 d->dateFormat = QLocale().dateFormat(QLocale::ShortFormat); 0130 0131 if (!d->dateFormat.contains(QLatin1String("yyyy"))) 0132 { 0133 d->dateFormat.replace(QLatin1String("yy"), 0134 QLatin1String("yyyy")); 0135 } 0136 0137 QString today = d->date.toString(d->dateFormat); 0138 0139 addItem(today); 0140 setCurrentIndex(0); 0141 setMinimumSize(sizeHint()); 0142 setMinimumSize(minimumSizeHint()); 0143 0144 connect(lineEdit(), SIGNAL(returnPressed()), 0145 this, SLOT(lineEnterPressed())); 0146 0147 connect(this, SIGNAL(currentTextChanged(QString)), 0148 this, SLOT(slotTextChanged(QString))); 0149 0150 d->popup = new DDatePickerPopup(DDatePickerPopup::DatePicker | DDatePickerPopup::Words); 0151 d->popup->hide(); 0152 d->popup->installEventFilter(this); 0153 0154 connect(d->popup, SIGNAL(dateChanged(QDate)), 0155 this, SLOT(dateSelected(QDate))); 0156 0157 // handle keyword entry 0158 0159 setupKeywords(); 0160 lineEdit()->installEventFilter(this); 0161 0162 setValidator(new DateValidator(d->keywordMap.keys(), d->dateFormat, this)); 0163 0164 d->textChanged = false; 0165 } 0166 0167 DDateEdit::~DDateEdit() 0168 { 0169 delete d->popup; 0170 d->popup = nullptr; 0171 delete d; 0172 } 0173 0174 void DDateEdit::setDate(const QDate& date) 0175 { 0176 assignDate(date); 0177 updateView(); 0178 } 0179 0180 QDate DDateEdit::date() const 0181 { 0182 return d->date; 0183 } 0184 0185 void DDateEdit::setReadOnly(bool readOnly) 0186 { 0187 d->readOnly = readOnly; 0188 lineEdit()->setReadOnly(readOnly); 0189 } 0190 0191 bool DDateEdit::isReadOnly() const 0192 { 0193 return d->readOnly; 0194 } 0195 0196 void DDateEdit::showPopup() 0197 { 0198 if (d->readOnly) 0199 { 0200 return; 0201 } 0202 0203 QScreen* screen = qApp->primaryScreen(); 0204 0205 if (QWidget* const widget = nativeParentWidget()) 0206 { 0207 if (QWindow* const window = widget->windowHandle()) 0208 { 0209 screen = window->screen(); 0210 } 0211 } 0212 0213 QRect desk = screen->geometry(); 0214 QPoint popupPoint = mapToGlobal(QPoint(0, 0)); 0215 int dateFrameHeight = d->popup->sizeHint().height(); 0216 0217 if ((popupPoint.y() + height() + dateFrameHeight) > desk.bottom()) 0218 { 0219 popupPoint.setY(popupPoint.y() - dateFrameHeight); 0220 } 0221 else 0222 { 0223 popupPoint.setY(popupPoint.y() + height()); 0224 } 0225 0226 int dateFrameWidth = d->popup->sizeHint().width(); 0227 0228 if ((popupPoint.x() + dateFrameWidth) > desk.right()) 0229 { 0230 popupPoint.setX(desk.right() - dateFrameWidth); 0231 } 0232 0233 if (popupPoint.x() < desk.left()) 0234 { 0235 popupPoint.setX(desk.left()); 0236 } 0237 0238 if (popupPoint.y() < desk.top()) 0239 { 0240 popupPoint.setY(desk.top()); 0241 } 0242 0243 if (d->date.isValid()) 0244 { 0245 d->popup->setDate(d->date); 0246 } 0247 else 0248 { 0249 d->popup->setDate(QDate::currentDate()); 0250 } 0251 0252 d->popup->popup(popupPoint); 0253 0254 // The combo box is now shown pressed. Make it show not pressed again 0255 // by causing its (invisible) list box to emit a 'selected' signal. 0256 // First, ensure that the list box contains the date currently displayed. 0257 0258 QDate date = parseDate(); 0259 assignDate(date); 0260 updateView(); 0261 0262 // Now, simulate an Enter to unpress it 0263 0264 QAbstractItemView* const lb = view(); 0265 0266 if (lb) 0267 { 0268 lb->setCurrentIndex(lb->model()->index(0, 0)); 0269 QKeyEvent* const keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Enter, Qt::NoModifier); 0270 QApplication::postEvent(lb, keyEvent); 0271 } 0272 } 0273 0274 void DDateEdit::dateSelected(const QDate& date) 0275 { 0276 // NOTE: use dynamic binding as this virtual method can be re-implemented in derived classes. 0277 0278 if (this->assignDate(date)) 0279 { 0280 updateView(); 0281 Q_EMIT dateChanged(date); 0282 0283 if (date.isValid()) 0284 { 0285 d->popup->hide(); 0286 } 0287 } 0288 } 0289 0290 void DDateEdit::dateEntered(const QDate& date) 0291 { 0292 if (assignDate(date)) 0293 { 0294 updateView(); 0295 Q_EMIT dateChanged(date); 0296 } 0297 } 0298 0299 void DDateEdit::lineEnterPressed() 0300 { 0301 bool replaced = false; 0302 QDate date = parseDate(&replaced); 0303 0304 // NOTE: use dynamic binding as this virtual method can be re-implemented in derived classes. 0305 0306 if (this->assignDate(date)) 0307 { 0308 if (replaced) 0309 { 0310 updateView(); 0311 } 0312 0313 Q_EMIT dateChanged(date); 0314 } 0315 } 0316 0317 QDate DDateEdit::parseDate(bool* replaced) const 0318 { 0319 QString text = currentText(); 0320 QDate result; 0321 0322 if (replaced) 0323 { 0324 (*replaced) = false; 0325 } 0326 0327 if (text.isEmpty()) 0328 { 0329 result = QDate(); 0330 } 0331 else if (d->keywordMap.contains(text.toLower())) 0332 { 0333 QDate today = QDate::currentDate(); 0334 int i = d->keywordMap[text.toLower()]; 0335 0336 if (i >= 100) 0337 { 0338 /* 0339 * A day name has been entered. Convert to offset from today. 0340 * This uses some math tricks to figure out the offset in days 0341 * to the next date the given day of the week occurs. There 0342 * are two cases, that the new day is >= the current day, which means 0343 * the new day has not occurred yet or that the new day < the current day, 0344 * which means the new day is already passed (so we need to find the 0345 * day in the next week). 0346 */ 0347 i -= 100; 0348 int currentDay = today.dayOfWeek(); 0349 0350 if (i >= currentDay) 0351 { 0352 i -= currentDay; 0353 } 0354 else 0355 { 0356 i += 7 - currentDay; 0357 } 0358 } 0359 0360 result = today.addDays(i); 0361 0362 if (replaced) 0363 { 0364 (*replaced) = true; 0365 } 0366 } 0367 else 0368 { 0369 result = QDate::fromString(text, d->dateFormat); 0370 } 0371 0372 return result; 0373 } 0374 0375 bool DDateEdit::eventFilter(QObject* object, QEvent* event) 0376 { 0377 if (object == lineEdit()) 0378 { 0379 // We only process the focus out event if the text has changed 0380 // since we got focus 0381 0382 if ((event->type() == QEvent::FocusOut) && d->textChanged) 0383 { 0384 lineEnterPressed(); 0385 d->textChanged = false; 0386 } 0387 else if (event->type() == QEvent::KeyPress) 0388 { 0389 // Up and down arrow keys step the date 0390 0391 QKeyEvent* const keyEvent = (QKeyEvent*)event; 0392 0393 if (keyEvent->key() == Qt::Key_Return) 0394 { 0395 lineEnterPressed(); 0396 return true; 0397 } 0398 0399 int step = 0; 0400 0401 if (keyEvent->key() == Qt::Key_Up) 0402 { 0403 step = 1; 0404 } 0405 else if (keyEvent->key() == Qt::Key_Down) 0406 { 0407 step = -1; 0408 } 0409 0410 if (step && !d->readOnly) 0411 { 0412 QDate date = parseDate(); 0413 0414 if (date.isValid()) 0415 { 0416 date = date.addDays(step); 0417 0418 if (assignDate(date)) 0419 { 0420 updateView(); 0421 0422 Q_EMIT dateChanged(date); 0423 0424 return true; 0425 } 0426 } 0427 } 0428 } 0429 } 0430 else 0431 { 0432 // It's a date picker event 0433 0434 switch (event->type()) 0435 { 0436 case QEvent::MouseButtonDblClick: 0437 case QEvent::MouseButtonPress: 0438 { 0439 QMouseEvent* const mouseEvent = (QMouseEvent*)event; 0440 0441 if (!d->popup->rect().contains(mouseEvent->pos())) 0442 { 0443 QPoint globalPos = d->popup->mapToGlobal(mouseEvent->pos()); 0444 0445 if (QApplication::widgetAt(globalPos) == this) 0446 { 0447 // The date picker is being closed by a click on the 0448 // DDateEdit widget. Avoid popping it up again immediately. 0449 0450 d->discardNextMousePress = true; 0451 } 0452 } 0453 0454 break; 0455 } 0456 0457 default: 0458 { 0459 break; 0460 } 0461 } 0462 } 0463 0464 return false; 0465 } 0466 0467 void DDateEdit::mousePressEvent(QMouseEvent* e) 0468 { 0469 if ((e->button() == Qt::LeftButton) && d->discardNextMousePress) 0470 { 0471 d->discardNextMousePress = false; 0472 return; 0473 } 0474 0475 QComboBox::mousePressEvent(e); 0476 } 0477 0478 void DDateEdit::slotTextChanged(const QString&) 0479 { 0480 QDate date = parseDate(); 0481 0482 // NOTE: use dynamic binding as this virtual method can be re-implemented in derived classes. 0483 0484 if (this->assignDate(date)) 0485 { 0486 Q_EMIT dateChanged(date); 0487 } 0488 0489 d->textChanged = true; 0490 } 0491 0492 void DDateEdit::setupKeywords() 0493 { 0494 // Create the keyword list. This will be used to match against when the user 0495 // enters information. 0496 0497 d->keywordMap.insert(i18nc("@item: date keyword", "tomorrow"), 1); 0498 d->keywordMap.insert(i18nc("@item: date keyword", "today"), 0); 0499 d->keywordMap.insert(i18nc("@item: date keyword", "yesterday"), -1); 0500 0501 QString dayName; 0502 0503 for (int i = 1 ; i <= 7 ; ++i) 0504 { 0505 dayName = QLocale().standaloneDayName(i, QLocale::LongFormat).toLower(); 0506 d->keywordMap.insert(dayName, i + 100); 0507 } 0508 } 0509 0510 bool DDateEdit::assignDate(const QDate& date) 0511 { 0512 d->date = date; 0513 d->textChanged = false; 0514 0515 return true; 0516 } 0517 0518 void DDateEdit::updateView() 0519 { 0520 QString dateString; 0521 0522 if (d->date.isValid()) 0523 { 0524 dateString = d->date.toString(d->dateFormat); 0525 } 0526 0527 // We do not want to generate a signal here, 0528 // since we explicitly setting the date 0529 0530 bool blocked = signalsBlocked(); 0531 blockSignals(true); 0532 removeItem(0); 0533 insertItem(0, dateString); 0534 blockSignals(blocked); 0535 } 0536 0537 } // namespace Digikam 0538 0539 #include "ddateedit.moc" 0540 0541 #include "moc_ddateedit.cpp"