File indexing completed on 2024-05-19 05:08:34

0001 /*
0002     SPDX-FileCopyrightText: 2016-2022 Thomas Baumgart <tbaumgart@kde.org>
0003     SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 #include "kmymoneydateedit.h"
0007 
0008 // ----------------------------------------------------------------------------
0009 // QT Includes
0010 
0011 #include <QDate>
0012 #include <QDateTimeEdit>
0013 #include <QDebug>
0014 #include <QKeyEvent>
0015 #include <QLineEdit>
0016 
0017 // ----------------------------------------------------------------------------
0018 // KDE Includes
0019 
0020 #include <KLocalizedString>
0021 #include <KMessageBox>
0022 
0023 class KMyMoneyDateEditSettings
0024 {
0025 public:
0026     QDateEdit::Section initialSection{QDateEdit::DaySection};
0027 };
0028 
0029 Q_GLOBAL_STATIC(KMyMoneyDateEditSettings, s_globalKMyMoneyDateEditSettings)
0030 
0031 class KMyMoneyDateEditPrivate
0032 {
0033 public:
0034     KMyMoneyDateEditPrivate(KMyMoneyDateEdit* qq)
0035         : q(qq)
0036         , m_originalDay(0)
0037         , m_emptyDateAllowed(false)
0038         , m_dateValidity(false)
0039         , m_lastKeyPressWasEscape(false)
0040         , m_firstFocusIn(true)
0041     {
0042         int sectionIndex(0);
0043         bool lastWasDelimiter(false);
0044         m_sections.resize(3);
0045         const auto format(q->locale().dateFormat(QLocale::ShortFormat).toLower());
0046 
0047         for (int pos = 0; pos < format.length(); ++pos) {
0048             const auto ch = format[pos];
0049             if (ch == QLatin1Char('d')) {
0050                 m_sections[sectionIndex] = QDateTimeEdit::DaySection;
0051                 lastWasDelimiter = false;
0052             } else if (ch == QLatin1Char('m')) {
0053                 m_sections[sectionIndex] = QDateTimeEdit::MonthSection;
0054                 lastWasDelimiter = false;
0055             } else if (ch == QLatin1Char('y')) {
0056                 m_sections[sectionIndex] = QDateTimeEdit::YearSection;
0057                 lastWasDelimiter = false;
0058             } else {
0059                 if (!m_validDelims.contains(ch)) {
0060                     m_validDelims.append(ch);
0061                 }
0062                 if (!lastWasDelimiter) {
0063                     ++sectionIndex;
0064                     lastWasDelimiter = true;
0065                 }
0066             }
0067         }
0068     }
0069 
0070     /**
0071      * Returns @c true if the character @a ch is a
0072      * valid delimiter otherwise @c false.
0073      */
0074     bool isValidDelimiter(const QChar& ch) const
0075     {
0076         return m_validDelims.contains(ch);
0077     }
0078 
0079     /**
0080      * Returns the section that the cursor currently
0081      * resides in
0082      */
0083     QDateTimeEdit::Section sectionByCursorPos() const
0084     {
0085         const auto text(q->lineEdit()->text());
0086         const auto pos(q->lineEdit()->cursorPosition());
0087         int sectionIndex(0);
0088 
0089         for (int idx(0); idx < pos; ++idx) {
0090             if (isValidDelimiter(text[idx])) {
0091                 ++sectionIndex;
0092             }
0093         }
0094         return m_sections[sectionIndex];
0095     }
0096 
0097     /**
0098      * Returns the location of the @a section in
0099      * the date string
0100      */
0101     int partBySection(QDateTimeEdit::Section section) const
0102     {
0103         for (int idx(0); idx < m_sections.size(); ++idx) {
0104             if (m_sections[idx] == section) {
0105                 return idx;
0106             }
0107         }
0108         return -1;
0109     }
0110 
0111     /**
0112      * Returns @c true if the character at position
0113      * @a pos is a delimiter, @c false otherwise.
0114      */
0115     bool isDelimiterAtPos(int pos) const
0116     {
0117         if (pos < 0 || pos >= q->lineEdit()->text().length()) {
0118             return false;
0119         }
0120         return isValidDelimiter(q->lineEdit()->text().at(pos));
0121     }
0122 
0123     /**
0124      * Dissects the current input into the individual parts
0125      * and returns them as a vector.
0126      */
0127     QVector<QString> editParts() const
0128     {
0129         const auto text(q->lineEdit()->text());
0130         QVector<QString> parts(3);
0131         int partIndex(0);
0132         for (int idx(0); idx < text.length(); ++idx) {
0133             const auto ch(text.at(idx));
0134             if (isValidDelimiter(ch)) {
0135                 ++partIndex;
0136             } else {
0137                 parts[partIndex].append(ch);
0138             }
0139         }
0140         return parts;
0141     }
0142 
0143     /**
0144      * Returns the QDate of the current input adjusted so that
0145      * - a missing year information is replaced by the current year
0146      * - a two digit year string is adjusted into the current century
0147      *
0148      * If any of the day or month part is missing, an invalid date is
0149      * returned. The same applies, when the current input represents
0150      * an invalid date (e.g. February 31st).
0151      *
0152      * In case the input is empty and empty input is allowed, a phony
0153      * date (which is valid but does not represent a real date) is
0154      * returned.
0155      */
0156     QDate fixupDate()
0157     {
0158         if (q->isNull() && m_emptyDateAllowed) {
0159             return QDate::fromJulianDay(1);
0160         }
0161         QVector<QString> parts(editParts());
0162         const auto dayIndex = partBySection(QDateTimeEdit::DaySection);
0163         const auto monthIndex = partBySection(QDateTimeEdit::MonthSection);
0164         const auto yearIndex = partBySection(QDateTimeEdit::YearSection);
0165 
0166         if (parts.at(dayIndex).isEmpty() || parts.at(monthIndex).isEmpty()) {
0167             return {};
0168         }
0169 
0170         if (parts.at(yearIndex).isEmpty()) {
0171             parts[yearIndex] = QString::number(QDate::currentDate().year());
0172         }
0173         if (parts.at(yearIndex).length() == 2) {
0174             parts[yearIndex] = QStringLiteral("%1%2").arg(QString::number(QDate::currentDate().year() / 100), parts[yearIndex]);
0175         }
0176 
0177         return QDate(parts[yearIndex].toInt(), parts[monthIndex].toInt(), parts[dayIndex].toInt());
0178     }
0179 
0180     /**
0181      * Adjust the day of the @a date by the amount provided by @a delta
0182      * in days. In case the resulting day is not within the bounds of
0183      * the month of @a date an invalid date is returned.
0184      */
0185     QDate adjustDay(QDate date, int delta)
0186     {
0187         const auto minValue(1);
0188         const auto maxValue(date.daysInMonth());
0189         const auto newDay(date.day() + delta);
0190         if ((newDay >= minValue) && (newDay <= maxValue)) {
0191             return QDate(date.year(), date.month(), newDay);
0192         }
0193         return {};
0194     }
0195 
0196     /**
0197      * Adjust the month of the @a date by the amount provided by @a delta
0198      * in months. In case the resulting month is not within the bounds of
0199      * valid months (1..12) an invalid date is returned. In case the
0200      * original day is larger than the current day, it will be used
0201      * instead. In case the day is not within the range of valid days
0202      * for the resulting month the day will be adjusted to the last day
0203      * of the month.
0204      */
0205     QDate adjustMonth(QDate date, int delta)
0206     {
0207         const auto minValue(1);
0208         const auto maxValue(12);
0209         const auto newMonth(date.month() + delta);
0210 
0211         if ((newMonth >= minValue) && (newMonth <= maxValue)) {
0212             auto day = date.day();
0213             if (m_originalDay > day)
0214                 day = m_originalDay;
0215             auto newDate = QDate(date.year(), newMonth, day);
0216             if (!newDate.isValid()) {
0217                 const auto maxDays(QDate(date.year(), newMonth, 1).daysInMonth());
0218                 newDate = QDate(date.year(), newMonth, maxDays);
0219             }
0220             return newDate;
0221         }
0222         return {};
0223     }
0224 
0225     /**
0226      * Adjust the year of the @a date by the amount provided by @a delta
0227      * in years. In case the resulting year is not within the bounds of
0228      * valid years (1..4000) an invalid date is returned. In case the
0229      * original day is larger than the current day, it will be used
0230      * instead. In case the day is not within the range of valid days
0231      * for the resulting month the day will be adjusted to the last day
0232      * of the month.
0233      */
0234     QDate adjustYear(QDate date, int delta)
0235     {
0236         const auto minValue(1);
0237         const auto maxValue(4000);
0238         const auto newYear(date.year() + delta);
0239         if ((newYear >= minValue) && (newYear <= maxValue)) {
0240             auto day = date.day();
0241             if (m_originalDay > day)
0242                 day = m_originalDay;
0243             auto newDate = QDate(newYear, date.month(), day);
0244             if (!newDate.isValid()) {
0245                 const auto maxDays(QDate(newYear, date.month(), 1).daysInMonth());
0246                 newDate = QDate(newYear, date.month(), maxDays);
0247             }
0248             return newDate;
0249         }
0250         return {};
0251     }
0252 
0253     /**
0254      * Mark the text of the @a section as selected and place the
0255      * cursor behind the last character of that section.
0256      *
0257      * @sa QLineEdit::setSelection(), QLineEdit::setCursorPosition()
0258      */
0259     void selectSection(QDateEdit::Section section)
0260     {
0261         auto part(partBySection(section));
0262         const auto text(q->lineEdit()->text());
0263         const auto length(text.length());
0264         int start(-1);
0265         int end(-1);
0266 
0267         for (int pos = 0; pos < length; ++pos) {
0268             const auto isDelimiter(isValidDelimiter(text.at(pos)));
0269             if (part == 0) {
0270                 if (start == -1) {
0271                     start = pos;
0272                 }
0273                 if (isDelimiter) {
0274                     end = pos;
0275                     break;
0276                 }
0277             } else {
0278                 if (isDelimiter) {
0279                     --part;
0280                 }
0281             }
0282         }
0283         if (end == -1) {
0284             end = length;
0285         }
0286         q->lineEdit()->setCursorPosition(end);
0287 
0288         // in case the text field is empty, we cannot select anything
0289         if (start >= 0) {
0290             q->lineEdit()->setSelection(start, end - start);
0291         }
0292     }
0293 
0294     /**
0295      * In case the lineEdit object contains text and represents
0296      * a valid date, the @a section of the date is adjusted
0297      * by @a delta units. In case the @a section is
0298      * QDateEdit::NoSection, delta is added to the days
0299      * of the date including wrapping into the next/previous
0300      * month. This may also affect the year when the month
0301      * wraps.
0302      *
0303      * If the new date is valid and a disting section (day,
0304      * month or year) was selected, the text of this section
0305      * will be selected as part of the operation.
0306      */
0307     void adjustDate(int delta, QDateEdit::Section section)
0308     {
0309         if (!q->lineEdit()->text().isEmpty()) {
0310             auto date = fixupDate();
0311             if (date.isValid()) {
0312                 switch (section) {
0313                 case QDateEdit::DaySection:
0314                     date = adjustDay(date, delta);
0315                     if (date.isValid()) {
0316                         m_originalDay = date.day();
0317                     }
0318                     break;
0319                 case QDateEdit::MonthSection:
0320                     date = adjustMonth(date, delta);
0321                     break;
0322                 case QDateEdit::YearSection:
0323                     date = adjustYear(date, delta);
0324                     break;
0325                 case QDateEdit::NoSection:
0326                 default:
0327                     date = date.addDays(delta);
0328                     m_originalDay = date.day();
0329                     break;
0330                 }
0331                 if (date.isValid()) {
0332                     setDate(date);
0333                     if (section != QDateEdit::NoSection) {
0334                         selectSection(section);
0335                     }
0336                 }
0337             }
0338         }
0339     }
0340 
0341     /**
0342      * Set the @a date into the widget. In case the empty date
0343      * is allowed and an invalid date is provided, the text of
0344      * the lineEdit widget will be cleared.
0345      */
0346     void setDate(const QDate& date)
0347     {
0348         q->KDateComboBox::setDate(date);
0349         if (m_emptyDateAllowed && !date.isValid()) {
0350             q->lineEdit()->clear();
0351         }
0352     }
0353 
0354     KMyMoneyDateEdit* q;
0355     QVector<QChar> m_validDelims;
0356     QVector<QDateTimeEdit::Section> m_sections;
0357     int m_originalDay;
0358     bool m_emptyDateAllowed;
0359     bool m_dateValidity;
0360     bool m_lastKeyPressWasEscape;
0361     bool m_firstFocusIn;
0362 };
0363 
0364 KMyMoneyDateEdit::KMyMoneyDateEdit(QWidget* parent)
0365     : KDateComboBox(parent)
0366     , d(new KMyMoneyDateEditPrivate(this))
0367 {
0368     setOptions(KDateComboBox::EditDate | KDateComboBox::SelectDate | KDateComboBox::DatePicker | KDateComboBox::WarnOnInvalid);
0369 
0370     connect(lineEdit(), &QLineEdit::textChanged, this, [&](const QString& text) {
0371         const auto newDate(d->fixupDate());
0372         auto newDateIsValid = newDate.isValid() || (d->m_emptyDateAllowed && text.isEmpty());
0373         if (newDateIsValid != d->m_dateValidity) {
0374             Q_EMIT dateValidityChanged(newDate);
0375             d->m_dateValidity = newDateIsValid;
0376         }
0377     });
0378 
0379     setDate(QDate::currentDate());
0380 }
0381 
0382 KMyMoneyDateEdit::~KMyMoneyDateEdit()
0383 {
0384     delete d;
0385 }
0386 
0387 void KMyMoneyDateEdit::connectNotify(const QMetaMethod& signal)
0388 {
0389     // Whenever a new object connects to our dateValidityChanged signal
0390     // we Q_EMIT the current status right away.
0391     if (signal == QMetaMethod::fromSignal(&KMyMoneyDateEdit::dateValidityChanged)) {
0392         const auto newDate(d->fixupDate());
0393         Q_EMIT dateValidityChanged(newDate);
0394     }
0395 }
0396 
0397 void KMyMoneyDateEdit::setDate(const QDate& date)
0398 {
0399     // force sending out a single dateValidityChanged signal
0400     QSignalBlocker blocker(lineEdit());
0401     d->setDate(date);
0402     d->m_originalDay = date.day();
0403     const auto newDate = d->fixupDate();
0404     Q_EMIT dateValidityChanged(newDate);
0405     d->m_dateValidity = newDate.isValid();
0406 }
0407 
0408 void KMyMoneyDateEdit::setInitialSection(QDateEdit::Section section)
0409 {
0410     switch (section) {
0411     case QDateEdit::DaySection:
0412     case QDateEdit::MonthSection:
0413     case QDateEdit::YearSection:
0414         break;
0415     default:
0416         section = QDateEdit::DaySection;
0417         break;
0418     }
0419     s_globalKMyMoneyDateEditSettings()->initialSection = section;
0420 }
0421 
0422 QDateEdit::Section KMyMoneyDateEdit::initialSection() const
0423 {
0424     return s_globalKMyMoneyDateEditSettings()->initialSection;
0425 }
0426 
0427 QDate KMyMoneyDateEdit::date() const
0428 {
0429     const auto date(d->fixupDate());
0430     if (date.toJulianDay() == 1)
0431         return {};
0432     return date;
0433 }
0434 
0435 bool KMyMoneyDateEdit::isValid() const
0436 {
0437     return d->fixupDate().isValid();
0438 }
0439 
0440 void KMyMoneyDateEdit::focusOutEvent(QFocusEvent* event)
0441 {
0442     const auto reason = event->reason();
0443     if ((reason == Qt::TabFocusReason) || (reason == Qt::BacktabFocusReason)) {
0444         const auto date(d->fixupDate());
0445         if (!date.isValid()) {
0446             // if the editor is quit due to pressing ESC, we don't
0447             // show an error for having an invalid date.
0448             if (!d->m_lastKeyPressWasEscape) {
0449                 KMessageBox::error(this, i18nc("@info", "The date you entered is invalid"));
0450                 setFocus();
0451             }
0452         } else {
0453             // skip setting the date, if it is empty and this is allowed
0454             if (!d->m_emptyDateAllowed || (date != QDate::fromJulianDay(1))) {
0455                 d->setDate(date);
0456                 if (!lineEdit()->text().isEmpty())
0457                     KDateComboBox::focusOutEvent(event);
0458             } else {
0459                 // prevent KDateComboBox from showing an error
0460                 QComboBox::focusOutEvent(event);
0461             }
0462         }
0463     } else {
0464         // prevent KDateComboBox from showing an error
0465         QComboBox::focusOutEvent(event);
0466     }
0467     d->m_lastKeyPressWasEscape = false;
0468 }
0469 
0470 void KMyMoneyDateEdit::focusInEvent(QFocusEvent* event)
0471 {
0472     KDateComboBox::focusInEvent(event);
0473 
0474     switch (event->reason()) {
0475     case Qt::TabFocusReason:
0476     case Qt::BacktabFocusReason:
0477     case Qt::OtherFocusReason:
0478         d->selectSection(s_globalKMyMoneyDateEditSettings()->initialSection);
0479         break;
0480     default:
0481         break;
0482     }
0483 }
0484 
0485 void KMyMoneyDateEdit::keyPressEvent(QKeyEvent* keyEvent)
0486 {
0487     auto dropKey(false);
0488     const auto pos = lineEdit()->cursorPosition();
0489     const auto key = keyEvent->key();
0490     QDate date;
0491     d->m_lastKeyPressWasEscape = false;
0492 
0493     switch (key) {
0494     case Qt::Key_Down:
0495     case Qt::Key_Up:
0496         if (isValid()) {
0497             d->adjustDate(key == Qt::Key_Up ? 1 : -1, d->sectionByCursorPos());
0498         }
0499         break;
0500 
0501     case Qt::Key_Plus:
0502     case Qt::Key_Minus:
0503         if (isValid()) {
0504             d->adjustDate(key == Qt::Key_Plus ? 1 : -1, QDateEdit::NoSection);
0505         }
0506         break;
0507 
0508     case Qt::Key_PageDown:
0509         date = d->fixupDate();
0510         break;
0511     case Qt::Key_PageUp:
0512         date = d->fixupDate();
0513         break;
0514 
0515     case Qt::Key_Enter:
0516     case Qt::Key_Return:
0517         date = d->fixupDate();
0518         if (!date.isValid()) {
0519             KMessageBox::error(this, i18nc("@info", "The date you entered is invalid"));
0520             keyEvent->accept();
0521             setFocus();
0522         } else {
0523             d->setDate(date);
0524             if (!lineEdit()->text().isEmpty()) {
0525                 QComboBox::keyPressEvent(keyEvent);
0526             }
0527         }
0528         break;
0529 
0530     case Qt::Key_Backspace:
0531         // make sure the user does not enter two delimiters in a row
0532         // by simply removing the duplicate as well
0533         if (d->isDelimiterAtPos(pos) && d->isDelimiterAtPos(pos - 2)) {
0534             QComboBox::keyPressEvent(keyEvent);
0535         }
0536         QComboBox::keyPressEvent(keyEvent);
0537         break;
0538 
0539     case Qt::Key_Delete:
0540         // make sure the user does not enter two delimiters in a row
0541         // by simply removing the duplicate as well
0542         if (d->isDelimiterAtPos(pos - 1) && d->isDelimiterAtPos(pos + 1)) {
0543             QComboBox::keyPressEvent(keyEvent);
0544         }
0545         QComboBox::keyPressEvent(keyEvent);
0546         break;
0547 
0548     case Qt::Key_T:
0549         d->setDate(QDate::currentDate());
0550         Q_EMIT dateValidityChanged(QDate::currentDate());
0551         d->selectSection(s_globalKMyMoneyDateEditSettings()->initialSection);
0552         break;
0553 
0554     default:
0555         if (keyEvent->text().length() > 0) {
0556             const auto ch(keyEvent->text().at(0));
0557 
0558             if (!(d->isValidDelimiter(ch) || (ch >= QLatin1Char('0') && ch <= QLatin1Char('9')))) {
0559                 dropKey = true;
0560             }
0561 
0562             // prevent two delimters in a row and simply
0563             // fake an overwrite and select the next section
0564             if (d->isValidDelimiter(ch)) {
0565                 if (d->isDelimiterAtPos(pos)) {
0566                     const auto section(d->sectionByCursorPos());
0567                     const auto part(d->partBySection(section));
0568                     if (part < 2) {
0569                         d->selectSection(d->m_sections[part + 1]);
0570                     }
0571                     dropKey = true;
0572                 }
0573             }
0574         }
0575 
0576         if (!dropKey) {
0577             const auto oldDate(d->fixupDate());
0578             const auto oldSection(d->sectionByCursorPos());
0579             QComboBox::keyPressEvent(keyEvent);
0580             const auto newDate(d->fixupDate());
0581             const auto newSection(d->sectionByCursorPos());
0582 
0583             if (newDate.isValid()) {
0584                 if (!oldDate.isValid() || (oldDate.day() != newDate.day())) {
0585                     d->m_originalDay = newDate.day();
0586                 }
0587                 if (oldSection != newSection) {
0588                     d->selectSection(newSection);
0589                 }
0590             }
0591         }
0592         break;
0593 
0594     case Qt::Key_Escape:
0595         d->m_lastKeyPressWasEscape = true;
0596         QComboBox::keyPressEvent(keyEvent);
0597         break;
0598     }
0599 }
0600 
0601 void KMyMoneyDateEdit::setDisplayFormat(QLocale::FormatType format)
0602 {
0603     Q_UNUSED(format)
0604 }
0605 
0606 void KMyMoneyDateEdit::setAllowEmptyDate(bool emptyDateAllowed)
0607 {
0608     d->m_emptyDateAllowed = emptyDateAllowed;
0609 }