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 ¤tDate) 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"