Incidenceeditor

incidencerecurrence.cpp
1/*
2 SPDX-FileCopyrightText: 2010 Bertjan Broeksema <broeksema@kde.org>
3 SPDX-FileCopyrightText: 2010 Klaralvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "incidencerecurrence.h"
9using namespace Qt::Literals::StringLiterals;
10
11#include "incidencedatetime.h"
12#include "ui_dialogdesktop.h"
13
14#include "incidenceeditor_debug.h"
15#include <QLocale>
16
17using namespace IncidenceEditorNG;
18
19enum {
20 // Keep in sync with mRecurrenceEndCombo
21 RecurrenceEndNever = 0,
22 RecurrenceEndOn,
23 RecurrenceEndAfter
24};
25
26/**
27
28Description of available recurrence types:
29
300 - None
311 -
322 -
333 - rDaily
344 - rWeekly
355 - rMonthlyPos - 3rd Saturday of month, last Wednesday of month...
366 - rMonthlyDay - 17th day of month
377 - rYearlyMonth - 10th of July
388 - rYearlyDay - on the 117th day of the year
399 - rYearlyPos - 1st Wednesday of July
40*/
41
42enum {
43 // Indexes of the month combo, keep in sync with descriptions.
44 ComboIndexMonthlyDay = 0, // 11th of June
45 ComboIndexMonthlyDayInverted, // 20th of June ( 11 to end )
46 ComboIndexMonthlyPos, // 1st Monday of the Month
47 ComboIndexMonthlyPosInverted // Last Monday of the Month
48};
49
50enum {
51 // Indexes of the year combo, keep in sync with descriptions.
52 ComboIndexYearlyMonth = 0,
53 ComboIndexYearlyMonthInverted,
54 ComboIndexYearlyPos,
55 ComboIndexYearlyPosInverted,
56 ComboIndexYearlyDay
57};
58
59static void setExDateTimesFromExDates(KCalendarCore::Recurrence *r, const KCalendarCore::DateList &exDates)
60{
62 const auto incidenceTz = r->startDateTime().timeZone();
64 dts.reserve(exDates.count());
65 // The exDates are always in local timezone, but the recurrence might be ocurring on a diffrent day
66 // in the incidence original timezone, so make sure to convert the date
67 for (const auto &e : exDates) {
68 dt.setDate(e);
69 dts.append(dt.toTimeZone(incidenceTz));
70 }
71 r->setExDateTimes(dts);
72}
73
74IncidenceRecurrence::IncidenceRecurrence(IncidenceDateTime *dateTime, Ui::EventOrTodoDesktop *ui)
75 : mUi(ui)
76 , mDateTime(dateTime)
77 , mMonthlyInitialType(0)
78 , mYearlyInitialType(0)
79{
80 setObjectName("IncidenceRecurrence"_L1);
81 // Set some sane defaults
82 mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeNone);
83 mUi->mRecurrenceEndCombo->setCurrentIndex(RecurrenceEndNever);
84 mUi->mRecurrenceEndStack->setCurrentIndex(0);
85 mUi->mRepeatStack->setCurrentIndex(0);
86 mUi->mEndDurationEdit->setValue(1);
87 handleEndAfterOccurrencesChange(1);
88 toggleRecurrenceWidgets(RecurrenceTypeNone);
89 fillCombos();
90 const QList<QLineEdit *> lineEdits{mUi->mExceptionDateEdit->lineEdit(), mUi->mRecurrenceEndDate->lineEdit()};
91 for (QLineEdit *lineEdit : lineEdits) {
92 if (lineEdit) {
93 lineEdit->setClearButtonEnabled(false);
94 }
95 }
96
97 connect(mDateTime, &IncidenceDateTime::startDateTimeToggled, this, &IncidenceRecurrence::handleDateTimeToggle);
98
99 connect(mDateTime, &IncidenceDateTime::startDateChanged, this, &IncidenceRecurrence::handleStartDateChange);
100
101 connect(mUi->mExceptionAddButton, &QPushButton::clicked, this, &IncidenceRecurrence::addException);
102 connect(mUi->mExceptionRemoveButton, &QPushButton::clicked, this, &IncidenceRecurrence::removeExceptions);
103 connect(mUi->mExceptionDateEdit, &KDateComboBox::dateChanged, this, &IncidenceRecurrence::handleExceptionDateChange);
104 connect(mUi->mExceptionList, &QListWidget::itemSelectionChanged, this, &IncidenceRecurrence::updateRemoveExceptionButton);
105 connect(mUi->mRecurrenceTypeCombo, &QComboBox::currentIndexChanged, this, &IncidenceRecurrence::handleRecurrenceTypeChange);
106 connect(mUi->mEndDurationEdit, &QSpinBox::valueChanged, this, &IncidenceRecurrence::handleEndAfterOccurrencesChange);
107 connect(mUi->mFrequencyEdit, &QSpinBox::valueChanged, this, &IncidenceRecurrence::handleFrequencyChange);
108
109 // Check the dirty status when the user changes values.
110 connect(mUi->mRecurrenceTypeCombo, &QComboBox::currentIndexChanged, this, &IncidenceRecurrence::checkDirtyStatus);
111 connect(mUi->mFrequencyEdit, &QSpinBox::valueChanged, this, &IncidenceRecurrence::checkDirtyStatus);
112 connect(mUi->mFrequencyEdit, &QSpinBox::valueChanged, this, &IncidenceRecurrence::checkDirtyStatus);
113 connect(mUi->mWeekDayCombo, &IncidenceEditorNG::KWeekdayCheckCombo::checkedItemsChanged, this, &IncidenceRecurrence::checkDirtyStatus);
114 connect(mUi->mMonthlyCombo, &QComboBox::currentIndexChanged, this, &IncidenceRecurrence::checkDirtyStatus);
115 connect(mUi->mYearlyCombo, &QComboBox::currentIndexChanged, this, &IncidenceRecurrence::checkDirtyStatus);
116 connect(mUi->mRecurrenceEndCombo, &QComboBox::currentIndexChanged, this, &IncidenceRecurrence::checkDirtyStatus);
117 connect(mUi->mEndDurationEdit, &QSpinBox::valueChanged, this, &IncidenceRecurrence::checkDirtyStatus);
118 connect(mUi->mRecurrenceEndDate, &KDateComboBox::dateChanged, this, &IncidenceRecurrence::checkDirtyStatus);
119
120 connect(mUi->mThisAndFutureCheck, &QCheckBox::checkStateChanged, this, &IncidenceRecurrence::checkDirtyStatus);
121}
122
123// this method must be at the top of this file in order to ensure
124// that its message to translators appears before any usages of this method.
125KLocalizedString IncidenceRecurrence::subsOrdinal(const KLocalizedString &text, int number) const
126{
127 QString q = i18nc(
128 "In several of the messages below, "
129 "an ordinal number is substituted into the message. "
130 "Translate this as \"0\" if English ordinal suffixes "
131 "should be added (1st, 22nd, 123rd); "
132 "translate this as \"1\" if just the number itself "
133 "should be substituted (1, 22, 123).",
134 "0");
135 if (q == QLatin1Char('0')) {
136 const QString ordinal = numberToString(number);
137 return text.subs(ordinal);
138 } else {
139 return text.subs(number);
140 }
141}
142
143void IncidenceRecurrence::load(const KCalendarCore::Incidence::Ptr &incidence)
144{
145 Q_ASSERT(incidence);
146
147 mLoadedIncidence = incidence;
148 // We must be sure that the date/time in mDateTime is the correct date time.
149 // So don't depend on CombinedIncidenceEditor or whatever external factor to
150 // load the date/time before loading the recurrence
151
152 mCurrentDate = mLoadedIncidence->dateTime(KCalendarCore::IncidenceBase::RoleRecurrenceStart).date();
153
154 mDateTime->load(incidence);
155 fillCombos();
156 setDefaults();
157
158 // This is an exception
159 if (mLoadedIncidence->hasRecurrenceId()) {
160 handleRecurrenceTypeChange(RecurrenceTypeException);
161 mUi->mThisAndFutureCheck->setChecked(mLoadedIncidence->thisAndFuture());
162 mWasDirty = false;
163 return;
164 }
165
166 int f = 0;
167 KCalendarCore::Recurrence *r = nullptr;
168 if (mLoadedIncidence->recurrenceType() != KCalendarCore::Recurrence::rNone) {
169 r = mLoadedIncidence->recurrence();
170 f = r->frequency();
171 }
172
173 switch (mLoadedIncidence->recurrenceType()) {
174 case KCalendarCore::Recurrence::rNone:
175 mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeNone);
176 handleRecurrenceTypeChange(RecurrenceTypeNone);
177 break;
178 case KCalendarCore::Recurrence::rDaily:
179 mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeDaily);
180 handleRecurrenceTypeChange(RecurrenceTypeDaily);
181 setFrequency(f);
182 break;
183 case KCalendarCore::Recurrence::rWeekly: {
184 mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeWeekly);
185 handleRecurrenceTypeChange(RecurrenceTypeWeekly);
186 QBitArray disableDays(7 /*size*/, false /*default value*/);
187 // dayOfWeek returns between 1 and 7
188 disableDays.setBit(currentDate().dayOfWeek() - 1, true);
189 mUi->mWeekDayCombo->setDays(r->days(), disableDays);
190 setFrequency(f);
191 break;
192 }
193 case KCalendarCore::Recurrence::rMonthlyPos: // Fall through
194 case KCalendarCore::Recurrence::rMonthlyDay:
195 mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeMonthly);
196 handleRecurrenceTypeChange(RecurrenceTypeMonthly);
197 selectMonthlyItem(r, mLoadedIncidence->recurrenceType());
198 setFrequency(f);
199 break;
200 case KCalendarCore::Recurrence::rYearlyMonth: // Fall through
201 case KCalendarCore::Recurrence::rYearlyPos: // Fall through
202 case KCalendarCore::Recurrence::rYearlyDay:
203 mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeYearly);
204 handleRecurrenceTypeChange(RecurrenceTypeYearly);
205 selectYearlyItem(r, mLoadedIncidence->recurrenceType());
206 setFrequency(f);
207 break;
208 default:
209 break;
210 }
211
212 if (mLoadedIncidence->recurs() && r) {
213 setDuration(r->duration());
214 if (r->duration() == 0) {
215 mUi->mRecurrenceEndDate->setDate(r->endDate());
216 }
217 }
218
219 r = mLoadedIncidence->recurrence();
220 if (r->allDay()) {
221 setExceptionDates(r->exDates());
222 } else {
223 if (!r->exDateTimes().isEmpty()) {
224 setExceptionDateTimes(r->exDateTimes());
225 } else if (!r->exDates().isEmpty()) {
226 // Compatibility: IncidenceEditorNG <= v5.16.3 stored EXDATES as
227 // dates only. Upgrade to date-times.
228 setExceptionDates(r->exDates());
229 setExDateTimesFromExDates(r, r->exDates());
230 r->setExDates({});
231 }
232 }
233 handleDateTimeToggle();
234 mWasDirty = false;
235}
236
237void IncidenceRecurrence::writeToIncidence(const KCalendarCore::Incidence::Ptr &incidence) const
238{
239 // clear out any old settings;
240 KCalendarCore::Recurrence *r = incidence->recurrence();
241 r->unsetRecurs(); // Why not clear() ?
242
243 const RecurrenceType recurrenceType = currentRecurrenceType();
244
245 if (recurrenceType == RecurrenceTypeException) {
246 incidence->setThisAndFuture(mUi->mThisAndFutureCheck->isChecked());
247 return;
248 }
249
250 if (recurrenceType == RecurrenceTypeNone || !mUi->mRecurrenceTypeCombo->isEnabled()) {
251 return;
252 }
253
254 const int lDuration = duration();
255 QDate endDate;
256 if (lDuration == 0) {
257 endDate = mUi->mRecurrenceEndDate->date();
258 }
259
260 if (recurrenceType == RecurrenceTypeDaily) {
261 r->setDaily(mUi->mFrequencyEdit->value());
262 } else if (recurrenceType == RecurrenceTypeWeekly) {
263 r->setWeekly(mUi->mFrequencyEdit->value(), mUi->mWeekDayCombo->days());
264 } else if (recurrenceType == RecurrenceTypeMonthly) {
265 r->setMonthly(mUi->mFrequencyEdit->value());
266
267 if (mUi->mMonthlyCombo->currentIndex() == ComboIndexMonthlyDay) {
268 // Every nth
269 r->addMonthlyDate(dayOfMonthFromStart());
270 } else if (mUi->mMonthlyCombo->currentIndex() == ComboIndexMonthlyDayInverted) {
271 // Every (last - n)th last day
272 r->addMonthlyDate(-dayOfMonthFromEnd());
273 } else if (mUi->mMonthlyCombo->currentIndex() == ComboIndexMonthlyPos) {
274 // Every ith weekday
275 r->addMonthlyPos(monthWeekFromStart(), weekday());
276 } else {
277 // Every (last - i)th last weekday
278 r->addMonthlyPos(-monthWeekFromEnd(), weekday());
279 }
280 } else if (recurrenceType == RecurrenceTypeYearly) {
281 r->setYearly(mUi->mFrequencyEdit->value());
282
283 if (mUi->mYearlyCombo->currentIndex() == ComboIndexYearlyMonth) {
284 // Every nth of month
285 r->addYearlyDate(dayOfMonthFromStart());
286 r->addYearlyMonth(currentDate().month());
287 } else if (mUi->mYearlyCombo->currentIndex() == ComboIndexYearlyMonthInverted) {
288 // Every (last - n)th last day of month
289 r->addYearlyDate(-dayOfMonthFromEnd());
290 r->addYearlyMonth(currentDate().month());
291 } else if (mUi->mYearlyCombo->currentIndex() == ComboIndexYearlyPos) {
292 // Every ith weekday of month
293 r->addYearlyMonth(currentDate().month());
294 r->addYearlyPos(monthWeekFromStart(), weekday());
295 } else if (mUi->mYearlyCombo->currentIndex() == ComboIndexYearlyPosInverted) {
296 // Every (last - i)th last weekday of month
297 r->addYearlyMonth(currentDate().month());
298 r->addYearlyPos(-monthWeekFromEnd(), weekday());
299 } else {
300 // The lth day of the year (l : 1 - 366)
301 r->addYearlyDay(dayOfYearFromStart());
302 }
303 }
304
305 r->setDuration(lDuration);
306 if (lDuration == 0) {
307 r->setEndDate(endDate);
308 }
309
310 if (r->allDay()) {
311 r->setExDates(mExceptionDates);
312 } else {
313 setExDateTimesFromExDates(r, mExceptionDates);
314 }
315}
316
317void IncidenceRecurrence::save(const KCalendarCore::Incidence::Ptr &incidence)
318{
319 writeToIncidence(incidence);
320 mMonthlyInitialType = mUi->mMonthlyCombo->currentIndex();
321 mYearlyInitialType = mUi->mYearlyCombo->currentIndex();
322}
323
324bool IncidenceRecurrence::isDirty() const
325{
326 const RecurrenceType recurrenceType = currentRecurrenceType();
327 if (mLoadedIncidence->recurs() && recurrenceType == RecurrenceTypeNone) {
328 return true;
329 }
330
331 if (recurrenceType == RecurrenceTypeException) {
332 return mLoadedIncidence->thisAndFuture() != mUi->mThisAndFutureCheck->isChecked();
333 }
334
335 if (!mLoadedIncidence->recurs() && recurrenceType != IncidenceEditorNG::RecurrenceTypeNone) {
336 return true;
337 }
338
339 // The incidence is not recurring and that hasn't changed, so don't check the
340 // other values.
341 if (recurrenceType == RecurrenceTypeNone) {
342 return false;
343 }
344
345 const KCalendarCore::Recurrence *recurrence = mLoadedIncidence->recurrence();
346 switch (recurrence->recurrenceType()) {
347 case KCalendarCore::Recurrence::rDaily:
348 if (recurrenceType != RecurrenceTypeDaily || mUi->mFrequencyEdit->value() != recurrence->frequency()) {
349 return true;
350 }
351
352 break;
353 case KCalendarCore::Recurrence::rWeekly:
354 if (recurrenceType != RecurrenceTypeWeekly || mUi->mFrequencyEdit->value() != recurrence->frequency()
355 || mUi->mWeekDayCombo->days() != recurrence->days()) {
356 return true;
357 }
358 break;
359 case KCalendarCore::Recurrence::rMonthlyDay:
360 if (recurrenceType != RecurrenceTypeMonthly || mUi->mFrequencyEdit->value() != recurrence->frequency()
361 || mUi->mMonthlyCombo->currentIndex() != mMonthlyInitialType) {
362 return true;
363 }
364 break;
365 case KCalendarCore::Recurrence::rMonthlyPos:
366 if (recurrenceType != RecurrenceTypeMonthly || mUi->mFrequencyEdit->value() != recurrence->frequency()
367 || mUi->mMonthlyCombo->currentIndex() != mMonthlyInitialType) {
368 return true;
369 }
370 break;
371 case KCalendarCore::Recurrence::rYearlyDay:
372 if (recurrenceType != RecurrenceTypeYearly || mUi->mFrequencyEdit->value() != recurrence->frequency()
373 || mUi->mYearlyCombo->currentIndex() != mYearlyInitialType) {
374 return true;
375 }
376 break;
377 case KCalendarCore::Recurrence::rYearlyMonth:
378 if (recurrenceType != RecurrenceTypeYearly || mUi->mFrequencyEdit->value() != recurrence->frequency()
379 || mUi->mYearlyCombo->currentIndex() != mYearlyInitialType) {
380 return true;
381 }
382 break;
383 case KCalendarCore::Recurrence::rYearlyPos:
384 if (recurrenceType != RecurrenceTypeYearly || mUi->mFrequencyEdit->value() != recurrence->frequency()
385 || mUi->mYearlyCombo->currentIndex() != mYearlyInitialType) {
386 return true;
387 }
388 break;
389 }
390
391 // Recurrence end
392 // -1 means "recurs forever"
393 if (recurrence->duration() == -1 && mUi->mRecurrenceEndCombo->currentIndex() != RecurrenceEndNever) {
394 return true;
395 } else if (recurrence->duration() == 0) {
396 // 0 means "end date is set"
397 if (mUi->mRecurrenceEndCombo->currentIndex() != RecurrenceEndOn || recurrence->endDate() != mUi->mRecurrenceEndDate->date()) {
398 return true;
399 }
400 } else if (recurrence->duration() > 0) {
401 if (mUi->mEndDurationEdit->value() != recurrence->duration() || mUi->mRecurrenceEndCombo->currentIndex() != RecurrenceEndAfter) {
402 return true;
403 }
404 }
405
406 // Exception dates
407 if (recurrence->allDay()) {
408 if (mExceptionDates != recurrence->exDates()) {
409 return true;
410 }
411 } else {
413 for (const auto &dt : recurrence->exDateTimes()) {
414 dates.append(dt.toLocalTime().date());
415 }
416 if (mExceptionDates != dates) {
417 return true;
418 }
419 }
420
421 return false;
422}
423
424void IncidenceRecurrence::focusInvalidField()
425{
426 KCalendarCore::Incidence::Ptr incidence(mLoadedIncidence->clone());
427 writeToIncidence(incidence);
428 if (incidence->recurs()) {
429 if (mUi->mRecurrenceEndCombo->currentIndex() == RecurrenceEndOn && !mUi->mRecurrenceEndDate->date().isValid()) {
430 mUi->mRecurrenceEndDate->setFocus();
431 }
432 }
433}
434
435bool IncidenceRecurrence::isValid() const
436{
437 mLastErrorString.clear();
438 if (currentRecurrenceType() == IncidenceEditorNG::RecurrenceTypeException) {
439 // Nothing you can do wrong here
440 return true;
441 }
442 KCalendarCore::Incidence::Ptr incidence(mLoadedIncidence->clone());
443
444 // Write start and end dates to the incidence
445 mDateTime->save(incidence);
446
447 // Write new recurring parameters to incidence
448 writeToIncidence(incidence);
449
450 // Check if the incidence will occur at least once
451 if (incidence->recurs()) {
452 // dtStart for events, dtDue for to-dos
454
455 if (referenceDate.isValid()) {
456 if (!(incidence->recurrence()->recursOn(referenceDate.date(), referenceDate.timeZone())
457 || incidence->recurrence()->getNextDateTime(referenceDate).isValid())) {
458 mLastErrorString = i18n(
459 "A recurring event or to-do must occur at least once. "
460 "Adjust the recurring parameters.");
461 qCDebug(INCIDENCEEDITOR_LOG) << mLastErrorString;
462 return false;
463 }
464 } else {
465 mLastErrorString = i18n("The incidence's start date is invalid.");
466 qCDebug(INCIDENCEEDITOR_LOG) << mLastErrorString;
467 return false;
468 }
469
470 if (mUi->mRecurrenceEndCombo->currentIndex() == RecurrenceEndOn && !mUi->mRecurrenceEndDate->date().isValid()) {
471 mLastErrorString = i18nc("@info", "The recurrence end date is invalid.");
472 qCDebug(INCIDENCEEDITOR_LOG) << mLastErrorString;
473 return false;
474 }
475 }
476
477 return true;
478}
479
480void IncidenceRecurrence::addException()
481{
482 const QDate date = mUi->mExceptionDateEdit->date();
483 if (!date.isValid()) {
484 qCWarning(INCIDENCEEDITOR_LOG) << "Refusing to add invalid date";
485 return;
486 }
487
488 const QString dateStr = QLocale().toString(date);
489 if (mUi->mExceptionList->findItems(dateStr, Qt::MatchExactly).isEmpty()) {
490 mExceptionDates.append(date);
491 mUi->mExceptionList->addItem(dateStr);
492 }
493
494 mUi->mExceptionAddButton->setEnabled(false);
496}
497
498void IncidenceRecurrence::fillCombos()
499{
500 if (!currentDate().isValid()) {
501 // Can happen if you're editing with keyboard
502 return;
503 }
504
505 // Next the monthly combo. This contains the following elements:
506 // - nth day of the month
507 // - (month.lastDay() - n)th day of the month
508 // - the ith ${weekday} of the month
509 // - the (month.weekCount() - i)th day of the month
510 const int currentMonthlyIndex = mUi->mMonthlyCombo->currentIndex();
511 mUi->mMonthlyCombo->clear();
512 const QDate date = mDateTime->startDate();
513
514 QString item = subsOrdinal(ki18nc("example: the 30th", "the %1"), dayOfMonthFromStart()).toString();
515 mUi->mMonthlyCombo->addItem(item);
516
517 item = subsOrdinal(ki18nc("example: the 4th to last day", "the %1 to last day"), dayOfMonthFromEnd()).toString();
518 mUi->mMonthlyCombo->addItem(item);
519
520 item = subsOrdinal(ki18nc("example: the 5th Wednesday", "the %1 %2"), monthWeekFromStart())
521 .subs(QLocale::system().dayName(date.dayOfWeek(), QLocale::QLocale::LongFormat))
522 .toString();
523 mUi->mMonthlyCombo->addItem(item);
524
525 if (monthWeekFromEnd() == 1) {
526 item = ki18nc("example: the last Wednesday", "the last %1").subs(QLocale::system().dayName(date.dayOfWeek(), QLocale::LongFormat)).toString();
527 } else {
528 item = subsOrdinal(ki18nc("example: the 5th to last Wednesday", "the %1 to last %2"), monthWeekFromEnd())
530 .toString();
531 }
532 mUi->mMonthlyCombo->addItem(item);
533 mUi->mMonthlyCombo->setCurrentIndex(currentMonthlyIndex == -1 ? 0 : currentMonthlyIndex);
534
535 // Finally the yearly combo. This contains the following options:
536 // - ${n}th of ${long-month-name}
537 // - ${month.lastDay() - n}th last day of ${long-month-name}
538 // - the ${i}th ${weekday} of ${long-month-name}
539 // - the ${month.weekCount() - i}th day of ${long-month-name}
540 // - the ${m}th day of the year
541 const int currentYearlyIndex = mUi->mYearlyCombo->currentIndex();
542 mUi->mYearlyCombo->clear();
543 const QString longMonthName = QLocale::system().monthName(date.month(), QLocale::LongFormat);
544 item = subsOrdinal(ki18nc("example: the 5th of June", "the %1 of %2"), date.day()).subs(longMonthName).toString();
545 mUi->mYearlyCombo->addItem(item);
546
547 item = subsOrdinal(ki18nc("example: the 3rd to last day of June", "the %1 to last day of %2"), dayOfMonthFromEnd()).subs(longMonthName).toString();
548 mUi->mYearlyCombo->addItem(item);
549
550 item = subsOrdinal(ki18nc("example: the 4th Wednesday of June", "the %1 %2 of %3"), monthWeekFromStart())
552 .subs(longMonthName)
553 .toString();
554 mUi->mYearlyCombo->addItem(item);
555
556 if (monthWeekFromEnd() == 1) {
557 item = ki18nc("example: the last Wednesday of June", "the last %1 of %2")
559 .subs(longMonthName)
560 .toString();
561 } else {
562 item = subsOrdinal(ki18nc("example: the 4th to last Wednesday of June", "the %1 to last %2 of %3 "), monthWeekFromEnd())
564 .subs(longMonthName)
565 .toString();
566 }
567 mUi->mYearlyCombo->addItem(item);
568
569 item = subsOrdinal(ki18nc("example: the 15th day of the year", "the %1 day of the year"), date.dayOfYear()).toString();
570 mUi->mYearlyCombo->addItem(item);
571 mUi->mYearlyCombo->setCurrentIndex(currentYearlyIndex == -1 ? 0 : currentYearlyIndex);
572}
573
574void IncidenceRecurrence::handleDateTimeToggle()
575{
576 QWidget *parent = mUi->mRepeatStack->parentWidget(); // Take the parent of a toplevel widget;
577 if (parent) {
578 parent->setEnabled(mDateTime->startDateTimeEnabled());
579 }
580}
581
582void IncidenceRecurrence::handleEndAfterOccurrencesChange(int currentValue)
583{
584 mUi->mRecurrenceOccurrencesLabel->setText(i18ncp("Recurrence ends after n occurrences", "occurrence", "occurrences", currentValue));
585}
586
587void IncidenceRecurrence::handleExceptionDateChange(const QDate &currentDate)
588{
589 const QDate date = mUi->mExceptionDateEdit->date();
590 const QString dateStr = QLocale().toString(date);
591
592 mUi->mExceptionAddButton->setEnabled(currentDate >= mDateTime->startDate() && mUi->mExceptionList->findItems(dateStr, Qt::MatchExactly).isEmpty());
593}
594
595void IncidenceRecurrence::handleFrequencyChange()
596{
597 handleRecurrenceTypeChange(currentRecurrenceType());
598}
599
600void IncidenceRecurrence::handleRecurrenceTypeChange(int currentIndex)
601{
602 toggleRecurrenceWidgets(currentIndex);
603 QString labelFreq;
604 QString freqKey;
605 int frequency = mUi->mFrequencyEdit->value();
606 switch (currentIndex) {
607 case 2:
608 labelFreq = i18ncp("repeat every N >weeks<", "week", "weeks", frequency);
609 freqKey = QLatin1Char('w');
610 break;
611 case 3:
612 labelFreq = i18ncp("repeat every N >months<", "month", "months", frequency);
613 freqKey = QLatin1Char('m');
614 break;
615 case 4:
616 labelFreq = i18ncp("repeat every N >years<", "year", "years", frequency);
617 freqKey = QLatin1Char('y');
618 break;
619 default:
620 labelFreq = i18ncp("repeat every N >days<", "day", "days", frequency);
621 freqKey = QLatin1Char('d');
622 }
623
624 const QString labelEvery = ki18ncp(
625 "repeat >every< N years/months/...; "
626 "dynamic context 'type': 'd' days, 'w' weeks, "
627 "'m' months, 'y' years",
628 "every",
629 "every")
630 .subs(frequency)
631 .inContext(QStringLiteral("type"), freqKey)
632 .toString();
633 mUi->mFrequencyLabel->setText(labelEvery);
634 mUi->mRecurrenceRuleLabel->setText(labelFreq);
635
636 Q_EMIT recurrenceChanged(static_cast<RecurrenceType>(currentIndex));
637}
638
639void IncidenceRecurrence::removeExceptions()
640{
641 const QList<QListWidgetItem *> selectedExceptions = mUi->mExceptionList->selectedItems();
642 for (QListWidgetItem *selectedException : selectedExceptions) {
643 const int row = mUi->mExceptionList->row(selectedException);
644 mExceptionDates.removeAt(row);
645 delete mUi->mExceptionList->takeItem(row);
646 }
647
648 handleExceptionDateChange(mUi->mExceptionDateEdit->date());
650}
651
652void IncidenceRecurrence::updateRemoveExceptionButton()
653{
654 mUi->mExceptionRemoveButton->setEnabled(!mUi->mExceptionList->selectedItems().isEmpty());
655}
656
657void IncidenceRecurrence::updateWeekDays(const QDate &newStartDate)
658{
659 const int oldStartDayIndex = mUi->mWeekDayCombo->weekdayIndex(mCurrentDate);
660 const int newStartDayIndex = mUi->mWeekDayCombo->weekdayIndex(newStartDate);
661
662 if (oldStartDayIndex >= 0) {
663 mUi->mWeekDayCombo->setItemCheckState(oldStartDayIndex, Qt::Unchecked);
664 mUi->mWeekDayCombo->setItemEnabled(oldStartDayIndex, true);
665 }
666
667 if (newStartDayIndex >= 0) {
668 mUi->mWeekDayCombo->setItemCheckState(newStartDayIndex, Qt::Checked);
669 mUi->mWeekDayCombo->setItemEnabled(newStartDayIndex, false);
670 }
671
672 if (newStartDate.isValid()) {
673 mCurrentDate = newStartDate;
674 }
675}
676
677short IncidenceRecurrence::dayOfMonthFromStart() const
678{
679 return currentDate().day();
680}
681
682short IncidenceRecurrence::dayOfMonthFromEnd() const
683{
684 const QDate start = currentDate();
685 return start.daysInMonth() - start.day() + 1;
686}
687
688short IncidenceRecurrence::dayOfYearFromStart() const
689{
690 return currentDate().dayOfYear();
691}
692
693int IncidenceRecurrence::duration() const
694{
695 if (mUi->mRecurrenceEndCombo->currentIndex() == RecurrenceEndNever) {
696 return -1;
697 } else if (mUi->mRecurrenceEndCombo->currentIndex() == RecurrenceEndAfter) {
698 return mUi->mEndDurationEdit->value();
699 } else {
700 // 0 means "end date set"
701 return 0;
702 }
703}
704
705short IncidenceRecurrence::monthWeekFromStart() const
706{
707 const QDate date = currentDate();
708 int count;
709 if (date.isValid()) {
710 count = 1;
711 QDate tmp = date.addDays(-7);
712 while (tmp.month() == date.month()) {
713 tmp = tmp.addDays(-7); // Count backward
714 ++count;
715 }
716 } else {
717 // date can be invalid if you're editing the date with your keyboard
718 count = -1;
719 }
720
721 // 1 is the first week, 4/5 is the last week of the month
722 return count;
723}
724
725short IncidenceRecurrence::monthWeekFromEnd() const
726{
727 const QDate date = currentDate();
728 int count;
729 if (date.isValid()) {
730 count = 1;
731 QDate tmp = date.addDays(7);
732 while (tmp.month() == date.month()) {
733 tmp = tmp.addDays(7); // Count forward
734 ++count;
735 }
736 } else {
737 // date can be invalid if you're editing the date with your keyboard
738 count = -1;
739 }
740
741 // 1 is the last week, 4/5 is the first week of the month
742 return count;
743}
744
745QString IncidenceRecurrence::numberToString(int number) const
746{
747 // The code in here was adapted from an article by Johnathan Wood, see:
748 // http://www.blackbeltcoder.com/Articles/strings/converting-numbers-to-ordinal-strings
749
750 static QString _numSuffixes[] = {QStringLiteral("th"),
751 QStringLiteral("st"),
752 QStringLiteral("nd"),
753 QStringLiteral("rd"),
754 QStringLiteral("th"),
755 QStringLiteral("th"),
756 QStringLiteral("th"),
757 QStringLiteral("th"),
758 QStringLiteral("th"),
759 QStringLiteral("th")};
760
761 int i = (number % 100);
762 int j = (i > 10 && i < 20) ? 0 : (number % 10);
763 return QString::number(number) + _numSuffixes[j];
764}
765
766void IncidenceRecurrence::selectMonthlyItem(KCalendarCore::Recurrence *recurrence, ushort recurenceType)
767{
768 Q_ASSERT(recurenceType == KCalendarCore::Recurrence::rMonthlyPos || recurenceType == KCalendarCore::Recurrence::rMonthlyDay);
769
770 if (recurenceType == KCalendarCore::Recurrence::rMonthlyPos) {
772 if (rmp.isEmpty()) {
773 return; // Use the default values. Probably marks the editor as dirty
774 }
775
776 if (rmp.first().pos() > 0) { // nth day
777 // TODO if ( rmp.first().pos() != mDateTime->startDate().day() ) { warn user }
778 // NOTE: This silently changes the recurrence when:
779 // rmp.first().pos() != mDateTime->startDate().day()
780 mUi->mMonthlyCombo->setCurrentIndex(ComboIndexMonthlyPos);
781 } else { // (month.last() - n)th day
782 // TODO: Handle recurrences we cannot represent
783 // QDate startDate = mDateTime->startDate();
784 // const int dayFromEnd = startDate.daysInMonth() - startDate.day();
785 // if ( qAbs( rmp.first().pos() ) != dayFromEnd ) { /* warn user */ }
786 mUi->mMonthlyCombo->setCurrentIndex(ComboIndexMonthlyPosInverted);
787 }
788 } else { // Monthly by day
789 // check if we have any setting for which day (vcs import is broken and
790 // does not set any day, thus we need to check)
791 const int day = recurrence->monthDays().isEmpty() ? currentDate().day() : recurrence->monthDays().at(0);
792
793 // Days from the end are after the ones from the begin, so correct for the
794 // negative sign and add 30 (index starting at 0)
795 // TODO: Do similar checks as in the monthlyPos case
796 if (day > 0 && day <= 31) {
797 mUi->mMonthlyCombo->setCurrentIndex(ComboIndexMonthlyDay);
798 } else if (day < 0) {
799 mUi->mMonthlyCombo->setCurrentIndex(ComboIndexMonthlyDayInverted);
800 }
801 }
802
803 // So we can easily detect if the user changed the type, without going through this logic ^
804 mMonthlyInitialType = mUi->mMonthlyCombo->currentIndex();
805}
806
807void IncidenceRecurrence::selectYearlyItem(KCalendarCore::Recurrence *recurrence, ushort recurenceType)
808{
809 Q_ASSERT(recurenceType == KCalendarCore::Recurrence::rYearlyDay || recurenceType == KCalendarCore::Recurrence::rYearlyMonth
810 || recurenceType == KCalendarCore::Recurrence::rYearlyPos);
811
812 if (recurenceType == KCalendarCore::Recurrence::rYearlyDay) {
813 /*
814 const int day = recurrence->yearDays().isEmpty() ? currentDate().dayOfYear() :
815 recurrence->yearDays().first();
816 */
817 // TODO Check if day has actually the same value as in the combo.
818 mUi->mYearlyCombo->setCurrentIndex(ComboIndexYearlyDay);
819 } else if (recurenceType == KCalendarCore::Recurrence::rYearlyMonth) {
820 const int day = recurrence->yearDates().isEmpty() ? currentDate().day() : recurrence->yearDates().at(0);
821
822 /*
823 int month = currentDate().month();
824 if ( !recurrence->yearMonths().isEmpty() ) {
825 month = recurrence->yearMonths().first();
826 }
827 */
828
829 // TODO check month and day to be correct values with respect to what is
830 // presented in the combo box.
831 if (day > 0) {
832 mUi->mYearlyCombo->setCurrentIndex(ComboIndexYearlyMonth);
833 } else {
834 mUi->mYearlyCombo->setCurrentIndex(ComboIndexYearlyMonthInverted);
835 }
836 } else { // KCalendarCore::Recurrence::rYearlyPos
837 /*
838 int month = currentDate().month();
839 if ( !recurrence->yearMonths().isEmpty() ) {
840 month = recurrence->yearMonths().first();
841 }
842 */
843
844 // count is the nth weekday of the month or the ith last weekday of the month.
845 int count = (currentDate().day() - 1) / 7;
846 if (!recurrence->yearPositions().isEmpty()) {
847 count = recurrence->yearPositions().at(0).pos();
848 }
849
850 // TODO check month,count and day to be correct values with respect to what is
851 // presented in the combo box.
852 if (count > 0) {
853 mUi->mYearlyCombo->setCurrentIndex(ComboIndexYearlyPos);
854 } else {
855 mUi->mYearlyCombo->setCurrentIndex(ComboIndexYearlyPosInverted);
856 }
857 }
858
859 // So we can easily detect if the user changed the type, without going through this logic ^
860 mYearlyInitialType = mUi->mYearlyCombo->currentIndex();
861}
862
863void IncidenceRecurrence::setDefaults()
864{
865 mUi->mRecurrenceEndCombo->setCurrentIndex(RecurrenceEndNever);
866 mUi->mRecurrenceEndDate->setDate(currentDate());
867 mUi->mRecurrenceTypeCombo->setCurrentIndex(RecurrenceTypeNone);
868
869 setFrequency(1);
870
871 // -1 because we want between 0 and 6
872 const int day = currentDate().dayOfWeek() - 1;
873
874 QBitArray checkDays(7, false);
875 checkDays.setBit(day);
876
877 QBitArray disableDays(7, false);
878 disableDays.setBit(day);
879
880 mUi->mWeekDayCombo->setDays(checkDays, disableDays);
881
882 mUi->mMonthlyCombo->setCurrentIndex(0); // Recur on the nth of the month
883 mUi->mYearlyCombo->setCurrentIndex(0); // Recur on the nth of the month
884}
885
886void IncidenceRecurrence::setDuration(int duration)
887{
888 if (duration == -1) { // No end date
889 mUi->mRecurrenceEndCombo->setCurrentIndex(RecurrenceEndNever);
890 mUi->mRecurrenceEndStack->setCurrentIndex(0);
891 } else if (duration == 0) {
892 mUi->mRecurrenceEndCombo->setCurrentIndex(RecurrenceEndOn);
893 mUi->mRecurrenceEndStack->setCurrentIndex(1);
894 } else {
895 mUi->mRecurrenceEndCombo->setCurrentIndex(RecurrenceEndAfter);
896 mUi->mRecurrenceEndStack->setCurrentIndex(2);
897 mUi->mEndDurationEdit->setValue(duration);
898 }
899}
900
901void IncidenceRecurrence::setExceptionDates(const KCalendarCore::DateList &dates)
902{
903 mUi->mExceptionList->clear();
904 mExceptionDates.clear();
905 for (const auto &d : dates) {
906 mUi->mExceptionList->addItem(QLocale().toString(d));
907 mExceptionDates.append(d);
908 }
909}
910
911void IncidenceRecurrence::setExceptionDateTimes(const KCalendarCore::DateTimeList &dateTimes)
912{
913 mUi->mExceptionList->clear();
914 mExceptionDates.clear();
915 for (const auto &dt : dateTimes) {
916 const auto date = dt.toLocalTime().date();
917 mUi->mExceptionList->addItem(QLocale().toString(date));
918 mExceptionDates.append(date);
919 }
920}
921
922void IncidenceRecurrence::setFrequency(int frequency)
923{
924 if (frequency < 1) {
925 frequency = 1;
926 }
927
928 mUi->mFrequencyEdit->setValue(frequency);
929}
930
931void IncidenceRecurrence::toggleRecurrenceWidgets(int recurrenceType)
932{
933 bool enable = (recurrenceType != RecurrenceTypeNone) && (recurrenceType != RecurrenceTypeException);
934 mUi->mRecurrenceTypeCombo->setVisible(recurrenceType != RecurrenceTypeException);
935 mUi->mRepeatLabel->setVisible(recurrenceType != RecurrenceTypeException);
936 mUi->mRecurrenceEndLabel->setVisible(enable);
937 mUi->mOnLabel->setVisible(enable && recurrenceType != RecurrenceTypeDaily);
938 if (!enable) {
939 // So we can hide the exceptions labels and not trigger column resizing.
940 mUi->mRepeatLabel->setMinimumSize(mUi->mExceptionsLabel->sizeHint());
941 }
942
943 mUi->mFrequencyLabel->setVisible(enable);
944 mUi->mFrequencyEdit->setVisible(enable);
945 mUi->mRecurrenceRuleLabel->setVisible(enable);
946 mUi->mRepeatStack->setVisible(enable && recurrenceType != RecurrenceTypeDaily);
947 mUi->mRepeatStack->setCurrentIndex(recurrenceType);
948 mUi->mRecurrenceEndCombo->setVisible(enable);
949 mUi->mEndDurationEdit->setVisible(enable);
950 mUi->mRecurrenceEndStack->setVisible(enable);
951
952 // Exceptions widgets
953 mUi->mExceptionsLabel->setVisible(enable);
954 mUi->mExceptionDateEdit->setVisible(enable);
955 mUi->mExceptionAddButton->setVisible(enable);
956 mUi->mExceptionAddButton->setEnabled(mUi->mExceptionDateEdit->date() >= currentDate());
957 mUi->mExceptionRemoveButton->setVisible(enable);
958 mUi->mExceptionRemoveButton->setEnabled(!mUi->mExceptionList->selectedItems().isEmpty());
959 mUi->mExceptionList->setVisible(enable);
960 mUi->mThisAndFutureCheck->setVisible(recurrenceType == RecurrenceTypeException);
961}
962
963QBitArray IncidenceRecurrence::weekday() const
964{
965 QBitArray days(7);
966 // QDate::dayOfWeek() -> returns [1 - 7], 1 == monday
967 days.setBit(currentDate().dayOfWeek() - 1, true);
968 return days;
969}
970
971int IncidenceRecurrence::weekdayCountForMonth(const QDate &date) const
972{
973 Q_ASSERT(date.isValid());
974 // This methods returns how often the weekday specified by @param date occurs
975 // in the month represented by @param date.
976
977 int count = 1;
978 QDate tmp = date.addDays(-7);
979 while (tmp.month() == date.month()) {
980 tmp = tmp.addDays(-7);
981 ++count;
982 }
983
984 tmp = date.addDays(7);
985 while (tmp.month() == date.month()) {
986 tmp = tmp.addDays(7);
987 ++count;
988 }
989
990 return count;
991}
992
993RecurrenceType IncidenceRecurrence::currentRecurrenceType() const
994{
995 if (mLoadedIncidence && mLoadedIncidence->hasRecurrenceId()) {
996 return RecurrenceTypeException;
997 }
998
999 const int currentIndex = mUi->mRecurrenceTypeCombo->currentIndex();
1000 Q_ASSERT_X(currentIndex >= 0 && currentIndex < RecurrenceTypeUnknown, "currentRecurrenceType", "Keep the combo-box values in sync with the enum");
1001 return static_cast<RecurrenceType>(currentIndex);
1002}
1003
1004void IncidenceRecurrence::handleStartDateChange(const QDate &date)
1005{
1006 if (currentDate().isValid()) {
1007 fillCombos();
1008 updateWeekDays(date);
1009 mUi->mExceptionDateEdit->setDate(date);
1010 }
1011}
1012
1013QDate IncidenceRecurrence::currentDate() const
1014{
1015 return mDateTime->startDate();
1016}
1017
1018#include "moc_incidencerecurrence.cpp"
void checkDirtyStatus()
Checks if the dirty status has changed until last check and emits the dirtyStatusChanged signal if ne...
QSharedPointer< IncidenceT > incidence() const
Convenience method to get a pointer for a specific const Incidence Type.
ushort recurrenceType() const
QList< RecurrenceRule::WDayPos > yearPositions() const
QList< int > yearDates() const
void addYearlyDay(int day)
void setYearly(int freq)
void setWeekly(int freq, const QBitArray &days, int weekStart=1)
void setMonthly(int freq)
QList< int > monthDays() const
void addYearlyMonth(short _rNum)
void addMonthlyPos(short pos, const QBitArray &days)
void addYearlyDate(int date)
QBitArray days() const
void addYearlyPos(short pos, const QBitArray &days)
QDateTime startDateTime() const
void setEndDate(const QDate &endDate)
void addMonthlyDate(short day)
QList< RecurrenceRule::WDayPos > monthPositions() const
void setDuration(int duration)
void dateChanged(const QDate &date)
KLocalizedString inContext(const QString &key, const QString &value) const
QString toString() const
KLocalizedString subs(const KLocalizedString &a, int fieldWidth=0, QChar fillChar=QLatin1Char(' ')) const
void checkedItemsChanged(const QStringList &items)
Q_SCRIPTABLE Q_NOREPLY void start()
QString i18nc(const char *context, const char *text, const TYPE &arg...)
KLocalizedString KI18N_EXPORT ki18ncp(const char *context, const char *singular, const char *plural)
KLocalizedString KI18N_EXPORT ki18nc(const char *context, const char *text)
QString i18n(const char *text, const TYPE &arg...)
QString i18ncp(const char *context, const char *singular, const char *plural, const TYPE &arg...)
char * toString(const EngineQuery &query)
KIOCORE_EXPORT QString number(KIO::filesize_t size)
void clicked(bool checked)
void currentIndexChanged(int index)
QDate addDays(qint64 ndays) const const
int day() const const
int dayOfWeek() const const
int dayOfYear() const const
bool isValid(int year, int month, int day)
int month() const const
QDate date() const const
bool isValid() const const
void setDate(QDate date)
QTimeZone timeZone() const const
QDateTime toLocalTime() const const
QDateTime toTimeZone(const QTimeZone &timeZone) const const
void append(QList< T > &&value)
void clear()
qsizetype count() const const
T & first()
bool isEmpty() const const
void removeAt(qsizetype i)
void reserve(qsizetype size)
void itemSelectionChanged()
QString monthName(int month, FormatType type) const const
QLocale system()
QString toString(QDate date, FormatType format) const const
Q_EMITQ_EMIT
QObject * parent() const const
void valueChanged(int i)
void clear()
QString number(double n, char format, int precision)
Unchecked
MatchExactly
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:16:44 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.