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"