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

KDE's Doxygen guidelines are available online.