[pim/kalarm] /: Provide facility to skip the next n recurrences/sub-repetitions of an alarm
David Jarvie
null at kde.org
Fri Jan 9 23:25:46 GMT 2026
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 <djarvie at kde.org>
+ * SPDX-FileCopyrightText: 2001-2026 David Jarvie <djarvie at kde.org>
*
* 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 <djarvie at kde.org>
+ * SPDX-FileCopyrightText: 2007-2026 David Jarvie <djarvie at kde.org>
*
* 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 <djarvie at kde.org>
+ * SPDX-FileCopyrightText: 2001-2026 David Jarvie <djarvie at kde.org>
*
* 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 <djarvie at kde.org>
+ * SPDX-FileCopyrightText: 2001-2026 David Jarvie <djarvie at kde.org>
*
* 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 <djarvie at kde.org>
+ * SPDX-FileCopyrightText: 2001-2026 David Jarvie <djarvie at kde.org>
*
* 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.
More information about the kde-doc-english
mailing list