Git commit aa8d023575c6946dc23fbf0df23b7ac167f5c2ba by David Jarvie. Committed on 09/01/2026 at 23:24. Pushed by djarvie into branch 'master'.
Provide facility to skip the next n recurrences/sub-repetitions of an alarm M +2 -1 Changelog M +1 -0 DESIGN-kalarmcalendar.html M +54 -2 doc/index.docbook M +2 -0 src/data/kalarmui.rc M +69 -1 src/functions.cpp M +15 -1 src/functions.h M +420 -131 src/kalarmcalendar/kaevent.cpp M +92 -33 src/kalarmcalendar/kaevent.h M +60 -1 src/mainwindow.cpp M +5 -1 src/mainwindow.h M +6 -6 src/resourcescalendar.cpp M +3 -2 src/resourcescalendar.h https://invent.kde.org/pim/kalarm/-/commit/aa8d023575c6946dc23fbf0df23b7ac167f5c2ba diff --git a/Changelog b/Changelog index 2899d673c..927ca07b2 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,7 @@ KAlarm Change Log -=== Version 3.13.0 (KDE Gear 26.04) --- 3 January 2026 === +=== Version 3.13.0 (KDE Gear 26.04) --- 9 January 2026 === +* Provide facility to skip the next few recurrences/sub-repetitions of an alarm. * Provide Edit Alarm dialogue option to override notification inhibition or display over X11 full screen applications. * Only show options for Late cancel/Reminder in Edit Alarm dialogue when they are enabled. * Fix date-only sub-repetition time calculations. diff --git a/DESIGN-kalarmcalendar.html b/DESIGN-kalarmcalendar.html index c5ed693e5..5f4ce961a 100644 --- a/DESIGN-kalarmcalendar.html +++ b/DESIGN-kalarmcalendar.html @@ -98,6 +98,7 @@ Compliant with the iCalendar specification, KAlarm defines a number of custom fi <tr><td class=cont></td><td class=cont><tt>EXHOLIDAYS</tt></td><td class=cont>The alarm will not trigger on holidays (as defined for the holiday region configured by the user)</td></tr> <tr><td class=cont></td><td class=cont><tt>WORKTIME</tt></td><td class=cont>The alarm will not trigger during working hours on working days (as configured by the user)</td></tr> <tr><td class=cont></td><td class=cont><tt>DEFER;<em>interval</em></tt></td><td class=cont>Records the default deferral parameters for the alarm, used when the Defer dialogue is displayed. <tt><em>interval</em></tt> holds the deferral interval in minutes, with an optional <tt>D</tt> suffix to specify a date-only deferral. E.g. <tt class=eg>DEFER;1440D</tt> = 1 day, date-only</td></tr> + <tr><td class=cont></td><td class=cont><tt>SKIP;<em>date-time</em></tt></td><td class=cont>For an active alarm, indicates that the alarm is being skipped (i.e. it is temporarily disabled). <tt><em>date-time</em></tt> specifies when it will resume normal functioning (i.e. when it will be enabled again). <tt><em>date-time</em></tt> is in the format <tt><em>YYYYMMDD</em>T<em>HHMMSS</em></tt> for a date/time alarm, or <tt><em>YYYYMMDD</em></tt> for a date-only alarm.</td></tr> <tr><td class=cont></td><td class=cont><tt>LATECANCEL;<em>interval</em></tt></td><td class=cont>How late the alarm can trigger (in minutes) before it will be cancelled; default = 1 minute</td></tr> <tr><td class=cont></td><td class=cont><tt>LATECLOSE;<em>interval</em></tt></td><td class=cont>For a display alarm, how long after the trigger time (in minutes) until the alarm window is automatically closed; default = 1 minute. It will also be cancelled if it triggers after this time.</td></tr> <tr><td class=cont></td><td class=cont><tt>TMPLAFTTIME;<em>interval</em></tt></td><td class=cont>For a template alarm, holds the value (in minutes) of the "After time" option</td></tr> diff --git a/doc/index.docbook b/doc/index.docbook index 71282730e..ce5bdf314 100644 --- a/doc/index.docbook +++ b/doc/index.docbook @@ -39,7 +39,7 @@ <!-- Don't change format of date and version of the documentation --> -<date>2026-1-7</date> +<date>2026-1-9</date> <releaseinfo>3.13.0 (KDE Gear 26.04)</releaseinfo> <abstract> @@ -538,7 +538,8 @@ from the context menu.</para> <title>Enabling/Disabling an Alarm</title> <para>See <link linkend="enable-disable">Enabling and Disabling Alarms</link> -for how to enable and disable alarms, either individually or as a whole.</para> +for how to enable and disable alarms, either individually or as a +whole, or temporarily skip individual alarms.</para> </sect2> @@ -2540,6 +2541,57 @@ from the context menu.</para> </listitem> </itemizedlist> +</sect2> + +<sect2> +<title>Skipping Individual Alarms</title> + +<para>You can choose to skip the next few (1 - 10) activations of an +alarm. This temporarily disables the alarm until that number of +recurrences or sub-repetitions have passed without activation. The +alarm will resume triggering as normal after that.</para> + +<note><para>No reminders will be displayed for skipped activations.</para> +<para>Skipping does not affect any outstanding deferral of the +alarm.</para></note> + +<para>To skip the next activation(s) of individual alarms, do one of the +following, and then enter the number of activations to skip:</para> + +<itemizedlist> +<listitem> +<para>Select one or more alarms by clicking on their entries in the +alarm list. Then choose <menuchoice> +<guimenu>Actions</guimenu><guimenuitem>Skip</guimenuitem> +</menuchoice>.</para> +</listitem> + +<listitem> +<para><mousebutton>Right</mousebutton> click on the desired entries in +the alarm list and choose +<menuchoice><guimenuitem>Skip</guimenuitem></menuchoice> +from the context menu.</para> +</listitem> +</itemizedlist> + +<para>To cancel skipping individual alarms, do one of the following:</para> + +<itemizedlist> +<listitem> +<para>Select one or more alarms by clicking on their entries in the +alarm list. Then choose <menuchoice> +<guimenu>Actions</guimenu><guimenuitem>Cancel skip</guimenuitem> +</menuchoice>.</para> +</listitem> + +<listitem> +<para><mousebutton>Right</mousebutton> click on the desired entries in +the alarm list and choose +<menuchoice><guimenuitem>Cancel skip</guimenuitem></menuchoice> +from the context menu.</para> +</listitem> +</itemizedlist> + </sect2> </sect1> diff --git a/src/data/kalarmui.rc b/src/data/kalarmui.rc index 6d087b37b..10bde356a 100644 --- a/src/data/kalarmui.rc +++ b/src/data/kalarmui.rc @@ -56,6 +56,7 @@ <text>Actions</text> <Action name="undelete" /> <Action name="disable" /> + <Action name="skip" /> <Separator/> <Action name="alarmsEnable" /> <Action name="refreshAlarms" /> @@ -84,6 +85,7 @@ <Separator/> <Action name="undelete" /> <Action name="disable" /> + <Action name="skip" /> <Separator/> <Action name="createTemplate" /> <Action name="export" /> diff --git a/src/functions.cpp b/src/functions.cpp index f30623a7c..851dcd078 100644 --- a/src/functions.cpp +++ b/src/functions.cpp @@ -1,7 +1,7 @@ /* * functions.cpp - miscellaneous functions * Program: kalarm - * SPDX-FileCopyrightText: 2001-2024 David Jarvie <[email protected]> + * SPDX-FileCopyrightText: 2001-2026 David Jarvie <[email protected]> * * SPDX-License-Identifier: GPL-2.0-or-later */ @@ -791,6 +791,74 @@ UpdateResult enableEvents(QList<KAEvent>& events, bool enable, QWidget* msgParen return status.status; } +/****************************************************************************** +* Enable or disable skipping of alarms. +* The new events will have the same event IDs as the old ones. +*/ +UpdateResult skipEvents(QList<KAEvent>& events, int skipCount, QWidget* msgParent) +{ + qCDebug(KALARM_LOG) << "KAlarm::skipEvents:" << events.count(); + if (events.isEmpty()) + return UpdateResult(UPDATE_OK); + UpdateStatusData status; +#if ENABLE_RTC_WAKE_FROM_SUSPEND + bool deleteWakeFromSuspendAlarm = false; + const QString wakeFromSuspendId = checkRtcWakeConfig().value(0); +#endif + QSet<ResourceId> resourceIds; // resources whose events have been updated + for (int i = 0, end = events.count(); i < end; ++i) + { + KAEvent* event = &events[i]; + const DateTime oldSkipTime = event->skipDateTime(); + if (event->skip(skipCount) && event->skipDateTime() != oldSkipTime) + { + qCDebug(KALARM_LOG) << "KAlarm::skipEvents: event skipped:" << event->id(); +#if ENABLE_RTC_WAKE_FROM_SUSPEND + if (event->id() == wakeFromSuspendId) + deleteWakeFromSuspendAlarm = true; +#endif + + // Update the event in the calendar file + const KAEvent newev = ResourcesCalendar::updateEvent(*event); + if (!newev.isValid()) + { + qCCritical(KALARM_LOG) << "KAlarm::skipEvents: Error updating event in calendar:" << event->id(); + status.appendFailed(i); + } + else + resourceIds.insert(event->resourceId()); + } + } + + if (status.failedCount()) + status.setError(status.failedCount() == events.count() ? UPDATE_FAILED : UPDATE_ERROR, status.failedCount()); + if (status.failedCount() < events.count()) + { + QString msg; + for (ResourceId id : resourceIds) + { + Resource res = Resources::resource(id); + if (!res.save(&msg)) + { + // Don't reload resource after failed save. It's better to + // keep the new enabled status of the alarms at least until + // KAlarm is restarted. + status.setError(SAVE_FAILED, status.failedCount(), msg); + } + } + } + if (status.status != UPDATE_OK && msgParent) + displayUpdateError(msgParent, ERR_ADD, status); + +#if ENABLE_RTC_WAKE_FROM_SUSPEND + // Remove any wake-from-suspend scheduled for a disabled alarm + if (deleteWakeFromSuspendAlarm && !wakeFromSuspendId.isEmpty()) + cancelRtcWake(msgParent, wakeFromSuspendId); +#endif + + return status.status; +} + /****************************************************************************** * This method must only be called from the main KAlarm queue processing loop, * to prevent asynchronous calendar operations interfering with one another. diff --git a/src/functions.h b/src/functions.h index fd1d39e07..796d45756 100644 --- a/src/functions.h +++ b/src/functions.h @@ -1,7 +1,7 @@ /* * functions.h - miscellaneous functions * Program: kalarm - * SPDX-FileCopyrightText: 2007-2022 David Jarvie <[email protected]> + * SPDX-FileCopyrightText: 2007-2026 David Jarvie <[email protected]> * * SPDX-License-Identifier: GPL-2.0-or-later */ @@ -273,6 +273,20 @@ UpdateResult reactivateEvents(QList<KAEvent>& events, QList<int>& ineligibleInde */ UpdateResult enableEvents(QList<KAEvent>& events, bool enable, QWidget* msgParent = nullptr); +/** Enable or disable skipping for alarms. + * The new events will have the same event IDs as the old ones. + * @param events Events to be skipped/cancelled. Each one's resourceId() + * must give the ID of the resource which contains it. + * @param skipCount New skip count for the events (0 to cancel skipping). + * @param msgParent Parent widget for any calendar selection prompt or error + * message. + * @return Success status; if == UPDATE_FAILED, the skipping status of all + * events is unchanged; if == SAVE_FAILED, the skipping status of at + * least one event has been successfully changed, but will be lost + * when its resource is reloaded. + */ +UpdateResult skipEvents(QList<KAEvent>& events, int skipCount, QWidget* msgParent = nullptr); + /** Return whether an event is read-only. * This depends on whether the event or its resource is read-only. */ diff --git a/src/kalarmcalendar/kaevent.cpp b/src/kalarmcalendar/kaevent.cpp index 475a436e4..4a1d6d74f 100644 --- a/src/kalarmcalendar/kaevent.cpp +++ b/src/kalarmcalendar/kaevent.cpp @@ -20,6 +20,20 @@ using namespace KCalendarCore; +namespace +{ +const int MAX_SKIP_COUNT = 10; // maximum value of KAEvent skip count + +// Which trigger times need to be recalculated and cached (bitmask). +enum TriggerChange +{ + TriggerChangeNone = 0, // no recalculation needed + TriggerChangeMain = 0x01, // recalculate main trigger times + TriggerChangeSkip = 0x02, // recalculate only skip trigger times + TriggerChangeAll = TriggerChangeMain | TriggerChangeSkip +}; +} + namespace KAlarmCal { @@ -157,11 +171,15 @@ public: Return, // return a sub-repetition if it's the next occurrence }; + // Details of the next recurrence or sub-repetition, and its reminder. + // Note that the reminder is for the same occurrence, never for a previous + // occurrence. struct Triggers { - DateTime main; // the next trigger time (recurrence or sub-repetition), ignoring reminders - DateTime all; // the next trigger time (recurrence or sub-repetition), including reminders + DateTime main; // the next trigger time (recurrence or sub-repetition), ignoring reminders + DateTime all; // the next trigger time (recurrence or sub-repetition), including reminders int repeatNum; // 0 = main is a recurrence, >0 = main sub-repetition number + void clear() { main = all = DateTime(); repeatNum = 0; } }; KAEventPrivate(); @@ -191,6 +209,9 @@ public: void activateReminderAfter(const DateTime& mainAlarmTime); void defer(const DateTime&, bool reminder, bool adjustRecurrence = false); void cancelDefer(); + bool skip(int count); + int skipCount() const; + DateTime skipDateTime() const; bool setDisplaying(const KAEventPrivate&, KAAlarm::Type, ResourceId, const KADateTime& dt, bool showEdit, bool showDefer); void reinstateFromDisplaying(const KCalendarCore::Event::Ptr&, ResourceId&, bool& showEdit, bool& showDefer); void startChanges() @@ -228,6 +249,7 @@ public: bool setRecur(KCalendarCore::RecurrenceRule::PeriodType, int freq, int count, const KADateTime& end, KARecurrence::Feb29Type = KARecurrence::Feb29_None); KARecurrence::Type checkRecur() const; void clearRecur(); + void setSkipTime(const DateTime& = {}) const; void calcTriggerTimes() const; #ifdef KDE_NO_DEBUG_OUTPUT void dumpDebug() const { } @@ -237,6 +259,8 @@ public: static bool convertRepetition(const KCalendarCore::Event::Ptr&); static bool convertStartOfDay(const KCalendarCore::Event::Ptr&); static DateTime readDateTime(const KCalendarCore::Event::Ptr&, bool localZone, bool dateOnly, DateTime& start); + static DateTime readDateTime(const QString& param, const DateTime& eventStart); + static QString writeDateTime(const QDateTime& dt, bool dateOnly); static void readAlarms(const KCalendarCore::Event::Ptr&, AlarmMap*, bool cmdDisplay = false); static void readAlarm(const KCalendarCore::Alarm::Ptr&, AlarmData&, bool audioMain, bool cmdDisplay = false); @@ -268,6 +292,8 @@ public: static int mWorkTimeIndex; // incremented every time working days/times are changed mutable Triggers mBaseTriggers; // next trigger time, ignoring working hours mutable Triggers mWorkTriggers; // next trigger time, taking account of working hours + mutable Triggers mBaseSkipTriggers; // next trigger time taking account of skipping, ignoring working hours + mutable Triggers mWorkSkipTriggers; // next trigger time taking account of skipping and working hours mutable KAEvent::CmdErr mCommandError{KAEvent::CmdErr::None}; // command execution error last time the alarm triggered QString mEventID; // UID: KCalendarCore::Event unique ID @@ -284,6 +310,7 @@ public: DateTime mNextMainDateTime; // next time to display the alarm, excluding repetitions KADateTime mAtLoginDateTime; // repeat-at-login end time DateTime mDeferralTime; // extra time to trigger alarm (if alarm or reminder deferred) + mutable DateTime mSkipTime; // next time to trigger alarm if it's currently being skipped DateTime mDisplayingTime; // date/time shown in the alarm currently being displayed int mDisplayingFlags; // type of alarm which is currently being displayed (for display alarm) int mReminderMinutes{0};// how long in advance reminder is to be, or 0 if none (<0 for reminder AFTER the alarm) @@ -308,7 +335,7 @@ public: QStringList mEmailAttachments; // ATTACH: email attachment file names mutable QTime mTriggerStartOfDay; // start of day time used by calcTriggerTimes() mutable int mChangeCount{0}; // >0 = inhibit calling calcTriggerTimes() - mutable bool mTriggerChanged{false}; // true if need to recalculate trigger times + mutable TriggerChange mTriggerChanged{TriggerChangeNone}; // true if need to recalculate trigger times QString mLogFile; // alarm output is to be logged to this URL float mSoundVolume{-1.0f}; // volume for sound file (range 0 - 1), or < 0 for unspecified float mFadeVolume{-1.0f}; // initial volume for sound file (range 0 - 1), or < 0 for no fade @@ -360,6 +387,7 @@ public: static const QString WORK_TIME_ONLY_FLAG; static const QString REMINDER_ONCE_FLAG; static const QString DEFER_FLAG; + static const QString SKIP_FLAG; static const QString LATE_CANCEL_FLAG; static const QString AUTO_CLOSE_FLAG; static const QString NOTIFY_FLAG; @@ -430,6 +458,7 @@ const QString KAEventPrivate::EXCLUDE_HOLIDAYS_FLAG = QStringLiteral("EXHOLID const QString KAEventPrivate::WORK_TIME_ONLY_FLAG = QStringLiteral("WORKTIME"); const QString KAEventPrivate::REMINDER_ONCE_FLAG = QStringLiteral("ONCE"); const QString KAEventPrivate::DEFER_FLAG = QStringLiteral("DEFER"); // default defer interval for this alarm +const QString KAEventPrivate::SKIP_FLAG = QStringLiteral("SKIP"); const QString KAEventPrivate::LATE_CANCEL_FLAG = QStringLiteral("LATECANCEL"); const QString KAEventPrivate::AUTO_CLOSE_FLAG = QStringLiteral("LATECLOSE"); const QString KAEventPrivate::NOTIFY_FLAG = QStringLiteral("NOTIFY"); @@ -597,7 +626,7 @@ KAEventPrivate::KAEventPrivate(const KADateTime& dateTime, const QString& name, mMainExpired = false; mChangeCount = changesPending ? 1 : 0; - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; } /****************************************************************************** @@ -622,7 +651,7 @@ KAEventPrivate::KAEventPrivate(const KCalendarCore::Event::Ptr& event) mEnabled = true; QString param; bool ok; - mCategory = CalEvent::status(event, ¶m); + mCategory = CalEvent::status(event, ¶m); if (mCategory == CalEvent::DISPLAYING) { // It's a displaying calendar event - set values specific to displaying alarms @@ -653,6 +682,7 @@ KAEventPrivate::KAEventPrivate(const KCalendarCore::Event::Ptr& event) ++it; } + QString skipParam; bool dateOnly = false; bool localZone = false; QStringList evFlags = event->customProperty(KACalendar::APPNAME, FLAGS_PROPERTY).split(SC, Qt::SkipEmptyParts); @@ -727,6 +757,12 @@ KAEventPrivate::KAEventPrivate(const KCalendarCore::Event::Ptr& event) mDeferDefaultMinutes = n; ++i; } + else if (flag == SKIP_FLAG) + { + // Note the skip date/time, and process once the start date/time + // has been fetched. + skipParam = evFlags.at(++i); + } else if (flag == TEMPL_AFTER_TIME_FLAG) { const int n = static_cast<int>(evFlags.at(i + 1).toUInt(&ok)); @@ -801,6 +837,9 @@ KAEventPrivate::KAEventPrivate(const KCalendarCore::Event::Ptr& event) if (event->customStatus() == DISABLED_STATUS) mEnabled = false; + if (!skipParam.isEmpty()) + mSkipTime = readDateTime(skipParam, mStartDateTime); + // Extract status from the event's alarms. // First set up defaults. mActionSubType = KAEvent::SubAction::Message; @@ -995,7 +1034,6 @@ KAEventPrivate::KAEventPrivate(const KCalendarCore::Event::Ptr& event) else setRecur(RecurrenceRule::rMinutely, mRepetition.intervalMinutes(), mRepetition.count() + 1, KADateTime()); mRepetition.set(0, 0); - mTriggerChanged = true; } if (mRepeatAtLogin) @@ -1032,7 +1070,7 @@ KAEventPrivate::KAEventPrivate(const KCalendarCore::Event::Ptr& event) if (setDeferralTime) mNextMainDateTime = mDeferralTime; } - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; endChanges(); } @@ -1061,6 +1099,8 @@ void KAEventPrivate::copy(const KAEventPrivate& event) { mBaseTriggers = event.mBaseTriggers; mWorkTriggers = event.mWorkTriggers; + mBaseSkipTriggers = event.mBaseSkipTriggers; + mWorkSkipTriggers = event.mWorkSkipTriggers; mCommandError = event.mCommandError; mEventID = event.mEventID; mCustomProperties = event.mCustomProperties; @@ -1075,6 +1115,7 @@ void KAEventPrivate::copy(const KAEventPrivate& event) mNextMainDateTime = event.mNextMainDateTime; mAtLoginDateTime = event.mAtLoginDateTime; mDeferralTime = event.mDeferralTime; + mSkipTime = event.mSkipTime; mDisplayingTime = event.mDisplayingTime; mDisplayingFlags = event.mDisplayingFlags; mReminderMinutes = event.mReminderMinutes; @@ -1231,6 +1272,8 @@ bool KAEventPrivate::updateKCalEvent(const Event::Ptr& ev, KAEvent::UidAction ui ddparam += QLatin1Char('D'); (evFlags += DEFER_FLAG) += ddparam; } + if (mSkipTime.isValid()) + (evFlags += SKIP_FLAG) += writeDateTime(mSkipTime.qDateTime(), mStartDateTime.isDateOnly()); if (mCategory == CalEvent::TEMPLATE && mTemplateAfterTime >= 0) (evFlags += TEMPL_AFTER_TIME_FLAG) += QString::number(mTemplateAfterTime); if (mEmailId >= 0) @@ -1286,7 +1329,7 @@ bool KAEventPrivate::updateKCalEvent(const Event::Ptr& ev, KAEvent::UidAction ui { QDateTime dt = mNextMainDateTime.kDateTime().toTimeSpec(mStartDateTime.timeSpec()).qDateTime(); ev->setCustomProperty(KACalendar::APPNAME, NEXT_RECUR_PROPERTY, - QLocale::c().toString(dt, mNextMainDateTime.isDateOnly() ? QStringLiteral("yyyyMMdd") : QStringLiteral("yyyyMMddThhmmss"))); + writeDateTime(dt, mNextMainDateTime.isDateOnly())); } // Add the main alarm initKCalAlarm(ev, 0, QStringList(), MAIN_ALARM); @@ -1604,6 +1647,8 @@ bool KAEvent::isValid() const void KAEvent::setEnabled(bool enable) { d->mEnabled = enable; + if (!enable) + d->setSkipTime(); } bool KAEvent::enabled() const @@ -1624,6 +1669,7 @@ bool KAEvent::isReadOnly() const void KAEvent::setArchive() { d->mArchive = true; + d->setSkipTime(); } bool KAEvent::toBeArchived() const @@ -1710,7 +1756,9 @@ void KAEventPrivate::setCategory(CalEvent::Type s) return; mEventID = CalEvent::uid(mEventID, s); mCategory = s; - mTriggerChanged = true; // templates and archived don't have trigger times + if (mCategory != CalEvent::ACTIVE) + setSkipTime(); // not an active alarm, so cancel skipping + mTriggerChanged = TriggerChangeAll; // templates and archived don't have trigger times } CalEvent::Type KAEvent::category() const @@ -2082,7 +2130,7 @@ void KAEvent::setTemplate(const QString& name, int afterTime) d->setCategory(CalEvent::TEMPLATE); d->mName = name; d->mTemplateAfterTime = afterTime; - d->mTriggerChanged = true; // templates and archived don't have trigger times + d->mTriggerChanged = TriggerChangeAll; // templates and archived don't have trigger times } bool KAEvent::isTemplate() const @@ -2146,7 +2194,7 @@ void KAEventPrivate::setReminder(int minutes, bool onceOnly) ++mAlarmCount; else if (mReminderActive == ReminderType::None && oldReminderActive != ReminderType::None) --mAlarmCount; - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; } } @@ -2244,7 +2292,7 @@ void KAEventPrivate::defer(const DateTime& dateTime, bool reminder, bool adjustR { // Deferring past the main alarm time, so adjust any existing deferral set_deferral(DeferType::None); - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; } } else if (reminder) @@ -2253,12 +2301,12 @@ void KAEventPrivate::defer(const DateTime& dateTime, bool reminder, bool adjustR { set_deferral(DeferType::Reminder); // defer reminder alarm mDeferralTime = dateTime; - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; } if (mReminderActive == ReminderType::Active) { activate_reminder(false); - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; } } if (mDeferral != DeferType::Reminder) @@ -2267,7 +2315,7 @@ void KAEventPrivate::defer(const DateTime& dateTime, bool reminder, bool adjustR // Main alarm has now expired. mNextMainDateTime = mDeferralTime = dateTime; set_deferral(DeferType::Normal); - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; checkReminderAfter = true; if (!mMainExpired) { @@ -2299,7 +2347,7 @@ void KAEventPrivate::defer(const DateTime& dateTime, bool reminder, bool adjustR mDeferralTime = dateTime; checkRepetition = true; } - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; } else { @@ -2307,7 +2355,7 @@ void KAEventPrivate::defer(const DateTime& dateTime, bool reminder, bool adjustR mDeferralTime = dateTime; if (mDeferral == DeferType::None) set_deferral(DeferType::Normal); - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; checkReminderAfter = true; if (adjustRecurrence) { @@ -2349,7 +2397,7 @@ void KAEventPrivate::defer(const DateTime& dateTime, bool reminder, bool adjustR mNextRepeat = 0; else mNextRepeat = mRepetition.nextRepeatCount(mNextMainDateTime.kDateTime(), mDeferralTime.kDateTime()); - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; } endChanges(); } @@ -2368,7 +2416,7 @@ void KAEventPrivate::cancelDefer() { mDeferralTime = DateTime(); set_deferral(DeferType::None); - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; } } @@ -2455,6 +2503,154 @@ bool KAEvent::deferDefaultDateOnly() const { return d->mDeferDefaultDateOnly; } + +int KAEvent::maxSkipCount() +{ + return MAX_SKIP_COUNT; +} + +/****************************************************************************** +* Set a number of times for the event to be skipped. +* Note that it isn't reliable to simply suppress the next 'n' occurrences, +* since if KAlarm isn't running when any of the skipped occurrences would +* trigger, it would be difficult to keep track. Therefore instead of storing +* the occurrence count to skip, we store the time when occurrences will resume +* triggering. +*/ +bool KAEvent::skip(int count) +{ + return d->skip(count); +} +bool KAEventPrivate::skip(int count) +{ + if (count <= 0 || mCategory != CalEvent::ACTIVE || !mEnabled || checkRecur() == KARecurrence::NO_RECUR) + { + setSkipTime(); + return false; + } + + if (count > MAX_SKIP_COUNT) + count = MAX_SKIP_COUNT; + KADateTime pre = KADateTime::currentDateTime(mStartDateTime.timeSpec()); + // Find the next occurrence after the skip. + DateTime last; + DateTime next; + for (int i = 0; i <= count; ++i) + { + if (nextDateTime(pre, next, KAEvent::NextTypes(KAEvent::NextRepeat | KAEvent::NextWorkHoliday)) == KAEvent::TriggerType::None) + { + if (!i) + { + setSkipTime(); // no occurrences remain, so exit with skip time clear + return false; + } + // Skipping all remaining occurrences, so skip to AFTER the last occurrence. + next = pre; + if (mStartDateTime.isDateOnly()) + { + next = next.addDays(1); + next.setDateOnly(true); + } + else + next.addSecs(1); + break; + } + last = next; + pre = next.effectiveKDateTime(); + } + + setSkipTime(next); + return mSkipTime.isValid(); +} + +/****************************************************************************** +* Cancel any skipping which is currently set. +*/ +void KAEvent::cancelSkip() +{ + d->setSkipTime(); +} + +/****************************************************************************** +* Return whether the event is currently being skipped. +*/ +bool KAEvent::skipping() const +{ + return d->skipDateTime().isValid(); +} + +/****************************************************************************** +* Return the number of triggers of the event remaining to be skipped, excluding +* reminders. +*/ +int KAEvent::skipCount() const +{ + return d->skipCount(); +} +int KAEventPrivate::skipCount() const +{ + const DateTime skipTime = skipDateTime(); + if (!skipTime.isValid()) + return 0; + + KADateTime pre = KADateTime::currentDateTime(mStartDateTime.timeSpec()); + int count = 0; + for ( ; count < MAX_SKIP_COUNT; ++count) + { + DateTime next; + if (nextDateTime(pre, next, KAEvent::NextTypes(KAEvent::NextRepeat | KAEvent::NextWorkHoliday)) == KAEvent::TriggerType::None) + { + if (!count) + { + // There is no occurrence after the current time, + // so clear skipping. + setSkipTime(); + } + return count; + } + if (next >= skipTime) + return count; + pre = next.effectiveKDateTime(); + } + return count; +} + +/****************************************************************************** +* Return the time at which the event should resume normal triggering. +* If the skip time has already passed, it is cleared. +*/ +DateTime KAEvent::skipDateTime() const +{ + return d->skipDateTime(); +} +DateTime KAEventPrivate::skipDateTime() const +{ + if (mSkipTime.isValid()) + { + if (mStartDateTime.isDateOnly()) + { + KADateTime now = KADateTime::currentDateTime(mStartDateTime.timeSpec()); + now.setDateOnly(true); + if (mSkipTime <= now) + setSkipTime(); // skip date has passed, so cancel it + } + else + { + if (mSkipTime <= KADateTime::currentUtcDateTime()) + setSkipTime(); // skip date has passed, so cancel it + } + } + return mSkipTime; +} + +void KAEventPrivate::setSkipTime(const DateTime& dt) const +{ + if (dt != mSkipTime) + { + mSkipTime = dt; + mTriggerChanged = TriggerChange(mTriggerChanged | TriggerChangeSkip); + } +} DateTime KAEvent::startDateTime() const { @@ -2464,7 +2660,7 @@ DateTime KAEvent::startDateTime() const void KAEvent::setTime(const KADateTime& dt) { d->mNextMainDateTime = dt; - d->mTriggerChanged = true; + d->mTriggerChanged = TriggerChangeAll; } /****************************************************************************** @@ -2476,26 +2672,11 @@ KAEvent::TriggerType KAEvent::nextDateTime(const KADateTime& preDateTime, DateTi return d->nextDateTime(preDateTime, result, type, endTime); } -KAEvent::TriggerType KAEventPrivate::nextDateTime(const KADateTime& preDateTime, DateTime& result, KAEvent::NextTypes type, const KADateTime& endTime) const +KAEvent::TriggerType KAEventPrivate::nextDateTime(const KADateTime& preDT, DateTime& result, KAEvent::NextTypes type, const KADateTime& endTime) const { - // No need for complicated code for the simple case of returning either the - // next recurrence or sub-repetition, when ignoring working time/holidays, - // reminders and deferrals. - if (!(type & (KAEvent::NextReminder | KAEvent::NextWorkHoliday | KAEvent::NextDeferral))) - { - Repeats option = (type & KAEvent::NextRepeat) ? Repeats::Return : Repeats::Ignore; - const KAEvent::OccurType ot = nextOccurrence(preDateTime, result, option); - if (endTime.isValid() && result > endTime) - { - result = DateTime(); - return KAEvent::TriggerType::None; - } - return static_cast<KAEvent::TriggerType>(ot); - } - // Remove flags from 'type' which are inapplicable to this alarm. if (!mRepetition) - type &= ~KAEvent::NextRepeat; // the alarm doesn't repeat + type &= ~KAEvent::NextRepeat; // the alarm doesn't repeat if (!mReminderMinutes) type &= ~KAEvent::NextReminder; // the alarm doesn't have a reminder if (checkRecur() == KARecurrence::NO_RECUR @@ -2517,11 +2698,32 @@ KAEvent::TriggerType KAEventPrivate::nextDateTime(const KADateTime& preDateTime, type &= ~KAEvent::NextDeferral; } - // Find the next recurrence (not sub-repetition) after preDateTime. + if (!mSkipTime.isValid()) + type &= ~KAEvent::NextSkip; // the alarm isn't being skipped + const KADateTime preDateTime = (type & KAEvent::NextSkip) && mSkipTime > preDT ? mSkipTime.kDateTime().addSecs(-60) : preDT; + + // No need for complicated code for the simple case of returning either the + // next recurrence or sub-repetition, when ignoring working time/holidays, + // reminders and deferrals. + if (!(type & (KAEvent::NextReminder | KAEvent::NextWorkHoliday | KAEvent::NextDeferral))) + { + const Repeats option = (type & KAEvent::NextRepeat) ? Repeats::Return : Repeats::Ignore; + const KAEvent::OccurType ot = nextOccurrence(preDateTime, result, option); + if (endTime.isValid() && result > endTime) + { + result = DateTime(); + return KAEvent::TriggerType::None; + } + return static_cast<KAEvent::TriggerType>(ot); + } + + // Find the next recurrence (not sub-repetition) after preDateTime (or if taking + // account of skipping, at or after the skip time). // If looking for reminders AFTER the alarm, start from preDateTime - reminder period. // If looking for repetitions, start from preDateTime - total repetition duration. KADateTime pre = preDateTime; - if ((type & KAEvent::NextReminder) && mReminderMinutes < 0) // if reminder AFTER the alarm + if ((type & KAEvent::NextReminder) && mReminderMinutes < 0 // if reminder AFTER the alarm + && !(type & KAEvent::NextSkip)) // but if skipping, the recurrence must after skipping pre = pre.addSecs(mReminderMinutes * 60); if (type & KAEvent::NextRepeat) { @@ -2581,6 +2783,8 @@ KAEvent::TriggerType KAEventPrivate::nextDateTime(const KADateTime& preDateTime, // If desired, check for the first reminder after preDateTime. // Reminders only apply to recurrences, not sub-repetitions. + // Note that any skip time applies to the recurrence or sub-repetition, + // and a reminder only occurs if the recurrence/sub-repetition triggers. DateTime resultReminder; if (type & KAEvent::NextReminder) { @@ -2604,9 +2808,10 @@ KAEvent::TriggerType KAEventPrivate::nextDateTime(const KADateTime& preDateTime, } else { - // Reminder is after the alarm. If the recurrence is before preDateTime, - // check if its reminder occurs after preDateTime. - if (offsetToRecur < 0 && -offsetToRecur < -reminderSecs) + // Reminder is after the alarm. If the recurrence is before preDateTime + // and is not skipped, check if its reminder occurs after preDateTime. + if (offsetToRecur < 0 && -offsetToRecur < -reminderSecs + && (!(type & KAEvent::NextSkip) || nextRecur >= mSkipTime)) resultReminder = nextRecur.addSecs(-reminderSecs); } @@ -2653,11 +2858,31 @@ KAEvent::TriggerType KAEventPrivate::nextDateTime(const KADateTime& preDateTime, // Find next recurrence/repetition which complies with working time/ // holiday restrictions. // Find a subsequent sub-repetition or recurrence which is not excluded. + const bool wantRepeats = type & KAEvent::NextRepeat; Triggers nextRec; nextRec.main = nextRecur; nextRec.repeatNum = 0; Triggers nextWT; nextHolidayWorkingTime(nextRec, true, nextWT, (type & KAEvent::NextRepeat), endTime); + if (!nextWT.main.isValid()) + { + result = DateTime(); + return KAEvent::TriggerType::None; + } + if (type & KAEvent::NextSkip) + { + while (nextWT.main < mSkipTime) + { + nextRec.main = nextWT.main; + nextRec.repeatNum = nextWT.repeatNum; + nextHolidayWorkingTime(nextRec, wantRepeats, nextWT, wantRepeats, endTime); + if (!nextWT.main.isValid()) + { + result = DateTime(); + return KAEvent::TriggerType::None; + } + } + } if (type & KAEvent::NextReminder) { result = nextWT.all; @@ -2741,7 +2966,7 @@ void KAEventPrivate::setRepeatAtLogin(bool rl) else if (!rl && mRepeatAtLogin) --mAlarmCount; mRepeatAtLogin = rl; - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; } /****************************************************************************** @@ -2796,7 +3021,9 @@ void KAEvent::setExcludeHolidays(bool ex) d->mExcludeHolidayRegion = KAEventPrivate::mHolidays->regionCode(); // Option only affects recurring alarms if (d->checkRecur() != KARecurrence::NO_RECUR) - d->mTriggerChanged = true; + { + d->mTriggerChanged = TriggerChangeAll; + } } } @@ -2827,7 +3054,9 @@ void KAEvent::setWorkTimeOnly(bool wto) d->mWorkTimeOnly = wto; // Option only affects recurring alarms if (d->checkRecur() != KARecurrence::NO_RECUR) - d->mTriggerChanged = true; + { + d->mTriggerChanged = TriggerChangeAll; + } } bool KAEvent::workTimeOnly() const @@ -2922,7 +3151,7 @@ void KAEventPrivate::clearRecur() delete mRecurrence; mRecurrence = nullptr; mRepetition.set(0, 0); - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; } mNextRepeat = 0; } @@ -2944,7 +3173,7 @@ void KAEventPrivate::setRecurrence(const KARecurrence& recurrence) delete mRecurrence; mRecurrence = new KARecurrence(recurrence); mRecurrence->setStartDateTime(mStartDateTime.effectiveKDateTime(), mStartDateTime.isDateOnly()); - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; // Adjust sub-repetition values to fit the recurrence. setRepetition(mRepetition); @@ -2968,7 +3197,7 @@ void KAEventPrivate::setRecurrence(const KARecurrence& recurrence) bool KAEvent::setRecurMinutely(int freq, int count, const KADateTime& end) { const bool success = d->setRecur(RecurrenceRule::rMinutely, freq, count, end); - d->mTriggerChanged = true; + d->mTriggerChanged = TriggerChangeAll; return success; } @@ -2997,7 +3226,7 @@ bool KAEvent::setRecurDaily(int freq, const QBitArray& days, int count, QDate en d->mRecurrence->addWeeklyDays(days); } } - d->mTriggerChanged = true; + d->mTriggerChanged = TriggerChangeAll; return success; } @@ -3017,7 +3246,7 @@ bool KAEvent::setRecurWeekly(int freq, const QBitArray& days, int count, QDate e const bool success = d->setRecur(RecurrenceRule::rWeekly, freq, count, end); if (success) d->mRecurrence->addWeeklyDays(days); - d->mTriggerChanged = true; + d->mTriggerChanged = TriggerChangeAll; return success; } @@ -3040,7 +3269,7 @@ bool KAEvent::setRecurMonthlyByDate(int freq, const QList<int>& days, int count, for (int day : days) d->mRecurrence->addMonthlyDate(day); } - d->mTriggerChanged = true; + d->mTriggerChanged = TriggerChangeAll; return success; } @@ -3064,7 +3293,7 @@ bool KAEvent::setRecurMonthlyByPos(int freq, const QList<MonthPos>& posns, int c for (const MonthPos& posn : posns) d->mRecurrence->addMonthlyPos(posn.weeknum, posn.days); } - d->mTriggerChanged = true; + d->mTriggerChanged = TriggerChangeAll; return success; } @@ -3093,7 +3322,7 @@ bool KAEvent::setRecurAnnualByDate(int freq, const QList<int>& months, int day, if (day) d->mRecurrence->addMonthlyDate(day); } - d->mTriggerChanged = true; + d->mTriggerChanged = TriggerChangeAll; return success; } @@ -3121,7 +3350,7 @@ bool KAEvent::setRecurAnnualByPos(int freq, const QList<MonthPos>& posns, for (const MonthPos& posn : posns) d->mRecurrence->addYearlyPos(posn.weeknum, posn.days); } - d->mTriggerChanged = true; + d->mTriggerChanged = TriggerChangeAll; return success; } @@ -3251,7 +3480,7 @@ void KAEventPrivate::setFirstRecurrence() { mRecurrence->setStartDateTime(next.effectiveKDateTime(), next.isDateOnly()); mStartDateTime = mNextMainDateTime = next; - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; } mRecurrence->setFrequency(frequency); // restore the frequency } @@ -3326,12 +3555,12 @@ bool KAEventPrivate::setRepetition(const Repetition& repetition) } else mRepetition = repetition; - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; } else if (mRepetition) { mRepetition.set(0, 0); - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; } return true; } @@ -3469,7 +3698,7 @@ bool KAEventPrivate::setNextOccurrence(const KADateTime& preDateTime, KAEvent::O } if (mDeferral == DeferType::Reminder) set_deferral(DeferType::None); - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; } } else @@ -3489,13 +3718,13 @@ bool KAEventPrivate::setNextOccurrence(const KADateTime& preDateTime, KAEvent::O activate_reminder(false); if (mDeferral == DeferType::Reminder) set_deferral(DeferType::None); - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; } else if (mNextRepeat) { // The next occurrence is the main occurrence, not a repetition mNextRepeat = 0; - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; } } return !KAEvent::isFirstRecur(type); @@ -3961,7 +4190,7 @@ void KAEventPrivate::removeExpiredAlarm(KAAlarm::Type type) break; } if (mAlarmCount != count) - mTriggerChanged = true; + mTriggerChanged = TriggerChangeAll; } /****************************************************************************** @@ -3998,7 +4227,8 @@ bool KAEventPrivate::compare(const KAEventPrivate& other, KAEvent::Comparison co || *mRecurrence != *other.mRecurrence || mExcludeHolidays != other.mExcludeHolidays || mWorkTimeOnly != other.mWorkTimeOnly - || mRepetition != other.mRepetition) + || mRepetition != other.mRepetition + || mSkipTime != other.mSkipTime) return false; } else @@ -4186,6 +4416,12 @@ void KAEventPrivate::dumpDebug() const qCDebug(KALARMCAL_LOG) << "-- mWorkTriggers.all:" << mWorkTriggers.all.toString(); qCDebug(KALARMCAL_LOG) << "-- mWorkTriggers.main:" << mWorkTriggers.main.toString(); qCDebug(KALARMCAL_LOG) << "-- mWorkTriggers.repeatNum:" << mWorkTriggers.repeatNum; + qCDebug(KALARMCAL_LOG) << "-- mBaseSkipTriggers.all:" << mBaseSkipTriggers.all.toString(); + qCDebug(KALARMCAL_LOG) << "-- mBaseSkipTriggers.main:" << mBaseSkipTriggers.main.toString(); + qCDebug(KALARMCAL_LOG) << "-- mBaseSkipTriggers.repeatNum:" << mBaseSkipTriggers.repeatNum; + qCDebug(KALARMCAL_LOG) << "-- mWorkSkipTriggers.all:" << mWorkSkipTriggers.all.toString(); + qCDebug(KALARMCAL_LOG) << "-- mWorkSkipTriggers.main:" << mWorkSkipTriggers.main.toString(); + qCDebug(KALARMCAL_LOG) << "-- mWorkSkipTriggers.repeatNum:" << mWorkSkipTriggers.repeatNum; qCDebug(KALARMCAL_LOG) << "-- mCategory:" << mCategory; qCDebug(KALARMCAL_LOG) << "-- mName:" << mName; if (mCategory == CalEvent::TEMPLATE) @@ -4277,6 +4513,8 @@ void KAEventPrivate::dumpDebug() const qCDebug(KALARMCAL_LOG) << "-- mDeferral:" << (mDeferral == DeferType::Normal ? "normal" : "reminder"); qCDebug(KALARMCAL_LOG) << "-- mDeferralTime:" << mDeferralTime.toString(); } + if (mSkipTime.isValid()) + qCDebug(KALARMCAL_LOG) << "-- mSkipTime:" << mSkipTime.toString(); qCDebug(KALARMCAL_LOG) << "-- mDeferDefaultMinutes:" << mDeferDefaultMinutes; if (mDeferDefaultMinutes) qCDebug(KALARMCAL_LOG) << "-- mDeferDefaultDateOnly:" << mDeferDefaultDateOnly; @@ -4323,7 +4561,19 @@ DateTime KAEventPrivate::readDateTime(const Event::Ptr& event, bool localZone, b // stored correctly in the calendar file. start.setTimeSpec(KADateTime::LocalZone); } - DateTime next = start; + const QString prop = event->customProperty(KACalendar::APPNAME, KAEventPrivate::NEXT_RECUR_PROPERTY); + DateTime next = readDateTime(prop, start); + if (!next.isValid() || next < start) + next = start; // ensure next recurrence time is valid + return next; +} + +/****************************************************************************** +* Read a date/time from a KCalendarCore::Event property or parameter. +* 'eventStart' gives the date-only property and the time spec. +*/ +DateTime KAEventPrivate::readDateTime(const QString& param, const DateTime& eventStart) +{ const int SZ_YEAR = 4; // number of digits in year value const int SZ_MONTH = 2; // number of digits in month value const int SZ_DAY = 2; // number of digits in day value @@ -4333,33 +4583,43 @@ DateTime KAEventPrivate::readDateTime(const Event::Ptr& event, bool localZone, b const int SZ_MIN = 2; // number of digits in minute value const int SZ_SEC = 2; // number of digits in second value const int SZ_TIME = SZ_HOUR + SZ_MIN + SZ_SEC; // total size of time value - const QString prop = event->customProperty(KACalendar::APPNAME, KAEventPrivate::NEXT_RECUR_PROPERTY); - if (prop.length() >= SZ_DATE) + DateTime value; + if (param.length() >= SZ_DATE) { // The next due recurrence time is specified - const QDate d(QStringView(prop).left(SZ_YEAR).toInt(), - QStringView(prop).mid(SZ_YEAR, SZ_MONTH).toInt(), - QStringView(prop).mid(SZ_YEAR + SZ_MONTH, SZ_DAY).toInt()); + const QDate d(QStringView(param).left(SZ_YEAR).toInt(), + QStringView(param).mid(SZ_YEAR, SZ_MONTH).toInt(), + QStringView(param).mid(SZ_YEAR + SZ_MONTH, SZ_DAY).toInt()); if (d.isValid()) { - if (dateOnly && prop.length() == SZ_DATE) - next.setDate(d); - else if (!dateOnly && prop.length() == IX_TIME + SZ_TIME && prop[SZ_DATE] == QLatin1Char('T')) + if (eventStart.isDateOnly() && param.length() == SZ_DATE) { - const QTime t(QStringView(prop).mid(IX_TIME, SZ_HOUR).toInt(), - QStringView(prop).mid(IX_TIME + SZ_HOUR, SZ_MIN).toInt(), - QStringView(prop).mid(IX_TIME + SZ_HOUR + SZ_MIN, SZ_SEC).toInt()); + value = eventStart; + value.setDate(d); + } + else if (!eventStart.isDateOnly() && param.length() == IX_TIME + SZ_TIME && param[SZ_DATE] == QLatin1Char('T')) + { + const QTime t(QStringView(param).mid(IX_TIME, SZ_HOUR).toInt(), + QStringView(param).mid(IX_TIME + SZ_HOUR, SZ_MIN).toInt(), + QStringView(param).mid(IX_TIME + SZ_HOUR + SZ_MIN, SZ_SEC).toInt()); if (t.isValid()) { - next.setDate(d); - next.setTime(t); + value = eventStart; + value.setDate(d); + value.setTime(t); } } - if (next < start) - next = start; // ensure next recurrence time is valid } } - return next; + return value; +} + +/****************************************************************************** +* Write a date/time in the format of a KCalendarCore::Event property. +*/ +QString KAEventPrivate::writeDateTime(const QDateTime& dt, bool dateOnly) +{ + return QLocale::c().toString(dt, dateOnly ? QStringLiteral("yyyyMMdd") : QStringLiteral("yyyyMMddThhmmss")); } /****************************************************************************** @@ -4633,26 +4893,29 @@ inline void KAEventPrivate::set_deferral(DeferType type) * Return the next time the alarm will trigger. * The value is cached to avoid recalculating unless changes have occurred. */ -DateTime KAEvent::nextTrigger(Trigger type) const +DateTime KAEvent::nextTrigger(Trigger type, bool skip) const { if (d->mCategory == CalEvent::ARCHIVED || d->mCategory == CalEvent::TEMPLATE) return {}; // it's a template or archived if (d->mDeferral == KAEventPrivate::DeferType::Normal) return d->mDeferralTime; // for a deferred alarm, working time setting is ignored + const bool skipping = d->skipDateTime().isValid(); + if (!skipping) + skip = false; d->calcTriggerTimes(); switch (type) { - case Trigger::All: return d->mBaseTriggers.all; - case Trigger::Main: return d->mBaseTriggers.main; - case Trigger::AllWork: return d->mWorkTriggers.all; - case Trigger::Work: return d->mWorkTriggers.main; + case Trigger::All: return skip ? d->mBaseSkipTriggers.all : d->mBaseTriggers.all; + case Trigger::Main: return skip ? d->mBaseSkipTriggers.main : d->mBaseTriggers.main; + case Trigger::AllWork: return skip ? d->mWorkSkipTriggers.all : d->mWorkTriggers.all; + case Trigger::Work: return skip ? d->mWorkSkipTriggers.main : d->mWorkTriggers.main; case Trigger::Actual: { - const bool reminderAfter = d->mMainExpired && d->mReminderActive != KAEventPrivate::ReminderType::None && d->mReminderMinutes < 0; + const bool reminderAfter = !skipping && d->mMainExpired && d->mReminderActive != KAEventPrivate::ReminderType::None && d->mReminderMinutes < 0; return d->checkRecur() != KARecurrence::NO_RECUR && (d->mWorkTimeOnly || d->mExcludeHolidays) - ? (reminderAfter ? d->mWorkTriggers.all : d->mWorkTriggers.main) - : (reminderAfter ? d->mBaseTriggers.all : d->mBaseTriggers.main); + ? (reminderAfter ? d->mWorkTriggers.all : skipping ? d->mWorkSkipTriggers.main : d->mWorkTriggers.main) + : (reminderAfter ? d->mBaseTriggers.all : skipping ? d->mBaseSkipTriggers.main : d->mBaseTriggers.main); } default: return {}; @@ -4669,14 +4932,19 @@ DateTime KAEvent::nextTrigger(Trigger type) const * mWorkTriggers.main is set to the next scheduled recurrence/sub-repetition * which occurs in working hours, if working-time-only is set. * mWorkTriggers.all is the same as mWorkTriggers.main, but takes account of reminders. +* mBaseSkipTriggers is equivalent to mBaseTriggers, but set to the next +* scheduled recurrence/sub-repetition after skipping. +* mWorkSkipTriggers is equivalent to mWorkTriggers, but set to the next +* scheduled recurrence/sub-repetition after skipping. */ void KAEventPrivate::calcTriggerTimes() const { if (mChangeCount) - return; + return; // don't evaluate when in the middle of a sequence of changes bool recurs = (checkRecur() != KARecurrence::NO_RECUR); - if (!mTriggerChanged) + if (!(mTriggerChanged & TriggerChangeMain)) { + // Probably no need to recalculate the main trigger times, but check. if ((recurs && mWorkTimeOnly && mWorkTimeOnly != mWorkTimeIndex) || (recurs && mExcludeHolidays && mExcludeHolidayRegion != mHolidays->regionCode()) || (mStartDateTime.isDateOnly() && mTriggerStartOfDay != DateTime::startOfDay())) @@ -4684,58 +4952,80 @@ void KAEventPrivate::calcTriggerTimes() const // It's a work time alarm, and work days/times have changed, or // it excludes holidays, and the holidays definition has changed. // Need to recalculate trigger times. + mTriggerChanged = TriggerChangeAll; } - else - return; } - mTriggerChanged = false; - mTriggerStartOfDay = DateTime::startOfDay(); // note start of day time used in calculation - if (recurs && mWorkTimeOnly) - mWorkTimeOnly = mWorkTimeIndex; // note which work time definition was used in calculation - if (recurs && mExcludeHolidays) - mExcludeHolidayRegion = mHolidays->regionCode(); // note which holiday definition was used in calculation - bool excludeHolidays = mExcludeHolidays && !mExcludeHolidayRegion.isEmpty(); - - mBaseTriggers.main = mainDateTime(true); // next recurrence or sub-repetition - mBaseTriggers.repeatNum = mRepetition ? mNextRepeat : 0; - mBaseTriggers.all = (mDeferral == DeferType::Reminder) ? mDeferralTime - : (mReminderActive != ReminderType::Active) ? mBaseTriggers.main - : (mReminderMinutes < 0) ? mReminderAfterTime - : mBaseTriggers.main.addMins(-mReminderMinutes); - // If only-during-working-time is set and it recurs, it won't actually trigger - // unless it falls during working hours. - 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) + if (mTriggerChanged & TriggerChangeMain) + { + mTriggerStartOfDay = DateTime::startOfDay(); // note start of day time used in calculation + if (recurs && mWorkTimeOnly) + mWorkTimeOnly = mWorkTimeIndex; // note which work time definition was used in calculation + if (recurs && mExcludeHolidays) + mExcludeHolidayRegion = mHolidays->regionCode(); // note which holiday definition was used in calculation + bool excludeHolidays = mExcludeHolidays && !mExcludeHolidayRegion.isEmpty(); + + mBaseTriggers.main = mainDateTime(true); // next recurrence or sub-repetition + mBaseTriggers.repeatNum = mRepetition ? mNextRepeat : 0; + mBaseTriggers.all = (mDeferral == DeferType::Reminder) ? mDeferralTime + : (mReminderActive != ReminderType::Active) ? mBaseTriggers.main + : (mReminderMinutes < 0) ? mReminderAfterTime + : mBaseTriggers.main.addMins(-mReminderMinutes); + + // If only-during-working-time is set and it recurs, it won't actually trigger + // unless it falls during working hours. + 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(mBaseTriggers.main.kDateTime()); + skipRepeats = excluded; + } + } + if (!excluded) { - // 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); + // It only occurs once, or it complies with any working hours/holiday + // restrictions. + mWorkTriggers = mBaseTriggers; } else { - // The next trigger is a recurrence. - excluded = excludedByWorkTimeOrHoliday(mBaseTriggers.main.kDateTime()); - skipRepeats = excluded; + // Find the next occurrence which complies with working time/holiday + // restrictions. + nextHolidayWorkingTime(mBaseTriggers, skipRepeats, mWorkTriggers, true, {}); } } - if (!excluded) - { - // It only occurs once, or it complies with any working hours/holiday - // restrictions. - mWorkTriggers = mBaseTriggers; - } - else + + if ((mTriggerChanged & TriggerChangeSkip) && mSkipTime.isValid()) { - // Find the next occurrence which complies with working time/holiday - // restrictions. - nextHolidayWorkingTime(mBaseTriggers, skipRepeats, mWorkTriggers, true, {}); + // Find next times after skipping. + // N.B. Reminders can still occur before the skip end time if the + // recurrence is at or after the skip end time. + if (mSkipTime <= mBaseTriggers.main) + mBaseSkipTriggers = mBaseTriggers; + else + { + const KAEvent::OccurType type = nextOccurrence(mSkipTime.kDateTime().addSecs(-60), mBaseSkipTriggers.main, Repeats::Return); + mBaseSkipTriggers.repeatNum = KAEvent::repeatNum(type); + } + + if (mSkipTime <= mWorkTriggers.main) + mWorkSkipTriggers = mWorkTriggers; + else + nextHolidayWorkingTime(mBaseSkipTriggers, false, mWorkSkipTriggers, true, {}); } + mTriggerChanged = TriggerChangeNone; } /****************************************************************************** @@ -6236,7 +6526,6 @@ bool KAEvent::convertKCalEvents(const Calendar::Ptr& calendar, int calendarVersi { // Append a time unit suffix to the event's REPEAT property interval parameter. const QStringList list = prop.split(QLatin1Char(':')); -qDebug()<<"convertKCalEvents: LIST:"<<list; if (list.count() >= 2) { int interval = static_cast<int>(list[0].toUInt()); diff --git a/src/kalarmcalendar/kaevent.h b/src/kalarmcalendar/kaevent.h index 0409ab174..2edc909d7 100644 --- a/src/kalarmcalendar/kaevent.h +++ b/src/kalarmcalendar/kaevent.h @@ -374,6 +374,7 @@ public: * of NextType. * Reminders are never returned if the recurrence to which they relate is excluded by working * hours or holiday restrictions, regardless of whether or not NextWorkHoliday is specified. + * Reminders are never returned for skipped occurrences, if NextSkip is specified. */ enum NextType { @@ -381,7 +382,8 @@ public: NextRepeat = 0x01, //!< check for sub-repetitions NextReminder = 0x02, //!< check for reminders NextWorkHoliday = 0x04, //!< take account of any working hours or holiday restrictions - NextDeferral = 0x08 //!< return the event deferral time, or reminder deferral time if NextReminder set + NextDeferral = 0x08, //!< return the event deferral time, or reminder deferral time if NextReminder set + NextSkip = 0x10 //!< take account of skipping }; Q_DECLARE_FLAGS(NextTypes, NextType) @@ -389,24 +391,25 @@ public: enum class Trigger { - /** Next trigger, including reminders. No account is taken of any - * working hours or holiday restrictions when evaluating this. */ + /** Next trigger, including reminders. No account is taken of any working + * hours or holiday restrictions, or skipping, when evaluating this. */ All, /** Next trigger of the main alarm, i.e. excluding reminders. No - * account is taken of any working hours or holiday restrictions when - * evaluating this. */ + * account is taken of any working hours or holiday restrictions, or + * skipping, when evaluating this. */ Main, /** Next trigger of the main alarm, i.e. excluding reminders, taking - * account of any working hours or holiday restrictions. If the event - * has no working hours or holiday restrictions, this is equivalent to - * Main. */ + * account of any working hours or holiday restrictions. No account is + * taken of skipping. If the event has no working hours or holiday + * restrictions, this is equivalent to Main. */ Work, /** Next trigger, including reminders, taking account of any working - * hours or holiday restrictions. If the event has no working hours or - * holiday restrictions, this is equivalent to All. */ + * hours or holiday restrictions. No account is taken of skipping. If + * the event has no working hours or holiday restrictions, this is + * equivalent to All. */ AllWork, /** Next time the alarm will actually trigger, i.e. the next recurrence @@ -447,7 +450,7 @@ public: KAEvent(); /** Construct an event and initialise with the specified parameters. - * @param dt start date/time. If @p dt is date-only, or if #AnyTime flag + * @param dt start date/time. If @p dt is date-only, or if #ANY_TIME flag * is specified, the event will be date-only. * @param name name of the alarm. * @param text alarm message (@p action = #Message); @@ -956,10 +959,62 @@ public: /** Return the default date-only setting used in the deferral dialog. */ bool deferDefaultDateOnly() const; + /** Return the maximum skip count. + * A limit is set in order to prevent excessive processing. + */ + static int maxSkipCount(); + + /** Set a number of times for the event to be skipped. + * This is the count of recurrences and sub-repetitions to skip. + * Do not include reminders in the count; these will automatically be + * skipped if their related recurrence or sub-repetition is skipped. + * Skipping does not affect any outstanding deferral of the alarm. + * + * Note that the date/time that triggering of the event will resume + * is calculated when this function is called. If the alarm is + * subject to working hours or holiday restrictions and a change is + * later made to working hours or holiday settings, the date/time + * that triggering of the event will resume will not be recalculated + * to comply with the new settings. + * + * @param count number of times to skip the event trigger. If zero, + * skipping will be cancelled. + * @return true if the event is now skipping, false if not. + * + * @see cancelSkip(), skipping(), skipCount(), skipDateTime() + */ + bool skip(int count); + + /** Cancel any skipping which is currently set. + * @see skip() + */ + void cancelSkip(); + + /** Return whether the event is currently being skipped. + * @see skip(), skipDateTime(), skipCount() + */ + bool skipping() const; + + /** Return whether the event is currently being skipped, and if so how + * many occurrences remain to be skipped. + * @return number of event triggers remaining to be skipped. Reminders + * are not included in this count. + * @see skip(), skipDateTime(), skipping() + */ + int skipCount() const; + + /** Return the time at which the event should resume normal triggering. + * The next trigger for the alarm will occur at or after this time. + * @return time when triggers will resume, or invalid if not currently + * being skipped. + * @see skip(), skipping(), skipCount() + */ + DateTime skipDateTime() const; + /** Return the start time for the event. If the event recurs, this is the * time of the first recurrence. If the event is date-only, this returns a * date-only value. - * @note No account is taken of any working hours or holiday restrictions. + * @note No account is taken of any working hours or holiday restrictions * when determining the start date/time. * * @see mainDateTime() @@ -987,6 +1042,8 @@ public: * returns the deferral time. * If a reminder has been deferred AND @p type * contains NextReminder, returns the deferral time. + * - @p type contains NextSkip: ignores all skipped occurrences, and their + * reminders. * * @param preDateTime the date/time after which to find the next trigger/display. * @param result date/time of next trigger/display, or invalid date/time if none. @@ -997,8 +1054,8 @@ public: TriggerType nextDateTime(const KADateTime& preDateTime, DateTime& result, NextTypes type, const KADateTime& endTime = {}) const; /** Return the next time the main alarm will trigger. - * @note No account is taken of any working hours or holiday restrictions. - * when determining the next trigger date/time. + * @note No account is taken of any working hours or holiday restrictions, + * or skipping, when determining the next trigger date/time. * * @param withRepeats true to include sub-repetitions, false to exclude them. * @see mainTime(), startDateTime(), setTime() @@ -1007,15 +1064,15 @@ public: /** Return the time at which the main alarm will next trigger. * Sub-repetitions are ignored. - * @note No account is taken of any working hours or holiday restrictions. - * when determining the next trigger time. + * @note No account is taken of any working hours or holiday restrictions, + * or skipping, when determining the next trigger time. */ QTime mainTime() const; /** Return the time at which the last sub-repetition of the current * recurrence of the main alarm will occur. - * @note No account is taken of any working hours or holiday restrictions - * when determining the last sub-repetition time. + * @note No account is taken of any working hours or holiday restrictions, + * or skipping, when determining the last sub-repetition time. * * @return last sub-repetition time, or main alarm time if no * sub-repetitions are configured. @@ -1036,10 +1093,12 @@ public: static void adjustStartOfDay(const KAEvent::List& events); /** Return the next time the alarm will trigger. - * @param type specifies whether to ignore reminders, working time - * restrictions, etc. + * @param type specifies whether to ignore reminders, working time + * restrictions, etc. + * @param skip whether to take account of skipping. + * @return next trigger time, or invalid if none. */ - DateTime nextTrigger(Trigger type) const; + DateTime nextTrigger(Trigger type, bool skip = false) const; /** Set the date/time the event was created, or saved in the archive calendar. * @see createdDateTime() @@ -1308,8 +1367,8 @@ public: int recurInterval() const; /** Return the longest interval which can occur between consecutive recurrences. - * @note No account is taken of any working hours or holiday restrictions - * when evaluating consecutive recurrence dates/times. + * @note No account is taken of any working hours or holiday restrictions, + * or skipping, when evaluating consecutive recurrence dates/times. * @see recurInterval() */ KCalendarCore::Duration longestRecurrenceInterval() const; @@ -1317,8 +1376,8 @@ public: /** Adjust the event date/time to the first recurrence of the event, on or after * the event start date/time. The event start date may not be a recurrence date, * in which case a later date will be set. - * @note No account is taken of any working hours or holiday restrictions - * when determining the first recurrence of the event. + * @note No account is taken of any working hours or holiday restrictions, + * or skipping, when determining the first recurrence of the event. */ void setFirstRecurrence(); @@ -1339,8 +1398,8 @@ public: Repetition repetition() const; /** Return the count of the next sub-repetition which is due. - * @note No account is taken of any working hours or holiday restrictions - * when determining the next event sub-repetition. + * @note No account is taken of any working hours or holiday restrictions, + * or skipping, when determining the next event sub-repetition. * * @return sub-repetition count (>=1), or 0 for the main recurrence. * @see nextDateTime() @@ -1352,8 +1411,8 @@ public: /** Determine whether the event will occur strictly after the specified * date/time. Reminders are ignored. - * @note No account is taken of any working hours or holiday restrictions - * when determining event occurrences. + * @note No account is taken of any working hours or holiday restrictions, + * or skipping, when determining event occurrences. * @note If the event is date-only, its occurrences are considered to occur * at the start-of-day time when comparing with @p preDateTime. * @@ -1371,8 +1430,8 @@ public: * If the alarm has a sub-repetition, and a sub-repetition of a previous * recurrence occurs after the specified date/time, that sub-repetition is * set as the next occurrence. - * @note No account is taken of any working hours or holiday restrictions - * when determining and setting the next occurrence date/time. + * @note No account is taken of any working hours or holiday restrictions, or + * skipping, when determining and setting the next occurrence date/time. * @note If the event is date-only, its occurrences are considered to occur * at the start-of-day time when comparing with @p preDateTime. * @@ -1388,8 +1447,8 @@ public: /** Get the date/time of the last previous occurrence of the event, * strictly before the specified date/time. Reminders are ignored. - * @note No account is taken of any working hours or holiday restrictions - * when determining the previous event occurrence. + * @note No account is taken of any working hours or holiday restrictions, + * or skipping, when determining the previous event occurrence. * @note If the event is date-only, its occurrences are considered to occur * at the start-of-day time when comparing with @p preDateTime. * diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index d81096316..0dec7e75a 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -505,6 +505,11 @@ void MainWindow::initActions() actions->setDefaultShortcut(mActionEnable, QKeySequence(Qt::CTRL | Qt::Key_B)); connect(mActionEnable, &QAction::triggered, this, &MainWindow::slotEnable); + mActionSkip = new QAction(this); + actions->addAction(QStringLiteral("skip"), mActionSkip); + actions->setDefaultShortcut(mActionSkip, QKeySequence(Qt::CTRL | Qt::Key_S)); + connect(mActionSkip, &QAction::triggered, this, &MainWindow::slotSkip); + #if ENABLE_RTC_WAKE_FROM_SUSPEND if (!KernelWakeAlarm::isAvailable()) { @@ -658,6 +663,7 @@ void MainWindow::initActions() mActionDelete->setEnabled(false); mActionReactivate->setEnabled(false); mActionEnable->setEnabled(false); + mActionSkip->setEnabled(false); mActionCreateTemplate->setEnabled(false); mActionExport->setEnabled(false); @@ -881,7 +887,7 @@ void MainWindow::slotReactivate() */ void MainWindow::slotEnable() { - bool enable = mActionEnableEnable; // save since changed in response to KAlarm::enableEvent() + bool enable = mActionEnableEnable; // save since changed in response to KAlarm::enableEvents() const QList<KAEvent> events = mListView->selectedEvents(); QList<KAEvent> eventCopies; eventCopies.reserve(events.count()); @@ -891,6 +897,31 @@ void MainWindow::slotEnable() slotSelection(); // update Enable/Disable action text } +/****************************************************************************** +* Called when the Skip/Cancel Skip button is clicked to enable or disable +* skipping the currently highlighted alarms in the list. +*/ +void MainWindow::slotSkip() +{ + int skipCount = 0; + if (mActionSkipSkip) + { + bool ok; + skipCount = QInputDialog::getInt(this, i18nc("@title:window", "Skip Alarm"), + i18nc("@label:textbox", "Number of activations to skip:"), + 1, 1, KAEvent::maxSkipCount(), 1, &ok); + if (!ok) + return; + } + const QList<KAEvent> events = mListView->selectedEvents(); + QList<KAEvent> eventCopies; + eventCopies.reserve(events.count()); + for (const KAEvent& event : events) + eventCopies += event; + KAlarm::skipEvents(eventCopies, skipCount, this); + slotSelection(); // update Skip/Cancel Skip action text +} + /****************************************************************************** * Called when the columns visible in the alarm list view have changed. */ @@ -1609,6 +1640,9 @@ void MainWindow::slotSelection() bool enableEnableDisable = true; bool enableEnable = false; bool enableDisable = false; + bool enableSkipUnskip = true; + bool enableSkip = false; + bool enableUnskip = false; const KADateTime now = KADateTime::currentUtcDateTime(); for (int i = 0; i < evCount; ++i) { @@ -1634,6 +1668,18 @@ void MainWindow::slotSelection() enableDisable = true; } } + if (enableSkipUnskip) + { + if (expired || !event.enabled() || !event.recurs()) + enableSkipUnskip = enableUnskip = enableSkip = false; + else + { + if (!enableUnskip && event.skipping()) + enableUnskip = true; + if (!enableSkip && !event.skipping()) + enableSkip = true; + } + } } qCDebug(KALARM_LOG) << "MainWindow::slotSelection: true"; @@ -1647,6 +1693,9 @@ void MainWindow::slotSelection() mActionEnable->setEnabled(active && !readOnly && (enableEnable || enableDisable)); if (enableEnable || enableDisable) setEnableText(enableEnable); + mActionSkip->setEnabled(active && !readOnly && (enableSkip || enableUnskip)); + if (enableSkip || enableUnskip) + setSkipText(enableSkip); Q_EMIT selectionChanged(); } @@ -1678,6 +1727,7 @@ void MainWindow::selectionCleared() mActionDelete->setEnabled(false); mActionReactivate->setEnabled(false); mActionEnable->setEnabled(false); + mActionSkip->setEnabled(false); } /****************************************************************************** @@ -1689,6 +1739,15 @@ void MainWindow::setEnableText(bool enable) mActionEnable->setText(enable ? i18nc("@action", "Enable") : i18nc("@action", "Disable")); } +/****************************************************************************** +* Set the text of the Skip/Cancel Skip menu action. +*/ +void MainWindow::setSkipText(bool skip) +{ + mActionSkipSkip = skip; + mActionSkip->setText(skip ? i18nc("@action", "Skip...") : i18nc("@action", "Cancel skip")); +} + /****************************************************************************** * Display or hide the specified main window. * This should only be called when the application doesn't run in the system tray. diff --git a/src/mainwindow.h b/src/mainwindow.h index 3610d99bf..96193b2ac 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -1,7 +1,7 @@ /* * mainwindow.h - main application window * Program: kalarm - * SPDX-FileCopyrightText: 2001-2024 David Jarvie <[email protected]> + * SPDX-FileCopyrightText: 2001-2026 David Jarvie <[email protected]> * * SPDX-License-Identifier: GPL-2.0-or-later */ @@ -96,6 +96,7 @@ private Q_SLOTS: void slotDeleteForce() { slotDelete(true); } void slotReactivate(); void slotEnable(); + void slotSkip(); void slotToggleTrayIcon(); void slotRefreshAlarms(); void slotImportAlarms(); @@ -145,6 +146,7 @@ private: void initActions(); void selectionCleared(); void setEnableText(bool enable); + void setSkipText(bool skip); void arrangePanel(); void setSplitterSizes(); void initUndoMenu(QMenu*, Undo::Type); @@ -181,6 +183,7 @@ private: QAction* mActionDeleteForce; QAction* mActionReactivate; QAction* mActionEnable; + QAction* mActionSkip; QAction* mActionFindNext; QAction* mActionFindPrev; KToolBarPopupAction* mActionUndo; @@ -201,6 +204,7 @@ private: bool mShowArchived; // include archived alarms in the displayed list bool mShown{false}; // true once the window has been displayed bool mActionEnableEnable; // Enable/Disable action is set to "Enable" + bool mActionSkipSkip; // Skip/Cancel Skip action is set to "Skip" bool mMenuError; // error occurred creating menus: need to show error message bool mResizing{false}; // window resize is in progress }; diff --git a/src/resourcescalendar.cpp b/src/resourcescalendar.cpp index 43b8f7d24..deaf92293 100644 --- a/src/resourcescalendar.cpp +++ b/src/resourcescalendar.cpp @@ -1,7 +1,7 @@ /* * resourcescalendar.cpp - KAlarm calendar resources access * Program: kalarm - * SPDX-FileCopyrightText: 2001-2025 David Jarvie <[email protected]> + * SPDX-FileCopyrightText: 2001-2026 David Jarvie <[email protected]> * * SPDX-License-Identifier: GPL-2.0-or-later */ @@ -208,13 +208,13 @@ void ResourcesCalendar::slotEventUpdated(Resource& resource, const KAEvent& even findEarliestAlarm(resource); else { - const KADateTime dt = event.nextTrigger(KAEvent::Trigger::All).effectiveKDateTime(); + const KADateTime dt = event.nextTrigger(KAEvent::Trigger::All, true).effectiveKDateTime(); if (dt.isValid()) { bool changed = false; DateTime next; if (!earliestId.isEmpty()) - next = resource.event(earliestId).nextTrigger(KAEvent::Trigger::All); + next = resource.event(earliestId).nextTrigger(KAEvent::Trigger::All, true); if (earliestId.isEmpty() || dt < next) { mEarliestAlarm[key] = event.id(); @@ -226,7 +226,7 @@ void ResourcesCalendar::slotEventUpdated(Resource& resource, const KAEvent& even // It is not a display or audio event, or it is never inhibited. DateTime nextNoInhibit; if (!earliestNoInhibitId.isEmpty()) - nextNoInhibit = (earliestId == earliestNoInhibitId) ? next : resource.event(earliestNoInhibitId).nextTrigger(KAEvent::Trigger::All); + nextNoInhibit = (earliestId == earliestNoInhibitId) ? next : resource.event(earliestNoInhibitId).nextTrigger(KAEvent::Trigger::All, true); if (earliestNoInhibitId.isEmpty() || dt < nextNoInhibit) { mEarliestNoInhibitAlarm[key] = event.id(); @@ -836,7 +836,7 @@ void ResourcesCalendar::findEarliestAlarm(const Resource& resource) || mPendingAlarms.contains(evnt.id()) || isInactive(evnt, resource)) continue; - const KADateTime dt = evnt.nextTrigger(KAEvent::Trigger::All).effectiveKDateTime(); + const KADateTime dt = evnt.nextTrigger(KAEvent::Trigger::All, true).effectiveKDateTime(); if (dt.isValid()) { if (!earliest.isValid() || dt < earliestTime) @@ -884,7 +884,7 @@ KAEvent ResourcesCalendar::earliestAlarm(KADateTime& nextTriggerTime, bool notif return earliestAlarm(nextTriggerTime, notificationsInhibited); } //TODO: use next trigger calculated in findEarliestAlarm() (allowing for it being out of date)? - const KADateTime dt = evnt.nextTrigger(KAEvent::Trigger::All).effectiveKDateTime(); + const KADateTime dt = evnt.nextTrigger(KAEvent::Trigger::All, true).effectiveKDateTime(); if (dt.isValid() && (!earliest.isValid() || dt < earliestTime)) { earliestTime = dt; diff --git a/src/resourcescalendar.h b/src/resourcescalendar.h index f3ee10e10..ca61d3300 100644 --- a/src/resourcescalendar.h +++ b/src/resourcescalendar.h @@ -1,7 +1,7 @@ /* * resourcescalendar.h - KAlarm calendar resources access * Program: kalarm - * SPDX-FileCopyrightText: 2001-2025 David Jarvie <[email protected]> + * SPDX-FileCopyrightText: 2001-2026 David Jarvie <[email protected]> * * SPDX-License-Identifier: GPL-2.0-or-later */ @@ -36,7 +36,8 @@ public: static void initialise(const QByteArray& appName, const QByteArray& appVersion); static void terminate(); - /** Return the active alarm with the earliest trigger time. + /** Return the active alarm with the earliest trigger time, taking account of + * skipping but ignoring any working hours or holiday restrictions. * @param nextTriggerTime The next trigger time of the earliest alarm. * @param notificationsInhibited Ignore display and audio alarms unless they have NoInhibit status. * @return The earliest alarm.
