Git commit d36ae6a61bcfc7aead668ec5c142bd40723b54a3 by David Jarvie. Committed on 30/08/2025 at 21:20. Pushed by djarvie into branch 'master'.
If a recurrence is suppressed by work time/holidays/exception, suppress its sub-repetitions. M +2 -2 CMakeLists.txt M +3 -0 Changelog M +12 -2 doc/index.docbook M +2 -2 src/kalarmapp.cpp M +216 -58 src/kalarmcalendar/kaevent.cpp M +47 -10 src/kalarmcalendar/kaevent.h M +1 -1 src/messagedisplayhelper.cpp https://invent.kde.org/pim/kalarm/-/commit/d36ae6a61bcfc7aead668ec5c142bd40723b54a3 diff --git a/CMakeLists.txt b/CMakeLists.txt index 87e9862d0..7a9ce1544 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,9 +9,9 @@ set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_ # Whenever KALARM_VERSION changes, set the 3 variables below it to the first # KDE Release Service version which will contain that KAlarm release. # (This must be the full release version, not an alpha or beta release.) -set(KALARM_VERSION "3.11.2") +set(KALARM_VERSION "3.12.0") set(KALARM_VERSION_RELEASE_SERVICE_MAJOR "25") -set(KALARM_VERSION_RELEASE_SERVICE_MINOR "08") +set(KALARM_VERSION_RELEASE_SERVICE_MINOR "12") set(KALARM_VERSION_RELEASE_SERVICE_MICRO "0") # If KAlarm's version has not changed since the last KDE Release Service version, diff --git a/Changelog b/Changelog index 7d06964da..1ad807fe3 100644 --- a/Changelog +++ b/Changelog @@ -1,5 +1,8 @@ KAlarm Change Log +=== Version 3.12.0 (KDE Gear 25.12) --- 30 August 2025 === +* If a recurrence is suppressed by work time/holidays/exception, suppress its sub-repetitions. + === Version 3.11.2 (KDE Gear 25.08) --- 14 July 2025 === * Remove excess spacing in Preferences dialogue. diff --git a/doc/index.docbook b/doc/index.docbook index d3c434ba6..950e1700d 100644 --- a/doc/index.docbook +++ b/doc/index.docbook @@ -39,8 +39,8 @@ <!-- Don't change format of date and version of the documentation --> -<date>2025-03-17</date> -<releaseinfo>3.11.0 (KDE Gear 25.04)</releaseinfo> +<date>2025-08-30</date> +<releaseinfo>3.12.0 (KDE Gear 25.12)</releaseinfo> <abstract> <para>&kalarm; is a personal alarm message, command and email scheduler by &kde;.</para> @@ -1883,6 +1883,16 @@ weekly recurrence on Thursday at 12:00, and use the Sub-Repetition dialog to specify an interval of 1 hour and either a count of 6 or a duration of 6 hours.</para> +<note><para>Sub-repetitions are only triggered after a main recurrence +which actually triggers. If a main recurrence is suppressed by an +exception date/time or by holiday or working time restrictions, the +sub-repetitions for that instance of the recurrence will not be +triggered.</para> + +<para>Individual sub-repetitions will also be suppressed if they occur +at an exception date/time, or during holiday or working time +restrictions.</para></note> + <para>In the Sub-Repetition dialog which is displayed when you click the <guibutton>Sub-Repetition</guibutton> button, check <guilabel>Repeat every</guilabel> to set up a repetition, or uncheck diff --git a/src/kalarmapp.cpp b/src/kalarmapp.cpp index 2f061b763..7b41920c8 100644 --- a/src/kalarmapp.cpp +++ b/src/kalarmapp.cpp @@ -1909,7 +1909,7 @@ int KAlarmApp::handleEvent(const EventId& id, QueuedAction action, bool findUniq // It's too late to display the scheduled occurrence. // Find the last previous occurrence of the alarm. DateTime next; - const KAEvent::OccurType type = event.previousOccurrence(now, next, true); + const KAEvent::OccurType type = event.previousOccurrence(now, next, KAEvent::RepeatsP::Return); switch (static_cast<KAEvent::OccurType>(type & ~KAEvent::OccurType::Repeat)) { case KAEvent::OccurType::FirstOrOnly: @@ -1941,7 +1941,7 @@ int KAlarmApp::handleEvent(const EventId& id, QueuedAction action, bool findUniq // It's over the maximum interval late. // Find the most recent occurrence of the alarm. DateTime next; - const KAEvent::OccurType type = event.previousOccurrence(now, next, true); + const KAEvent::OccurType type = event.previousOccurrence(now, next, KAEvent::RepeatsP::Return); switch (static_cast<KAEvent::OccurType>(type & ~KAEvent::OccurType::Repeat)) { case KAEvent::OccurType::FirstOrOnly: diff --git a/src/kalarmcalendar/kaevent.cpp b/src/kalarmcalendar/kaevent.cpp index fcf2cc469..7d4ebb1b6 100644 --- a/src/kalarmcalendar/kaevent.cpp +++ b/src/kalarmcalendar/kaevent.cpp @@ -188,11 +188,13 @@ public: DateTime deferralLimit(KAEvent::DeferLimit* = nullptr) const; KAEvent::Flags flags() const; bool excludedByWorkTimeOrHoliday(const KADateTime& dt) const; + bool excludedByWorkTime(const KADateTime& wdt) const; + KAEvent::SubRepExclude repExcludedByWorkTimeOrHoliday(const KADateTime& recurDt, int count) const; bool setRepetition(const Repetition&); DateTime nextDateTime(KAEvent::NextTypes) const; bool occursAfter(const KADateTime& preDateTime, bool includeRepetitions) const; KAEvent::OccurType nextOccurrence(const KADateTime& preDateTime, DateTime& result, KAEvent::Repeats = KAEvent::Repeats::Ignore) const; - KAEvent::OccurType previousOccurrence(const KADateTime& afterDateTime, DateTime& result, bool includeRepetitions = false) const; + KAEvent::OccurType previousOccurrence(const KADateTime& afterDateTime, DateTime& result, KAEvent::RepeatsP) const; void setRecurrence(const KARecurrence&); bool setRecur(KCalendarCore::RecurrenceRule::PeriodType, int freq, int count, QDate end, KARecurrence::Feb29Type = KARecurrence::Feb29_None); bool setRecur(KCalendarCore::RecurrenceRule::PeriodType, int freq, int count, const KADateTime& end, KARecurrence::Feb29Type = KARecurrence::Feb29_None); @@ -214,7 +216,7 @@ private: void copy(const KAEventPrivate&); bool mayOccurDailyDuringWork(const KADateTime&) const; int nextWorkRepetition(const KADateTime& pre) const; - void calcNextWorkingTime(const DateTime& nextTrigger) const; + void calcNextWorkingTime(const DateTime& nextTrigger, bool skipRepeats) const; DateTime nextWorkingTime() const; KAEvent::OccurType nextRecurrence(const KADateTime& preDateTime, DateTime& result) const; void setAudioAlarm(const KCalendarCore::Alarm::Ptr&) const; @@ -237,6 +239,7 @@ public: mutable DateTime mMainTrigger; // next trigger time, ignoring reminders and working hours mutable DateTime mAllWorkTrigger; // next trigger time, taking account of reminders and working hours mutable DateTime mMainWorkTrigger; // next trigger time, ignoring reminders but taking account of working hours + mutable bool mMainWorkTriggerIsRecur; // whether mMainWorkTrigger is a recurrence, not a sub-repetition mutable KAEvent::CmdErr mCommandError{KAEvent::CmdErr::None}; // command execution error last time the alarm triggered QString mEventID; // UID: KCalendarCore::Event unique ID @@ -1015,6 +1018,7 @@ void KAEventPrivate::copy(const KAEventPrivate& event) mMainTrigger = event.mMainTrigger; mAllWorkTrigger = event.mAllWorkTrigger; mMainWorkTrigger = event.mMainWorkTrigger; + mMainWorkTriggerIsRecur = event.mMainWorkTriggerIsRecur; mCommandError = event.mCommandError; mEventID = event.mEventID; mCustomProperties = event.mCustomProperties; @@ -2458,6 +2462,9 @@ DateTime KAEventPrivate::nextDateTime(KAEvent::NextTypes type) const return DateTime(); const int offsetToRecur = now.secsTo(nextRecur.effectiveKDateTime()); + bool checkedWorkHol = false; // whether nextRecur has been checked for working time/holiday + bool excludeWorkHol = false; // whether nextRecur is excluded by working time/holiday + // If desired, check for the first sub-repetition after now. DateTime result; if (type & KAEvent::NextRepeat) @@ -2465,27 +2472,47 @@ DateTime KAEventPrivate::nextDateTime(KAEvent::NextTypes type) const // If the recurrence is before now, find the first sub-repetition after now. if (offsetToRecur <= 0) { - const int repInterval = mRepetition.intervalSeconds(); - const int count = -offsetToRecur / repInterval + 1; // first sub-repetition AFTER now - if (count <= mRepetition.count()) + if (type & KAEvent::NextWorkHoliday) { - const DateTime nextRep = nextRecur.addSecs(repInterval * count); - if (!(type & KAEvent::NextWorkHoliday) - || !excludedByWorkTimeOrHoliday(nextRep.effectiveKDateTime())) - result = nextRep; + // If the previous recurrence was excluded by working time/holiday + // retrictions, its sub-repetitions are also excluded. + excludeWorkHol = excludedByWorkTimeOrHoliday(nextRecur.effectiveKDateTime()); + checkedWorkHol = true; + } + if (!excludeWorkHol) + { + const int repInterval = mRepetition.intervalSeconds(); + bool foundRep = false; // haven't found a sub-repetition ok for working time/holidays yet + for (int count = -offsetToRecur / repInterval + 1; // first sub-repetition AFTER now + count <= mRepetition.count(); ++count) + { + const DateTime nextRep = nextRecur.addSecs(repInterval * count); + if (!(type & KAEvent::NextWorkHoliday) + || !excludedByWorkTimeOrHoliday(nextRep.effectiveKDateTime())) + { + result = nextRep; + foundRep = true; // found a sub-repeition ok for working time/holidays + break; + } + } + if (!foundRep) + { + excludeWorkHol = true; // the recurrence found is excluded by work time/holidays + checkedWorkHol = true; + } } } } - bool checkedWorkHol = false; // whether nextRecur has been checked for working time/holiday - bool excludeWorkHol = false; // whether nextRecur is excluded by working time/holiday - // If desired, check for the first reminder after now. DateTime resultReminder; if (type & KAEvent::NextReminder) { - excludeWorkHol = excludedByWorkTimeOrHoliday(nextRecur.effectiveKDateTime()); - checkedWorkHol = true; + if (!checkedWorkHol) + { + excludeWorkHol = excludedByWorkTimeOrHoliday(nextRecur.effectiveKDateTime()); + checkedWorkHol = true; + } if (!excludeWorkHol) { // Reminders are never returned if the recurrence which they relate to @@ -2524,9 +2551,9 @@ DateTime KAEventPrivate::nextDateTime(KAEvent::NextTypes type) const excludeWorkHol = excludedByWorkTimeOrHoliday(nextRecur.effectiveKDateTime()); if (excludeWorkHol) { - // Find first recurrence/repetition which complies with working time/ + // The recurrence found is excluded by work time or holidays. + // Find next recurrence/repetition which complies with working time/ // holiday restrictions. - // The next recurrence found is excluded by work time or holidays. // Find a subsequent sub-repetition or recurrence which is not excluded. calcTriggerTimes(); return (type & KAEvent::NextReminder) ? mAllWorkTrigger : mMainWorkTrigger; @@ -2691,6 +2718,11 @@ void KAEvent::setHolidays() KAEventPrivate::mHolidays = &KAEventPrivate::mDummyHolidays; } +void KAEvent::setHolidays() +{ + KAEventPrivate::mHolidays = &KAEventPrivate::mDummyHolidays; +} + void KAEvent::setWorkTimeOnly(bool wto) { d->mWorkTimeOnly = wto; @@ -2720,12 +2752,44 @@ bool KAEventPrivate::excludedByWorkTimeOrHoliday(const KADateTime& dt) const if (!mWorkTimeOnly) return false; const KADateTime wdt = dt.toTimeSpec(mWorkDayTimeSpec); + return excludedByWorkTime(wdt); +} + +/****************************************************************************** +* Check whether a date/time (which must use mWorkDayTimeSpec) conflicts with +* working hours restrictions for the alarm. +*/ +bool KAEventPrivate::excludedByWorkTime(const KADateTime& wdt) const +{ + Q_ASSERT(wdt.timeSpec() == mWorkDayTimeSpec); if (!mWorkDays.testBit(wdt.date().dayOfWeek() - 1)) return true; return !wdt.isDateOnly() && (wdt.time() < mWorkDayStart || wdt.time() >= mWorkDayEnd); } +/****************************************************************************** +* Check whether a sub-repetition conflicts with working hours and/or holiday +* restrictions for the alarm. If its recurrence conflicts, the sub-repetition +* automatically conflicts. +*/ +KAEvent::SubRepExclude KAEvent::repExcludedByWorkTimeOrHoliday(const KADateTime& recurDt, int count) const +{ + return d->repExcludedByWorkTimeOrHoliday(recurDt, count); +} + +KAEvent::SubRepExclude KAEventPrivate::repExcludedByWorkTimeOrHoliday(const KADateTime& recurDt, int count) const +{ + if (!mExcludeHolidays && !mWorkTimeOnly) + return KAEvent::SubRepExclude::Ok; + // Check the recurrence + if (excludedByWorkTimeOrHoliday(recurDt)) + return KAEvent::SubRepExclude::Recur; + // Check the sub-repetition + KADateTime repDt(mRepetition.duration(count).end(recurDt.qDateTime())); + return excludedByWorkTimeOrHoliday(repDt) ? KAEvent::SubRepExclude::Repeat : KAEvent::SubRepExclude::Ok; +} + /****************************************************************************** * Set new working days and times. * Increment a counter so that working-time-only alarms can detect that they @@ -3387,7 +3451,7 @@ KAEvent::OccurType KAEventPrivate::nextOccurrence(const KADateTime& preDateTime, // However, if the intervals between recurrences vary, we could possibly // have missed a later recurrence which fits the criterion, so check again. DateTime dt; - const KAEvent::OccurType newType = previousOccurrence(repeatDT.effectiveKDateTime(), dt, false); + const KAEvent::OccurType newType = previousOccurrence(repeatDT.effectiveKDateTime(), dt, KAEvent::RepeatsP::Ignore); if (dt > result) { type = newType; @@ -3419,13 +3483,13 @@ KAEvent::OccurType KAEventPrivate::nextOccurrence(const KADateTime& preDateTime, * last previous repetition is returned if appropriate. * 'result' = date/time of previous occurrence, or invalid date/time if none. */ -KAEvent::OccurType KAEvent::previousOccurrence(const KADateTime& afterDateTime, DateTime& result, bool includeRepetitions) const +KAEvent::OccurType KAEvent::previousOccurrence(const KADateTime& afterDateTime, DateTime& result, RepeatsP o) const { - return d->previousOccurrence(afterDateTime, result, includeRepetitions); + return d->previousOccurrence(afterDateTime, result, o); } KAEvent::OccurType KAEventPrivate::previousOccurrence(const KADateTime& afterDateTime, DateTime& result, - bool includeRepetitions) const + KAEvent::RepeatsP includeRepetitions) const { Q_ASSERT(!afterDateTime.isDateOnly()); if (mStartDateTime >= afterDateTime) @@ -3460,7 +3524,7 @@ KAEvent::OccurType KAEventPrivate::previousOccurrence(const KADateTime& afterDat type = KAEvent::OccurType::LastRecur; } - if (includeRepetitions && mRepetition) + if (includeRepetitions == KAEvent::RepeatsP::Return && mRepetition) { // Find the latest repetition which is before the specified time. const int repetition = mRepetition.previousRepeatCount(result.effectiveKDateTime(), afterDateTime); @@ -4019,6 +4083,7 @@ void KAEventPrivate::dumpDebug() const qCDebug(KALARMCAL_LOG) << "-- mMainTrigger:" << mMainTrigger.toString(); qCDebug(KALARMCAL_LOG) << "-- mAllWorkTrigger:" << mAllWorkTrigger.toString(); qCDebug(KALARMCAL_LOG) << "-- mMainWorkTrigger:" << mMainWorkTrigger.toString(); + qCDebug(KALARMCAL_LOG) << "-- mMainWorkTriggerIsRecur:" << mMainWorkTriggerIsRecur; qCDebug(KALARMCAL_LOG) << "-- mCategory:" << mCategory; qCDebug(KALARMCAL_LOG) << "-- mName:" << mName; if (mCategory == CalEvent::TEMPLATE) @@ -4507,9 +4572,26 @@ void KAEventPrivate::calcTriggerTimes() const : mMainTrigger.addMins(-mReminderMinutes); // If only-during-working-time is set and it recurs, it won't actually trigger // unless it falls during working hours. - if ((!mWorkTimeOnly && !excludeHolidays) - || !recurs - || !excludedByWorkTimeOrHoliday(mMainTrigger.kDateTime())) + bool excluded = false; // whether next occurrence is excluded by working time/holidays + bool skipRepeats = false; // whether next sub-rep is excluded by recurrence working time/holidays + if (recurs && (mWorkTimeOnly || excludeHolidays)) + { + // Check if current recurrence is excluded by working time/holidays. + if (mNextRepeat && mRepetition) + { + // The next trigger is a sub-repetition. + KAEvent::SubRepExclude excl = repExcludedByWorkTimeOrHoliday(mainDateTime(false).kDateTime(), mNextRepeat); + skipRepeats = (excl == KAEvent::SubRepExclude::Recur); + excluded = (excl != KAEvent::SubRepExclude::Ok); + } + else + { + // The next trigger is a recurrence. + excluded = excludedByWorkTimeOrHoliday(mMainTrigger.kDateTime()); + skipRepeats = excluded; + } + } + if (!excluded) { // It only occurs once, or it complies with any working hours/holiday // restrictions. @@ -4525,7 +4607,7 @@ void KAEventPrivate::calcTriggerTimes() const if (!excludeHolidays) { // There are no holiday restrictions. - calcNextWorkingTime(mMainTrigger); + calcNextWorkingTime(mMainTrigger, skipRepeats); } else if (mHolidays->isValid()) { @@ -4534,12 +4616,15 @@ void KAEventPrivate::calcTriggerTimes() const KADateTime kdt; for (int i = 0; i < 20; ++i) { - calcNextWorkingTime(nextTrigger); + calcNextWorkingTime(nextTrigger, skipRepeats); if (!mHolidays->isHoliday(mMainWorkTrigger.date())) return; // found a non-holiday occurrence kdt = mMainWorkTrigger.effectiveKDateTime(); kdt.setTime(QTime(23, 59, 59)); - const KAEvent::OccurType type = nextOccurrence(kdt, nextTrigger, KAEvent::Repeats::Return); + // If mMainWorkTrigger is a recurrence, skip its sub-repetitions. + // If it's a sub-repetition, look for the next sub-repetition. + KAEvent::Repeats repType = mMainWorkTriggerIsRecur ? KAEvent::Repeats::Ignore : KAEvent::Repeats::Return; + const KAEvent::OccurType type = nextOccurrence(kdt, nextTrigger, repType); if (!nextTrigger.isValid()) break; if (!excludedByWorkTimeOrHoliday(nextTrigger.kDateTime())) @@ -4555,14 +4640,15 @@ void KAEventPrivate::calcTriggerTimes() const } else if (excludeHolidays && mHolidays->isValid()) { - // Holidays are excluded. + // Holidays are excluded, but there are no working time restrictions. DateTime nextTrigger = mMainTrigger; KADateTime kdt; for (int i = 0; i < 20; ++i) { kdt = nextTrigger.effectiveKDateTime(); kdt.setTime(QTime(23, 59, 59)); - const KAEvent::OccurType type = nextOccurrence(kdt, nextTrigger, KAEvent::Repeats::Return); + KAEvent::Repeats repType = skipRepeats ? KAEvent::Repeats::Ignore : KAEvent::Repeats::Return; + const KAEvent::OccurType type = nextOccurrence(kdt, nextTrigger, repType); if (!nextTrigger.isValid()) break; if (!mHolidays->isHoliday(nextTrigger.date())) @@ -4572,18 +4658,24 @@ void KAEventPrivate::calcTriggerTimes() const mAllWorkTrigger = (type & KAEvent::OccurType::Repeat) ? mMainWorkTrigger : mMainWorkTrigger.addMins(-reminder); return; // found a non-holiday occurrence } + // If nextTrigger is a recurrence, skip its sub-repetitions. + skipRepeats = !(type & KAEvent::OccurType::Repeat); } mMainWorkTrigger = mAllWorkTrigger = DateTime(); } } /****************************************************************************** -* Return the time of the next scheduled occurrence of the event during working +* Set the time of the next scheduled occurrence of the event during working * hours, for an alarm which is restricted to working hours. +* mMainWorkTriggerIsRecur is set to indicate whether mMainWorkTrigger is a +* recurrence or a sub-repetition. * On entry, 'nextTrigger' = the next recurrence or repetition (as returned by * mainDateTime(true) ). +* 'skipRepeats' is true if the next occurrence is a sub-repetition and the +* current recurrence is excluded by working time/holidays. */ -void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const +void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger, bool skipRepeats) const { qCDebug(KALARMCAL_LOG) << "next=" << nextTrigger.kDateTime().toString(QStringLiteral("%Y-%m-%d %H:%M")); @@ -4636,6 +4728,8 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const || nDayPos == 1) { // It recurs on the same day each week + if (skipRepeats) + return; // it recurs on a non-working day if (!mRepetition || weeklyRepeat) return; // any repetitions are also weekly @@ -4644,7 +4738,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const // on a working day. KADateTime dt(nextTriggerWT.kDateTime().addDays(1)); dt.setTime(QTime(0, 0, 0)); - previousOccurrence(dt, newdt, false); + previousOccurrence(dt, newdt, KAEvent::RepeatsP::Ignore); if (!newdt.isValid()) return; // this should never happen kdt = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime(); @@ -4657,18 +4751,23 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const break; if (!repeatNum) { + // Have checked all sub-repetitions: now find the next recurrence. nextOccurrence(newdt.kDateTime(), newdt, KAEvent::Repeats::Ignore); if (mWorkDays.testBit(day)) { newdt = newdt.toTimeSpec(nextTrigger.timeSpec()); mMainWorkTrigger = newdt; mAllWorkTrigger = mMainWorkTrigger.addMins(-reminder); + mMainWorkTriggerIsRecur = true; return; } + if (skipRepeats) + return; kdt = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime(); } else { + // Check a sub-repetition. const int inc = repeatFreq * repeatNum; if (mWorkDays.testBit((day + inc) % 7)) { @@ -4676,6 +4775,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const kdt.setDateOnly(true); kdt = kdt.toTimeSpec(nextTrigger.timeSpec()); mMainWorkTrigger = mAllWorkTrigger = kdt; + mMainWorkTriggerIsRecur = false; return; } } @@ -4686,7 +4786,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const { // It's a date-only alarm with either no sub-repetition or a // sub-repetition which always falls on the same day of the week - // as the recurrence (if any). + // as the recurrence. unsigned days = 0; for (; ;) { @@ -4707,6 +4807,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const kdt = kdt.toTimeSpec(nextTrigger.timeSpec()); mMainWorkTrigger = kdt; mAllWorkTrigger = kdt.addSecs(-60 * reminder); + mMainWorkTriggerIsRecur = true; return; } @@ -4714,16 +4815,24 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const // as does the sub-repetition. // Find the previous recurrence (as opposed to sub-repetition) unsigned days = 1 << (kdt.date().dayOfWeek() - 1); - KADateTime dt(nextTriggerWT.kDateTime().addDays(1)); - dt.setTime(QTime(0, 0, 0)); - previousOccurrence(dt, newdt, false); - if (!newdt.isValid()) - return; // this should never happen - kdt = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime(); - int day = kdt.date().dayOfWeek() - 1; // Monday = 0 + int day = 0; + if (!skipRepeats) + { + // The previous recurrence was on a working day, so its remaining + // sub-repetitions need to be checked. + KADateTime dt(nextTriggerWT.kDateTime().addDays(1)); + dt.setTime(QTime(0, 0, 0)); + previousOccurrence(dt, newdt, KAEvent::RepeatsP::Ignore); + if (!newdt.isValid()) + return; // this should never happen + kdt = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime(); + day = kdt.date().dayOfWeek() - 1; // Monday = 0 + } for (int repeatNum = mNextRepeat; ; repeatNum = 0) { - while (++repeatNum <= mRepetition.count()) + // Check sub-repetitions - but only if the recurrence was in + // working time. + while (!skipRepeats && ++repeatNum <= mRepetition.count()) { const int inc = repeatFreq * repeatNum; if (mWorkDays.testBit((day + inc) % 7)) @@ -4732,12 +4841,15 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const kdt.setDateOnly(true); kdt = kdt.toTimeSpec(nextTrigger.timeSpec()); mMainWorkTrigger = mAllWorkTrigger = kdt; + mMainWorkTriggerIsRecur = false; return; } if ((days & allDaysMask) == allDaysMask) return; // found an occurrence on every possible day of the week!?! days |= 1 << day; } + + // Get the next recurrence. nextOccurrence(kdt, newdt, KAEvent::Repeats::Ignore); if (!newdt.isValid()) return; @@ -4745,15 +4857,18 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const day = kdt.date().dayOfWeek() - 1; if (mWorkDays.testBit(day)) { + // Found a recurrence on a working day. kdt.setDateOnly(true); kdt = kdt.toTimeSpec(nextTrigger.timeSpec()); mMainWorkTrigger = kdt; mAllWorkTrigger = kdt.addSecs(-60 * reminder); + mMainWorkTriggerIsRecur = true; return; } if ((days & allDaysMask) == allDaysMask) return; // found an occurrence on every possible day of the week!?! days |= 1 << day; + skipRepeats = true; // the recurrence is not on a working day } return; } @@ -4783,7 +4898,9 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const unsigned days = 0; for (; ;) { - const KAEvent::OccurType type = nextOccurrence(kdt, newdt, KAEvent::Repeats::Return); + // Skip sub-repetitions if the recurrence wasn't in working time. + KAEvent::Repeats repType = skipRepeats ? KAEvent::Repeats::Ignore : KAEvent::Repeats::Return; + const KAEvent::OccurType type = nextOccurrence(kdt, newdt, repType); if (!newdt.isValid()) return; repetition = (type & KAEvent::OccurType::Repeat); @@ -4797,12 +4914,14 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const if ((days & allDaysMask) == allDaysMask) return; // found a recurrence on every possible day of the week!?! days |= 1 << day; + skipRepeats = true; // the recurrence is not on a working day } } mMainWorkTrigger = nextTriggerWT; mMainWorkTrigger.setDate(kdt.date()); mMainWorkTrigger = mMainWorkTrigger.toTimeSpec(nextTrigger.timeSpec()); mAllWorkTrigger = repetition ? mMainWorkTrigger : mMainWorkTrigger.addMins(-reminder); + mMainWorkTriggerIsRecur = !repetition; return; } @@ -4832,14 +4951,30 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const // It's a repetition inside a recurrence, each of which occurs // at different times of day (bearing in mind that the repetition // may occur at daily intervals after each recurrence). - // Find the previous recurrence (as opposed to sub-repetition) repeatFreq = mRepetition.intervalSeconds(); - previousOccurrence(kdt.addSecs(1), newdt, false); - if (!newdt.isValid()) - return; // this should never happen - kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime(); - repeatNum = kdtRecur.secsTo(kdt) / repeatFreq; - kdt = kdtRecur.addSecs(repeatNum * repeatFreq); + + if (skipRepeats) + { + // The last recurrence was not in working time, so skip its + // sub-repetitions. + nextOccurrence(kdt, newdt, KAEvent::Repeats::Ignore); + if (!newdt.isValid()) + return; // this should never happen + kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime(); + skipRepeats = excludedByWorkTime(kdtRecur); + repeatNum = 0; + kdt = kdtRecur; + } + else + { + // Find the previous recurrence (as opposed to sub-repetition) + previousOccurrence(kdt.addSecs(1), newdt, KAEvent::RepeatsP::Ignore); + if (!newdt.isValid()) + return; // this should never happen + kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime(); + repeatNum = kdtRecur.secsTo(kdt) / repeatFreq; + kdt = kdtRecur.addSecs(repeatNum * repeatFreq); + } } else { @@ -4865,7 +5000,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const int transitionIx = -1; for (int n = 0; n < 7 * 24 * 60; ++n) { - if (mRepetition) + if (!skipRepeats && mRepetition) { // Check the sub-repetitions for this recurrence for (; ;) @@ -4883,6 +5018,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const { kdt = kdt.toTimeSpec(nextTrigger.timeSpec()); mMainWorkTrigger = mAllWorkTrigger = kdt; + mMainWorkTriggerIsRecur = false; return; } } @@ -4893,6 +5029,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const if (!newdt.isValid()) return; kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime(); + skipRepeats = excludedByWorkTime(kdtRecur); dayRecur = kdtRecur.date().dayOfWeek() - 1; // Monday = 0 const QTime t = kdtRecur.time(); if (t >= mWorkDayStart && t < mWorkDayEnd) @@ -4902,6 +5039,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const kdtRecur = kdtRecur.toTimeSpec(nextTrigger.timeSpec()); mMainWorkTrigger = kdtRecur; mAllWorkTrigger = kdtRecur.addSecs(-60 * reminder); + mMainWorkTriggerIsRecur = true; return; } } @@ -4922,10 +5060,11 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const transitionIx = i; if (++transitionIx >= tzTransitions.count()) return; - previousOccurrence(KADateTime(tzTransitions[transitionIx].atUtc), newdt, false); + previousOccurrence(KADateTime(tzTransitions[transitionIx].atUtc), newdt, KAEvent::RepeatsP::Ignore); kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime(); if (finalDate.daysTo(kdtRecur.date()) > 365) return; + skipRepeats = excludedByWorkTime(kdtRecur); firstTime = kdtRecur.time(); firstOffset = kdtRecur.utcOffset(); currentOffset = firstOffset; @@ -4945,12 +5084,27 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const * Then, it is still possible that a working time sub-repetition * could occur immediately after a seasonal time change. */ - // Find the previous recurrence (as opposed to sub-repetition) const int repeatFreq = mRepetition.intervalSeconds(); - previousOccurrence(kdt.addSecs(1), newdt, false); - if (!newdt.isValid()) - return; // this should never happen - KADateTime kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime(); + KADateTime kdtRecur; + if (skipRepeats) + { + // The last recurrence was not in working time, so skip its + // sub-repetitions. + nextOccurrence(kdt, newdt, KAEvent::Repeats::Ignore); + if (!newdt.isValid()) + return; // this should never happen + kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime(); + skipRepeats = excludedByWorkTime(kdtRecur); + kdt = kdtRecur; + } + else + { + // Find the previous recurrence (as opposed to sub-repetition) + previousOccurrence(kdt.addSecs(1), newdt, KAEvent::RepeatsP::Ignore); + if (!newdt.isValid()) + return; // this should never happen + kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime(); + } const bool recurDuringWork = (kdtRecur.time() >= mWorkDayStart && kdtRecur.time() < mWorkDayEnd); // Use the previous recurrence as a base for checking whether @@ -4980,7 +5134,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const for (; ;) { // Check the sub-repetitions for this recurrence - if (repeatsDuringWork >= 0) + if (!skipRepeats && repeatsDuringWork >= 0) { for (; ;) { @@ -5008,6 +5162,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const { kdt = kdt.toTimeSpec(nextTrigger.timeSpec()); mMainWorkTrigger = mAllWorkTrigger = kdt; + mMainWorkTriggerIsRecur = false; return; } repeatsDuringWork = 1; @@ -5032,6 +5187,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const kdtRecur = kdtNextRecur; nextOccurrence(kdtRecur, newdt, KAEvent::Repeats::Ignore); kdtNextRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime(); + skipRepeats = excludedByWorkTime(kdtNextRecur); dateRecur = kdtRecur.date(); const int dayRecur = dateRecur.dayOfWeek() - 1; // Monday = 0 if (recurDuringWork && mWorkDays.testBit(dayRecur)) @@ -5039,6 +5195,7 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const kdtRecur = kdtRecur.toTimeSpec(nextTrigger.timeSpec()); mMainWorkTrigger = kdtRecur; mAllWorkTrigger = kdtRecur.addSecs(-60 * reminder); + mMainWorkTriggerIsRecur = true; return; } days |= 1 << dayRecur; @@ -5056,9 +5213,10 @@ void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const if (++transitionIx >= tzTransitions.count()) return; kdt = KADateTime(tzTransitions[transitionIx].atUtc); - previousOccurrence(kdt, newdt, false); + previousOccurrence(kdt, newdt, KAEvent::RepeatsP::Ignore); kdt = kdt.toTimeSpec(mWorkDayTimeSpec); kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime(); + skipRepeats = excludedByWorkTime(kdtRecur); } return; // not found - give up } diff --git a/src/kalarmcalendar/kaevent.h b/src/kalarmcalendar/kaevent.h index 53a94d446..345dae386 100644 --- a/src/kalarmcalendar/kaevent.h +++ b/src/kalarmcalendar/kaevent.h @@ -256,14 +256,29 @@ public: Repeat = 0x10 //!< a sub-repetition of an occurrence (bitmask) }; + /** Whether a sub-repetition is excluded due to working time/holidays. */ + enum class SubRepExclude + { + Ok, //!< not affected by working time/holidays + Recur, //!< the recurrence is excluded due to working time/holidays + Repeat //!< the sub-repetition (but not the recurrence) is excluded due to working time/holidays + }; + /** How to treat sub-repetitions in nextOccurrence(). */ enum class Repeats { Ignore = 0, //!< check for recurrences only, ignore sub-repetitions - Return, //!< return a sub-repetition if it's the next occurrence + Return, //!< return a sub-repetition if it's the next/previous occurrence RecurBefore //!< if a sub-repetition is the next occurrence, return the previous recurrence, not the sub-repetition }; + /** How to treat sub-repetitions in previousOccurrence(). */ + enum class RepeatsP + { + Ignore = 0, //!< check for recurrences only, ignore sub-repetitions + Return //!< return a sub-repetition if it's the next/previous occurrence + }; + /** What type of occurrence currently limits how long the alarm can be deferred. */ enum class DeferLimit { @@ -883,8 +898,8 @@ public: * be returned. N.B. Reminders are not displayed for * sub-repetitions. * - @p type contains NextWorkHoliday: as above but the search continues until a - * recurrence is found which occurs during working - * hours and not on a holiday. + * recurrence is found which complies with any + * working hours and holiday restrictions. * - @p type contains NextDeferral: if the event (not a reminder) has been deferred, * returns the deferral time. * If a reminder has been deferred AND @p type @@ -1007,6 +1022,9 @@ public: /** Clear the holiday data used by all KAEvent instances. */ static void setHolidays(); + /** Clear the holiday data used by all KAEvent instances. */ + static void setHolidays(); + /** Enable or disable the alarm on non-working days and outside working hours. * Note that this option only has any effect for recurring alarms. * @param wto true to restrict to working time, false to enable any time @@ -1034,6 +1052,23 @@ public: */ bool excludedByWorkTimeOrHoliday(const KADateTime& dt) const; + /** Check whether a sub-repetition conflicts with working hours and/or holiday + * restrictions for the alarm. Note that if its recurrence conflicts, the + * sub-repetition automatically conflicts. + * @note If @p recurDt is date-only, only holidays and/or working days are + * taken account of; working hours are ignored. + * @note No check is made as to whether a recurrence is actually scheduled + * to occur at time @p recurDt. + * @param recurDt the date/time of the sub-repetition's recurrence. + * @param index the index to the sub-repetition to check. + * @return whether the alarm is disabled from occurring either because the + * recurrence at @p recurDt is outside working hours (if the alarm + * is working time only) or is during a holiday (if the alarm is + * disabled on holidays), or if the time of the sub-repetition + * is similarly restricted; + */ + SubRepExclude repExcludedByWorkTimeOrHoliday(const KADateTime& recurDt, int index) const; + /** Set working days and times, to be used by all KAEvent instances. * @param days bits set to 1 for each working day. Array element 0 = Monday ... 6 = Sunday. * @param start start time in working day. @@ -1274,15 +1309,17 @@ public: * @note If the event is date-only, its occurrences are considered to occur * at the start-of-day time when comparing with @p preDateTime. * - * @param afterDateTime the specified date/time. - * @param result date/time of previous occurrence, or invalid - * date/time if none. - * @param includeRepetitions if true and the alarm has a sub-repetition, the - * last previous repetition is returned if - * appropriate. + * @param afterDateTime the specified date/time. + * @param result date/time of previous occurrence, or invalid + * date/time if none. + * @param option = Ignore: only recurrences are returned; + * = Return: if the alarm has a sub-repetition, the + * last previous repetition is returned if + * appropriate; + * Other values must not be specified. * @see nextOccurrence() */ - OccurType previousOccurrence(const KADateTime& afterDateTime, DateTime& result, bool includeRepetitions = false) const; + OccurType previousOccurrence(const KADateTime& afterDateTime, DateTime& result, RepeatsP includeRepetitions) const; /** Set the event to be a copy of the specified event, making the specified * alarm the 'displaying' alarm. diff --git a/src/messagedisplayhelper.cpp b/src/messagedisplayhelper.cpp index 0c824df43..d987e9449 100644 --- a/src/messagedisplayhelper.cpp +++ b/src/messagedisplayhelper.cpp @@ -113,7 +113,7 @@ MessageDisplayHelper::MessageDisplayHelper(MessageDisplay* parent, const KAEvent { if (event.reminderMinutes() < 0) { - event.previousOccurrence(alarm.dateTime(false).effectiveKDateTime(), mDateTime, false); + event.previousOccurrence(alarm.dateTime(false).effectiveKDateTime(), mDateTime, KAEvent::RepeatsP::Ignore); if (!mDateTime.isValid() && event.repeatAtLogin()) mDateTime = alarm.dateTime().addSecs(event.reminderMinutes() * 60); }
