File indexing completed on 2024-04-28 05:11:36

0001 /*
0002   SPDX-FileCopyrightText: 2010 Bertjan Broeksema <broeksema@kde.org>
0003   SPDX-FileCopyrightText: 2010 Klaralvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
0004 
0005   SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "incidencerecurrence.h"
0009 #include "incidencedatetime.h"
0010 #include "ui_dialogdesktop.h"
0011 
0012 #include "incidenceeditor_debug.h"
0013 #include <QLocale>
0014 
0015 using namespace IncidenceEditorNG;
0016 
0017 enum {
0018     // Keep in sync with mRecurrenceEndCombo
0019     RecurrenceEndNever = 0,
0020     RecurrenceEndOn,
0021     RecurrenceEndAfter
0022 };
0023 
0024 /**
0025 
0026 Description of available recurrence types:
0027 
0028 0 - None
0029 1 -
0030 2 -
0031 3 - rDaily
0032 4 - rWeekly
0033 5 - rMonthlyPos  - 3rd Saturday of month, last Wednesday of month...
0034 6 - rMonthlyDay  - 17th day of month
0035 7 - rYearlyMonth - 10th of July
0036 8 - rYearlyDay   - on the 117th day of the year
0037 9 - rYearlyPos   - 1st Wednesday of July
0038 */
0039 
0040 enum {
0041     // Indexes of the month combo, keep in sync with descriptions.
0042     ComboIndexMonthlyDay = 0, // 11th of June
0043     ComboIndexMonthlyDayInverted, // 20th of June ( 11 to end )
0044     ComboIndexMonthlyPos, // 1st Monday of the Month
0045     ComboIndexMonthlyPosInverted // Last Monday of the Month
0046 };
0047 
0048 enum {
0049     // Indexes of the year combo, keep in sync with descriptions.
0050     ComboIndexYearlyMonth = 0,
0051     ComboIndexYearlyMonthInverted,
0052     ComboIndexYearlyPos,
0053     ComboIndexYearlyPosInverted,
0054     ComboIndexYearlyDay
0055 };
0056 
0057 static void setExDateTimesFromExDates(KCalendarCore::Recurrence *r, const KCalendarCore::DateList &exDates)
0058 {
0059     KCalendarCore::DateTimeList dts;
0060     QDateTime dt = r->startDateTime();
0061     dts.reserve(exDates.count());
0062     for (const auto &e : exDates) {
0063         dt.setDate(e);
0064         dts.append(dt);
0065     }
0066     r->setExDateTimes(dts);
0067 }
0068 
0069 IncidenceRecurrence::IncidenceRecurrence(IncidenceDateTime *dateTime, Ui::EventOrTodoDesktop *ui)
0070     : mUi(ui)
0071     , mDateTime(dateTime)
0072     , mMonthlyInitialType(0)
0073     , mYearlyInitialType(0)
0074 {
0075     setObjectName(QLatin1StringView("IncidenceRecurrence"));
0076     // Set some sane defaults
0077     mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeNone);
0078     mUi->mRecurrenceEndCombo->setCurrentIndex(RecurrenceEndNever);
0079     mUi->mRecurrenceEndStack->setCurrentIndex(0);
0080     mUi->mRepeatStack->setCurrentIndex(0);
0081     mUi->mEndDurationEdit->setValue(1);
0082     handleEndAfterOccurrencesChange(1);
0083     toggleRecurrenceWidgets(RecurrenceTypeNone);
0084     fillCombos();
0085     const QList<QLineEdit *> lineEdits{mUi->mExceptionDateEdit->lineEdit(), mUi->mRecurrenceEndDate->lineEdit()};
0086     for (QLineEdit *lineEdit : lineEdits) {
0087         if (lineEdit) {
0088             lineEdit->setClearButtonEnabled(false);
0089         }
0090     }
0091 
0092     connect(mDateTime, &IncidenceDateTime::startDateTimeToggled, this, &IncidenceRecurrence::handleDateTimeToggle);
0093 
0094     connect(mDateTime, &IncidenceDateTime::startDateChanged, this, &IncidenceRecurrence::handleStartDateChange);
0095 
0096     connect(mUi->mExceptionAddButton, &QPushButton::clicked, this, &IncidenceRecurrence::addException);
0097     connect(mUi->mExceptionRemoveButton, &QPushButton::clicked, this, &IncidenceRecurrence::removeExceptions);
0098     connect(mUi->mExceptionDateEdit, &KDateComboBox::dateChanged, this, &IncidenceRecurrence::handleExceptionDateChange);
0099     connect(mUi->mExceptionList, &QListWidget::itemSelectionChanged, this, &IncidenceRecurrence::updateRemoveExceptionButton);
0100     connect(mUi->mRecurrenceTypeCombo, &QComboBox::currentIndexChanged, this, &IncidenceRecurrence::handleRecurrenceTypeChange);
0101     connect(mUi->mEndDurationEdit, &QSpinBox::valueChanged, this, &IncidenceRecurrence::handleEndAfterOccurrencesChange);
0102     connect(mUi->mFrequencyEdit, &QSpinBox::valueChanged, this, &IncidenceRecurrence::handleFrequencyChange);
0103 
0104     // Check the dirty status when the user changes values.
0105     connect(mUi->mRecurrenceTypeCombo, &QComboBox::currentIndexChanged, this, &IncidenceRecurrence::checkDirtyStatus);
0106     connect(mUi->mFrequencyEdit, &QSpinBox::valueChanged, this, &IncidenceRecurrence::checkDirtyStatus);
0107     connect(mUi->mFrequencyEdit, &QSpinBox::valueChanged, this, &IncidenceRecurrence::checkDirtyStatus);
0108     connect(mUi->mWeekDayCombo, &IncidenceEditorNG::KWeekdayCheckCombo::checkedItemsChanged, this, &IncidenceRecurrence::checkDirtyStatus);
0109     connect(mUi->mMonthlyCombo, &QComboBox::currentIndexChanged, this, &IncidenceRecurrence::checkDirtyStatus);
0110     connect(mUi->mYearlyCombo, &QComboBox::currentIndexChanged, this, &IncidenceRecurrence::checkDirtyStatus);
0111     connect(mUi->mRecurrenceEndCombo, &QComboBox::currentIndexChanged, this, &IncidenceRecurrence::checkDirtyStatus);
0112     connect(mUi->mEndDurationEdit, &QSpinBox::valueChanged, this, &IncidenceRecurrence::checkDirtyStatus);
0113     connect(mUi->mRecurrenceEndDate, &KDateComboBox::dateChanged, this, &IncidenceRecurrence::checkDirtyStatus);
0114     connect(mUi->mThisAndFutureCheck, &QCheckBox::stateChanged, this, &IncidenceRecurrence::checkDirtyStatus);
0115 }
0116 
0117 // this method must be at the top of this file in order to ensure
0118 // that its message to translators appears before any usages of this method.
0119 KLocalizedString IncidenceRecurrence::subsOrdinal(const KLocalizedString &text, int number) const
0120 {
0121     QString q = i18nc(
0122         "In several of the messages below, "
0123         "an ordinal number is substituted into the message. "
0124         "Translate this as \"0\" if English ordinal suffixes "
0125         "should be added (1st, 22nd, 123rd); "
0126         "translate this as \"1\" if just the number itself "
0127         "should be substituted (1, 22, 123).",
0128         "0");
0129     if (q == QLatin1Char('0')) {
0130         const QString ordinal = numberToString(number);
0131         return text.subs(ordinal);
0132     } else {
0133         return text.subs(number);
0134     }
0135 }
0136 
0137 void IncidenceRecurrence::load(const KCalendarCore::Incidence::Ptr &incidence)
0138 {
0139     Q_ASSERT(incidence);
0140 
0141     mLoadedIncidence = incidence;
0142     // We must be sure that the date/time in mDateTime is the correct date time.
0143     // So don't depend on CombinedIncidenceEditor or whatever external factor to
0144     // load the date/time before loading the recurrence
0145 
0146     mCurrentDate = mLoadedIncidence->dateTime(KCalendarCore::IncidenceBase::RoleRecurrenceStart).date();
0147 
0148     mDateTime->load(incidence);
0149     fillCombos();
0150     setDefaults();
0151 
0152     // This is an exception
0153     if (mLoadedIncidence->hasRecurrenceId()) {
0154         handleRecurrenceTypeChange(RecurrenceTypeException);
0155         mUi->mThisAndFutureCheck->setChecked(mLoadedIncidence->thisAndFuture());
0156         mWasDirty = false;
0157         return;
0158     }
0159 
0160     int f = 0;
0161     KCalendarCore::Recurrence *r = nullptr;
0162     if (mLoadedIncidence->recurrenceType() != KCalendarCore::Recurrence::rNone) {
0163         r = mLoadedIncidence->recurrence();
0164         f = r->frequency();
0165     }
0166 
0167     switch (mLoadedIncidence->recurrenceType()) {
0168     case KCalendarCore::Recurrence::rNone:
0169         mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeNone);
0170         handleRecurrenceTypeChange(RecurrenceTypeNone);
0171         break;
0172     case KCalendarCore::Recurrence::rDaily:
0173         mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeDaily);
0174         handleRecurrenceTypeChange(RecurrenceTypeDaily);
0175         setFrequency(f);
0176         break;
0177     case KCalendarCore::Recurrence::rWeekly: {
0178         mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeWeekly);
0179         handleRecurrenceTypeChange(RecurrenceTypeWeekly);
0180         QBitArray disableDays(7 /*size*/, false /*default value*/);
0181         // dayOfWeek returns between 1 and 7
0182         disableDays.setBit(currentDate().dayOfWeek() - 1, true);
0183         mUi->mWeekDayCombo->setDays(r->days(), disableDays);
0184         setFrequency(f);
0185         break;
0186     }
0187     case KCalendarCore::Recurrence::rMonthlyPos: // Fall through
0188     case KCalendarCore::Recurrence::rMonthlyDay:
0189         mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeMonthly);
0190         handleRecurrenceTypeChange(RecurrenceTypeMonthly);
0191         selectMonthlyItem(r, mLoadedIncidence->recurrenceType());
0192         setFrequency(f);
0193         break;
0194     case KCalendarCore::Recurrence::rYearlyMonth: // Fall through
0195     case KCalendarCore::Recurrence::rYearlyPos: // Fall through
0196     case KCalendarCore::Recurrence::rYearlyDay:
0197         mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeYearly);
0198         handleRecurrenceTypeChange(RecurrenceTypeYearly);
0199         selectYearlyItem(r, mLoadedIncidence->recurrenceType());
0200         setFrequency(f);
0201         break;
0202     default:
0203         break;
0204     }
0205 
0206     if (mLoadedIncidence->recurs() && r) {
0207         setDuration(r->duration());
0208         if (r->duration() == 0) {
0209             mUi->mRecurrenceEndDate->setDate(r->endDate());
0210         }
0211     }
0212 
0213     r = mLoadedIncidence->recurrence();
0214     if (r->allDay()) {
0215         setExceptionDates(r->exDates());
0216     } else {
0217         if (!r->exDateTimes().isEmpty()) {
0218             setExceptionDateTimes(r->exDateTimes());
0219         } else if (!r->exDates().isEmpty()) {
0220             // Compatibility: IncidenceEditorNG <= v5.16.3 stored EXDATES as
0221             // dates only. Upgrade to date-times.
0222             setExceptionDates(r->exDates());
0223             setExDateTimesFromExDates(r, r->exDates());
0224             r->setExDates({});
0225         }
0226     }
0227     handleDateTimeToggle();
0228     mWasDirty = false;
0229 }
0230 
0231 void IncidenceRecurrence::writeToIncidence(const KCalendarCore::Incidence::Ptr &incidence) const
0232 {
0233     // clear out any old settings;
0234     KCalendarCore::Recurrence *r = incidence->recurrence();
0235     r->unsetRecurs(); // Why not clear() ?
0236 
0237     const RecurrenceType recurrenceType = currentRecurrenceType();
0238 
0239     if (recurrenceType == RecurrenceTypeException) {
0240         incidence->setThisAndFuture(mUi->mThisAndFutureCheck->isChecked());
0241         return;
0242     }
0243 
0244     if (recurrenceType == RecurrenceTypeNone || !mUi->mRecurrenceTypeCombo->isEnabled()) {
0245         return;
0246     }
0247 
0248     const int lDuration = duration();
0249     QDate endDate;
0250     if (lDuration == 0) {
0251         endDate = mUi->mRecurrenceEndDate->date();
0252     }
0253 
0254     if (recurrenceType == RecurrenceTypeDaily) {
0255         r->setDaily(mUi->mFrequencyEdit->value());
0256     } else if (recurrenceType == RecurrenceTypeWeekly) {
0257         r->setWeekly(mUi->mFrequencyEdit->value(), mUi->mWeekDayCombo->days());
0258     } else if (recurrenceType == RecurrenceTypeMonthly) {
0259         r->setMonthly(mUi->mFrequencyEdit->value());
0260 
0261         if (mUi->mMonthlyCombo->currentIndex() == ComboIndexMonthlyDay) {
0262             // Every nth
0263             r->addMonthlyDate(dayOfMonthFromStart());
0264         } else if (mUi->mMonthlyCombo->currentIndex() == ComboIndexMonthlyDayInverted) {
0265             // Every (last - n)th last day
0266             r->addMonthlyDate(-dayOfMonthFromEnd());
0267         } else if (mUi->mMonthlyCombo->currentIndex() == ComboIndexMonthlyPos) {
0268             // Every ith weekday
0269             r->addMonthlyPos(monthWeekFromStart(), weekday());
0270         } else {
0271             // Every (last - i)th last weekday
0272             r->addMonthlyPos(-monthWeekFromEnd(), weekday());
0273         }
0274     } else if (recurrenceType == RecurrenceTypeYearly) {
0275         r->setYearly(mUi->mFrequencyEdit->value());
0276 
0277         if (mUi->mYearlyCombo->currentIndex() == ComboIndexYearlyMonth) {
0278             // Every nth of month
0279             r->addYearlyDate(dayOfMonthFromStart());
0280             r->addYearlyMonth(currentDate().month());
0281         } else if (mUi->mYearlyCombo->currentIndex() == ComboIndexYearlyMonthInverted) {
0282             // Every (last - n)th last day of month
0283             r->addYearlyDate(-dayOfMonthFromEnd());
0284             r->addYearlyMonth(currentDate().month());
0285         } else if (mUi->mYearlyCombo->currentIndex() == ComboIndexYearlyPos) {
0286             // Every ith weekday of month
0287             r->addYearlyMonth(currentDate().month());
0288             r->addYearlyPos(monthWeekFromStart(), weekday());
0289         } else if (mUi->mYearlyCombo->currentIndex() == ComboIndexYearlyPosInverted) {
0290             // Every (last - i)th last weekday of month
0291             r->addYearlyMonth(currentDate().month());
0292             r->addYearlyPos(-monthWeekFromEnd(), weekday());
0293         } else {
0294             // The lth day of the year (l : 1 - 366)
0295             r->addYearlyDay(dayOfYearFromStart());
0296         }
0297     }
0298 
0299     r->setDuration(lDuration);
0300     if (lDuration == 0) {
0301         r->setEndDate(endDate);
0302     }
0303 
0304     if (r->allDay()) {
0305         r->setExDates(mExceptionDates);
0306     } else {
0307         setExDateTimesFromExDates(r, mExceptionDates);
0308     }
0309 }
0310 
0311 void IncidenceRecurrence::save(const KCalendarCore::Incidence::Ptr &incidence)
0312 {
0313     writeToIncidence(incidence);
0314     mMonthlyInitialType = mUi->mMonthlyCombo->currentIndex();
0315     mYearlyInitialType = mUi->mYearlyCombo->currentIndex();
0316 }
0317 
0318 bool IncidenceRecurrence::isDirty() const
0319 {
0320     const RecurrenceType recurrenceType = currentRecurrenceType();
0321     if (mLoadedIncidence->recurs() && recurrenceType == RecurrenceTypeNone) {
0322         return true;
0323     }
0324 
0325     if (recurrenceType == RecurrenceTypeException) {
0326         return mLoadedIncidence->thisAndFuture() != mUi->mThisAndFutureCheck->isChecked();
0327     }
0328 
0329     if (!mLoadedIncidence->recurs() && recurrenceType != IncidenceEditorNG::RecurrenceTypeNone) {
0330         return true;
0331     }
0332 
0333     // The incidence is not recurring and that hasn't changed, so don't check the
0334     // other values.
0335     if (recurrenceType == RecurrenceTypeNone) {
0336         return false;
0337     }
0338 
0339     const KCalendarCore::Recurrence *recurrence = mLoadedIncidence->recurrence();
0340     switch (recurrence->recurrenceType()) {
0341     case KCalendarCore::Recurrence::rDaily:
0342         if (recurrenceType != RecurrenceTypeDaily || mUi->mFrequencyEdit->value() != recurrence->frequency()) {
0343             return true;
0344         }
0345 
0346         break;
0347     case KCalendarCore::Recurrence::rWeekly:
0348         if (recurrenceType != RecurrenceTypeWeekly || mUi->mFrequencyEdit->value() != recurrence->frequency()
0349             || mUi->mWeekDayCombo->days() != recurrence->days()) {
0350             return true;
0351         }
0352         break;
0353     case KCalendarCore::Recurrence::rMonthlyDay:
0354         if (recurrenceType != RecurrenceTypeMonthly || mUi->mFrequencyEdit->value() != recurrence->frequency()
0355             || mUi->mMonthlyCombo->currentIndex() != mMonthlyInitialType) {
0356             return true;
0357         }
0358         break;
0359     case KCalendarCore::Recurrence::rMonthlyPos:
0360         if (recurrenceType != RecurrenceTypeMonthly || mUi->mFrequencyEdit->value() != recurrence->frequency()
0361             || mUi->mMonthlyCombo->currentIndex() != mMonthlyInitialType) {
0362             return true;
0363         }
0364         break;
0365     case KCalendarCore::Recurrence::rYearlyDay:
0366         if (recurrenceType != RecurrenceTypeYearly || mUi->mFrequencyEdit->value() != recurrence->frequency()
0367             || mUi->mYearlyCombo->currentIndex() != mYearlyInitialType) {
0368             return true;
0369         }
0370         break;
0371     case KCalendarCore::Recurrence::rYearlyMonth:
0372         if (recurrenceType != RecurrenceTypeYearly || mUi->mFrequencyEdit->value() != recurrence->frequency()
0373             || mUi->mYearlyCombo->currentIndex() != mYearlyInitialType) {
0374             return true;
0375         }
0376         break;
0377     case KCalendarCore::Recurrence::rYearlyPos:
0378         if (recurrenceType != RecurrenceTypeYearly || mUi->mFrequencyEdit->value() != recurrence->frequency()
0379             || mUi->mYearlyCombo->currentIndex() != mYearlyInitialType) {
0380             return true;
0381         }
0382         break;
0383     }
0384 
0385     // Recurrence end
0386     // -1 means "recurs forever"
0387     if (recurrence->duration() == -1 && mUi->mRecurrenceEndCombo->currentIndex() != RecurrenceEndNever) {
0388         return true;
0389     } else if (recurrence->duration() == 0) {
0390         // 0 means "end date is set"
0391         if (mUi->mRecurrenceEndCombo->currentIndex() != RecurrenceEndOn || recurrence->endDate() != mUi->mRecurrenceEndDate->date()) {
0392             return true;
0393         }
0394     } else if (recurrence->duration() > 0) {
0395         if (mUi->mEndDurationEdit->value() != recurrence->duration() || mUi->mRecurrenceEndCombo->currentIndex() != RecurrenceEndAfter) {
0396             return true;
0397         }
0398     }
0399 
0400     // Exception dates
0401     if (recurrence->allDay()) {
0402         if (mExceptionDates != recurrence->exDates()) {
0403             return true;
0404         }
0405     } else {
0406         KCalendarCore::DateList dates;
0407         for (const auto &dt : recurrence->exDateTimes()) {
0408             dates.append(dt.date());
0409         }
0410         if (mExceptionDates != dates) {
0411             return true;
0412         }
0413     }
0414 
0415     return false;
0416 }
0417 
0418 void IncidenceRecurrence::focusInvalidField()
0419 {
0420     KCalendarCore::Incidence::Ptr incidence(mLoadedIncidence->clone());
0421     writeToIncidence(incidence);
0422     if (incidence->recurs()) {
0423         if (mUi->mRecurrenceEndCombo->currentIndex() == RecurrenceEndOn && !mUi->mRecurrenceEndDate->date().isValid()) {
0424             mUi->mRecurrenceEndDate->setFocus();
0425         }
0426     }
0427 }
0428 
0429 bool IncidenceRecurrence::isValid() const
0430 {
0431     mLastErrorString.clear();
0432     if (currentRecurrenceType() == IncidenceEditorNG::RecurrenceTypeException) {
0433         // Nothing you can do wrong here
0434         return true;
0435     }
0436     KCalendarCore::Incidence::Ptr incidence(mLoadedIncidence->clone());
0437 
0438     // Write start and end dates to the incidence
0439     mDateTime->save(incidence);
0440 
0441     // Write new recurring parameters to incidence
0442     writeToIncidence(incidence);
0443 
0444     // Check if the incidence will occur at least once
0445     if (incidence->recurs()) {
0446         // dtStart for events, dtDue for to-dos
0447         const QDateTime referenceDate = incidence->dateTime(KCalendarCore::Incidence::RoleRecurrenceStart);
0448 
0449         if (referenceDate.isValid()) {
0450             if (!(incidence->recurrence()->recursOn(referenceDate.date(), referenceDate.timeZone())
0451                   || incidence->recurrence()->getNextDateTime(referenceDate).isValid())) {
0452                 mLastErrorString = i18n(
0453                     "A recurring event or to-do must occur at least once. "
0454                     "Adjust the recurring parameters.");
0455                 qCDebug(INCIDENCEEDITOR_LOG) << mLastErrorString;
0456                 return false;
0457             }
0458         } else {
0459             mLastErrorString = i18n("The incidence's start date is invalid.");
0460             qCDebug(INCIDENCEEDITOR_LOG) << mLastErrorString;
0461             return false;
0462         }
0463 
0464         if (mUi->mRecurrenceEndCombo->currentIndex() == RecurrenceEndOn && !mUi->mRecurrenceEndDate->date().isValid()) {
0465             mLastErrorString = i18nc("@info", "The recurrence end date is invalid.");
0466             qCDebug(INCIDENCEEDITOR_LOG) << mLastErrorString;
0467             return false;
0468         }
0469     }
0470 
0471     return true;
0472 }
0473 
0474 void IncidenceRecurrence::addException()
0475 {
0476     const QDate date = mUi->mExceptionDateEdit->date();
0477     if (!date.isValid()) {
0478         qCWarning(INCIDENCEEDITOR_LOG) << "Refusing to add invalid date";
0479         return;
0480     }
0481 
0482     const QString dateStr = QLocale().toString(date);
0483     if (mUi->mExceptionList->findItems(dateStr, Qt::MatchExactly).isEmpty()) {
0484         mExceptionDates.append(date);
0485         mUi->mExceptionList->addItem(dateStr);
0486     }
0487 
0488     mUi->mExceptionAddButton->setEnabled(false);
0489     checkDirtyStatus();
0490 }
0491 
0492 void IncidenceRecurrence::fillCombos()
0493 {
0494     if (!currentDate().isValid()) {
0495         // Can happen if you're editing with keyboard
0496         return;
0497     }
0498 
0499     // Next the monthly combo. This contains the following elements:
0500     // - nth day of the month
0501     // - (month.lastDay() - n)th day of the month
0502     // - the ith ${weekday} of the month
0503     // - the (month.weekCount() - i)th day of the month
0504     const int currentMonthlyIndex = mUi->mMonthlyCombo->currentIndex();
0505     mUi->mMonthlyCombo->clear();
0506     const QDate date = mDateTime->startDate();
0507 
0508     QString item = subsOrdinal(ki18nc("example: the 30th", "the %1"), dayOfMonthFromStart()).toString();
0509     mUi->mMonthlyCombo->addItem(item);
0510 
0511     item = subsOrdinal(ki18nc("example: the 4th to last day", "the %1 to last day"), dayOfMonthFromEnd()).toString();
0512     mUi->mMonthlyCombo->addItem(item);
0513 
0514     item = subsOrdinal(ki18nc("example: the 5th Wednesday", "the %1 %2"), monthWeekFromStart())
0515                .subs(QLocale::system().dayName(date.dayOfWeek(), QLocale::QLocale::LongFormat))
0516                .toString();
0517     mUi->mMonthlyCombo->addItem(item);
0518 
0519     if (monthWeekFromEnd() == 1) {
0520         item = ki18nc("example: the last Wednesday", "the last %1").subs(QLocale::system().dayName(date.dayOfWeek(), QLocale::LongFormat)).toString();
0521     } else {
0522         item = subsOrdinal(ki18nc("example: the 5th to last Wednesday", "the %1 to last %2"), monthWeekFromEnd())
0523                    .subs(QLocale::system().dayName(date.dayOfWeek(), QLocale::LongFormat))
0524                    .toString();
0525     }
0526     mUi->mMonthlyCombo->addItem(item);
0527     mUi->mMonthlyCombo->setCurrentIndex(currentMonthlyIndex == -1 ? 0 : currentMonthlyIndex);
0528 
0529     // Finally the yearly combo. This contains the following options:
0530     // - ${n}th of ${long-month-name}
0531     // - ${month.lastDay() - n}th last day of ${long-month-name}
0532     // - the ${i}th ${weekday} of ${long-month-name}
0533     // - the ${month.weekCount() - i}th day of ${long-month-name}
0534     // - the ${m}th day of the year
0535     const int currentYearlyIndex = mUi->mYearlyCombo->currentIndex();
0536     mUi->mYearlyCombo->clear();
0537     const QString longMonthName = QLocale::system().monthName(date.month(), QLocale::LongFormat);
0538     item = subsOrdinal(ki18nc("example: the 5th of June", "the %1 of %2"), date.day()).subs(longMonthName).toString();
0539     mUi->mYearlyCombo->addItem(item);
0540 
0541     item = subsOrdinal(ki18nc("example: the 3rd to last day of June", "the %1 to last day of %2"), dayOfMonthFromEnd()).subs(longMonthName).toString();
0542     mUi->mYearlyCombo->addItem(item);
0543 
0544     item = subsOrdinal(ki18nc("example: the 4th Wednesday of June", "the %1 %2 of %3"), monthWeekFromStart())
0545                .subs(QLocale::system().dayName(date.dayOfWeek(), QLocale::LongFormat))
0546                .subs(longMonthName)
0547                .toString();
0548     mUi->mYearlyCombo->addItem(item);
0549 
0550     if (monthWeekFromEnd() == 1) {
0551         item = ki18nc("example: the last Wednesday of June", "the last %1 of %2")
0552                    .subs(QLocale::system().dayName(date.dayOfWeek(), QLocale::LongFormat))
0553                    .subs(longMonthName)
0554                    .toString();
0555     } else {
0556         item = subsOrdinal(ki18nc("example: the 4th to last Wednesday of June", "the %1 to last %2 of %3 "), monthWeekFromEnd())
0557                    .subs(QLocale::system().dayName(date.dayOfWeek(), QLocale::LongFormat))
0558                    .subs(longMonthName)
0559                    .toString();
0560     }
0561     mUi->mYearlyCombo->addItem(item);
0562 
0563     item = subsOrdinal(ki18nc("example: the 15th day of the year", "the %1 day of the year"), date.dayOfYear()).toString();
0564     mUi->mYearlyCombo->addItem(item);
0565     mUi->mYearlyCombo->setCurrentIndex(currentYearlyIndex == -1 ? 0 : currentYearlyIndex);
0566 }
0567 
0568 void IncidenceRecurrence::handleDateTimeToggle()
0569 {
0570     QWidget *parent = mUi->mRepeatStack->parentWidget(); // Take the parent of a toplevel widget;
0571     if (parent) {
0572         parent->setEnabled(mDateTime->startDateTimeEnabled());
0573     }
0574 }
0575 
0576 void IncidenceRecurrence::handleEndAfterOccurrencesChange(int currentValue)
0577 {
0578     mUi->mRecurrenceOccurrencesLabel->setText(i18ncp("Recurrence ends after n occurrences", "occurrence", "occurrences", currentValue));
0579 }
0580 
0581 void IncidenceRecurrence::handleExceptionDateChange(const QDate &currentDate)
0582 {
0583     const QDate date = mUi->mExceptionDateEdit->date();
0584     const QString dateStr = QLocale().toString(date);
0585 
0586     mUi->mExceptionAddButton->setEnabled(currentDate >= mDateTime->startDate() && mUi->mExceptionList->findItems(dateStr, Qt::MatchExactly).isEmpty());
0587 }
0588 
0589 void IncidenceRecurrence::handleFrequencyChange()
0590 {
0591     handleRecurrenceTypeChange(currentRecurrenceType());
0592 }
0593 
0594 void IncidenceRecurrence::handleRecurrenceTypeChange(int currentIndex)
0595 {
0596     toggleRecurrenceWidgets(currentIndex);
0597     QString labelFreq;
0598     QString freqKey;
0599     int frequency = mUi->mFrequencyEdit->value();
0600     switch (currentIndex) {
0601     case 2:
0602         labelFreq = i18ncp("repeat every N >weeks<", "week", "weeks", frequency);
0603         freqKey = QLatin1Char('w');
0604         break;
0605     case 3:
0606         labelFreq = i18ncp("repeat every N >months<", "month", "months", frequency);
0607         freqKey = QLatin1Char('m');
0608         break;
0609     case 4:
0610         labelFreq = i18ncp("repeat every N >years<", "year", "years", frequency);
0611         freqKey = QLatin1Char('y');
0612         break;
0613     default:
0614         labelFreq = i18ncp("repeat every N >days<", "day", "days", frequency);
0615         freqKey = QLatin1Char('d');
0616     }
0617 
0618     const QString labelEvery = ki18ncp(
0619                                    "repeat >every< N years/months/...; "
0620                                    "dynamic context 'type': 'd' days, 'w' weeks, "
0621                                    "'m' months, 'y' years",
0622                                    "every",
0623                                    "every")
0624                                    .subs(frequency)
0625                                    .inContext(QStringLiteral("type"), freqKey)
0626                                    .toString();
0627     mUi->mFrequencyLabel->setText(labelEvery);
0628     mUi->mRecurrenceRuleLabel->setText(labelFreq);
0629 
0630     Q_EMIT recurrenceChanged(static_cast<RecurrenceType>(currentIndex));
0631 }
0632 
0633 void IncidenceRecurrence::removeExceptions()
0634 {
0635     const QList<QListWidgetItem *> selectedExceptions = mUi->mExceptionList->selectedItems();
0636     for (QListWidgetItem *selectedException : selectedExceptions) {
0637         const int row = mUi->mExceptionList->row(selectedException);
0638         mExceptionDates.removeAt(row);
0639         delete mUi->mExceptionList->takeItem(row);
0640     }
0641 
0642     handleExceptionDateChange(mUi->mExceptionDateEdit->date());
0643     checkDirtyStatus();
0644 }
0645 
0646 void IncidenceRecurrence::updateRemoveExceptionButton()
0647 {
0648     mUi->mExceptionRemoveButton->setEnabled(!mUi->mExceptionList->selectedItems().isEmpty());
0649 }
0650 
0651 void IncidenceRecurrence::updateWeekDays(const QDate &newStartDate)
0652 {
0653     const int oldStartDayIndex = mUi->mWeekDayCombo->weekdayIndex(mCurrentDate);
0654     const int newStartDayIndex = mUi->mWeekDayCombo->weekdayIndex(newStartDate);
0655 
0656     if (oldStartDayIndex >= 0) {
0657         mUi->mWeekDayCombo->setItemCheckState(oldStartDayIndex, Qt::Unchecked);
0658         mUi->mWeekDayCombo->setItemEnabled(oldStartDayIndex, true);
0659     }
0660 
0661     if (newStartDayIndex >= 0) {
0662         mUi->mWeekDayCombo->setItemCheckState(newStartDayIndex, Qt::Checked);
0663         mUi->mWeekDayCombo->setItemEnabled(newStartDayIndex, false);
0664     }
0665 
0666     if (newStartDate.isValid()) {
0667         mCurrentDate = newStartDate;
0668     }
0669 }
0670 
0671 short IncidenceRecurrence::dayOfMonthFromStart() const
0672 {
0673     return currentDate().day();
0674 }
0675 
0676 short IncidenceRecurrence::dayOfMonthFromEnd() const
0677 {
0678     const QDate start = currentDate();
0679     return start.daysInMonth() - start.day() + 1;
0680 }
0681 
0682 short IncidenceRecurrence::dayOfYearFromStart() const
0683 {
0684     return currentDate().dayOfYear();
0685 }
0686 
0687 int IncidenceRecurrence::duration() const
0688 {
0689     if (mUi->mRecurrenceEndCombo->currentIndex() == RecurrenceEndNever) {
0690         return -1;
0691     } else if (mUi->mRecurrenceEndCombo->currentIndex() == RecurrenceEndAfter) {
0692         return mUi->mEndDurationEdit->value();
0693     } else {
0694         // 0 means "end date set"
0695         return 0;
0696     }
0697 }
0698 
0699 short IncidenceRecurrence::monthWeekFromStart() const
0700 {
0701     const QDate date = currentDate();
0702     int count;
0703     if (date.isValid()) {
0704         count = 1;
0705         QDate tmp = date.addDays(-7);
0706         while (tmp.month() == date.month()) {
0707             tmp = tmp.addDays(-7); // Count backward
0708             ++count;
0709         }
0710     } else {
0711         // date can be invalid if you're editing the date with your keyboard
0712         count = -1;
0713     }
0714 
0715     // 1 is the first week, 4/5 is the last week of the month
0716     return count;
0717 }
0718 
0719 short IncidenceRecurrence::monthWeekFromEnd() const
0720 {
0721     const QDate date = currentDate();
0722     int count;
0723     if (date.isValid()) {
0724         count = 1;
0725         QDate tmp = date.addDays(7);
0726         while (tmp.month() == date.month()) {
0727             tmp = tmp.addDays(7); // Count forward
0728             ++count;
0729         }
0730     } else {
0731         // date can be invalid if you're editing the date with your keyboard
0732         count = -1;
0733     }
0734 
0735     // 1 is the last week, 4/5 is the first week of the month
0736     return count;
0737 }
0738 
0739 QString IncidenceRecurrence::numberToString(int number) const
0740 {
0741     // The code in here was adapted from an article by Johnathan Wood, see:
0742     // http://www.blackbeltcoder.com/Articles/strings/converting-numbers-to-ordinal-strings
0743 
0744     static QString _numSuffixes[] = {QStringLiteral("th"),
0745                                      QStringLiteral("st"),
0746                                      QStringLiteral("nd"),
0747                                      QStringLiteral("rd"),
0748                                      QStringLiteral("th"),
0749                                      QStringLiteral("th"),
0750                                      QStringLiteral("th"),
0751                                      QStringLiteral("th"),
0752                                      QStringLiteral("th"),
0753                                      QStringLiteral("th")};
0754 
0755     int i = (number % 100);
0756     int j = (i > 10 && i < 20) ? 0 : (number % 10);
0757     return QString::number(number) + _numSuffixes[j];
0758 }
0759 
0760 void IncidenceRecurrence::selectMonthlyItem(KCalendarCore::Recurrence *recurrence, ushort recurenceType)
0761 {
0762     Q_ASSERT(recurenceType == KCalendarCore::Recurrence::rMonthlyPos || recurenceType == KCalendarCore::Recurrence::rMonthlyDay);
0763 
0764     if (recurenceType == KCalendarCore::Recurrence::rMonthlyPos) {
0765         QList<KCalendarCore::RecurrenceRule::WDayPos> rmp = recurrence->monthPositions();
0766         if (rmp.isEmpty()) {
0767             return; // Use the default values. Probably marks the editor as dirty
0768         }
0769 
0770         if (rmp.first().pos() > 0) { // nth day
0771             // TODO if ( rmp.first().pos() != mDateTime->startDate().day() ) { warn user }
0772             // NOTE: This silently changes the recurrence when:
0773             //       rmp.first().pos() != mDateTime->startDate().day()
0774             mUi->mMonthlyCombo->setCurrentIndex(ComboIndexMonthlyPos);
0775         } else { // (month.last() - n)th day
0776             // TODO: Handle recurrences we cannot represent
0777             // QDate startDate = mDateTime->startDate();
0778             // const int dayFromEnd = startDate.daysInMonth() - startDate.day();
0779             // if ( qAbs( rmp.first().pos() ) != dayFromEnd ) { /* warn user */ }
0780             mUi->mMonthlyCombo->setCurrentIndex(ComboIndexMonthlyPosInverted);
0781         }
0782     } else { // Monthly by day
0783         // check if we have any setting for which day (vcs import is broken and
0784         // does not set any day, thus we need to check)
0785         const int day = recurrence->monthDays().isEmpty() ? currentDate().day() : recurrence->monthDays().at(0);
0786 
0787         // Days from the end are after the ones from the begin, so correct for the
0788         // negative sign and add 30 (index starting at 0)
0789         // TODO: Do similar checks as in the monthlyPos case
0790         if (day > 0 && day <= 31) {
0791             mUi->mMonthlyCombo->setCurrentIndex(ComboIndexMonthlyDay);
0792         } else if (day < 0) {
0793             mUi->mMonthlyCombo->setCurrentIndex(ComboIndexMonthlyDayInverted);
0794         }
0795     }
0796 
0797     // So we can easily detect if the user changed the type, without going through this logic ^
0798     mMonthlyInitialType = mUi->mMonthlyCombo->currentIndex();
0799 }
0800 
0801 void IncidenceRecurrence::selectYearlyItem(KCalendarCore::Recurrence *recurrence, ushort recurenceType)
0802 {
0803     Q_ASSERT(recurenceType == KCalendarCore::Recurrence::rYearlyDay || recurenceType == KCalendarCore::Recurrence::rYearlyMonth
0804              || recurenceType == KCalendarCore::Recurrence::rYearlyPos);
0805 
0806     if (recurenceType == KCalendarCore::Recurrence::rYearlyDay) {
0807         /*
0808         const int day = recurrence->yearDays().isEmpty() ? currentDate().dayOfYear() :
0809                                                            recurrence->yearDays().first();
0810         */
0811         // TODO Check if day has actually the same value as in the combo.
0812         mUi->mYearlyCombo->setCurrentIndex(ComboIndexYearlyDay);
0813     } else if (recurenceType == KCalendarCore::Recurrence::rYearlyMonth) {
0814         const int day = recurrence->yearDates().isEmpty() ? currentDate().day() : recurrence->yearDates().at(0);
0815 
0816         /*
0817         int month = currentDate().month();
0818         if ( !recurrence->yearMonths().isEmpty() ) {
0819           month = recurrence->yearMonths().first();
0820         }
0821         */
0822 
0823         // TODO check month and day to be correct values with respect to what is
0824         //      presented in the combo box.
0825         if (day > 0) {
0826             mUi->mYearlyCombo->setCurrentIndex(ComboIndexYearlyMonth);
0827         } else {
0828             mUi->mYearlyCombo->setCurrentIndex(ComboIndexYearlyMonthInverted);
0829         }
0830     } else { // KCalendarCore::Recurrence::rYearlyPos
0831         /*
0832         int month = currentDate().month();
0833         if ( !recurrence->yearMonths().isEmpty() ) {
0834           month = recurrence->yearMonths().first();
0835         }
0836         */
0837 
0838         // count is the nth weekday of the month or the ith last weekday of the month.
0839         int count = (currentDate().day() - 1) / 7;
0840         if (!recurrence->yearPositions().isEmpty()) {
0841             count = recurrence->yearPositions().at(0).pos();
0842         }
0843 
0844         // TODO check month,count and day to be correct values with respect to what is
0845         //      presented in the combo box.
0846         if (count > 0) {
0847             mUi->mYearlyCombo->setCurrentIndex(ComboIndexYearlyPos);
0848         } else {
0849             mUi->mYearlyCombo->setCurrentIndex(ComboIndexYearlyPosInverted);
0850         }
0851     }
0852 
0853     // So we can easily detect if the user changed the type, without going through this logic ^
0854     mYearlyInitialType = mUi->mYearlyCombo->currentIndex();
0855 }
0856 
0857 void IncidenceRecurrence::setDefaults()
0858 {
0859     mUi->mRecurrenceEndCombo->setCurrentIndex(RecurrenceEndNever);
0860     mUi->mRecurrenceEndDate->setDate(currentDate());
0861     mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeNone);
0862 
0863     setFrequency(1);
0864 
0865     // -1 because we want between 0 and 6
0866     const int day = currentDate().dayOfWeek() - 1;
0867 
0868     QBitArray checkDays(7, false);
0869     checkDays.setBit(day);
0870 
0871     QBitArray disableDays(7, false);
0872     disableDays.setBit(day);
0873 
0874     mUi->mWeekDayCombo->setDays(checkDays, disableDays);
0875 
0876     mUi->mMonthlyCombo->setCurrentIndex(0); // Recur on the nth of the month
0877     mUi->mYearlyCombo->setCurrentIndex(0); // Recur on the nth of the month
0878 }
0879 
0880 void IncidenceRecurrence::setDuration(int duration)
0881 {
0882     if (duration == -1) { // No end date
0883         mUi->mRecurrenceEndCombo->setCurrentIndex(RecurrenceEndNever);
0884         mUi->mRecurrenceEndStack->setCurrentIndex(0);
0885     } else if (duration == 0) {
0886         mUi->mRecurrenceEndCombo->setCurrentIndex(RecurrenceEndOn);
0887         mUi->mRecurrenceEndStack->setCurrentIndex(1);
0888     } else {
0889         mUi->mRecurrenceEndCombo->setCurrentIndex(RecurrenceEndAfter);
0890         mUi->mRecurrenceEndStack->setCurrentIndex(2);
0891         mUi->mEndDurationEdit->setValue(duration);
0892     }
0893 }
0894 
0895 void IncidenceRecurrence::setExceptionDates(const KCalendarCore::DateList &dates)
0896 {
0897     mUi->mExceptionList->clear();
0898     mExceptionDates.clear();
0899     for (const auto &d : dates) {
0900         mUi->mExceptionList->addItem(QLocale().toString(d));
0901         mExceptionDates.append(d);
0902     }
0903 }
0904 
0905 void IncidenceRecurrence::setExceptionDateTimes(const KCalendarCore::DateTimeList &dateTimes)
0906 {
0907     mUi->mExceptionList->clear();
0908     mExceptionDates.clear();
0909     for (const auto &dt : dateTimes) {
0910         mUi->mExceptionList->addItem(QLocale().toString(dt.date()));
0911         mExceptionDates.append(dt.date());
0912     }
0913 }
0914 
0915 void IncidenceRecurrence::setFrequency(int frequency)
0916 {
0917     if (frequency < 1) {
0918         frequency = 1;
0919     }
0920 
0921     mUi->mFrequencyEdit->setValue(frequency);
0922 }
0923 
0924 void IncidenceRecurrence::toggleRecurrenceWidgets(int recurrenceType)
0925 {
0926     bool enable = (recurrenceType != RecurrenceTypeNone) && (recurrenceType != RecurrenceTypeException);
0927     mUi->mRecurrenceTypeCombo->setVisible(recurrenceType != RecurrenceTypeException);
0928     mUi->mRepeatLabel->setVisible(recurrenceType != RecurrenceTypeException);
0929     mUi->mRecurrenceEndLabel->setVisible(enable);
0930     mUi->mOnLabel->setVisible(enable && recurrenceType != RecurrenceTypeDaily);
0931     if (!enable) {
0932         // So we can hide the exceptions labels and not trigger column resizing.
0933         mUi->mRepeatLabel->setMinimumSize(mUi->mExceptionsLabel->sizeHint());
0934     }
0935 
0936     mUi->mFrequencyLabel->setVisible(enable);
0937     mUi->mFrequencyEdit->setVisible(enable);
0938     mUi->mRecurrenceRuleLabel->setVisible(enable);
0939     mUi->mRepeatStack->setVisible(enable && recurrenceType != RecurrenceTypeDaily);
0940     mUi->mRepeatStack->setCurrentIndex(recurrenceType);
0941     mUi->mRecurrenceEndCombo->setVisible(enable);
0942     mUi->mEndDurationEdit->setVisible(enable);
0943     mUi->mRecurrenceEndStack->setVisible(enable);
0944 
0945     // Exceptions widgets
0946     mUi->mExceptionsLabel->setVisible(enable);
0947     mUi->mExceptionDateEdit->setVisible(enable);
0948     mUi->mExceptionAddButton->setVisible(enable);
0949     mUi->mExceptionAddButton->setEnabled(mUi->mExceptionDateEdit->date() >= currentDate());
0950     mUi->mExceptionRemoveButton->setVisible(enable);
0951     mUi->mExceptionRemoveButton->setEnabled(!mUi->mExceptionList->selectedItems().isEmpty());
0952     mUi->mExceptionList->setVisible(enable);
0953     mUi->mThisAndFutureCheck->setVisible(recurrenceType == RecurrenceTypeException);
0954 }
0955 
0956 QBitArray IncidenceRecurrence::weekday() const
0957 {
0958     QBitArray days(7);
0959     // QDate::dayOfWeek() -> returns [1 - 7], 1 == monday
0960     days.setBit(currentDate().dayOfWeek() - 1, true);
0961     return days;
0962 }
0963 
0964 int IncidenceRecurrence::weekdayCountForMonth(const QDate &date) const
0965 {
0966     Q_ASSERT(date.isValid());
0967     // This methods returns how often the weekday specified by @param date occurs
0968     // in the month represented by @param date.
0969 
0970     int count = 1;
0971     QDate tmp = date.addDays(-7);
0972     while (tmp.month() == date.month()) {
0973         tmp = tmp.addDays(-7);
0974         ++count;
0975     }
0976 
0977     tmp = date.addDays(7);
0978     while (tmp.month() == date.month()) {
0979         tmp = tmp.addDays(7);
0980         ++count;
0981     }
0982 
0983     return count;
0984 }
0985 
0986 RecurrenceType IncidenceRecurrence::currentRecurrenceType() const
0987 {
0988     if (mLoadedIncidence && mLoadedIncidence->hasRecurrenceId()) {
0989         return RecurrenceTypeException;
0990     }
0991 
0992     const int currentIndex = mUi->mRecurrenceTypeCombo->currentIndex();
0993     Q_ASSERT_X(currentIndex >= 0 && currentIndex < RecurrenceTypeUnknown, "currentRecurrenceType", "Keep the combo-box values in sync with the enum");
0994     return static_cast<RecurrenceType>(currentIndex);
0995 }
0996 
0997 void IncidenceRecurrence::handleStartDateChange(const QDate &date)
0998 {
0999     if (currentDate().isValid()) {
1000         fillCombos();
1001         updateWeekDays(date);
1002         mUi->mExceptionDateEdit->setDate(date);
1003     }
1004 }
1005 
1006 QDate IncidenceRecurrence::currentDate() const
1007 {
1008     return mDateTime->startDate();
1009 }
1010 
1011 #include "moc_incidencerecurrence.cpp"