[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, &param);
+    mCategory       = CalEvent::status(event, &param);
     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