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 }