File indexing completed on 2024-11-24 04:42:23

0001 /*
0002  *  kaevent.cpp  -  represents KAlarm calendar events
0003  *  This file is part of kalarmprivate library, which provides access to KAlarm
0004  *  calendar data.
0005  *  Program:  kalarm
0006  *  SPDX-FileCopyrightText: 2001-2023 David Jarvie <djarvie@kde.org>
0007  *
0008  *  SPDX-License-Identifier: LGPL-2.0-or-later
0009  */
0010 
0011 #include "kaevent.h"
0012 
0013 #include "alarmtext.h"
0014 #include "holidays.h"
0015 #include "identities.h"
0016 #include "version.h"
0017 #include "kalarmcal_debug.h"
0018 
0019 #include <KLocalizedString>
0020 
0021 using namespace KCalendarCore;
0022 
0023 namespace KAlarmCal
0024 {
0025 
0026 //=============================================================================
0027 
0028 using EmailAddress = KCalendarCore::Person;
0029 class EmailAddressList : public KCalendarCore::Person::List
0030 {
0031 public:
0032     EmailAddressList() : KCalendarCore::Person::List() {}
0033     explicit EmailAddressList(const KCalendarCore::Person::List& list)
0034     {
0035         operator=(list);
0036     }
0037     EmailAddressList& operator=(const KCalendarCore::Person::List&);
0038     operator QStringList() const;
0039     QString     join(const QString& separator) const;
0040     QStringList pureAddresses() const;
0041     QString     pureAddresses(const QString& separator) const;
0042 private:
0043     QString     address(int index) const;
0044 };
0045 
0046 //=============================================================================
0047 
0048 class Q_DECL_HIDDEN KAAlarm::Private
0049 {
0050 public:
0051     Private();
0052 
0053     Action      mActionType;           // alarm action type
0054     Type        mType{Type::Invalid};  // alarm type
0055     DateTime    mNextMainDateTime;     // next time to display the alarm, excluding repetitions
0056     Repetition  mRepetition;           // sub-repetition count and interval
0057     int         mNextRepeat{0};        // repetition count of next due sub-repetition
0058     bool        mRepeatAtLogin{false}; // whether to repeat the alarm at every login
0059     bool        mRecurs;               // there is a recurrence rule for the alarm
0060     bool        mDeferred{false};      // whether the alarm is an extra deferred/deferred-reminder alarm
0061     bool        mTimedDeferral;        // if mDeferred = true: true if the deferral is timed, false if date-only
0062 };
0063 
0064 //=============================================================================
0065 
0066 class Q_DECL_HIDDEN KAEventPrivate : public QSharedData
0067 {
0068 public:
0069     // Read-only internal flags additional to KAEvent::Flags enum values.
0070     // NOTE: If any values are added to those in KAEvent::Flags, ensure
0071     //       that these values don't overlap them.
0072     enum
0073     {
0074         REMINDER        = 0x200000,
0075         DEFERRAL        = 0x400000,
0076         TIMED_FLAG      = 0x800000,
0077         DATE_DEFERRAL   = DEFERRAL,
0078         TIME_DEFERRAL   = DEFERRAL | TIMED_FLAG,
0079         DISPLAYING_     = 0x1000000,
0080         READ_ONLY_FLAGS = 0x1E00000  //!< mask for all read-only internal values
0081     };
0082     enum class ReminderType   // current active state of reminder
0083     {
0084         None,     // reminder is not due
0085         Active,   // reminder is due
0086         Hidden    // reminder-after is disabled due to main alarm being deferred past it
0087     };
0088     enum class DeferType
0089     {
0090         None = 0,   // there is no deferred alarm
0091         Normal,     // the main alarm, a recurrence or a repeat is deferred
0092         Reminder    // a reminder alarm is deferred
0093     };
0094     // Alarm types.
0095     // This uses the same scheme as KAAlarm::Type, with some extra values.
0096     // Note that the actual enum values need not be the same as in KAAlarm::Type.
0097     enum AlarmType
0098     {
0099         INVALID_ALARM       = 0,     // Not an alarm
0100         MAIN_ALARM          = 1,     // THE real alarm. Must be the first in the enumeration.
0101         REMINDER_ALARM      = 0x02,  // Reminder in advance of/after the main alarm
0102         DEFERRED_ALARM      = 0x04,  // Deferred alarm
0103         DEFERRED_REMINDER_ALARM = REMINDER_ALARM | DEFERRED_ALARM,  // Deferred reminder alarm
0104         // The following values must be greater than the preceding ones, to
0105         // ensure that in ordered processing they are processed afterwards.
0106         AT_LOGIN_ALARM      = 0x10,  // Additional repeat-at-login trigger
0107         DISPLAYING_ALARM    = 0x20,  // Copy of the alarm currently being displayed
0108         // The following are extra internal KAEvent values
0109         AUDIO_ALARM         = 0x30,  // sound to play when displaying the alarm
0110         PRE_ACTION_ALARM    = 0x40,  // command to execute before displaying the alarm
0111         POST_ACTION_ALARM   = 0x50   // command to execute after the alarm window is closed
0112     };
0113 
0114     struct AlarmData
0115     {
0116         Alarm::Ptr                  alarm;
0117         QString                     cleanText;       // text or audio file name
0118         QFont                       font;
0119         QColor                      bgColour, fgColour;
0120         float                       soundVolume;
0121         float                       fadeVolume;
0122         int                         fadeSeconds;
0123         int                         repeatSoundPause;
0124         int                         nextRepeat;
0125         uint                        emailFromId;
0126         KAEventPrivate::AlarmType   type;
0127         KAAlarm::Action             action;
0128         int                         displayingFlags;
0129         KAEvent::ExtraActionOptions extraActionOptions;
0130         bool                        speak;
0131         bool                        defaultFont;
0132         bool                        isEmailText;
0133         bool                        commandScript;
0134         bool                        timedDeferral;
0135         bool                        hiddenReminder;
0136     };
0137     using AlarmMap = QMap<AlarmType, AlarmData>;
0138 
0139     KAEventPrivate();
0140     KAEventPrivate(const KADateTime&, const QString& name, const QString& message,
0141                    const QColor& bg, const QColor& fg, const QFont& f,
0142                    KAEvent::SubAction, int lateCancel, KAEvent::Flags flags,
0143                    bool changesPending = false);
0144     explicit KAEventPrivate(const KCalendarCore::Event::Ptr&);
0145     KAEventPrivate(const KAEventPrivate&);
0146     ~KAEventPrivate()
0147     {
0148         delete mRecurrence;
0149     }
0150     KAEventPrivate& operator=(const KAEventPrivate& e)
0151     {
0152         if (&e != this)
0153             copy(e);
0154         return *this;
0155     }
0156     void               setAudioFile(const QString& filename, float volume, float fadeVolume, int fadeSeconds, int repeatPause, bool allowEmptyFile);
0157     KAEvent::OccurType setNextOccurrence(const KADateTime& preDateTime);
0158     void               setFirstRecurrence();
0159     void               setCategory(CalEvent::Type);
0160     void               setRepeatAtLogin(bool);
0161     void               setRepeatAtLoginTrue(bool clearReminder);
0162     void               setReminder(int minutes, bool onceOnly);
0163     void               activateReminderAfter(const DateTime& mainAlarmTime);
0164     void               defer(const DateTime&, bool reminder, bool adjustRecurrence = false);
0165     void               cancelDefer();
0166     bool               setDisplaying(const KAEventPrivate&, KAAlarm::Type, ResourceId, const KADateTime& dt, bool showEdit, bool showDefer);
0167     void               reinstateFromDisplaying(const KCalendarCore::Event::Ptr&, ResourceId&, bool& showEdit, bool& showDefer);
0168     void               startChanges()
0169     {
0170         ++mChangeCount;
0171     }
0172     void               endChanges();
0173     void               removeExpiredAlarm(KAAlarm::Type);
0174     bool               compare(const KAEventPrivate&, KAEvent::Comparison) const;
0175     KAAlarm            alarm(KAAlarm::Type) const;
0176     KAAlarm            firstAlarm() const;
0177     KAAlarm            nextAlarm(KAAlarm::Type) const;
0178     bool               updateKCalEvent(const KCalendarCore::Event::Ptr&, KAEvent::UidAction, bool setCustomProperties = true) const;
0179     DateTime           mainDateTime(bool withRepeats = false) const
0180     {
0181         return (withRepeats && mNextRepeat && mRepetition)
0182                ? DateTime(mRepetition.duration(mNextRepeat).end(mNextMainDateTime.qDateTime())) : mNextMainDateTime;
0183     }
0184     DateTime           mainEndRepeatTime() const
0185     {
0186         return mRepetition ? DateTime(mRepetition.duration().end(mNextMainDateTime.qDateTime())) : mNextMainDateTime;
0187     }
0188     DateTime           deferralLimit(KAEvent::DeferLimit* = nullptr) const;
0189     KAEvent::Flags     flags() const;
0190     bool               excludedByWorkTimeOrHoliday(const KADateTime& dt) const;
0191     bool               setRepetition(const Repetition&);
0192     DateTime           nextDateTime(KAEvent::NextTypes) const;
0193     bool               occursAfter(const KADateTime& preDateTime, bool includeRepetitions) const;
0194     KAEvent::OccurType nextOccurrence(const KADateTime& preDateTime, DateTime& result, KAEvent::Repeats = KAEvent::Repeats::Ignore) const;
0195     KAEvent::OccurType previousOccurrence(const KADateTime& afterDateTime, DateTime& result, bool includeRepetitions = false) const;
0196     void               setRecurrence(const KARecurrence&);
0197     bool               setRecur(KCalendarCore::RecurrenceRule::PeriodType, int freq, int count, QDate end, KARecurrence::Feb29Type = KARecurrence::Feb29_None);
0198     bool               setRecur(KCalendarCore::RecurrenceRule::PeriodType, int freq, int count, const KADateTime& end, KARecurrence::Feb29Type = KARecurrence::Feb29_None);
0199     KARecurrence::Type checkRecur() const;
0200     void               clearRecur();
0201     void               calcTriggerTimes() const;
0202 #ifdef KDE_NO_DEBUG_OUTPUT
0203     void               dumpDebug() const  { }
0204 #else
0205     void               dumpDebug() const;
0206 #endif
0207     static bool        convertRepetition(const KCalendarCore::Event::Ptr&);
0208     static bool        convertStartOfDay(const KCalendarCore::Event::Ptr&);
0209     static DateTime    readDateTime(const KCalendarCore::Event::Ptr&, bool localZone, bool dateOnly, DateTime& start);
0210     static void        readAlarms(const KCalendarCore::Event::Ptr&, AlarmMap*, bool cmdDisplay = false);
0211     static void        readAlarm(const KCalendarCore::Alarm::Ptr&, AlarmData&, bool audioMain, bool cmdDisplay = false);
0212 
0213 private:
0214     void               copy(const KAEventPrivate&);
0215     bool               mayOccurDailyDuringWork(const KADateTime&) const;
0216     int                nextWorkRepetition(const KADateTime& pre) const;
0217     void               calcNextWorkingTime(const DateTime& nextTrigger) const;
0218     DateTime           nextWorkingTime() const;
0219     KAEvent::OccurType nextRecurrence(const KADateTime& preDateTime, DateTime& result) const;
0220     void               setAudioAlarm(const KCalendarCore::Alarm::Ptr&) const;
0221     KCalendarCore::Alarm::Ptr initKCalAlarm(const KCalendarCore::Event::Ptr&, const DateTime&, const QStringList& types, AlarmType = INVALID_ALARM) const;
0222     KCalendarCore::Alarm::Ptr initKCalAlarm(const KCalendarCore::Event::Ptr&, int startOffsetSecs, const QStringList& types, AlarmType = INVALID_ALARM) const;
0223     inline void        set_deferral(DeferType);
0224     inline void        activate_reminder(bool activate);
0225     static int         transitionIndex(const QDateTime& utc, const QTimeZone::OffsetDataList& transitions);
0226 
0227 public:
0228     static QFont       mDefaultFont;       // default alarm message font
0229     static const Holidays  mDummyHolidays; // empty holiday data to avoid initial mHolidays null pointer
0230     static const Holidays* mHolidays;      // holiday data to use
0231     static QBitArray   mWorkDays;          // working days of the week
0232     static QTime       mWorkDayStart;      // start time of the working day
0233     static QTime       mWorkDayEnd;        // end time of the working day
0234     static KADateTime::Spec mWorkDayTimeSpec;  // time spec for start and end of working day
0235     static int         mWorkTimeIndex;     // incremented every time working days/times are changed
0236     mutable DateTime   mAllTrigger;        // next trigger time, including reminders, ignoring working hours
0237     mutable DateTime   mMainTrigger;       // next trigger time, ignoring reminders and working hours
0238     mutable DateTime   mAllWorkTrigger;    // next trigger time, taking account of reminders and working hours
0239     mutable DateTime   mMainWorkTrigger;   // next trigger time, ignoring reminders but taking account of working hours
0240     mutable KAEvent::CmdErr mCommandError{KAEvent::CmdErr::None}; // command execution error last time the alarm triggered
0241 
0242     QString            mEventID;           // UID: KCalendarCore::Event unique ID
0243     QMap<QByteArray, QString> mCustomProperties; // KCalendarCore::Event's non-KAlarm custom properties
0244     mutable ResourceId mResourceId{-1};    // ID of resource containing the event, or for a displaying event,
0245                                            // saved resource ID (not the resource the event is in)
0246     QString            mName;              // name of the alarm
0247     QString            mText;              // message text, file URL, command, email body [or audio file for KAAlarm]
0248     QString            mAudioFile;         // ATTACH: audio file to play
0249     QString            mPreAction;         // command to execute before alarm is displayed
0250     QString            mPostAction;        // command to execute after alarm window is closed
0251     DateTime           mStartDateTime;     // DTSTART and DTEND: start and end time for event
0252     KADateTime         mCreatedDateTime;   // CREATED: date event was created, or saved in archive calendar
0253     DateTime           mNextMainDateTime;  // next time to display the alarm, excluding repetitions
0254     KADateTime         mAtLoginDateTime;   // repeat-at-login end time
0255     DateTime           mDeferralTime;      // extra time to trigger alarm (if alarm or reminder deferred)
0256     DateTime           mDisplayingTime;    // date/time shown in the alarm currently being displayed
0257     int                mDisplayingFlags;   // type of alarm which is currently being displayed (for display alarm)
0258     int                mReminderMinutes{0};// how long in advance reminder is to be, or 0 if none (<0 for reminder AFTER the alarm)
0259     DateTime           mReminderAfterTime; // if mReminderActive true, time to trigger reminder AFTER the main alarm, or invalid if not pending
0260     ReminderType       mReminderActive{ReminderType::None}; // whether a reminder is due (before next, or after last, main alarm/recurrence)
0261     int                mDeferDefaultMinutes{0}; // default number of minutes for deferral dialog, or 0 to select time control
0262     bool               mDeferDefaultDateOnly{false}; // select date-only by default in deferral dialog
0263     int                mRevision{0};           // SEQUENCE: revision number of the original alarm, or 0
0264     KARecurrence*      mRecurrence{nullptr};   // RECUR: recurrence specification, or 0 if none
0265     Repetition         mRepetition;            // sub-repetition count and interval
0266     int                mNextRepeat{0};         // repetition count of next due sub-repetition
0267     int                mAlarmCount{0};         // number of alarms: count of !mMainExpired, mRepeatAtLogin, mDeferral, mReminderActive, mDisplaying
0268     DeferType          mDeferral{DeferType::None}; // whether the alarm is an extra deferred/deferred-reminder alarm
0269     KAEvent::EmailId   mEmailId{-1};           // if email text, message's Akonadi item ID
0270     int                mTemplateAfterTime{-1}; // time not specified: use n minutes after default time, or -1 (applies to templates only)
0271     QColor             mBgColour;              // background colour of alarm message
0272     QColor             mFgColour;              // foreground colour of alarm message, or invalid for default
0273     QFont              mFont;                  // font of alarm message (ignored if mUseDefaultFont true)
0274     uint               mEmailFromIdentity{0};  // standard email identity uoid for 'From' field, or empty
0275     EmailAddressList   mEmailAddresses;        // ATTENDEE: addresses to send email to
0276     QString            mEmailSubject;          // SUMMARY: subject line of email
0277     QStringList        mEmailAttachments;      // ATTACH: email attachment file names
0278     mutable QTime      mTriggerStartOfDay;     // start of day time used by calcTriggerTimes()
0279     mutable int        mChangeCount{0};        // >0 = inhibit calling calcTriggerTimes()
0280     mutable bool       mTriggerChanged{false}; // true if need to recalculate trigger times
0281     QString            mLogFile;               // alarm output is to be logged to this URL
0282     float              mSoundVolume{-1.0f};    // volume for sound file (range 0 - 1), or < 0 for unspecified
0283     float              mFadeVolume{-1.0f};     // initial volume for sound file (range 0 - 1), or < 0 for no fade
0284     int                mFadeSeconds{0};        // fade time (seconds) for sound file, or 0 if none
0285     int                mRepeatSoundPause{-1};  // seconds to pause between sound file repetitions, or -1 if no repetition
0286     int                mLateCancel{0};         // how many minutes late will cancel the alarm, or 0 for no cancellation
0287     bool               mWakeFromSuspend{false}; // wake system from suspend when alarm is due
0288     bool               mExcludeHolidays{false}; // don't trigger alarms on holidays
0289     mutable QString    mExcludeHolidayRegion;   // holiday region code used to exclude alarms on holidays (= mHolidays region when trigger calculated)
0290     mutable int        mWorkTimeOnly{0};         // non-zero to trigger alarm only during working hours (= mWorkTimeIndex when trigger calculated)
0291     KAEvent::SubAction mActionSubType;           // sub-action type for the event's main alarm
0292     CalEvent::Type     mCategory{CalEvent::EMPTY};   // event category (active, archived, template, ...)
0293     KAEvent::ExtraActionOptions mExtraActionOptions; // options for pre- or post-alarm actions
0294     KACalendar::Compat mCompatibility{KACalendar::Current}; // event's storage format compatibility
0295     bool               mReadOnly{false};         // event is read-only in its original calendar file
0296     bool               mConfirmAck{false};       // alarm acknowledgement requires confirmation by user
0297     bool               mUseDefaultFont;          // use default message font, not mFont
0298     bool               mCommandScript{false};    // the command text is a script, not a shell command line
0299     bool               mCommandXterm{false};     // command alarm is to be executed in a terminal window
0300     bool               mCommandDisplay{false};   // command output is to be displayed in an alarm window
0301     bool               mCommandHideError{false}; // don't show command execution errors to user
0302     bool               mEmailBcc{false};         // blind copy the email to the user
0303     bool               mBeep{false};             // whether to beep when the alarm is displayed
0304     bool               mSpeak{false};            // whether to speak the message when the alarm is displayed
0305     bool               mCopyToKOrganizer{false}; // KOrganizer should hold a copy of the event
0306     bool               mReminderOnceOnly{false}; // the reminder is output only for the first recurrence
0307     bool               mAutoClose{false};        // whether to close the alarm window after the late-cancel period
0308     bool               mNotify{false};           // alarm should be shown by the notification system, not in a window
0309     bool               mMainExpired;             // main alarm has expired (in which case a deferral alarm will exist)
0310     bool               mRepeatAtLogin{false};    // whether to repeat the alarm at every login
0311     bool               mArchiveRepeatAtLogin{false}; // if now archived, original event was repeat-at-login
0312     bool               mArchive{false};          // event has triggered in the past, so archive it when closed
0313     bool               mDisplaying{false};       // whether the alarm is currently being displayed (i.e. in displaying calendar)
0314     bool               mDisplayingDefer{false};  // show Defer button (applies to displaying calendar only)
0315     bool               mDisplayingEdit{false};   // show Edit button (applies to displaying calendar only)
0316     bool               mEnabled;                 // false if event is disabled
0317 
0318 public:
0319     static const QByteArray FLAGS_PROPERTY;
0320     static const QString DATE_ONLY_FLAG;
0321     static const QString LOCAL_ZONE_FLAG;
0322     static const QString EMAIL_BCC_FLAG;
0323     static const QString CONFIRM_ACK_FLAG;
0324     static const QString KORGANIZER_FLAG;
0325     static const QString WAKE_SUSPEND_FLAG;
0326     static const QString EXCLUDE_HOLIDAYS_FLAG;
0327     static const QString WORK_TIME_ONLY_FLAG;
0328     static const QString REMINDER_ONCE_FLAG;
0329     static const QString DEFER_FLAG;
0330     static const QString LATE_CANCEL_FLAG;
0331     static const QString AUTO_CLOSE_FLAG;
0332     static const QString NOTIFY_FLAG;
0333     static const QString TEMPL_AFTER_TIME_FLAG;
0334     static const QString KMAIL_ITEM_FLAG;
0335     static const QString ARCHIVE_FLAG;
0336     static const QByteArray NEXT_RECUR_PROPERTY;
0337     static const QByteArray REPEAT_PROPERTY;
0338     static const QByteArray LOG_PROPERTY;
0339     static const QString xtermURL;
0340     static const QString displayURL;
0341     static const QByteArray TYPE_PROPERTY;
0342     static const QString FILE_TYPE;
0343     static const QString AT_LOGIN_TYPE;
0344     static const QString REMINDER_TYPE;
0345     static const QString REMINDER_ONCE_TYPE;
0346     static const QString TIME_DEFERRAL_TYPE;
0347     static const QString DATE_DEFERRAL_TYPE;
0348     static const QString DISPLAYING_TYPE;
0349     static const QString PRE_ACTION_TYPE;
0350     static const QString POST_ACTION_TYPE;
0351     static const QString SOUND_REPEAT_TYPE;
0352     static const QByteArray NEXT_REPEAT_PROPERTY;
0353     static const QString HIDDEN_REMINDER_FLAG;
0354     static const QByteArray FONT_COLOUR_PROPERTY;
0355     static const QByteArray VOLUME_PROPERTY;
0356     static const QString EMAIL_ID_FLAG;
0357     static const QString SPEAK_FLAG;
0358     static const QString EXEC_ON_DEFERRAL_FLAG;
0359     static const QString CANCEL_ON_ERROR_FLAG;
0360     static const QString DONT_SHOW_ERROR_FLAG;
0361     static const QString DISABLED_STATUS;
0362     static const QString DISP_DEFER;
0363     static const QString DISP_EDIT;
0364     static const QString CMD_ERROR_VALUE;
0365     static const QString CMD_ERROR_PRE_VALUE;
0366     static const QString CMD_ERROR_POST_VALUE;
0367     static const QString SC;
0368 };
0369 
0370 //=============================================================================
0371 
0372 // KAlarm version which first used the current calendar/event format.
0373 // If this changes, KAEvent::convertKCalEvents() must be changed correspondingly.
0374 // The string version is the KAlarm version string used in the calendar file.
0375 
0376 QByteArray KAEvent::currentCalendarVersionString()
0377 {
0378     return {"2.7.0"};   // This is NOT the KAlarmCal library .so version!
0379 }
0380 int KAEvent::currentCalendarVersion()
0381 {
0382     return Version(2, 7, 0);   // This is NOT the KAlarmCal library .so version!
0383 }
0384 
0385 // Custom calendar properties.
0386 // Note that all custom property names are prefixed with X-KDE-KALARM- in the calendar file.
0387 
0388 // Event properties
0389 const QByteArray KAEventPrivate::FLAGS_PROPERTY("FLAGS");              // X-KDE-KALARM-FLAGS property
0390 const QString    KAEventPrivate::DATE_ONLY_FLAG        = QStringLiteral("DATE");
0391 const QString    KAEventPrivate::LOCAL_ZONE_FLAG       = QStringLiteral("LOCAL");
0392 const QString    KAEventPrivate::EMAIL_BCC_FLAG        = QStringLiteral("BCC");
0393 const QString    KAEventPrivate::CONFIRM_ACK_FLAG      = QStringLiteral("ACKCONF");
0394 const QString    KAEventPrivate::KORGANIZER_FLAG       = QStringLiteral("KORG");
0395 const QString    KAEventPrivate::WAKE_SUSPEND_FLAG     = QStringLiteral("WAKESUSPEND");
0396 const QString    KAEventPrivate::EXCLUDE_HOLIDAYS_FLAG = QStringLiteral("EXHOLIDAYS");
0397 const QString    KAEventPrivate::WORK_TIME_ONLY_FLAG   = QStringLiteral("WORKTIME");
0398 const QString    KAEventPrivate::REMINDER_ONCE_FLAG    = QStringLiteral("ONCE");
0399 const QString    KAEventPrivate::DEFER_FLAG            = QStringLiteral("DEFER");   // default defer interval for this alarm
0400 const QString    KAEventPrivate::LATE_CANCEL_FLAG      = QStringLiteral("LATECANCEL");
0401 const QString    KAEventPrivate::AUTO_CLOSE_FLAG       = QStringLiteral("LATECLOSE");
0402 const QString    KAEventPrivate::NOTIFY_FLAG           = QStringLiteral("NOTIFY");
0403 const QString    KAEventPrivate::TEMPL_AFTER_TIME_FLAG = QStringLiteral("TMPLAFTTIME");
0404 const QString    KAEventPrivate::KMAIL_ITEM_FLAG       = QStringLiteral("KMAIL");
0405 const QString    KAEventPrivate::ARCHIVE_FLAG          = QStringLiteral("ARCHIVE");
0406 
0407 const QByteArray KAEventPrivate::NEXT_RECUR_PROPERTY("NEXTRECUR");     // X-KDE-KALARM-NEXTRECUR property
0408 const QByteArray KAEventPrivate::REPEAT_PROPERTY("REPEAT");            // X-KDE-KALARM-REPEAT property
0409 const QByteArray KAEventPrivate::LOG_PROPERTY("LOG");                  // X-KDE-KALARM-LOG property
0410 const QString    KAEventPrivate::xtermURL = QStringLiteral("xterm:");
0411 const QString    KAEventPrivate::displayURL = QStringLiteral("display:");
0412 
0413 // - General alarm properties
0414 const QByteArray KAEventPrivate::TYPE_PROPERTY("TYPE");                // X-KDE-KALARM-TYPE property
0415 const QString    KAEventPrivate::FILE_TYPE             = QStringLiteral("FILE");
0416 const QString    KAEventPrivate::AT_LOGIN_TYPE         = QStringLiteral("LOGIN");
0417 const QString    KAEventPrivate::REMINDER_TYPE         = QStringLiteral("REMINDER");
0418 const QString    KAEventPrivate::TIME_DEFERRAL_TYPE    = QStringLiteral("DEFERRAL");
0419 const QString    KAEventPrivate::DATE_DEFERRAL_TYPE    = QStringLiteral("DATE_DEFERRAL");
0420 const QString    KAEventPrivate::DISPLAYING_TYPE       = QStringLiteral("DISPLAYING");   // used only in displaying calendar
0421 const QString    KAEventPrivate::PRE_ACTION_TYPE       = QStringLiteral("PRE");
0422 const QString    KAEventPrivate::POST_ACTION_TYPE      = QStringLiteral("POST");
0423 const QString    KAEventPrivate::SOUND_REPEAT_TYPE     = QStringLiteral("SOUNDREPEAT");
0424 const QByteArray KAEventPrivate::NEXT_REPEAT_PROPERTY("NEXTREPEAT");   // X-KDE-KALARM-NEXTREPEAT property
0425 const QString    KAEventPrivate::HIDDEN_REMINDER_FLAG  = QStringLiteral("HIDE");
0426 // - Display alarm properties
0427 const QByteArray KAEventPrivate::FONT_COLOUR_PROPERTY("FONTCOLOR");    // X-KDE-KALARM-FONTCOLOR property
0428 // - Email alarm properties
0429 const QString    KAEventPrivate::EMAIL_ID_FLAG         = QStringLiteral("EMAILID");
0430 // - Audio alarm properties
0431 const QByteArray KAEventPrivate::VOLUME_PROPERTY("VOLUME");            // X-KDE-KALARM-VOLUME property
0432 const QString    KAEventPrivate::SPEAK_FLAG            = QStringLiteral("SPEAK");
0433 // - Command alarm properties
0434 const QString    KAEventPrivate::EXEC_ON_DEFERRAL_FLAG = QStringLiteral("EXECDEFER");
0435 const QString    KAEventPrivate::CANCEL_ON_ERROR_FLAG  = QStringLiteral("ERRCANCEL");
0436 const QString    KAEventPrivate::DONT_SHOW_ERROR_FLAG  = QStringLiteral("ERRNOSHOW");
0437 
0438 // Event status strings
0439 const QString    KAEventPrivate::DISABLED_STATUS       = QStringLiteral("DISABLED");
0440 
0441 // Displaying event ID identifier
0442 const QString    KAEventPrivate::DISP_DEFER = QStringLiteral("DEFER");
0443 const QString    KAEventPrivate::DISP_EDIT  = QStringLiteral("EDIT");
0444 
0445 // Command error strings
0446 const QString    KAEventPrivate::CMD_ERROR_VALUE      = QStringLiteral("MAIN");
0447 const QString    KAEventPrivate::CMD_ERROR_PRE_VALUE  = QStringLiteral("PRE");
0448 const QString    KAEventPrivate::CMD_ERROR_POST_VALUE = QStringLiteral("POST");
0449 
0450 const QString    KAEventPrivate::SC = QStringLiteral(";");
0451 
0452 QFont            KAEventPrivate::mDefaultFont;
0453 const Holidays   KAEventPrivate::mDummyHolidays;
0454 const Holidays*  KAEventPrivate::mHolidays{&mDummyHolidays};
0455 QBitArray        KAEventPrivate::mWorkDays(7);
0456 QTime            KAEventPrivate::mWorkDayStart(9, 0, 0);
0457 QTime            KAEventPrivate::mWorkDayEnd(17, 0, 0);
0458 KADateTime::Spec KAEventPrivate::mWorkDayTimeSpec(KADateTime::LocalZone);
0459 int              KAEventPrivate::mWorkTimeIndex = 1;
0460 
0461 static void setProcedureAlarm(const Alarm::Ptr&, const QString& commandLine);
0462 static QString reminderToString(int minutes);
0463 
0464 /*=============================================================================
0465 = Class KAEvent
0466 = Corresponds to a KCalendarCore::Event instance.
0467 =============================================================================*/
0468 
0469 inline void KAEventPrivate::activate_reminder(bool activate)
0470 {
0471     if (activate  &&  mReminderActive != ReminderType::Active  &&  mReminderMinutes)
0472     {
0473         if (mReminderActive == ReminderType::None)
0474             ++mAlarmCount;
0475         mReminderActive = ReminderType::Active;
0476     }
0477     else if (!activate  &&  mReminderActive != ReminderType::None)
0478     {
0479         mReminderActive = ReminderType::None;
0480         mReminderAfterTime = DateTime();
0481         --mAlarmCount;
0482     }
0483 }
0484 
0485 Q_GLOBAL_STATIC_WITH_ARGS(QSharedDataPointer<KAEventPrivate>,
0486                           emptyKAEventPrivate, (new KAEventPrivate))
0487 
0488 KAEvent::KAEvent()
0489     : d(*emptyKAEventPrivate)
0490 { }
0491 
0492 KAEventPrivate::KAEventPrivate() = default;
0493 
0494 /******************************************************************************
0495 * Initialise the instance with the specified parameters.
0496 */
0497 KAEvent::KAEvent(const KADateTime& dt, const QString& name, const QString& message,
0498                  const QColor& bg, const QColor& fg, const QFont& f,
0499                  SubAction action, int lateCancel, Flags flags, bool changesPending)
0500     : d(new KAEventPrivate(dt, name, message, bg, fg, f, action, lateCancel, flags, changesPending))
0501 {
0502 }
0503 
0504 KAEventPrivate::KAEventPrivate(const KADateTime& dateTime, const QString& name, const QString& text,
0505                                const QColor& bg, const QColor& fg, const QFont& font,
0506                                KAEvent::SubAction action, int lateCancel, KAEvent::Flags flags,
0507                                bool changesPending)
0508     : mName(name)
0509     , mStartDateTime(dateTime)
0510     , mAlarmCount(1)
0511     , mBgColour(bg)
0512     , mFgColour(fg)
0513     , mFont(font)
0514     , mLateCancel(lateCancel)     // do this before setting flags
0515     , mCategory(CalEvent::ACTIVE)
0516 {
0517     if (flags & KAEvent::ANY_TIME)
0518         mStartDateTime.setDateOnly(true);
0519     mNextMainDateTime = mStartDateTime;
0520     switch (action)
0521     {
0522         case KAEvent::SubAction::Message:
0523         case KAEvent::SubAction::File:
0524         case KAEvent::SubAction::Command:
0525         case KAEvent::SubAction::Email:
0526         case KAEvent::SubAction::Audio:
0527             mActionSubType = static_cast<KAEvent::SubAction>(action);
0528             break;
0529         default:
0530             mActionSubType = KAEvent::SubAction::Message;
0531             break;
0532     }
0533     mText                   = (mActionSubType == KAEvent::SubAction::Command) ? text.trimmed()
0534                             : (mActionSubType == KAEvent::SubAction::Audio)   ? QString() : text;
0535     mAudioFile              = (mActionSubType == KAEvent::SubAction::Audio) ? text : QString();
0536     set_deferral((flags & DEFERRAL) ? DeferType::Normal : DeferType::None);
0537     mRepeatAtLogin          = flags & KAEvent::REPEAT_AT_LOGIN;
0538     mConfirmAck             = flags & KAEvent::CONFIRM_ACK;
0539     mUseDefaultFont         = flags & KAEvent::DEFAULT_FONT;
0540     mCommandScript          = flags & KAEvent::SCRIPT;
0541     mCommandXterm           = flags & KAEvent::EXEC_IN_XTERM;
0542     mCommandDisplay         = flags & KAEvent::DISPLAY_COMMAND;
0543     mCommandHideError       = flags & KAEvent::DONT_SHOW_ERROR;
0544     mCopyToKOrganizer       = flags & KAEvent::COPY_KORGANIZER;
0545     mWakeFromSuspend        = flags & KAEvent::WAKE_SUSPEND;
0546     mExcludeHolidays        = flags & KAEvent::EXCL_HOLIDAYS;
0547     mExcludeHolidayRegion   = mHolidays->regionCode();
0548     mWorkTimeOnly           = flags & KAEvent::WORK_TIME_ONLY;
0549     mEmailBcc               = flags & KAEvent::EMAIL_BCC;
0550     mEnabled                = !(flags & KAEvent::DISABLED);
0551     mDisplaying             = flags & DISPLAYING_;
0552     mReminderOnceOnly       = flags & KAEvent::REMINDER_ONCE;
0553     mAutoClose              = (flags & KAEvent::AUTO_CLOSE) && mLateCancel;
0554     mNotify                 = flags & KAEvent::NOTIFY;
0555     mRepeatSoundPause       = (flags & KAEvent::REPEAT_SOUND) ? 0 : -1;
0556     mSpeak                  = (flags & KAEvent::SPEAK) && action != KAEvent::SubAction::Audio;
0557     mBeep                   = (flags & KAEvent::BEEP) && action != KAEvent::SubAction::Audio && !mSpeak;
0558     if (mRepeatAtLogin)
0559     {              // do this after setting other flags
0560         ++mAlarmCount;
0561         setRepeatAtLoginTrue(false);
0562     }
0563 
0564     mMainExpired            = false;
0565     mChangeCount            = changesPending ? 1 : 0;
0566     mTriggerChanged         = true;
0567 }
0568 
0569 /******************************************************************************
0570 * Initialise the KAEvent from a KCalendarCore::Event.
0571 */
0572 KAEvent::KAEvent(const KCalendarCore::Event::Ptr& event)
0573     : d(new KAEventPrivate(event))
0574 {
0575 }
0576 
0577 KAEventPrivate::KAEventPrivate(const KCalendarCore::Event::Ptr& event)
0578 {
0579     startChanges();
0580     // Extract status from the event
0581     mEventID        = event->uid();
0582     mRevision       = event->revision();
0583     mName           = event->summary();
0584     mBgColour       = QColor(255, 255, 255);    // missing/invalid colour - return white background
0585     mFgColour       = QColor(0, 0, 0);          // and black foreground
0586     mReadOnly       = event->isReadOnly();
0587     mUseDefaultFont = true;
0588     mEnabled        = true;
0589     QString param;
0590     bool ok;
0591     mCategory               = CalEvent::status(event, &param);
0592     if (mCategory == CalEvent::DISPLAYING)
0593     {
0594         // It's a displaying calendar event - set values specific to displaying alarms
0595         const QStringList params = param.split(SC, Qt::KeepEmptyParts);
0596         int n = params.count();
0597         if (n)
0598         {
0599             const qlonglong id = params[0].toLongLong(&ok);
0600             if (ok)
0601                 mResourceId = id;    // original resource ID which contained the event
0602             for (int i = 1;  i < n;  ++i)
0603             {
0604                 if (params[i] == DISP_DEFER)
0605                     mDisplayingDefer = true;
0606                 if (params[i] == DISP_EDIT)
0607                     mDisplayingEdit = true;
0608             }
0609         }
0610     }
0611     // Store the non-KAlarm custom properties of the event
0612     const QByteArray kalarmKey = "X-KDE-" + KACalendar::APPNAME + '-';
0613     mCustomProperties = event->customProperties();
0614     for (QMap<QByteArray, QString>::Iterator it = mCustomProperties.begin();  it != mCustomProperties.end();)
0615     {
0616         if (it.key().startsWith(kalarmKey))
0617             it = mCustomProperties.erase(it);
0618         else
0619             ++it;
0620     }
0621 
0622     bool dateOnly = false;
0623     bool localZone = false;
0624     QStringList flags = event->customProperty(KACalendar::APPNAME, FLAGS_PROPERTY).split(SC, Qt::SkipEmptyParts);
0625     flags << QString() << QString();    // to avoid having to check for end of list
0626     for (int i = 0, end = flags.count() - 1;  i < end;  ++i)
0627     {
0628         QString flag = flags.at(i);
0629         if (flag == DATE_ONLY_FLAG)
0630             dateOnly = true;
0631         else if (flag == LOCAL_ZONE_FLAG)
0632             localZone = true;
0633         else if (flag == CONFIRM_ACK_FLAG)
0634             mConfirmAck = true;
0635         else if (flag == EMAIL_BCC_FLAG)
0636             mEmailBcc = true;
0637         else if (flag == KORGANIZER_FLAG)
0638             mCopyToKOrganizer = true;
0639         else if (flag == WAKE_SUSPEND_FLAG)
0640             mWakeFromSuspend = true;
0641         else if (flag == EXCLUDE_HOLIDAYS_FLAG)
0642         {
0643             mExcludeHolidays      = true;
0644             mExcludeHolidayRegion = mHolidays->regionCode();
0645         }
0646         else if (flag == WORK_TIME_ONLY_FLAG)
0647             mWorkTimeOnly = 1;
0648         else if (flag == NOTIFY_FLAG)
0649             mNotify = true;
0650         else if (flag == KMAIL_ITEM_FLAG)
0651         {
0652             const KAEvent::EmailId id = flags.at(i + 1).toLongLong(&ok);
0653             if (!ok)
0654                 continue;
0655             mEmailId = id;
0656             ++i;
0657         }
0658         else if (flag == KAEventPrivate::ARCHIVE_FLAG)
0659             mArchive = true;
0660         else if (flag == KAEventPrivate::AT_LOGIN_TYPE)
0661             mArchiveRepeatAtLogin = true;
0662         else if (flag == KAEventPrivate::REMINDER_TYPE)
0663         {
0664             flag = flags.at(++i);
0665             if (flag == KAEventPrivate::REMINDER_ONCE_FLAG)
0666             {
0667                 mReminderOnceOnly = true;
0668                 flag = flags.at(++i);
0669             }
0670             const int len = flag.length() - 1;
0671             mReminderMinutes = -QStringView(flag).left(len).toInt();    // -> 0 if conversion fails
0672             switch (flag.at(len).toLatin1())
0673             {
0674                 case 'M':  break;
0675                 case 'H':  mReminderMinutes *= 60;  break;
0676                 case 'D':  mReminderMinutes *= 1440;  break;
0677                 default:   mReminderMinutes = 0;  break;
0678             }
0679         }
0680         else if (flag == DEFER_FLAG)
0681         {
0682             QString mins = flags.at(i + 1);
0683             if (mins.endsWith(QLatin1Char('D')))
0684             {
0685                 mDeferDefaultDateOnly = true;
0686                 mins.chop(1);
0687             }
0688             const int n = static_cast<int>(mins.toUInt(&ok));
0689             if (!ok)
0690                 continue;
0691             mDeferDefaultMinutes = n;
0692             ++i;
0693         }
0694         else if (flag == TEMPL_AFTER_TIME_FLAG)
0695         {
0696             const int n = static_cast<int>(flags.at(i + 1).toUInt(&ok));
0697             if (!ok)
0698                 continue;
0699             mTemplateAfterTime = n;
0700             ++i;
0701         }
0702         else if (flag == LATE_CANCEL_FLAG)
0703         {
0704             mLateCancel = static_cast<int>(flags.at(i + 1).toUInt(&ok));
0705             if (ok)
0706                 ++i;
0707             if (!ok  ||  !mLateCancel)
0708                 mLateCancel = 1;    // invalid parameter defaults to 1 minute
0709         }
0710         else if (flag == AUTO_CLOSE_FLAG)
0711         {
0712             mLateCancel = static_cast<int>(flags.at(i + 1).toUInt(&ok));
0713             if (ok)
0714                 ++i;
0715             if (!ok  ||  !mLateCancel)
0716                 mLateCancel = 1;    // invalid parameter defaults to 1 minute
0717             mAutoClose = true;
0718         }
0719     }
0720 
0721     QString prop = event->customProperty(KACalendar::APPNAME, LOG_PROPERTY);
0722     if (!prop.isEmpty())
0723     {
0724         if (prop == xtermURL)
0725             mCommandXterm = true;
0726         else if (prop == displayURL)
0727             mCommandDisplay = true;
0728         else
0729             mLogFile = prop;
0730     }
0731     prop = event->customProperty(KACalendar::APPNAME, REPEAT_PROPERTY);
0732     if (!prop.isEmpty())
0733     {
0734         // This property is used only when the main alarm has expired.
0735         // If a main alarm is found, this property is ignored (see below).
0736         const QStringList list = prop.split(QLatin1Char(':'));
0737         if (list.count() >= 2)
0738         {
0739             const int interval = static_cast<int>(list[0].toUInt());
0740             const int count = static_cast<int>(list[1].toUInt());
0741             if (interval && count)
0742             {
0743                 if (interval % (24 * 60))
0744                     mRepetition.set(Duration(interval * 60, Duration::Seconds), count);
0745                 else
0746                     mRepetition.set(Duration(interval / (24 * 60), Duration::Days), count);
0747             }
0748         }
0749     }
0750     mNextMainDateTime = readDateTime(event, localZone, dateOnly, mStartDateTime);
0751     mCreatedDateTime = KADateTime(event->created());
0752     if (dateOnly  &&  !mRepetition.isDaily())
0753         mRepetition.set(Duration(mRepetition.intervalDays(), Duration::Days));
0754     if (event->customStatus() == DISABLED_STATUS)
0755         mEnabled = false;
0756 
0757     // Extract status from the event's alarms.
0758     // First set up defaults.
0759     mActionSubType = KAEvent::SubAction::Message;
0760     mMainExpired   = true;
0761 
0762     // Extract data from all the event's alarms and index the alarms by sequence number
0763     AlarmMap alarmMap;
0764     readAlarms(event, &alarmMap, mCommandDisplay);
0765 
0766     // Incorporate the alarms' details into the overall event
0767     mAlarmCount = 0;       // initialise as invalid
0768     DateTime alTime;
0769     bool set = false;
0770     bool isEmailText = false;
0771     bool setDeferralTime = false;
0772     Duration deferralOffset;
0773     for (AlarmMap::ConstIterator it = alarmMap.constBegin();  it != alarmMap.constEnd();  ++it)
0774     {
0775         const AlarmData& data = it.value();
0776         const DateTime dateTime(data.alarm->hasStartOffset() ? data.alarm->startOffset().end(mNextMainDateTime.effectiveDateTime()) : data.alarm->time());
0777         switch (data.type)
0778         {
0779             case MAIN_ALARM:
0780                 mMainExpired = false;
0781                 alTime = dateTime;
0782                 alTime.setDateOnly(mStartDateTime.isDateOnly());
0783                 mRepetition.set(0, 0);   // ignore X-KDE-KALARM-REPEAT if main alarm exists
0784                 if (data.alarm->repeatCount()  &&  !data.alarm->snoozeTime().isNull())
0785                 {
0786                     mRepetition.set(data.alarm->snoozeTime(), data.alarm->repeatCount());   // values may be adjusted in setRecurrence()
0787                     mNextRepeat = data.nextRepeat;
0788                 }
0789                 if (data.action != KAAlarm::Action::Audio)
0790                     break;
0791                 [[fallthrough]]; // Fall through to AUDIO_ALARM
0792             case AUDIO_ALARM:
0793                 mAudioFile   = data.cleanText;
0794                 mSpeak       = data.speak  &&  mAudioFile.isEmpty();
0795                 mBeep        = !mSpeak  &&  mAudioFile.isEmpty();
0796                 mSoundVolume = (!mBeep && !mSpeak) ? data.soundVolume : -1;
0797                 mFadeVolume  = (mSoundVolume >= 0  &&  data.fadeSeconds > 0) ? data.fadeVolume : -1;
0798                 mFadeSeconds = (mFadeVolume >= 0) ? data.fadeSeconds : 0;
0799                 mRepeatSoundPause = (!mBeep && !mSpeak) ? data.repeatSoundPause : -1;
0800                 break;
0801             case AT_LOGIN_ALARM:
0802                 mRepeatAtLogin   = true;
0803                 mAtLoginDateTime = dateTime.kDateTime();
0804                 alTime = mAtLoginDateTime;
0805                 break;
0806             case REMINDER_ALARM:
0807                 --mAlarmCount;   // reminder alarms only contribute to the alarm count if active
0808                 // N.B. there can be a start offset but no valid date/time (e.g. in template)
0809                 if (data.alarm->startOffset().asSeconds() / 60)
0810                 {
0811                     mReminderActive = ReminderType::Active;
0812                     if (mReminderMinutes < 0)
0813                     {
0814                         mReminderAfterTime = dateTime;   // the reminder is AFTER the main alarm
0815                         mReminderAfterTime.setDateOnly(dateOnly);
0816                         if (data.hiddenReminder)
0817                             mReminderActive = ReminderType::Hidden;
0818                     }
0819                 }
0820                 break;
0821             case DEFERRED_REMINDER_ALARM:
0822             case DEFERRED_ALARM:
0823                 mDeferral = (data.type == DEFERRED_REMINDER_ALARM) ? DeferType::Reminder : DeferType::Normal;
0824                 if (data.timedDeferral)
0825                 {
0826                     // Don't use start-of-day time for applying timed deferral alarm offset
0827                     mDeferralTime = DateTime(data.alarm->hasStartOffset() ? data.alarm->startOffset().end(mNextMainDateTime.calendarDateTime()) : data.alarm->time());
0828                 }
0829                 else
0830                 {
0831                     mDeferralTime = dateTime;
0832                     mDeferralTime.setDateOnly(true);
0833                 }
0834                 if (data.alarm->hasStartOffset())
0835                     deferralOffset = data.alarm->startOffset();
0836                 break;
0837             case DISPLAYING_ALARM:
0838             {
0839                 mDisplaying      = true;
0840                 mDisplayingFlags = data.displayingFlags;
0841                 const bool dateonly = (mDisplayingFlags & DEFERRAL) ? !(mDisplayingFlags & TIMED_FLAG)
0842                                                                     : mStartDateTime.isDateOnly();
0843                 mDisplayingTime = dateTime;
0844                 mDisplayingTime.setDateOnly(dateonly);
0845                 alTime = mDisplayingTime;
0846                 break;
0847             }
0848             case PRE_ACTION_ALARM:
0849                 mPreAction          = data.cleanText;
0850                 mExtraActionOptions = data.extraActionOptions;
0851                 break;
0852             case POST_ACTION_ALARM:
0853                 mPostAction = data.cleanText;
0854                 break;
0855             case INVALID_ALARM:
0856             default:
0857                 break;
0858         }
0859 
0860         bool noSetNextTime = false;
0861         switch (data.type)
0862         {
0863             case DEFERRED_REMINDER_ALARM:
0864             case DEFERRED_ALARM:
0865                 if (!set)
0866                 {
0867                     // The recurrence has to be evaluated before we can
0868                     // calculate the time of a deferral alarm.
0869                     setDeferralTime = true;
0870                     noSetNextTime = true;
0871                 }
0872                 [[fallthrough]]; // fall through to REMINDER_ALARM
0873             case REMINDER_ALARM:
0874             case AT_LOGIN_ALARM:
0875             case DISPLAYING_ALARM:
0876                 if (!set  &&  !noSetNextTime)
0877                     mNextMainDateTime = alTime;
0878                 [[fallthrough]]; // fall through to MAIN_ALARM
0879             case MAIN_ALARM:
0880                 // Ensure that the basic fields are set up even if there is no main
0881                 // alarm in the event (if it has expired and then been deferred)
0882                 if (!set)
0883                 {
0884                     mActionSubType = static_cast<KAEvent::SubAction>(data.action);
0885                     mText = (mActionSubType == KAEvent::SubAction::Command) ? data.cleanText.trimmed() : data.cleanText;
0886                     switch (data.action)
0887                     {
0888                         case KAAlarm::Action::Command:
0889                             mCommandScript = data.commandScript;
0890                             if (data.extraActionOptions & KAEvent::DontShowPreActError)
0891                                 mCommandHideError = true;
0892                             if (!mCommandDisplay)
0893                                 break;
0894                             [[fallthrough]]; // fall through to MESSAGE
0895                         case KAAlarm::Action::Message:
0896                             mFont           = data.font;
0897                             mUseDefaultFont = data.defaultFont;
0898                             if (data.isEmailText)
0899                                 isEmailText = true;
0900                             [[fallthrough]]; // fall through to File
0901                         case KAAlarm::Action::File:
0902                             mBgColour = data.bgColour;
0903                             mFgColour = data.fgColour;
0904                             break;
0905                         case KAAlarm::Action::Email:
0906                             mEmailFromIdentity = data.emailFromId;
0907                             mEmailAddresses    = data.alarm->mailAddresses();
0908                             mEmailSubject      = data.alarm->mailSubject();
0909                             mEmailAttachments  = data.alarm->mailAttachments();
0910                             break;
0911                         case KAAlarm::Action::Audio:
0912                             // Already mostly handled above
0913                             mRepeatSoundPause = data.repeatSoundPause;
0914                             break;
0915                         default:
0916                             break;
0917                     }
0918                     set = true;
0919                 }
0920                 if (data.action == KAAlarm::Action::File  &&  mActionSubType == KAEvent::SubAction::Message)
0921                     mActionSubType = KAEvent::SubAction::File;
0922                 ++mAlarmCount;
0923                 break;
0924             case AUDIO_ALARM:
0925             case PRE_ACTION_ALARM:
0926             case POST_ACTION_ALARM:
0927             case INVALID_ALARM:
0928             default:
0929                 break;
0930         }
0931     }
0932     if (!isEmailText)
0933         mEmailId = -1;
0934 
0935     Recurrence* recur = event->recurrence();
0936     if (recur  &&  recur->recurs())
0937     {
0938         const int nextRepeat = mNextRepeat;    // setRecurrence() clears mNextRepeat
0939         setRecurrence(*recur);
0940         if (nextRepeat <= mRepetition.count())
0941             mNextRepeat = nextRepeat;
0942     }
0943     else if (mRepetition)
0944     {
0945         // Convert a repetition with no recurrence into a recurrence
0946         if (mRepetition.isDaily())
0947             setRecur(RecurrenceRule::rDaily, mRepetition.intervalDays(), mRepetition.count() + 1, QDate());
0948         else
0949             setRecur(RecurrenceRule::rMinutely, mRepetition.intervalMinutes(), mRepetition.count() + 1, KADateTime());
0950         mRepetition.set(0, 0);
0951         mTriggerChanged = true;
0952     }
0953 
0954     if (mRepeatAtLogin)
0955     {
0956         mArchiveRepeatAtLogin = false;
0957         if (mReminderMinutes > 0)
0958         {
0959             mReminderMinutes = 0;      // pre-alarm reminder not allowed for at-login alarm
0960             mReminderActive  = ReminderType::None;
0961         }
0962         setRepeatAtLoginTrue(false);   // clear other incompatible statuses
0963     }
0964 
0965     // Adjust the alarm count if there is an active reminder alarm.
0966     if (mReminderActive != ReminderType::None)
0967         ++mAlarmCount;
0968 
0969     if (mMainExpired  &&  !deferralOffset.isNull()  &&  checkRecur() != KARecurrence::NO_RECUR)
0970     {
0971         // Adjust the deferral time for an expired recurrence, since the
0972         // offset is relative to the first actual occurrence.
0973         DateTime dt = mRecurrence->getNextDateTime(mStartDateTime.addDays(-1).kDateTime());
0974         dt.setDateOnly(mStartDateTime.isDateOnly());
0975         if (mDeferralTime.isDateOnly())
0976         {
0977             mDeferralTime = DateTime(deferralOffset.end(dt.qDateTime()));
0978             mDeferralTime.setDateOnly(true);
0979         }
0980         else
0981             mDeferralTime = DateTime(deferralOffset.end(dt.effectiveDateTime()));
0982     }
0983     if (mDeferral != DeferType::None)
0984     {
0985         if (setDeferralTime)
0986             mNextMainDateTime = mDeferralTime;
0987     }
0988     mTriggerChanged = true;
0989     endChanges();
0990 }
0991 
0992 KAEventPrivate::KAEventPrivate(const KAEventPrivate& other)
0993     : QSharedData(other)
0994     , mRecurrence(nullptr)
0995 {
0996     copy(other);
0997 }
0998 
0999 KAEvent::KAEvent(const KAEvent& other) = default;
1000 
1001 KAEvent::~KAEvent() = default;
1002 
1003 KAEvent& KAEvent::operator=(const KAEvent& other)
1004 {
1005     if (&other != this)
1006         d = other.d;
1007     return *this;
1008 }
1009 
1010 /******************************************************************************
1011 * Copies the data from another instance.
1012 */
1013 void KAEventPrivate::copy(const KAEventPrivate& event)
1014 {
1015     mAllTrigger              = event.mAllTrigger;
1016     mMainTrigger             = event.mMainTrigger;
1017     mAllWorkTrigger          = event.mAllWorkTrigger;
1018     mMainWorkTrigger         = event.mMainWorkTrigger;
1019     mCommandError            = event.mCommandError;
1020     mEventID                 = event.mEventID;
1021     mCustomProperties        = event.mCustomProperties;
1022     mResourceId              = event.mResourceId;
1023     mName                    = event.mName;
1024     mText                    = event.mText;
1025     mAudioFile               = event.mAudioFile;
1026     mPreAction               = event.mPreAction;
1027     mPostAction              = event.mPostAction;
1028     mStartDateTime           = event.mStartDateTime;
1029     mCreatedDateTime         = event.mCreatedDateTime;
1030     mNextMainDateTime        = event.mNextMainDateTime;
1031     mAtLoginDateTime         = event.mAtLoginDateTime;
1032     mDeferralTime            = event.mDeferralTime;
1033     mDisplayingTime          = event.mDisplayingTime;
1034     mDisplayingFlags         = event.mDisplayingFlags;
1035     mReminderMinutes         = event.mReminderMinutes;
1036     mReminderAfterTime       = event.mReminderAfterTime;
1037     mReminderActive          = event.mReminderActive;
1038     mDeferDefaultMinutes     = event.mDeferDefaultMinutes;
1039     mDeferDefaultDateOnly    = event.mDeferDefaultDateOnly;
1040     mRevision                = event.mRevision;
1041     mRepetition              = event.mRepetition;
1042     mNextRepeat              = event.mNextRepeat;
1043     mAlarmCount              = event.mAlarmCount;
1044     mDeferral                = event.mDeferral;
1045     mEmailId                 = event.mEmailId;
1046     mTemplateAfterTime       = event.mTemplateAfterTime;
1047     mBgColour                = event.mBgColour;
1048     mFgColour                = event.mFgColour;
1049     mFont                    = event.mFont;
1050     mEmailFromIdentity       = event.mEmailFromIdentity;
1051     mEmailAddresses          = event.mEmailAddresses;
1052     mEmailSubject            = event.mEmailSubject;
1053     mEmailAttachments        = event.mEmailAttachments;
1054     mLogFile                 = event.mLogFile;
1055     mSoundVolume             = event.mSoundVolume;
1056     mFadeVolume              = event.mFadeVolume;
1057     mFadeSeconds             = event.mFadeSeconds;
1058     mRepeatSoundPause        = event.mRepeatSoundPause;
1059     mLateCancel              = event.mLateCancel;
1060     mWakeFromSuspend         = event.mWakeFromSuspend;
1061     mExcludeHolidays         = event.mExcludeHolidays;
1062     mExcludeHolidayRegion    = event.mExcludeHolidayRegion;
1063     mWorkTimeOnly            = event.mWorkTimeOnly;
1064     mActionSubType           = event.mActionSubType;
1065     mCategory                = event.mCategory;
1066     mExtraActionOptions      = event.mExtraActionOptions;
1067     mCompatibility           = event.mCompatibility;
1068     mReadOnly                = event.mReadOnly;
1069     mConfirmAck              = event.mConfirmAck;
1070     mUseDefaultFont          = event.mUseDefaultFont;
1071     mCommandScript           = event.mCommandScript;
1072     mCommandXterm            = event.mCommandXterm;
1073     mCommandDisplay          = event.mCommandDisplay;
1074     mCommandHideError        = event.mCommandHideError;
1075     mEmailBcc                = event.mEmailBcc;
1076     mBeep                    = event.mBeep;
1077     mSpeak                   = event.mSpeak;
1078     mCopyToKOrganizer        = event.mCopyToKOrganizer;
1079     mReminderOnceOnly        = event.mReminderOnceOnly;
1080     mAutoClose               = event.mAutoClose;
1081     mNotify                  = event.mNotify;
1082     mMainExpired             = event.mMainExpired;
1083     mRepeatAtLogin           = event.mRepeatAtLogin;
1084     mArchiveRepeatAtLogin    = event.mArchiveRepeatAtLogin;
1085     mArchive                 = event.mArchive;
1086     mDisplaying              = event.mDisplaying;
1087     mDisplayingDefer         = event.mDisplayingDefer;
1088     mDisplayingEdit          = event.mDisplayingEdit;
1089     mEnabled                 = event.mEnabled;
1090     mChangeCount             = 0;
1091     mTriggerChanged          = event.mTriggerChanged;
1092     delete mRecurrence;
1093     if (event.mRecurrence)
1094         mRecurrence = new KARecurrence(*event.mRecurrence);
1095     else
1096         mRecurrence = nullptr;
1097 }
1098 
1099 /******************************************************************************
1100 * Update an existing KCalendarCore::Event with the KAEventPrivate data.
1101 * If 'setCustomProperties' is true, all the KCalendarCore::Event's existing
1102 * custom properties are cleared and replaced with the KAEvent's custom
1103 * properties. If false, the KCalendarCore::Event's non-KAlarm custom properties
1104 * are left untouched.
1105 */
1106 bool KAEvent::updateKCalEvent(const KCalendarCore::Event::Ptr& e, UidAction u, bool setCustomProperties) const
1107 {
1108     return d->updateKCalEvent(e, u, setCustomProperties);
1109 }
1110 
1111 bool KAEventPrivate::updateKCalEvent(const Event::Ptr& ev, KAEvent::UidAction uidact, bool setCustomProperties) const
1112 {
1113     // If it's an archived event, the event start date/time will be adjusted to its original
1114     // value instead of its next occurrence, and the expired main alarm will be reinstated.
1115     const bool archived = (mCategory == CalEvent::ARCHIVED);
1116 
1117     if (!ev
1118     ||  (uidact == KAEvent::UidAction::Check  &&  !mEventID.isEmpty()  &&  mEventID != ev->uid())
1119     ||  (!mAlarmCount  && (!archived || !mMainExpired)))
1120         return false;
1121 
1122     ev->startUpdates();   // prevent multiple update notifications
1123     checkRecur();         // ensure recurrence/repetition data is consistent
1124     const bool readOnly = ev->isReadOnly();
1125     if (uidact == KAEvent::UidAction::Set)
1126         ev->setUid(mEventID);
1127     ev->setReadOnly(mReadOnly);
1128     ev->setTransparency(Event::Transparent);
1129 
1130     // Set up event-specific data
1131     ev->setSummary(mName);
1132 
1133     // Set up custom properties.
1134     if (setCustomProperties)
1135         ev->setCustomProperties(mCustomProperties);
1136     ev->removeCustomProperty(KACalendar::APPNAME, FLAGS_PROPERTY);
1137     ev->removeCustomProperty(KACalendar::APPNAME, NEXT_RECUR_PROPERTY);
1138     ev->removeCustomProperty(KACalendar::APPNAME, REPEAT_PROPERTY);
1139     ev->removeCustomProperty(KACalendar::APPNAME, LOG_PROPERTY);
1140 
1141     QString param;
1142     if (mCategory == CalEvent::DISPLAYING)
1143     {
1144         param = QString::number(mResourceId);   // original resource ID which contained the event
1145         if (mDisplayingDefer)
1146             param += SC + DISP_DEFER;
1147         if (mDisplayingEdit)
1148             param += SC + DISP_EDIT;
1149     }
1150     CalEvent::setStatus(ev, mCategory, param);
1151     QStringList flags;
1152     if (mStartDateTime.isDateOnly())
1153         flags += DATE_ONLY_FLAG;
1154     if (mStartDateTime.timeType() == KADateTime::LocalZone)
1155         flags += LOCAL_ZONE_FLAG;
1156     if (mConfirmAck)
1157         flags += CONFIRM_ACK_FLAG;
1158     if (mEmailBcc)
1159         flags += EMAIL_BCC_FLAG;
1160     if (mCopyToKOrganizer)
1161         flags += KORGANIZER_FLAG;
1162     if (mWakeFromSuspend)
1163         flags += WAKE_SUSPEND_FLAG;
1164     if (mExcludeHolidays)
1165         flags += EXCLUDE_HOLIDAYS_FLAG;
1166     if (mWorkTimeOnly)
1167         flags += WORK_TIME_ONLY_FLAG;
1168     if (mNotify)
1169         flags += NOTIFY_FLAG;
1170     if (mLateCancel)
1171         (flags += (mAutoClose ? AUTO_CLOSE_FLAG : LATE_CANCEL_FLAG)) += QString::number(mLateCancel);
1172     if (mReminderMinutes)
1173     {
1174         flags += REMINDER_TYPE;
1175         if (mReminderOnceOnly)
1176             flags += REMINDER_ONCE_FLAG;
1177         flags += reminderToString(-mReminderMinutes);
1178     }
1179     if (mDeferDefaultMinutes)
1180     {
1181         QString ddparam = QString::number(mDeferDefaultMinutes);
1182         if (mDeferDefaultDateOnly)
1183             ddparam += QLatin1Char('D');
1184         (flags += DEFER_FLAG) += ddparam;
1185     }
1186     if (mCategory == CalEvent::TEMPLATE  &&  mTemplateAfterTime >= 0)
1187         (flags += TEMPL_AFTER_TIME_FLAG) += QString::number(mTemplateAfterTime);
1188     if (mEmailId >= 0)
1189         (flags += KMAIL_ITEM_FLAG) += QString::number(mEmailId);
1190     if (mArchive  &&  !archived)
1191     {
1192         flags += ARCHIVE_FLAG;
1193         if (mArchiveRepeatAtLogin)
1194             flags += AT_LOGIN_TYPE;
1195     }
1196     if (!flags.isEmpty())
1197         ev->setCustomProperty(KACalendar::APPNAME, FLAGS_PROPERTY, flags.join(SC));
1198 
1199     if (mCommandXterm)
1200         ev->setCustomProperty(KACalendar::APPNAME, LOG_PROPERTY, xtermURL);
1201     else if (mCommandDisplay)
1202         ev->setCustomProperty(KACalendar::APPNAME, LOG_PROPERTY, displayURL);
1203     else if (!mLogFile.isEmpty())
1204         ev->setCustomProperty(KACalendar::APPNAME, LOG_PROPERTY, mLogFile);
1205 
1206     ev->setCustomStatus(mEnabled ? QString() : DISABLED_STATUS);
1207     ev->setRevision(mRevision);
1208     ev->clearAlarms();
1209 
1210     /* Always set DTSTART as date/time, and use the category "DATE" to indicate
1211      * a date-only event, instead of calling setAllDay(). This is necessary to
1212      * allow a time zone to be specified for a date-only event. Also, KAlarm
1213      * allows the alarm to float within the 24-hour period defined by the
1214      * start-of-day time (which is user-dependent and therefore can't be
1215      * written into the calendar) rather than midnight to midnight, and there
1216      * is no RFC2445 conformant way to specify this.
1217      * RFC2445 states that alarm trigger times specified in absolute terms
1218      * (rather than relative to DTSTART or DTEND) can only be specified as a
1219      * UTC DATE-TIME value. So always use a time relative to DTSTART instead of
1220      * an absolute time.
1221      */
1222     ev->setDtStart(mStartDateTime.calendarDateTime());
1223     ev->setAllDay(false);
1224     ev->setDtEnd(QDateTime());
1225 
1226     const DateTime dtMain = archived ? mStartDateTime : mNextMainDateTime;
1227     int      ancillaryType = 0;   // 0 = invalid, 1 = time, 2 = offset
1228     DateTime ancillaryTime;       // time for ancillary alarms (pre-action, extra audio, etc)
1229     int      ancillaryOffset = 0; // start offset for ancillary alarms
1230     if (!mMainExpired  ||  archived)
1231     {
1232         /* The alarm offset must always be zero for the main alarm. To determine
1233          * which recurrence is due, the property X-KDE-KALARM_NEXTRECUR is used.
1234          * If the alarm offset was non-zero, exception dates and rules would not
1235          * work since they apply to the event time, not the alarm time.
1236          */
1237         if (!archived  &&  checkRecur() != KARecurrence::NO_RECUR)
1238         {
1239             QDateTime dt = mNextMainDateTime.kDateTime().toTimeSpec(mStartDateTime.timeSpec()).qDateTime();
1240             ev->setCustomProperty(KACalendar::APPNAME, NEXT_RECUR_PROPERTY,
1241                                   QLocale::c().toString(dt, mNextMainDateTime.isDateOnly() ? QStringLiteral("yyyyMMdd") : QStringLiteral("yyyyMMddThhmmss")));
1242         }
1243         // Add the main alarm
1244         initKCalAlarm(ev, 0, QStringList(), MAIN_ALARM);
1245         ancillaryOffset = 0;
1246         ancillaryType = dtMain.isValid() ? 2 : 0;
1247     }
1248     else if (mRepetition)
1249     {
1250         // Alarm repetition is normally held in the main alarm, but since
1251         // the main alarm has expired, store in a custom property.
1252         const QString repparam = QStringLiteral("%1:%2").arg(mRepetition.intervalMinutes()).arg(mRepetition.count());
1253         ev->setCustomProperty(KACalendar::APPNAME, REPEAT_PROPERTY, repparam);
1254     }
1255 
1256     // Add subsidiary alarms
1257     if (mRepeatAtLogin  || (mArchiveRepeatAtLogin && archived))
1258     {
1259         DateTime dtl;
1260         if (mArchiveRepeatAtLogin)
1261             dtl = mStartDateTime.calendarKDateTime().addDays(-1);
1262         else if (mAtLoginDateTime.isValid())
1263             dtl = mAtLoginDateTime;
1264         else if (mStartDateTime.isDateOnly())
1265             dtl = DateTime(KADateTime::currentLocalDate().addDays(-1), mStartDateTime.timeSpec());
1266         else
1267             dtl = KADateTime::currentUtcDateTime();
1268         initKCalAlarm(ev, dtl, QStringList(AT_LOGIN_TYPE));
1269         if (!ancillaryType  &&  dtl.isValid())
1270         {
1271             ancillaryTime = dtl;
1272             ancillaryType = 1;
1273         }
1274     }
1275 
1276     // Find the base date/time for calculating alarm offsets
1277     DateTime nextDateTime = mNextMainDateTime;
1278     if (mMainExpired)
1279     {
1280         if (checkRecur() == KARecurrence::NO_RECUR)
1281             nextDateTime = mStartDateTime;
1282         else if (!archived)
1283         {
1284             // It's a deferral of an expired recurrence.
1285             // Need to ensure that the alarm offset is to an occurrence
1286             // which isn't excluded by an exception - otherwise, it will
1287             // never be triggered. So choose the first recurrence which
1288             // isn't an exception.
1289             KADateTime dt = mRecurrence->getNextDateTime(mStartDateTime.addDays(-1).kDateTime());
1290             dt.setDateOnly(mStartDateTime.isDateOnly());
1291             nextDateTime = dt;
1292         }
1293     }
1294 
1295     if (mReminderMinutes  && (mReminderActive != ReminderType::None || archived))
1296     {
1297         int startOffset;
1298         if (mReminderMinutes < 0  &&  mReminderActive != ReminderType::None)
1299         {
1300             // A reminder AFTER the main alarm is active or disabled
1301             startOffset = nextDateTime.calendarKDateTime().secsTo(mReminderAfterTime.calendarKDateTime());
1302         }
1303         else
1304         {
1305             // A reminder BEFORE the main alarm is active
1306             startOffset = -mReminderMinutes * 60;
1307         }
1308         initKCalAlarm(ev, startOffset, QStringList(REMINDER_TYPE));
1309         // Don't set ancillary time if the reminder AFTER is hidden by a deferral
1310         if (!ancillaryType  && (mReminderActive == ReminderType::Active || archived))
1311         {
1312             ancillaryOffset = startOffset;
1313             ancillaryType = 2;
1314         }
1315     }
1316     if (mDeferral != DeferType::None)
1317     {
1318         int startOffset;
1319         QStringList list;
1320         if (mDeferralTime.isDateOnly())
1321         {
1322             startOffset = nextDateTime.secsTo(mDeferralTime.calendarKDateTime());
1323             list += DATE_DEFERRAL_TYPE;
1324         }
1325         else
1326         {
1327             startOffset = nextDateTime.calendarKDateTime().secsTo(mDeferralTime.calendarKDateTime());
1328             list += TIME_DEFERRAL_TYPE;
1329         }
1330         if (mDeferral == DeferType::Reminder)
1331             list += REMINDER_TYPE;
1332         initKCalAlarm(ev, startOffset, list);
1333         if (!ancillaryType  &&  mDeferralTime.isValid())
1334         {
1335             ancillaryOffset = startOffset;
1336             ancillaryType = 2;
1337         }
1338     }
1339     if (mDisplaying  &&  mCategory != CalEvent::TEMPLATE)
1340     {
1341         QStringList list(DISPLAYING_TYPE);
1342         if (mDisplayingFlags & KAEvent::REPEAT_AT_LOGIN)
1343             list += AT_LOGIN_TYPE;
1344         else if (mDisplayingFlags & DEFERRAL)
1345         {
1346             if (mDisplayingFlags & TIMED_FLAG)
1347                 list += TIME_DEFERRAL_TYPE;
1348             else
1349                 list += DATE_DEFERRAL_TYPE;
1350         }
1351         if (mDisplayingFlags & REMINDER)
1352             list += REMINDER_TYPE;
1353         initKCalAlarm(ev, mDisplayingTime, list);
1354         if (!ancillaryType  &&  mDisplayingTime.isValid())
1355         {
1356             ancillaryTime = mDisplayingTime;
1357             ancillaryType = 1;
1358         }
1359     }
1360     if ((mBeep  ||  mSpeak  ||  !mAudioFile.isEmpty())  &&  mActionSubType != KAEvent::SubAction::Audio)
1361     {
1362         // A sound is specified
1363         if (ancillaryType == 2)
1364             initKCalAlarm(ev, ancillaryOffset, QStringList(), AUDIO_ALARM);
1365         else
1366             initKCalAlarm(ev, ancillaryTime, QStringList(), AUDIO_ALARM);
1367     }
1368     if (!mPreAction.isEmpty())
1369     {
1370         // A pre-display action is specified
1371         if (ancillaryType == 2)
1372             initKCalAlarm(ev, ancillaryOffset, QStringList(PRE_ACTION_TYPE), PRE_ACTION_ALARM);
1373         else
1374             initKCalAlarm(ev, ancillaryTime, QStringList(PRE_ACTION_TYPE), PRE_ACTION_ALARM);
1375     }
1376     if (!mPostAction.isEmpty())
1377     {
1378         // A post-display action is specified
1379         if (ancillaryType == 2)
1380             initKCalAlarm(ev, ancillaryOffset, QStringList(POST_ACTION_TYPE), POST_ACTION_ALARM);
1381         else
1382             initKCalAlarm(ev, ancillaryTime, QStringList(POST_ACTION_TYPE), POST_ACTION_ALARM);
1383     }
1384 
1385     if (mRecurrence)
1386         mRecurrence->writeRecurrence(*ev->recurrence());
1387     else
1388         ev->clearRecurrence();
1389     if (mCreatedDateTime.isValid())
1390         ev->setCreated(mCreatedDateTime.qDateTime());
1391     ev->setReadOnly(readOnly);
1392     ev->endUpdates();     // finally issue an update notification
1393     return true;
1394 }
1395 
1396 /******************************************************************************
1397 * Create a new alarm for a libkcal event, and initialise it according to the
1398 * alarm action. If 'types' is non-null, it is appended to the X-KDE-KALARM-TYPE
1399 * property value list.
1400 * NOTE: The variant taking a DateTime calculates the offset from mStartDateTime,
1401 *       which is not suitable for an alarm in a recurring event.
1402 */
1403 Alarm::Ptr KAEventPrivate::initKCalAlarm(const KCalendarCore::Event::Ptr& event, const DateTime& dt, const QStringList& types, AlarmType type) const
1404 {
1405     const int startOffset = dt.isDateOnly() ? mStartDateTime.secsTo(dt)
1406                                             : mStartDateTime.calendarKDateTime().secsTo(dt.calendarKDateTime());
1407     return initKCalAlarm(event, startOffset, types, type);
1408 }
1409 
1410 Alarm::Ptr KAEventPrivate::initKCalAlarm(const KCalendarCore::Event::Ptr& event, int startOffsetSecs, const QStringList& types, AlarmType type) const
1411 {
1412     QStringList alltypes;
1413     QStringList flags;
1414     Alarm::Ptr alarm = event->newAlarm();
1415     alarm->setEnabled(true);
1416     if (type != MAIN_ALARM)
1417     {
1418         // RFC2445 specifies that absolute alarm times must be stored as a UTC DATE-TIME value.
1419         // Set the alarm time as an offset to DTSTART for the reasons described in updateKCalEvent().
1420         alarm->setStartOffset(startOffsetSecs);
1421     }
1422 
1423     switch (type)
1424     {
1425         case AUDIO_ALARM:
1426             setAudioAlarm(alarm);
1427             if (mSpeak)
1428                 flags << KAEventPrivate::SPEAK_FLAG;
1429             if (mRepeatSoundPause >= 0)
1430             {
1431                 // Alarm::setSnoozeTime() sets 5 seconds if duration parameter is zero,
1432                 // so repeat count = -1 represents 0 pause, -2 represents non-zero pause.
1433                 alarm->setRepeatCount(mRepeatSoundPause ? -2 : -1);
1434                 alarm->setSnoozeTime(Duration(mRepeatSoundPause, Duration::Seconds));
1435             }
1436             break;
1437         case PRE_ACTION_ALARM:
1438             setProcedureAlarm(alarm, mPreAction);
1439             if (mExtraActionOptions & KAEvent::ExecPreActOnDeferral)
1440                 flags << KAEventPrivate::EXEC_ON_DEFERRAL_FLAG;
1441             if (mExtraActionOptions & KAEvent::CancelOnPreActError)
1442                 flags << KAEventPrivate::CANCEL_ON_ERROR_FLAG;
1443             if (mExtraActionOptions & KAEvent::DontShowPreActError)
1444                 flags << KAEventPrivate::DONT_SHOW_ERROR_FLAG;
1445             break;
1446         case POST_ACTION_ALARM:
1447             setProcedureAlarm(alarm, mPostAction);
1448             break;
1449         case MAIN_ALARM:
1450             alarm->setSnoozeTime(mRepetition.interval());
1451             alarm->setRepeatCount(mRepetition.count());
1452             if (mRepetition)
1453                 alarm->setCustomProperty(KACalendar::APPNAME, NEXT_REPEAT_PROPERTY,
1454                                          QString::number(mNextRepeat));
1455             [[fallthrough]]; // fall through to INVALID_ALARM
1456         case REMINDER_ALARM:
1457         case INVALID_ALARM:
1458         {
1459             if (types == QStringList(REMINDER_TYPE)
1460             &&  mReminderMinutes < 0  &&  mReminderActive == ReminderType::Hidden)
1461             {
1462                 // It's a reminder AFTER the alarm which is currently disabled
1463                 // due to the main alarm being deferred past it.
1464                 flags << HIDDEN_REMINDER_FLAG;
1465             }
1466             bool display = false;
1467             switch (mActionSubType)
1468             {
1469                 case KAEvent::SubAction::File:
1470                     alltypes += FILE_TYPE;
1471                     [[fallthrough]]; // fall through to Message
1472                 case KAEvent::SubAction::Message:
1473                     alarm->setDisplayAlarm(AlarmText::toCalendarText(mText));
1474                     display = true;
1475                     break;
1476                 case KAEvent::SubAction::Command:
1477                     if (mCommandScript)
1478                         alarm->setProcedureAlarm(QString(), mText);
1479                     else
1480                         setProcedureAlarm(alarm, mText);
1481                     display = mCommandDisplay;
1482                     if (mCommandHideError)
1483                         flags += DONT_SHOW_ERROR_FLAG;
1484                     break;
1485                 case KAEvent::SubAction::Email:
1486                     alarm->setEmailAlarm(mEmailSubject, mText, mEmailAddresses, mEmailAttachments);
1487                     if (mEmailFromIdentity)
1488                         flags << KAEventPrivate::EMAIL_ID_FLAG << QString::number(mEmailFromIdentity);
1489                     break;
1490                 case KAEvent::SubAction::Audio:
1491                     setAudioAlarm(alarm);
1492                     if (mRepeatSoundPause >= 0  &&  type == MAIN_ALARM)
1493                     {
1494                         // Indicate repeating sound in the main alarm by a non-standard
1495                         // method, since it might have a sub-repetition too.
1496                         alltypes << SOUND_REPEAT_TYPE << QString::number(mRepeatSoundPause);
1497                     }
1498                     break;
1499             }
1500             if (display && !mNotify)
1501                 alarm->setCustomProperty(KACalendar::APPNAME, FONT_COLOUR_PROPERTY,
1502                                          QStringLiteral("%1;%2;%3").arg(mBgColour.name(), mFgColour.name(), mUseDefaultFont ? QString() : mFont.toString()));
1503             break;
1504         }
1505         case DEFERRED_ALARM:
1506         case DEFERRED_REMINDER_ALARM:
1507         case AT_LOGIN_ALARM:
1508         case DISPLAYING_ALARM:
1509             break;
1510     }
1511     alltypes += types;
1512     if (!alltypes.isEmpty())
1513         alarm->setCustomProperty(KACalendar::APPNAME, TYPE_PROPERTY, alltypes.join(QLatin1Char(',')));
1514     if (!flags.isEmpty())
1515         alarm->setCustomProperty(KACalendar::APPNAME, FLAGS_PROPERTY, flags.join(SC));
1516     return alarm;
1517 }
1518 
1519 /******************************************************************************
1520 * Find the index to the last daylight savings time transition at or before a
1521 * given UTC time.
1522 * Returns index, or -1 if before the first transition.
1523 */
1524 int KAEventPrivate::transitionIndex(const QDateTime& utc, const QTimeZone::OffsetDataList& transitions)
1525 {
1526     if (utc.timeSpec() != Qt::UTC || transitions.isEmpty())
1527         return -1;
1528     int start = 0;
1529     int end = transitions.size() - 1;
1530     while (start != end)
1531     {
1532         int i = (start + end + 1) / 2;
1533         if (transitions[i].atUtc == utc)
1534             return i;
1535         if (transitions[i].atUtc > utc)
1536         {
1537             end = i - 1;
1538             if (end < 0)
1539                 return -1;
1540         }
1541         else
1542             start = i;
1543     }
1544     return start;
1545 }
1546 
1547 bool KAEvent::isValid() const
1548 {
1549     return d->mAlarmCount  && (d->mAlarmCount != 1 || !d->mRepeatAtLogin);
1550 }
1551 
1552 void KAEvent::setEnabled(bool enable)
1553 {
1554     d->mEnabled = enable;
1555 }
1556 
1557 bool KAEvent::enabled() const
1558 {
1559     return d->mEnabled;
1560 }
1561 
1562 void KAEvent::setReadOnly(bool ro)
1563 {
1564     d->mReadOnly = ro;
1565 }
1566 
1567 bool KAEvent::isReadOnly() const
1568 {
1569     return d->mReadOnly;
1570 }
1571 
1572 void KAEvent::setArchive()
1573 {
1574     d->mArchive = true;
1575 }
1576 
1577 bool KAEvent::toBeArchived() const
1578 {
1579     return d->mArchive;
1580 }
1581 
1582 bool KAEvent::mainExpired() const
1583 {
1584     return d->mMainExpired;
1585 }
1586 
1587 bool KAEvent::expired() const
1588 {
1589     return (d->mDisplaying && d->mMainExpired)  ||  d->mCategory == CalEvent::ARCHIVED;
1590 }
1591 
1592 KAEvent::Flags KAEvent::flags() const
1593 {
1594     return d->flags();
1595 }
1596 
1597 KAEvent::Flags KAEventPrivate::flags() const
1598 {
1599     KAEvent::Flags result{};
1600     if (mBeep)
1601         result |= KAEvent::BEEP;
1602     if (mRepeatSoundPause >= 0)
1603         result |= KAEvent::REPEAT_SOUND;
1604     if (mEmailBcc)
1605         result |= KAEvent::EMAIL_BCC;
1606     if (mStartDateTime.isDateOnly())
1607         result |= KAEvent::ANY_TIME;
1608     if (mSpeak)
1609         result |= KAEvent::SPEAK;
1610     if (mRepeatAtLogin)
1611         result |= KAEvent::REPEAT_AT_LOGIN;
1612     if (mConfirmAck)
1613         result |= KAEvent::CONFIRM_ACK;
1614     if (mUseDefaultFont)
1615         result |= KAEvent::DEFAULT_FONT;
1616     if (mCommandScript)
1617         result |= KAEvent::SCRIPT;
1618     if (mCommandXterm)
1619         result |= KAEvent::EXEC_IN_XTERM;
1620     if (mCommandDisplay)
1621         result |= KAEvent::DISPLAY_COMMAND;
1622     if (mCommandHideError)
1623         result |= KAEvent::DONT_SHOW_ERROR;
1624     if (mCopyToKOrganizer)
1625         result |= KAEvent::COPY_KORGANIZER;
1626     if (mWakeFromSuspend)
1627         result |= KAEvent::WAKE_SUSPEND;
1628     if (mExcludeHolidays)
1629         result |= KAEvent::EXCL_HOLIDAYS;
1630     if (mWorkTimeOnly)
1631         result |= KAEvent::WORK_TIME_ONLY;
1632     if (mReminderOnceOnly)
1633         result |= KAEvent::REMINDER_ONCE;
1634     if (mAutoClose)
1635         result |= KAEvent::AUTO_CLOSE;
1636     if (mNotify)
1637         result |= KAEvent::NOTIFY;
1638     if (!mEnabled)
1639         result |= KAEvent::DISABLED;
1640     return result;
1641 }
1642 
1643 /******************************************************************************
1644 * Change the type of an event.
1645 * If it is being set to archived, set the archived indication in the event ID;
1646 * otherwise, remove the archived indication from the event ID.
1647 */
1648 void KAEvent::setCategory(CalEvent::Type s)
1649 {
1650     d->setCategory(s);
1651 }
1652 
1653 void KAEventPrivate::setCategory(CalEvent::Type s)
1654 {
1655     if (s == mCategory)
1656         return;
1657     mEventID = CalEvent::uid(mEventID, s);
1658     mCategory = s;
1659     mTriggerChanged = true;   // templates and archived don't have trigger times
1660 }
1661 
1662 CalEvent::Type KAEvent::category() const
1663 {
1664     return d->mCategory;
1665 }
1666 
1667 void KAEvent::setEventId(const QString& id)
1668 {
1669     d->mEventID = id;
1670 }
1671 
1672 QString KAEvent::id() const
1673 {
1674     return d->mEventID;
1675 }
1676 
1677 void KAEvent::incrementRevision()
1678 {
1679     ++d->mRevision;
1680 }
1681 
1682 int KAEvent::revision() const
1683 {
1684     return d->mRevision;
1685 }
1686 
1687 void KAEvent::setResourceId(ResourceId id)
1688 {
1689     d->mResourceId = id;
1690 }
1691 
1692 ResourceId KAEvent::resourceId() const
1693 {
1694     // A displaying alarm contains the event's original resource ID
1695     return d->mDisplaying ? -1 : d->mResourceId;
1696 }
1697 
1698 void KAEvent::setCompatibility(KACalendar::Compat c)
1699 {
1700     d->mCompatibility = c;
1701 }
1702 
1703 KACalendar::Compat KAEvent::compatibility() const
1704 {
1705     return d->mCompatibility;
1706 }
1707 
1708 QMap<QByteArray, QString> KAEvent::customProperties() const
1709 {
1710     return d->mCustomProperties;
1711 }
1712 
1713 KAEvent::SubAction KAEvent::actionSubType() const
1714 {
1715     return d->mActionSubType;
1716 }
1717 
1718 KAEvent::Action KAEvent::actionTypes() const
1719 {
1720     switch (d->mActionSubType)
1721     {
1722         case SubAction::Message:
1723         case SubAction::File:     return Action::Display;
1724         case SubAction::Command:  return d->mCommandDisplay ? Action::DisplayCommand : Action::Command;
1725         case SubAction::Email:    return Action::Email;
1726         case SubAction::Audio:    return Action::Audio;
1727         default:                  return Action::None;
1728     }
1729 }
1730 
1731 void KAEvent::setLateCancel(int minutes)
1732 {
1733     if (d->mRepeatAtLogin)
1734         minutes = 0;
1735     d->mLateCancel = minutes;
1736     if (!minutes)
1737         d->mAutoClose = false;
1738 }
1739 
1740 int KAEvent::lateCancel() const
1741 {
1742     return d->mLateCancel;
1743 }
1744 
1745 void KAEvent::setAutoClose(bool ac)
1746 {
1747     d->mAutoClose = ac;
1748 }
1749 
1750 bool KAEvent::autoClose() const
1751 {
1752     return d->mAutoClose;
1753 }
1754 
1755 void KAEvent::setNotify(bool useNotify)
1756 {
1757     d->mNotify = useNotify;
1758 }
1759 
1760 bool KAEvent::notify() const
1761 {
1762     return d->mNotify;
1763 }
1764 
1765 void KAEvent::setEmailId(EmailId id)
1766 {
1767     d->mEmailId = id;
1768 }
1769 
1770 KAEvent::EmailId KAEvent::emailId() const
1771 {
1772     return d->mEmailId;
1773 }
1774 
1775 void KAEvent::setName(const QString& newName)
1776 {
1777     d->mName = newName;
1778 }
1779 
1780 QString KAEvent::name() const
1781 {
1782     return d->mName;
1783 }
1784 
1785 QString KAEvent::cleanText() const
1786 {
1787     return d->mText;
1788 }
1789 
1790 QString KAEvent::message() const
1791 {
1792     return (d->mActionSubType == SubAction::Message
1793         ||  d->mActionSubType == SubAction::Email) ? d->mText : QString();
1794 }
1795 
1796 QString KAEvent::displayMessage() const
1797 {
1798     return (d->mActionSubType == SubAction::Message) ? d->mText : QString();
1799 }
1800 
1801 QString KAEvent::fileName() const
1802 {
1803     return (d->mActionSubType == SubAction::File) ? d->mText : QString();
1804 }
1805 
1806 QColor KAEvent::bgColour() const
1807 {
1808     return d->mBgColour;
1809 }
1810 
1811 QColor KAEvent::fgColour() const
1812 {
1813     return d->mFgColour;
1814 }
1815 
1816 void KAEvent::setDefaultFont(const QFont& f)
1817 {
1818     KAEventPrivate::mDefaultFont = f;
1819 }
1820 
1821 bool KAEvent::useDefaultFont() const
1822 {
1823     return d->mUseDefaultFont;
1824 }
1825 
1826 QFont KAEvent::font() const
1827 {
1828     return d->mUseDefaultFont ? KAEventPrivate::mDefaultFont : d->mFont;
1829 }
1830 
1831 QString KAEvent::command() const
1832 {
1833     return (d->mActionSubType == SubAction::Command) ? d->mText : QString();
1834 }
1835 
1836 bool KAEvent::commandScript() const
1837 {
1838     return d->mCommandScript;
1839 }
1840 
1841 bool KAEvent::commandXterm() const
1842 {
1843     return d->mCommandXterm;
1844 }
1845 
1846 bool KAEvent::commandDisplay() const
1847 {
1848     return d->mCommandDisplay;
1849 }
1850 
1851 void KAEvent::setCommandError(CmdErr t) const
1852 {
1853     d->mCommandError = t;
1854 }
1855 
1856 KAEvent::CmdErr KAEvent::commandError() const
1857 {
1858     return d->mCommandError;
1859 }
1860 
1861 bool KAEvent::commandHideError() const
1862 {
1863     return d->mCommandHideError;
1864 }
1865 
1866 void KAEvent::setLogFile(const QString& logfile)
1867 {
1868     d->mLogFile = logfile;
1869     if (!logfile.isEmpty())
1870         d->mCommandDisplay = d->mCommandXterm = false;
1871 }
1872 
1873 QString KAEvent::logFile() const
1874 {
1875     return d->mLogFile;
1876 }
1877 
1878 bool KAEvent::confirmAck() const
1879 {
1880     return d->mConfirmAck;
1881 }
1882 
1883 bool KAEvent::copyToKOrganizer() const
1884 {
1885     return d->mCopyToKOrganizer;
1886 }
1887 
1888 void KAEvent::setEmail(uint from, const KCalendarCore::Person::List& addresses, const QString& subject,
1889                        const QStringList& attachments)
1890 {
1891     d->mEmailFromIdentity = from;
1892     d->mEmailAddresses    = addresses;
1893     d->mEmailSubject      = subject;
1894     d->mEmailAttachments  = attachments;
1895 }
1896 
1897 QString KAEvent::emailMessage() const
1898 {
1899     return (d->mActionSubType == SubAction::Email) ? d->mText : QString();
1900 }
1901 
1902 uint KAEvent::emailFromId() const
1903 {
1904     return d->mEmailFromIdentity;
1905 }
1906 
1907 KCalendarCore::Person::List KAEvent::emailAddressees() const
1908 {
1909     return d->mEmailAddresses;
1910 }
1911 
1912 QStringList KAEvent::emailAddresses() const
1913 {
1914     return static_cast<QStringList>(d->mEmailAddresses);
1915 }
1916 
1917 QString KAEvent::emailAddresses(const QString& sep) const
1918 {
1919     return d->mEmailAddresses.join(sep);
1920 }
1921 
1922 QString KAEvent::joinEmailAddresses(const KCalendarCore::Person::List& addresses, const QString& separator)
1923 {
1924     return EmailAddressList(addresses).join(separator);
1925 }
1926 
1927 QStringList KAEvent::emailPureAddresses() const
1928 {
1929     return d->mEmailAddresses.pureAddresses();
1930 }
1931 
1932 QString KAEvent::emailPureAddresses(const QString& sep) const
1933 {
1934     return d->mEmailAddresses.pureAddresses(sep);
1935 }
1936 
1937 QString KAEvent::emailSubject() const
1938 {
1939     return d->mEmailSubject;
1940 }
1941 
1942 QStringList KAEvent::emailAttachments() const
1943 {
1944     return d->mEmailAttachments;
1945 }
1946 
1947 QString KAEvent::emailAttachments(const QString& sep) const
1948 {
1949     return d->mEmailAttachments.join(sep);
1950 }
1951 
1952 bool KAEvent::emailBcc() const
1953 {
1954     return d->mEmailBcc;
1955 }
1956 
1957 void KAEvent::setAudioFile(const QString& filename, float volume, float fadeVolume, int fadeSeconds,
1958                            int repeatPause, bool allowEmptyFile)
1959 {
1960     d->setAudioFile(filename, volume, fadeVolume, fadeSeconds, repeatPause, allowEmptyFile);
1961 }
1962 
1963 void KAEventPrivate::setAudioFile(const QString& filename, float volume, float fadeVolume, int fadeSeconds,
1964                                   int repeatPause, bool allowEmptyFile)
1965 {
1966     mAudioFile = filename;
1967     mSoundVolume = (!allowEmptyFile && filename.isEmpty()) ? -1 : volume;
1968     if (mSoundVolume >= 0)
1969     {
1970         mFadeVolume  = (fadeSeconds > 0) ? fadeVolume : -1;
1971         mFadeSeconds = (mFadeVolume >= 0) ? fadeSeconds : 0;
1972     }
1973     else
1974     {
1975         mFadeVolume  = -1;
1976         mFadeSeconds = 0;
1977     }
1978     mRepeatSoundPause = repeatPause;
1979 }
1980 
1981 QString KAEvent::audioFile() const
1982 {
1983     return d->mAudioFile;
1984 }
1985 
1986 float KAEvent::soundVolume() const
1987 {
1988     return d->mSoundVolume;
1989 }
1990 
1991 float KAEvent::fadeVolume() const
1992 {
1993     return d->mSoundVolume >= 0 && d->mFadeSeconds ? d->mFadeVolume : -1;
1994 }
1995 
1996 int KAEvent::fadeSeconds() const
1997 {
1998     return d->mSoundVolume >= 0 && d->mFadeVolume >= 0 ? d->mFadeSeconds : 0;
1999 }
2000 
2001 bool KAEvent::repeatSound() const
2002 {
2003     return d->mRepeatSoundPause >= 0;
2004 }
2005 
2006 int KAEvent::repeatSoundPause() const
2007 {
2008     return d->mRepeatSoundPause;
2009 }
2010 
2011 bool KAEvent::beep() const
2012 {
2013     return d->mBeep;
2014 }
2015 
2016 bool KAEvent::speak() const
2017 {
2018     return (d->mActionSubType == SubAction::Message
2019         || (d->mActionSubType == SubAction::Command && d->mCommandDisplay))
2020        &&  d->mSpeak;
2021 }
2022 
2023 /******************************************************************************
2024 * Set the event to be an alarm template.
2025 */
2026 void KAEvent::setTemplate(const QString& name, int afterTime)
2027 {
2028     d->setCategory(CalEvent::TEMPLATE);
2029     d->mName = name;
2030     d->mTemplateAfterTime = afterTime;
2031     d->mTriggerChanged = true;   // templates and archived don't have trigger times
2032 }
2033 
2034 bool KAEvent::isTemplate() const
2035 {
2036     return d->mCategory == CalEvent::TEMPLATE;
2037 }
2038 
2039 bool KAEvent::usingDefaultTime() const
2040 {
2041     return d->mTemplateAfterTime == 0;
2042 }
2043 
2044 int KAEvent::templateAfterTime() const
2045 {
2046     return d->mTemplateAfterTime;
2047 }
2048 
2049 void KAEvent::setActions(const QString& pre, const QString& post, ExtraActionOptions options)
2050 {
2051     d->mPreAction = pre;
2052     d->mPostAction = post;
2053     d->mExtraActionOptions = options;
2054 }
2055 
2056 QString KAEvent::preAction() const
2057 {
2058     return d->mPreAction;
2059 }
2060 
2061 QString KAEvent::postAction() const
2062 {
2063     return d->mPostAction;
2064 }
2065 
2066 KAEvent::ExtraActionOptions KAEvent::extraActionOptions() const
2067 {
2068     return d->mExtraActionOptions;
2069 }
2070 
2071 /******************************************************************************
2072 * Set a reminder.
2073 * 'minutes' = number of minutes BEFORE the main alarm.
2074 */
2075 void KAEvent::setReminder(int minutes, bool onceOnly)
2076 {
2077     d->setReminder(minutes, onceOnly);
2078 }
2079 
2080 void KAEventPrivate::setReminder(int minutes, bool onceOnly)
2081 {
2082     if (minutes > 0  &&  mRepeatAtLogin)
2083         minutes = 0;
2084     if (minutes != mReminderMinutes  || (minutes && mReminderActive != ReminderType::Active))
2085     {
2086         const ReminderType oldReminderActive = mReminderActive;
2087         mReminderMinutes   = minutes;
2088         mReminderActive    = minutes > 0 ? ReminderType::Active : ReminderType::None;
2089         mReminderOnceOnly  = onceOnly;
2090         mReminderAfterTime = DateTime();
2091         if (mReminderActive != ReminderType::None  &&  oldReminderActive == ReminderType::None)
2092             ++mAlarmCount;
2093         else if (mReminderActive == ReminderType::None  &&  oldReminderActive != ReminderType::None)
2094             --mAlarmCount;
2095         mTriggerChanged = true;
2096     }
2097 }
2098 
2099 /******************************************************************************
2100 * Activate the event's reminder which occurs AFTER the given main alarm time.
2101 * Reply = true if successful (i.e. reminder falls before the next main alarm).
2102 */
2103 void KAEvent::activateReminderAfter(const DateTime& mainAlarmTime)
2104 {
2105     d->activateReminderAfter(mainAlarmTime);
2106 }
2107 
2108 void KAEventPrivate::activateReminderAfter(const DateTime& mainAlarmTime)
2109 {
2110     if (mReminderMinutes >= 0  ||  mReminderActive == ReminderType::Active  ||  !mainAlarmTime.isValid())
2111         return;
2112     // There is a reminder AFTER the main alarm.
2113     if (checkRecur() != KARecurrence::NO_RECUR)
2114     {
2115         // For a recurring alarm, the given alarm time must be a recurrence, not a sub-repetition.
2116         DateTime next;
2117         //???? For some unknown reason, addSecs(-1) returns the recurrence after the next,
2118         //???? so addSecs(-60) is used instead.
2119         if (nextRecurrence(mainAlarmTime.addSecs(-60).effectiveKDateTime(), next) == KAEvent::OccurType::None
2120         ||  mainAlarmTime != next)
2121             return;
2122     }
2123     else if (!mRepeatAtLogin)
2124     {
2125         // For a non-recurring alarm, the given alarm time must be the main alarm time.
2126         if (mainAlarmTime != mStartDateTime)
2127             return;
2128     }
2129 
2130     const DateTime reminderTime = mainAlarmTime.addMins(-mReminderMinutes);
2131     DateTime next;
2132     if (nextOccurrence(mainAlarmTime.effectiveKDateTime(), next, KAEvent::Repeats::Return) != KAEvent::OccurType::None
2133     &&  reminderTime >= next)
2134         return;    // the reminder time is after the next occurrence of the main alarm
2135 
2136     qCDebug(KALARMCAL_LOG) << "Setting reminder at" << reminderTime.effectiveKDateTime().toString(QStringLiteral("%Y-%m-%d %H:%M"));
2137     activate_reminder(true);
2138     mReminderAfterTime = reminderTime;
2139 }
2140 
2141 int KAEvent::reminderMinutes() const
2142 {
2143     return d->mReminderMinutes;
2144 }
2145 
2146 bool KAEvent::reminderActive() const
2147 {
2148     return d->mReminderActive == KAEventPrivate::ReminderType::Active;
2149 }
2150 
2151 bool KAEvent::reminderOnceOnly() const
2152 {
2153     return d->mReminderOnceOnly;
2154 }
2155 
2156 bool KAEvent::reminderDeferral() const
2157 {
2158     return d->mDeferral == KAEventPrivate::DeferType::Reminder;
2159 }
2160 
2161 /******************************************************************************
2162 * Defer the event to the specified time.
2163 * If the main alarm time has passed, the main alarm is marked as expired.
2164 * If 'adjustRecurrence' is true, ensure that the next scheduled recurrence is
2165 * after the current time.
2166 */
2167 void KAEvent::defer(const DateTime& dt, bool reminder, bool adjustRecurrence)
2168 {
2169     d->defer(dt, reminder, adjustRecurrence);
2170 }
2171 
2172 void KAEventPrivate::defer(const DateTime& dateTime, bool reminder, bool adjustRecurrence)
2173 {
2174     startChanges();   // prevent multiple trigger time evaluation here
2175     bool setNextRepetition = false;
2176     bool checkRepetition = false;
2177     bool checkReminderAfter = false;
2178     if (checkRecur() == KARecurrence::NO_RECUR)
2179     {
2180         // Deferring a non-recurring alarm
2181         if (mReminderMinutes)
2182         {
2183             bool deferReminder = false;
2184             if (mReminderMinutes > 0)
2185             {
2186                 // There's a reminder BEFORE the main alarm
2187                 if (dateTime < mNextMainDateTime.effectiveKDateTime())
2188                     deferReminder = true;
2189                 else if (mReminderActive == ReminderType::Active  ||  mDeferral == DeferType::Reminder)
2190                 {
2191                     // Deferring past the main alarm time, so adjust any existing deferral
2192                     set_deferral(DeferType::None);
2193                     mTriggerChanged = true;
2194                 }
2195             }
2196             else if (reminder)
2197                 deferReminder = true;    // deferring a reminder AFTER the main alarm
2198             if (deferReminder)
2199             {
2200                 set_deferral(DeferType::Reminder);   // defer reminder alarm
2201                 mDeferralTime = dateTime;
2202                 mTriggerChanged = true;
2203             }
2204             if (mReminderActive == ReminderType::Active)
2205             {
2206                 activate_reminder(false);
2207                 mTriggerChanged = true;
2208             }
2209         }
2210         if (mDeferral != DeferType::Reminder)
2211         {
2212             // We're deferring the main alarm.
2213             // Main alarm has now expired.
2214             mNextMainDateTime = mDeferralTime = dateTime;
2215             set_deferral(DeferType::Normal);
2216             mTriggerChanged = true;
2217             checkReminderAfter = true;
2218             if (!mMainExpired)
2219             {
2220                 // Mark the alarm as expired now
2221                 mMainExpired = true;
2222                 --mAlarmCount;
2223                 if (mRepeatAtLogin)
2224                 {
2225                     // Remove the repeat-at-login alarm, but keep a note of it for archiving purposes
2226                     mArchiveRepeatAtLogin = true;
2227                     mRepeatAtLogin = false;
2228                     --mAlarmCount;
2229                 }
2230             }
2231         }
2232     }
2233     else if (reminder)
2234     {
2235         // Deferring a reminder for a recurring alarm
2236         if (dateTime >= mNextMainDateTime.effectiveKDateTime())
2237         {
2238             // Trying to defer it past the next main alarm (regardless of whether
2239             // the reminder triggered before or after the main alarm).
2240             set_deferral(DeferType::None);    // (error)
2241         }
2242         else
2243         {
2244             set_deferral(DeferType::Reminder);
2245             mDeferralTime = dateTime;
2246             checkRepetition = true;
2247         }
2248         mTriggerChanged = true;
2249     }
2250     else
2251     {
2252         // Deferring a recurring alarm
2253         mDeferralTime = dateTime;
2254         if (mDeferral == DeferType::None)
2255             set_deferral(DeferType::Normal);
2256         mTriggerChanged = true;
2257         checkReminderAfter = true;
2258         if (adjustRecurrence)
2259         {
2260             const KADateTime now = KADateTime::currentUtcDateTime();
2261             if (mainEndRepeatTime() < now)
2262             {
2263                 // The last repetition (if any) of the current recurrence has already passed.
2264                 // Adjust to the next scheduled recurrence after now.
2265                 if (!mMainExpired  &&  setNextOccurrence(now) == KAEvent::OccurType::None)
2266                 {
2267                     mMainExpired = true;
2268                     --mAlarmCount;
2269                 }
2270             }
2271             else
2272                 setNextRepetition = mRepetition;
2273         }
2274         else
2275             checkRepetition = true;
2276     }
2277     if (checkReminderAfter  &&  mReminderMinutes < 0  &&  mReminderActive != ReminderType::None)
2278     {
2279         // Enable/disable the active reminder AFTER the main alarm,
2280         // depending on whether the deferral is before or after the reminder.
2281         mReminderActive = (mDeferralTime < mReminderAfterTime) ? ReminderType::Active : ReminderType::Hidden;
2282     }
2283     if (checkRepetition)
2284         setNextRepetition = (mRepetition  &&  mDeferralTime < mainEndRepeatTime());
2285     if (setNextRepetition)
2286     {
2287         // The alarm is repeated, and we're deferring to a time before the last repetition.
2288         // Set the next scheduled repetition to the one after the deferral.
2289         if (mNextMainDateTime >= mDeferralTime)
2290             mNextRepeat = 0;
2291         else
2292             mNextRepeat = mRepetition.nextRepeatCount(mNextMainDateTime.kDateTime(), mDeferralTime.kDateTime());
2293         mTriggerChanged = true;
2294     }
2295     endChanges();
2296 }
2297 
2298 /******************************************************************************
2299 * Cancel any deferral alarm.
2300 */
2301 void KAEvent::cancelDefer()
2302 {
2303     d->cancelDefer();
2304 }
2305 
2306 void KAEventPrivate::cancelDefer()
2307 {
2308     if (mDeferral != DeferType::None)
2309     {
2310         mDeferralTime = DateTime();
2311         set_deferral(DeferType::None);
2312         mTriggerChanged = true;
2313     }
2314 }
2315 
2316 void KAEvent::setDeferDefaultMinutes(int minutes, bool dateOnly)
2317 {
2318     d->mDeferDefaultMinutes = minutes;
2319     d->mDeferDefaultDateOnly = dateOnly;
2320 }
2321 
2322 bool KAEvent::deferred() const
2323 {
2324     return d->mDeferral != KAEventPrivate::DeferType::None;
2325 }
2326 
2327 DateTime KAEvent::deferDateTime() const
2328 {
2329     return d->mDeferralTime;
2330 }
2331 
2332 /******************************************************************************
2333 * Find the latest time which the alarm can currently be deferred to.
2334 */
2335 DateTime KAEvent::deferralLimit(DeferLimit* limitType) const
2336 {
2337     return d->deferralLimit(limitType);
2338 }
2339 
2340 DateTime KAEventPrivate::deferralLimit(KAEvent::DeferLimit* limitType) const
2341 {
2342     KAEvent::DeferLimit ltype = KAEvent::DeferLimit::None;
2343     DateTime endTime;
2344     if (checkRecur() != KARecurrence::NO_RECUR)
2345     {
2346         // It's a recurring alarm. Find the latest time it can be deferred to:
2347         // it cannot be deferred past its next occurrence or sub-repetition,
2348         // or any advance reminder before that.
2349         DateTime reminderTime;
2350         const KADateTime now = KADateTime::currentUtcDateTime();
2351         const KAEvent::OccurType type = nextOccurrence(now, endTime, KAEvent::Repeats::Return);
2352         if (type & KAEvent::OccurType::Repeat)
2353             ltype = KAEvent::DeferLimit::Repetition;
2354         else if (type == KAEvent::OccurType::None)
2355             ltype = KAEvent::DeferLimit::None;
2356         else if (mReminderActive == ReminderType::Active  &&  mReminderMinutes > 0
2357              &&  (now < (reminderTime = endTime.addMins(-mReminderMinutes))))
2358         {
2359             endTime = reminderTime;
2360             ltype = KAEvent::DeferLimit::Reminder;
2361         }
2362         else
2363             ltype = KAEvent::DeferLimit::Recurrence;
2364     }
2365     else if (mReminderMinutes < 0)
2366     {
2367         // There is a reminder alarm which occurs AFTER the main alarm.
2368         // Don't allow the reminder to be deferred past the next main alarm time.
2369         if (KADateTime::currentUtcDateTime() < mNextMainDateTime.effectiveKDateTime())
2370         {
2371             endTime = mNextMainDateTime;
2372             ltype = KAEvent::DeferLimit::Main;
2373         }
2374     }
2375     else if (mReminderMinutes > 0
2376          &&  KADateTime::currentUtcDateTime() < mNextMainDateTime.effectiveKDateTime())
2377     {
2378         // It's a reminder BEFORE the main alarm.
2379         // Don't allow it to be deferred past its main alarm time.
2380         endTime = mNextMainDateTime;
2381         ltype = KAEvent::DeferLimit::Main;
2382     }
2383     if (ltype != KAEvent::DeferLimit::None)
2384         endTime = endTime.addMins(-1);
2385     if (limitType)
2386         *limitType = ltype;
2387     return endTime;
2388 }
2389 
2390 int KAEvent::deferDefaultMinutes() const
2391 {
2392     return d->mDeferDefaultMinutes;
2393 }
2394 
2395 bool KAEvent::deferDefaultDateOnly() const
2396 {
2397     return d->mDeferDefaultDateOnly;
2398 }
2399 
2400 DateTime KAEvent::startDateTime() const
2401 {
2402     return d->mStartDateTime;
2403 }
2404 
2405 void KAEvent::setTime(const KADateTime& dt)
2406 {
2407     d->mNextMainDateTime = dt;
2408     d->mTriggerChanged = true;
2409 }
2410 
2411 /******************************************************************************
2412 */
2413 DateTime KAEvent::nextDateTime(NextTypes type) const
2414 {
2415     return d->nextDateTime(type);
2416 }
2417 DateTime KAEventPrivate::nextDateTime(KAEvent::NextTypes type) const
2418 {
2419     // Remove flags from 'type' which are inapplicable to this alarm.
2420     if (!mRepetition)
2421         type &= ~KAEvent::NextRepeat;    // the alarm doesn't repeat
2422     if (!mReminderMinutes)
2423         type &= ~KAEvent::NextReminder;   // the alarm doesn't have a reminder
2424     if (checkRecur() == KARecurrence::NO_RECUR
2425     ||  (!mWorkTimeOnly  &&  (!mExcludeHolidays || !mHolidays->isValid())))
2426         type &= ~KAEvent::NextWorkHoliday;   // this alarm isn't affected by work time/holidays
2427 
2428     // Deferral time overrides anything else if NextDeferral is set.
2429     if (type & KAEvent::NextDeferral)
2430     {
2431         if (mDeferral == DeferType::Normal
2432         ||  (mDeferral == DeferType::Reminder  &&  (type & KAEvent::NextReminder)))
2433             return mDeferralTime;
2434     }
2435     type &= ~KAEvent::NextDeferral;
2436 
2437     // Find the next recurrence (not sub-repetition) after now.
2438     // If looking for reminders AFTER the alarm, start from now - reminder period.
2439     // If looking for repetitions, start from now - total repetition duration.
2440     const KADateTime now = KADateTime::currentUtcDateTime();
2441     const int repDuration = mRepetition.duration().asSeconds();
2442     int preAdjust = 0;
2443     if ((type & KAEvent::NextReminder)  &&  mReminderMinutes < 0)   // if reminder AFTER the alarm
2444         preAdjust = mReminderMinutes * 60;
2445     if (type & KAEvent::NextRepeat)
2446     {
2447         const int adjustment = -repDuration;
2448         if (adjustment < preAdjust)
2449             preAdjust = adjustment;
2450     }
2451     KADateTime pre = preAdjust ? now.addSecs(preAdjust) : now;
2452     DateTime nextRecur;
2453     if (nextOccurrence(pre, nextRecur, KAEvent::Repeats::Ignore) == KAEvent::OccurType::None)
2454         return DateTime();
2455     const int offsetToRecur = now.secsTo(nextRecur.effectiveKDateTime());
2456 
2457     // If desired, check for the first sub-repetition after now.
2458     DateTime result;
2459     if (type & KAEvent::NextRepeat)
2460     {
2461         // If the recurrence is before now, find the first sub-repetition after now.
2462         if (offsetToRecur <= 0)
2463         {
2464             const int repInterval = mRepetition.intervalSeconds();
2465             const int count = -offsetToRecur / repInterval + 1;   // first sub-repetition AFTER now
2466             if (count <= mRepetition.count())
2467             {
2468                 const DateTime nextRep = nextRecur.addSecs(repInterval * count);
2469                 if (!(type & KAEvent::NextWorkHoliday)
2470                 ||  !excludedByWorkTimeOrHoliday(nextRep.effectiveKDateTime()))
2471                     result = nextRep;
2472             }
2473         }
2474     }
2475 
2476     bool checkedWorkHol = false;   // whether nextRecur has been checked for working time/holiday
2477     bool excludeWorkHol = false;   // whether nextRecur is excluded by working time/holiday
2478 
2479     // If desired, check for the first reminder after now.
2480     DateTime resultReminder;
2481     if (type & KAEvent::NextReminder)
2482     {
2483         excludeWorkHol = excludedByWorkTimeOrHoliday(nextRecur.effectiveKDateTime());
2484         checkedWorkHol = true;
2485         if (!excludeWorkHol)
2486         {
2487             // Reminders are never returned if the recurrence which they relate to
2488             // is excluded by working time/holiday restrictions, regardless of
2489             // whether or not NextWorkHoliday is specified.
2490             const int reminderSecs = mReminderMinutes * 60;
2491             if (mReminderMinutes > 0)
2492             {
2493                 // Reminder is before the alarm. If the recurrence is after now,
2494                 // check if its reminder occurs after now.
2495                 if (offsetToRecur > 0  &&  offsetToRecur > reminderSecs)
2496                     resultReminder = nextRecur.addSecs(-reminderSecs);
2497             }
2498             else
2499             {
2500                 // Reminder is after the alarm. If the recurrence is before now,
2501                 // check if its reminder occurs after now.
2502                 if (offsetToRecur < 0  &&  -offsetToRecur < reminderSecs)
2503                     resultReminder = nextRecur.addSecs(reminderSecs);
2504             }
2505 
2506             if (resultReminder.isValid())
2507             {
2508                 // Find the earliest of the next sub-repetition or the reminder.
2509                 if (!result.isValid()  ||  resultReminder < result)
2510                     result = resultReminder;
2511             }
2512         }
2513     }
2514 
2515     if (result.isValid())
2516         return result;    // return the sub-repetition or reminder time
2517     if (type & KAEvent::NextWorkHoliday)
2518     {
2519         if (!checkedWorkHol)
2520             excludeWorkHol = excludedByWorkTimeOrHoliday(nextRecur.effectiveKDateTime());
2521         if (excludeWorkHol)
2522         {
2523             // Find first recurrence/repetition which complies with working time/
2524             // holiday restrictions.
2525             // The next recurrence found is excluded by work time or holidays.
2526             // Find a subsequent sub-repetition or recurrence which is not excluded.
2527             calcTriggerTimes();
2528             return (type & KAEvent::NextReminder) ? mAllWorkTrigger : mMainWorkTrigger;
2529         }
2530     }
2531     return nextRecur;  // return the recurrence, which must be after now
2532 }
2533 
2534 DateTime KAEvent::mainDateTime(bool withRepeats) const
2535 {
2536     return d->mainDateTime(withRepeats);
2537 }
2538 
2539 QTime KAEvent::mainTime() const
2540 {
2541     return d->mNextMainDateTime.effectiveTime();
2542 }
2543 
2544 DateTime KAEvent::mainEndRepeatTime() const
2545 {
2546     return d->mainEndRepeatTime();
2547 }
2548 
2549 /******************************************************************************
2550 * Set the start-of-day time for date-only alarms.
2551 */
2552 void KAEvent::setStartOfDay(const QTime& startOfDay)
2553 {
2554     DateTime::setStartOfDay(startOfDay);
2555 }
2556 
2557 /******************************************************************************
2558 * Called when the user changes the start-of-day time.
2559 * Adjust the start time of the recurrence to match, for each date-only event in
2560 * a list.
2561 */
2562 void KAEvent::adjustStartOfDay(const KAEvent::List& events)
2563 {
2564     for (KAEvent* event : events)
2565     {
2566         KAEventPrivate* const p = event->d;
2567         if (p->mStartDateTime.isDateOnly()  &&  p->checkRecur() != KARecurrence::NO_RECUR)
2568             p->mRecurrence->setStartDateTime(p->mStartDateTime.effectiveKDateTime(), true);
2569     }
2570 }
2571 
2572 DateTime KAEvent::nextTrigger(Trigger type) const
2573 {
2574     if (d->mCategory == CalEvent::ARCHIVED  ||  d->mCategory == CalEvent::TEMPLATE)
2575         return {};   // it's a template or archived
2576     if (d->mDeferral == KAEventPrivate::DeferType::Normal)
2577         return d->mDeferralTime;   // for a deferred alarm, working time setting is ignored
2578 
2579     d->calcTriggerTimes();
2580     switch (type)
2581     {
2582         case Trigger::All:      return d->mAllTrigger;
2583         case Trigger::Main:     return d->mMainTrigger;
2584         case Trigger::AllWork:  return d->mAllWorkTrigger;
2585         case Trigger::Work:     return d->mMainWorkTrigger;
2586         case Trigger::Display:
2587         {
2588             const bool reminderAfter = d->mMainExpired && d->mReminderActive != KAEventPrivate::ReminderType::None && d->mReminderMinutes < 0;
2589             return d->checkRecur() != KARecurrence::NO_RECUR  && (d->mWorkTimeOnly || d->mExcludeHolidays)
2590                    ? (reminderAfter ? d->mAllWorkTrigger : d->mMainWorkTrigger)
2591                    : (reminderAfter ? d->mAllTrigger     : d->mMainTrigger);
2592         }
2593         default:                return {};
2594     }
2595 }
2596 
2597 void KAEvent::setCreatedDateTime(const KADateTime& dt)
2598 {
2599     d->mCreatedDateTime = dt;
2600 }
2601 
2602 KADateTime KAEvent::createdDateTime() const
2603 {
2604     return d->mCreatedDateTime;
2605 }
2606 
2607 /******************************************************************************
2608 * Set or clear repeat-at-login.
2609 */
2610 void KAEvent::setRepeatAtLogin(bool rl)
2611 {
2612     d->setRepeatAtLogin(rl);
2613 }
2614 
2615 void KAEventPrivate::setRepeatAtLogin(bool rl)
2616 {
2617     if (rl  &&  !mRepeatAtLogin)
2618     {
2619         setRepeatAtLoginTrue(true);   // clear incompatible statuses
2620         ++mAlarmCount;
2621     }
2622     else if (!rl  &&  mRepeatAtLogin)
2623         --mAlarmCount;
2624     mRepeatAtLogin = rl;
2625     mTriggerChanged = true;
2626 }
2627 
2628 /******************************************************************************
2629 * Clear incompatible statuses when repeat-at-login is set.
2630 */
2631 void KAEventPrivate::setRepeatAtLoginTrue(bool clearReminder)
2632 {
2633     clearRecur();                // clear recurrences
2634     if (mReminderMinutes >= 0 && clearReminder)
2635         setReminder(0, false);    // clear pre-alarm reminder
2636     mLateCancel = 0;
2637     mAutoClose = false;
2638     mCopyToKOrganizer = false;
2639 }
2640 
2641 bool KAEvent::repeatAtLogin(bool includeArchived) const
2642 {
2643     return d->mRepeatAtLogin || (includeArchived && d->mArchiveRepeatAtLogin);
2644 }
2645 
2646 /******************************************************************************
2647 * Enable or disable wake-from-suspend when the alarm is due.
2648 */
2649 void KAEvent::setWakeFromSuspend(bool wake)
2650 {
2651     d->mWakeFromSuspend = wake;
2652 }
2653 
2654 bool KAEvent::wakeFromSuspend() const
2655 {
2656     return d->mWakeFromSuspend;
2657 }
2658 
2659 void KAEvent::setExcludeHolidays(bool ex)
2660 {
2661     d->mExcludeHolidays      = ex;
2662     d->mExcludeHolidayRegion = KAEventPrivate::mHolidays->regionCode();
2663     // Option only affects recurring alarms
2664     d->mTriggerChanged = (d->checkRecur() != KARecurrence::NO_RECUR);
2665 }
2666 
2667 bool KAEvent::holidaysExcluded() const
2668 {
2669     return d->mExcludeHolidays;
2670 }
2671 
2672 /******************************************************************************
2673 * Set a new holiday region.
2674 * Alarms which exclude holidays record the pointer to the holiday definition
2675 * at the time their next trigger times were last calculated. The change in
2676 * holiday definition pointer will cause their next trigger times to be
2677 * recalculated.
2678 */
2679 void KAEvent::setHolidays(const Holidays& h)
2680 {
2681     KAEventPrivate::mHolidays = &h;
2682 }
2683 
2684 void KAEvent::setWorkTimeOnly(bool wto)
2685 {
2686     d->mWorkTimeOnly = wto;
2687     // Option only affects recurring alarms
2688     d->mTriggerChanged = (d->checkRecur() != KARecurrence::NO_RECUR);
2689 }
2690 
2691 bool KAEvent::workTimeOnly() const
2692 {
2693     return d->mWorkTimeOnly;
2694 }
2695 
2696 /******************************************************************************
2697 * Check whether a date/time conflicts with working hours and/or holiday
2698 * restrictions for the alarm.
2699 */
2700 bool KAEvent::excludedByWorkTimeOrHoliday(const KADateTime& dt) const
2701 {
2702     return d->excludedByWorkTimeOrHoliday(dt);
2703 }
2704 
2705 bool KAEventPrivate::excludedByWorkTimeOrHoliday(const KADateTime& dt) const
2706 {
2707     if (mExcludeHolidays  &&  mHolidays->isHoliday(dt.date()))
2708         return true;
2709     if (!mWorkTimeOnly)
2710         return false;
2711     const KADateTime wdt = dt.toTimeSpec(mWorkDayTimeSpec);
2712     if (!mWorkDays.testBit(wdt.date().dayOfWeek() - 1))
2713         return true;
2714     return !wdt.isDateOnly()
2715        &&  (wdt.time() < mWorkDayStart  ||  wdt.time() >= mWorkDayEnd);
2716 }
2717 
2718 /******************************************************************************
2719 * Set new working days and times.
2720 * Increment a counter so that working-time-only alarms can detect that they
2721 * need to update their next trigger time.
2722 */
2723 void KAEvent::setWorkTime(const QBitArray& days, const QTime& start, const QTime& end, const KADateTime::Spec& timeSpec)
2724 {
2725     if (days != KAEventPrivate::mWorkDays  ||  start != KAEventPrivate::mWorkDayStart  ||  end != KAEventPrivate::mWorkDayEnd  ||  timeSpec != KAEventPrivate::mWorkDayTimeSpec)
2726     {
2727         KAEventPrivate::mWorkDays        = days;
2728         KAEventPrivate::mWorkDayStart    = start;
2729         KAEventPrivate::mWorkDayEnd      = end;
2730         KAEventPrivate::mWorkDayTimeSpec = timeSpec;
2731         if (!++KAEventPrivate::mWorkTimeIndex)
2732             ++KAEventPrivate::mWorkTimeIndex;
2733     }
2734 }
2735 
2736 /******************************************************************************
2737 * Clear the event's recurrence and alarm repetition data.
2738 */
2739 void KAEvent::setNoRecur()
2740 {
2741     d->clearRecur();
2742 }
2743 
2744 void KAEventPrivate::clearRecur()
2745 {
2746     if (mRecurrence || mRepetition)
2747     {
2748         delete mRecurrence;
2749         mRecurrence = nullptr;
2750         mRepetition.set(0, 0);
2751         mTriggerChanged = true;
2752     }
2753     mNextRepeat = 0;
2754 }
2755 
2756 /******************************************************************************
2757 * Initialise the event's recurrence from a KCalendarCore::Recurrence.
2758 * The event's start date/time is not changed.
2759 */
2760 void KAEvent::setRecurrence(const KARecurrence& recurrence)
2761 {
2762     d->setRecurrence(recurrence);
2763 }
2764 
2765 void KAEventPrivate::setRecurrence(const KARecurrence& recurrence)
2766 {
2767     startChanges();   // prevent multiple trigger time evaluation here
2768     if (recurrence.recurs())
2769     {
2770         delete mRecurrence;
2771         mRecurrence = new KARecurrence(recurrence);
2772         mRecurrence->setStartDateTime(mStartDateTime.effectiveKDateTime(), mStartDateTime.isDateOnly());
2773         mTriggerChanged = true;
2774 
2775         // Adjust sub-repetition values to fit the recurrence.
2776         setRepetition(mRepetition);
2777     }
2778     else
2779         clearRecur();
2780 
2781     endChanges();
2782 }
2783 
2784 /******************************************************************************
2785 * Set the recurrence to recur at a minutes interval.
2786 * Parameters:
2787 *    freq  = how many minutes between recurrences.
2788 *    count = number of occurrences, including first and last.
2789 *          = -1 to recur indefinitely.
2790 *          = 0 to use 'end' instead.
2791 *    end   = end date/time (invalid to use 'count' instead).
2792 * Reply = false if no recurrence was set up.
2793 */
2794 bool KAEvent::setRecurMinutely(int freq, int count, const KADateTime& end)
2795 {
2796     const bool success = d->setRecur(RecurrenceRule::rMinutely, freq, count, end);
2797     d->mTriggerChanged = true;
2798     return success;
2799 }
2800 
2801 /******************************************************************************
2802 * Set the recurrence to recur daily.
2803 * Parameters:
2804 *    freq  = how many days between recurrences.
2805 *    days  = which days of the week alarms are allowed to occur on.
2806 *    count = number of occurrences, including first and last.
2807 *          = -1 to recur indefinitely.
2808 *          = 0 to use 'end' instead.
2809 *    end   = end date (invalid to use 'count' instead).
2810 * Reply = false if no recurrence was set up.
2811 */
2812 bool KAEvent::setRecurDaily(int freq, const QBitArray& days, int count, QDate end)
2813 {
2814     const bool success = d->setRecur(RecurrenceRule::rDaily, freq, count, end);
2815     if (success)
2816     {
2817         if (days.size() != 7)
2818             qCWarning(KALARMCAL_LOG) << "KAEvent::setRecurDaily: Error! 'days' parameter must have 7 elements: actual size" << days.size();
2819         else
2820         {
2821             int n = days.count(true);   // number of days when alarms occur
2822             if (n < 7)
2823                 d->mRecurrence->addWeeklyDays(days);
2824         }
2825     }
2826     d->mTriggerChanged = true;
2827     return success;
2828 }
2829 
2830 /******************************************************************************
2831 * Set the recurrence to recur weekly, on the specified weekdays.
2832 * Parameters:
2833 *    freq  = how many weeks between recurrences.
2834 *    days  = which days of the week alarms should occur on.
2835 *    count = number of occurrences, including first and last.
2836 *          = -1 to recur indefinitely.
2837 *          = 0 to use 'end' instead.
2838 *    end   = end date (invalid to use 'count' instead).
2839 * Reply = false if no recurrence was set up.
2840 */
2841 bool KAEvent::setRecurWeekly(int freq, const QBitArray& days, int count, QDate end)
2842 {
2843     const bool success = d->setRecur(RecurrenceRule::rWeekly, freq, count, end);
2844     if (success)
2845         d->mRecurrence->addWeeklyDays(days);
2846     d->mTriggerChanged = true;
2847     return success;
2848 }
2849 
2850 /******************************************************************************
2851 * Set the recurrence to recur monthly, on the specified days within the month.
2852 * Parameters:
2853 *    freq  = how many months between recurrences.
2854 *    days  = which days of the month alarms should occur on.
2855 *    count = number of occurrences, including first and last.
2856 *          = -1 to recur indefinitely.
2857 *          = 0 to use 'end' instead.
2858 *    end   = end date (invalid to use 'count' instead).
2859 * Reply = false if no recurrence was set up.
2860 */
2861 bool KAEvent::setRecurMonthlyByDate(int freq, const QList<int>& days, int count, QDate end)
2862 {
2863     const bool success = d->setRecur(RecurrenceRule::rMonthly, freq, count, end);
2864     if (success)
2865     {
2866         for (int day : days)
2867             d->mRecurrence->addMonthlyDate(day);
2868     }
2869     d->mTriggerChanged = true;
2870     return success;
2871 }
2872 
2873 /******************************************************************************
2874 * Set the recurrence to recur monthly, on the specified weekdays in the
2875 * specified weeks of the month.
2876 * Parameters:
2877 *    freq  = how many months between recurrences.
2878 *    posns = which days of the week/weeks of the month alarms should occur on.
2879 *    count = number of occurrences, including first and last.
2880 *          = -1 to recur indefinitely.
2881 *          = 0 to use 'end' instead.
2882 *    end   = end date (invalid to use 'count' instead).
2883 * Reply = false if no recurrence was set up.
2884 */
2885 bool KAEvent::setRecurMonthlyByPos(int freq, const QList<MonthPos>& posns, int count, QDate end)
2886 {
2887     const bool success = d->setRecur(RecurrenceRule::rMonthly, freq, count, end);
2888     if (success)
2889     {
2890         for (const MonthPos& posn : posns)
2891             d->mRecurrence->addMonthlyPos(posn.weeknum, posn.days);
2892     }
2893     d->mTriggerChanged = true;
2894     return success;
2895 }
2896 
2897 /******************************************************************************
2898 * Set the recurrence to recur annually, on the specified start date in each
2899 * of the specified months.
2900 * Parameters:
2901 *    freq   = how many years between recurrences.
2902 *    months = which months of the year alarms should occur on.
2903 *    day    = day of month, or 0 to use start date
2904 *    feb29  = when February 29th should recur in non-leap years.
2905 *    count  = number of occurrences, including first and last.
2906 *           = -1 to recur indefinitely.
2907 *           = 0 to use 'end' instead.
2908 *    end    = end date (invalid to use 'count' instead).
2909 * Reply = false if no recurrence was set up.
2910 */
2911 bool KAEvent::setRecurAnnualByDate(int freq, const QList<int>& months, int day,
2912                                    KARecurrence::Feb29Type feb29, int count, QDate end)
2913 {
2914     const bool success = d->setRecur(RecurrenceRule::rYearly, freq, count, end, feb29);
2915     if (success)
2916     {
2917         for (int month : months)
2918             d->mRecurrence->addYearlyMonth(month);
2919         if (day)
2920             d->mRecurrence->addMonthlyDate(day);
2921     }
2922     d->mTriggerChanged = true;
2923     return success;
2924 }
2925 
2926 /******************************************************************************
2927 * Set the recurrence to recur annually, on the specified weekdays in the
2928 * specified weeks of the specified months.
2929 * Parameters:
2930 *    freq   = how many years between recurrences.
2931 *    posns  = which days of the week/weeks of the month alarms should occur on.
2932 *    months = which months of the year alarms should occur on.
2933 *    count  = number of occurrences, including first and last.
2934 *           = -1 to recur indefinitely.
2935 *           = 0 to use 'end' instead.
2936 *    end    = end date (invalid to use 'count' instead).
2937 * Reply = false if no recurrence was set up.
2938 */
2939 bool KAEvent::setRecurAnnualByPos(int freq, const QList<MonthPos>& posns,
2940                                   const QList<int>& months, int count, QDate end)
2941 {
2942     const bool success = d->setRecur(RecurrenceRule::rYearly, freq, count, end);
2943     if (success)
2944     {
2945         for (int month : months)
2946             d->mRecurrence->addYearlyMonth(month);
2947         for (const MonthPos& posn : posns)
2948             d->mRecurrence->addYearlyPos(posn.weeknum, posn.days);
2949     }
2950     d->mTriggerChanged = true;
2951     return success;
2952 }
2953 
2954 /******************************************************************************
2955 * Initialise the event's recurrence data.
2956 * Parameters:
2957 *    freq  = how many intervals between recurrences.
2958 *    count = number of occurrences, including first and last.
2959 *          = -1 to recur indefinitely.
2960 *          = 0 to use 'end' instead.
2961 *    end   = end date/time (invalid to use 'count' instead).
2962 * Reply = false if no recurrence was set up.
2963 */
2964 bool KAEventPrivate::setRecur(KCalendarCore::RecurrenceRule::PeriodType recurType, int freq, int count, QDate end, KARecurrence::Feb29Type feb29)
2965 {
2966     KADateTime edt = mNextMainDateTime.kDateTime();
2967     edt.setDate(end);
2968     return setRecur(recurType, freq, count, edt, feb29);
2969 }
2970 bool KAEventPrivate::setRecur(KCalendarCore::RecurrenceRule::PeriodType recurType, int freq, int count, const KADateTime& end, KARecurrence::Feb29Type feb29)
2971 {
2972     if (count >= -1  && (count || end.date().isValid()))
2973     {
2974         if (!mRecurrence)
2975             mRecurrence = new KARecurrence;
2976         if (mRecurrence->init(recurType, freq, count, mNextMainDateTime.kDateTime(), end, feb29))
2977             return true;
2978     }
2979     clearRecur();
2980     return false;
2981 }
2982 
2983 bool KAEvent::recurs() const
2984 {
2985     return d->checkRecur() != KARecurrence::NO_RECUR;
2986 }
2987 
2988 KARecurrence::Type KAEvent::recurType() const
2989 {
2990     return d->checkRecur();
2991 }
2992 
2993 KARecurrence* KAEvent::recurrence() const
2994 {
2995     return d->mRecurrence;
2996 }
2997 
2998 /******************************************************************************
2999 * Return the recurrence interval in units of the recurrence period type.
3000 */
3001 int KAEvent::recurInterval() const
3002 {
3003     if (d->mRecurrence)
3004     {
3005         switch (d->mRecurrence->type())
3006         {
3007             case KARecurrence::MINUTELY:
3008             case KARecurrence::DAILY:
3009             case KARecurrence::WEEKLY:
3010             case KARecurrence::MONTHLY_DAY:
3011             case KARecurrence::MONTHLY_POS:
3012             case KARecurrence::ANNUAL_DATE:
3013             case KARecurrence::ANNUAL_POS:
3014                 return d->mRecurrence->frequency();
3015             default:
3016                 break;
3017         }
3018     }
3019     return 0;
3020 }
3021 
3022 Duration KAEvent::longestRecurrenceInterval() const
3023 {
3024     return d->mRecurrence ? d->mRecurrence->longestInterval() : Duration(0);
3025 }
3026 
3027 /******************************************************************************
3028 * Adjust the event date/time to the first recurrence of the event, on or after
3029 * start date/time. The event start date may not be a recurrence date, in which
3030 * case a later date will be set.
3031 */
3032 void KAEvent::setFirstRecurrence()
3033 {
3034     d->setFirstRecurrence();
3035 }
3036 
3037 void KAEventPrivate::setFirstRecurrence()
3038 {
3039     switch (checkRecur())
3040     {
3041         case KARecurrence::NO_RECUR:
3042         case KARecurrence::MINUTELY:
3043             return;
3044         case KARecurrence::ANNUAL_DATE:
3045         case KARecurrence::ANNUAL_POS:
3046             if (mRecurrence->yearMonths().isEmpty())
3047                 return;    // (presumably it's a template)
3048             break;
3049         case KARecurrence::DAILY:
3050         case KARecurrence::WEEKLY:
3051         case KARecurrence::MONTHLY_POS:
3052         case KARecurrence::MONTHLY_DAY:
3053             break;
3054     }
3055     const KADateTime recurStart = mRecurrence->startDateTime();
3056     if (mRecurrence->recursOn(recurStart.date(), recurStart.timeSpec()))
3057         return;    // it already recurs on the start date
3058 
3059     // Set the frequency to 1 to find the first possible occurrence
3060     const int frequency = mRecurrence->frequency();
3061     mRecurrence->setFrequency(1);
3062     DateTime next;
3063     nextRecurrence(mNextMainDateTime.effectiveKDateTime(), next);
3064     if (!next.isValid())
3065         mRecurrence->setStartDateTime(recurStart, mStartDateTime.isDateOnly());    // reinstate the old value
3066     else
3067     {
3068         mRecurrence->setStartDateTime(next.effectiveKDateTime(), next.isDateOnly());
3069         mStartDateTime = mNextMainDateTime = next;
3070         mTriggerChanged = true;
3071     }
3072     mRecurrence->setFrequency(frequency);    // restore the frequency
3073 }
3074 
3075 /******************************************************************************
3076 * Return the recurrence interval as text suitable for display.
3077 */
3078 QString KAEvent::recurrenceText(bool brief) const
3079 {
3080     if (d->mRepeatAtLogin)
3081         return brief ? i18nc("@info Brief form of 'At Login'", "Login") : i18nc("@info", "At login");
3082     if (d->mRecurrence)
3083     {
3084         QLocale locale;
3085         const int frequency = d->mRecurrence->frequency();
3086         switch (d->mRecurrence->defaultRRuleConst()->recurrenceType())
3087         {
3088             case RecurrenceRule::rMinutely:
3089                 if (frequency < 60)
3090                     return i18ncp("@info", "1 Minute", "%1 Minutes", frequency);
3091                 else if (frequency % 60 == 0)
3092                     return i18ncp("@info", "1 Hour", "%1 Hours", frequency / 60);
3093                 else
3094                     return i18nc("@info Hours and minutes", "%1h %2m", locale.toString(frequency / 60),
3095                                                                        locale.toString(frequency % 60));
3096             case RecurrenceRule::rDaily:
3097                 return i18ncp("@info", "1 Day", "%1 Days", frequency);
3098             case RecurrenceRule::rWeekly:
3099                 return i18ncp("@info", "1 Week", "%1 Weeks", frequency);
3100             case RecurrenceRule::rMonthly:
3101                 return i18ncp("@info", "1 Month", "%1 Months", frequency);
3102             case RecurrenceRule::rYearly:
3103                 return i18ncp("@info", "1 Year", "%1 Years", frequency);
3104             case RecurrenceRule::rNone:
3105             default:
3106                 break;
3107         }
3108     }
3109     return brief ? QString() : i18nc("@info No recurrence", "None");
3110 }
3111 
3112 /******************************************************************************
3113 * Initialise the event's sub-repetition.
3114 * The repetition length is adjusted if necessary to fit the recurrence interval.
3115 * If the event doesn't recur, the sub-repetition is cleared.
3116 * Reply = false if a non-daily interval was specified for a date-only recurrence.
3117 */
3118 bool KAEvent::setRepetition(const Repetition& r)
3119 {
3120     return d->setRepetition(r);
3121 }
3122 
3123 bool KAEventPrivate::setRepetition(const Repetition& repetition)
3124 {
3125     // Don't set mRepetition to zero at the start of this function, in case the
3126     // 'repetition' parameter passed in is a reference to mRepetition.
3127     mNextRepeat = 0;
3128     if (repetition  &&  !mRepeatAtLogin)
3129     {
3130         Q_ASSERT(checkRecur() != KARecurrence::NO_RECUR);
3131         if (!repetition.isDaily()  &&  mStartDateTime.isDateOnly())
3132         {
3133             mRepetition.set(0, 0);
3134             return false;    // interval must be in units of days for date-only alarms
3135         }
3136         Duration longestInterval = mRecurrence->longestInterval();
3137         if (repetition.duration() >= longestInterval)
3138         {
3139             const int count = mStartDateTime.isDateOnly()
3140                             ? (longestInterval.asDays() - 1) / repetition.intervalDays()
3141                             : (longestInterval.asSeconds() - 1) / repetition.intervalSeconds();
3142             mRepetition.set(repetition.interval(), count);
3143         }
3144         else
3145             mRepetition = repetition;
3146         mTriggerChanged = true;
3147     }
3148     else if (mRepetition)
3149     {
3150         mRepetition.set(0, 0);
3151         mTriggerChanged = true;
3152     }
3153     return true;
3154 }
3155 
3156 Repetition KAEvent::repetition() const
3157 {
3158     return d->mRepetition;
3159 }
3160 
3161 int KAEvent::nextRepetition() const
3162 {
3163     return d->mNextRepeat;
3164 }
3165 
3166 /******************************************************************************
3167 * Return the repetition interval as text suitable for display.
3168 */
3169 QString KAEvent::repetitionText(bool brief) const
3170 {
3171     if (d->mRepetition)
3172     {
3173         if (!d->mRepetition.isDaily())
3174         {
3175             const int minutes = d->mRepetition.intervalMinutes();
3176             if (minutes < 60)
3177                 return i18ncp("@info", "1 Minute", "%1 Minutes", minutes);
3178             if (minutes % 60 == 0)
3179                 return i18ncp("@info", "1 Hour", "%1 Hours", minutes / 60);
3180             return i18nc("@info Hours and minutes", "%1h %2m", minutes / 60, QString::asprintf("%02d", minutes % 60));
3181         }
3182         const int days = d->mRepetition.intervalDays();
3183         if (days % 7)
3184             return i18ncp("@info", "1 Day", "%1 Days", days);
3185         return i18ncp("@info", "1 Week", "%1 Weeks", days / 7);
3186     }
3187     return brief ? QString() : i18nc("@info No repetition", "None");
3188 }
3189 
3190 /******************************************************************************
3191 * Determine whether the event will occur after the specified date/time.
3192 * If 'includeRepetitions' is true and the alarm has a sub-repetition, it
3193 * returns true if any repetitions occur after the specified date/time.
3194 */
3195 bool KAEvent::occursAfter(const KADateTime& preDateTime, bool includeRepetitions) const
3196 {
3197     return d->occursAfter(preDateTime, includeRepetitions);
3198 }
3199 
3200 bool KAEventPrivate::occursAfter(const KADateTime& preDateTime, bool includeRepetitions) const
3201 {
3202     KADateTime dt;
3203     if (checkRecur() != KARecurrence::NO_RECUR)
3204     {
3205         if (mRecurrence->duration() < 0)
3206             return true;    // infinite recurrence
3207         dt = mRecurrence->endDateTime();
3208     }
3209     else
3210         dt = mNextMainDateTime.effectiveKDateTime();
3211     if (mStartDateTime.isDateOnly())
3212     {
3213         QDate pre = preDateTime.date();
3214         if (preDateTime.toTimeSpec(mStartDateTime.timeSpec()).time() < DateTime::startOfDay())
3215             pre = pre.addDays(-1);    // today's recurrence (if today recurs) is still to come
3216         if (pre < dt.date())
3217             return true;
3218     }
3219     else if (preDateTime < dt)
3220         return true;
3221 
3222     if (includeRepetitions  &&  mRepetition)
3223     {
3224         if (preDateTime < KADateTime(mRepetition.duration().end(dt.qDateTime())))
3225             return true;
3226     }
3227     return false;
3228 }
3229 
3230 /******************************************************************************
3231 * Set the date/time of the event to the next scheduled occurrence after the
3232 * specified date/time, provided that this is later than its current date/time.
3233 * Any reminder alarm is adjusted accordingly.
3234 * If the alarm has a sub-repetition, and a repetition of a previous recurrence
3235 * occurs after the specified date/time, that repetition is set as the next
3236 * occurrence.
3237 */
3238 KAEvent::OccurType KAEvent::setNextOccurrence(const KADateTime& preDateTime)
3239 {
3240     return d->setNextOccurrence(preDateTime);
3241 }
3242 
3243 KAEvent::OccurType KAEventPrivate::setNextOccurrence(const KADateTime& preDateTime)
3244 {
3245     if (preDateTime < mNextMainDateTime.effectiveKDateTime())
3246         return KAEvent::OccurType::FirstOrOnly;    // it might not be the first recurrence - tant pis
3247     KADateTime pre = preDateTime;
3248     // If there are repetitions, adjust the comparison date/time so that
3249     // we find the earliest recurrence which has a repetition falling after
3250     // the specified preDateTime.
3251     if (mRepetition)
3252         pre = KADateTime(mRepetition.duration(-mRepetition.count()).end(preDateTime.qDateTime()));
3253 
3254     DateTime afterPre;          // next recurrence after 'pre'
3255     KAEvent::OccurType type;
3256     if (pre < mNextMainDateTime.effectiveKDateTime())
3257     {
3258         afterPre = mNextMainDateTime;
3259         type = KAEvent::OccurType::FirstOrOnly;   // may not actually be the first occurrence
3260     }
3261     else if (checkRecur() != KARecurrence::NO_RECUR)
3262     {
3263         type = nextRecurrence(pre, afterPre);
3264         if (type == KAEvent::OccurType::None)
3265             return KAEvent::OccurType::None;
3266         if (type != KAEvent::OccurType::FirstOrOnly  &&  afterPre != mNextMainDateTime)
3267         {
3268             // Need to reschedule the next trigger date/time
3269             mNextMainDateTime = afterPre;
3270             if (mReminderMinutes > 0  && (mDeferral == DeferType::Reminder || mReminderActive != ReminderType::Active))
3271             {
3272                 // Reinstate the advance reminder for the rescheduled recurrence.
3273                 // Note that a reminder AFTER the main alarm will be left active.
3274                 activate_reminder(!mReminderOnceOnly);
3275             }
3276             if (mDeferral == DeferType::Reminder)
3277                 set_deferral(DeferType::None);
3278             mTriggerChanged = true;
3279         }
3280     }
3281     else
3282         return KAEvent::OccurType::None;
3283 
3284     if (mRepetition)
3285     {
3286         if (afterPre <= preDateTime)
3287         {
3288             // The next occurrence is a sub-repetition.
3289             type = type | KAEvent::OccurType::Repeat;
3290             mNextRepeat = mRepetition.nextRepeatCount(afterPre.effectiveKDateTime(), preDateTime);
3291             // Repetitions can't have a reminder, so remove any.
3292             activate_reminder(false);
3293             if (mDeferral == DeferType::Reminder)
3294                 set_deferral(DeferType::None);
3295             mTriggerChanged = true;
3296         }
3297         else if (mNextRepeat)
3298         {
3299             // The next occurrence is the main occurrence, not a repetition
3300             mNextRepeat = 0;
3301             mTriggerChanged = true;
3302         }
3303     }
3304     return type;
3305 }
3306 
3307 /******************************************************************************
3308 * Get the date/time of the next occurrence of the event, after the specified
3309 * date/time.
3310 * 'result' = date/time of next occurrence, or invalid date/time if none.
3311 */
3312 KAEvent::OccurType KAEvent::nextOccurrence(const KADateTime& preDateTime, DateTime& result, Repeats o) const
3313 {
3314     return d->nextOccurrence(preDateTime, result, o);
3315 }
3316 
3317 KAEvent::OccurType KAEventPrivate::nextOccurrence(const KADateTime& preDateTime, DateTime& result,
3318         KAEvent::Repeats includeRepetitions) const
3319 {
3320     KADateTime pre = preDateTime;
3321     if (includeRepetitions != KAEvent::Repeats::Ignore)
3322     {
3323         // Repeats::Return or Repeats::RecurBefore
3324         if (!mRepetition)
3325             includeRepetitions = KAEvent::Repeats::Ignore;
3326         else
3327             pre = KADateTime(mRepetition.duration(-mRepetition.count()).end(preDateTime.qDateTime()));
3328     }
3329 
3330     KAEvent::OccurType type;
3331     const bool recurs = (checkRecur() != KARecurrence::NO_RECUR);
3332     if (recurs)
3333         type = nextRecurrence(pre, result);
3334     else if (pre < mNextMainDateTime.effectiveKDateTime())
3335     {
3336         result = mNextMainDateTime;
3337         type = KAEvent::OccurType::FirstOrOnly;
3338     }
3339     else
3340     {
3341         result = DateTime();
3342         type = KAEvent::OccurType::None;
3343     }
3344 
3345     if (type != KAEvent::OccurType::None  &&  result <= preDateTime  &&  includeRepetitions != KAEvent::Repeats::Ignore)
3346     {
3347         // Repeats::Return or Repeats::RecurBefore
3348         // The next occurrence is a sub-repetition
3349         int repetition = mRepetition.nextRepeatCount(result.kDateTime(), preDateTime);
3350         const DateTime repeatDT(mRepetition.duration(repetition).end(result.qDateTime()));
3351         if (recurs)
3352         {
3353             // We've found a recurrence before the specified date/time, which has
3354             // a sub-repetition after the date/time.
3355             // However, if the intervals between recurrences vary, we could possibly
3356             // have missed a later recurrence which fits the criterion, so check again.
3357             DateTime dt;
3358             const KAEvent::OccurType newType = previousOccurrence(repeatDT.effectiveKDateTime(), dt, false);
3359             if (dt > result)
3360             {
3361                 type = newType;
3362                 result = dt;
3363                 if (includeRepetitions == KAEvent::Repeats::Return  &&  result <= preDateTime)
3364                 {
3365                     // The next occurrence is a sub-repetition
3366                     repetition = mRepetition.nextRepeatCount(result.kDateTime(), preDateTime);
3367                     result = DateTime(mRepetition.duration(repetition).end(result.qDateTime()));
3368                     type = type | KAEvent::OccurType::Repeat;
3369                 }
3370                 return type;
3371             }
3372         }
3373         if (includeRepetitions == KAEvent::Repeats::Return)
3374         {
3375             // The next occurrence is a sub-repetition
3376             result = repeatDT;
3377             type = type | KAEvent::OccurType::Repeat;
3378         }
3379     }
3380     return type;
3381 }
3382 
3383 /******************************************************************************
3384 * Get the date/time of the last previous occurrence of the event, before the
3385 * specified date/time.
3386 * If 'includeRepetitions' is true and the alarm has a sub-repetition, the
3387 * last previous repetition is returned if appropriate.
3388 * 'result' = date/time of previous occurrence, or invalid date/time if none.
3389 */
3390 KAEvent::OccurType KAEvent::previousOccurrence(const KADateTime& afterDateTime, DateTime& result, bool includeRepetitions) const
3391 {
3392     return d->previousOccurrence(afterDateTime, result, includeRepetitions);
3393 }
3394 
3395 KAEvent::OccurType KAEventPrivate::previousOccurrence(const KADateTime& afterDateTime, DateTime& result,
3396         bool includeRepetitions) const
3397 {
3398     Q_ASSERT(!afterDateTime.isDateOnly());
3399     if (mStartDateTime >= afterDateTime)
3400     {
3401         result = KADateTime();
3402         return KAEvent::OccurType::None;     // the event starts after the specified date/time
3403     }
3404 
3405     // Find the latest recurrence of the event
3406     KAEvent::OccurType type;
3407     if (checkRecur() == KARecurrence::NO_RECUR)
3408     {
3409         result = mStartDateTime;
3410         type = KAEvent::OccurType::FirstOrOnly;
3411     }
3412     else
3413     {
3414         const KADateTime recurStart = mRecurrence->startDateTime();
3415         KADateTime after = afterDateTime.toTimeSpec(mStartDateTime.timeSpec());
3416         if (mStartDateTime.isDateOnly()  &&  afterDateTime.time() > DateTime::startOfDay())
3417             after = after.addDays(1);    // today's recurrence (if today recurs) has passed
3418         const KADateTime dt = mRecurrence->getPreviousDateTime(after);
3419         result = dt;
3420         result.setDateOnly(mStartDateTime.isDateOnly());
3421         if (!dt.isValid())
3422             return KAEvent::OccurType::None;
3423         if (dt == recurStart)
3424             type = KAEvent::OccurType::FirstOrOnly;
3425         else if (mRecurrence->getNextDateTime(dt).isValid())
3426             type = result.isDateOnly() ? KAEvent::OccurType::RecurDate : KAEvent::OccurType::RecurDateTime;
3427         else
3428             type = KAEvent::OccurType::LastRecur;
3429     }
3430 
3431     if (includeRepetitions  &&  mRepetition)
3432     {
3433         // Find the latest repetition which is before the specified time.
3434         const int repetition = mRepetition.previousRepeatCount(result.effectiveKDateTime(), afterDateTime);
3435         if (repetition > 0)
3436         {
3437             result = DateTime(mRepetition.duration(qMin(repetition, mRepetition.count())).end(result.qDateTime()));
3438             return type | KAEvent::OccurType::Repeat;
3439         }
3440     }
3441     return type;
3442 }
3443 
3444 /******************************************************************************
3445 * Set the event to be a copy of the specified event, making the specified
3446 * alarm the 'displaying' alarm.
3447 * The purpose of setting up a 'displaying' alarm is to be able to reinstate
3448 * the alarm message in case of a crash, or to reinstate it should the user
3449 * choose to defer the alarm. Note that even repeat-at-login alarms need to be
3450 * saved in case their end time expires before the next login.
3451 * Reply = true if successful, false if alarm was not copied.
3452 */
3453 bool KAEvent::setDisplaying(const KAEvent& e, KAAlarm::Type t, ResourceId id, const KADateTime& dt, bool showEdit, bool showDefer)
3454 {
3455     return d->setDisplaying(*e.d, t, id, dt, showEdit, showDefer);
3456 }
3457 
3458 bool KAEventPrivate::setDisplaying(const KAEventPrivate& event, KAAlarm::Type alarmType, ResourceId resourceId,
3459                                    const KADateTime& repeatAtLoginTime, bool showEdit, bool showDefer)
3460 {
3461     if (!mDisplaying
3462     &&  (alarmType == KAAlarm::Type::Main
3463       || alarmType == KAAlarm::Type::Reminder
3464       || alarmType == KAAlarm::Type::DeferredReminder
3465       || alarmType == KAAlarm::Type::Deferred
3466       || alarmType == KAAlarm::Type::AtLogin))
3467     {
3468 //qCDebug(KALARMCAL_LOG)<<event.id()<<","<<(alarmType==KAAlarm::Type::Main?"MAIN":alarmType==KAAlarm::Type::Reminder?"REMINDER":alarmType==KAAlarm::Type::DeferredReminder?"DeferType::Reminder":alarmType==KAAlarm::Type::Deferred?"DEFERRAL":"LOGIN")<<"): time="<<repeatAtLoginTime.toString();
3469         KAAlarm al = event.alarm(alarmType);
3470         if (al.isValid())
3471         {
3472             *this = event;
3473             // Change the event ID to avoid duplicating the same unique ID as the original event
3474             setCategory(CalEvent::DISPLAYING);
3475             mResourceId         = resourceId;  // original resource ID which contained the event
3476             mDisplayingDefer    = showDefer;
3477             mDisplayingEdit     = showEdit;
3478             mDisplaying         = true;
3479             mDisplayingTime     = (alarmType == KAAlarm::Type::AtLogin) ? repeatAtLoginTime : al.dateTime().kDateTime();
3480             switch (al.type())
3481             {
3482                 case KAAlarm::Type::AtLogin:            mDisplayingFlags = KAEvent::REPEAT_AT_LOGIN;  break;
3483                 case KAAlarm::Type::Reminder:           mDisplayingFlags = REMINDER;  break;
3484                 case KAAlarm::Type::DeferredReminder:   mDisplayingFlags = al.timedDeferral() ? (REMINDER | TIME_DEFERRAL) : (REMINDER | DATE_DEFERRAL);  break;
3485                 case KAAlarm::Type::Deferred:           mDisplayingFlags = al.timedDeferral() ? TIME_DEFERRAL : DATE_DEFERRAL;  break;
3486                 default:                                mDisplayingFlags = 0;  break;
3487             }
3488             ++mAlarmCount;
3489             return true;
3490         }
3491     }
3492     return false;
3493 }
3494 
3495 /******************************************************************************
3496 * Reinstate the original event from the 'displaying' event.
3497 */
3498 void KAEvent::reinstateFromDisplaying(const KCalendarCore::Event::Ptr& e, ResourceId& id, bool& showEdit, bool& showDefer)
3499 {
3500     d->reinstateFromDisplaying(e, id, showEdit, showDefer);
3501 }
3502 
3503 void KAEventPrivate::reinstateFromDisplaying(const Event::Ptr& kcalEvent, ResourceId& resourceId, bool& showEdit, bool& showDefer)
3504 {
3505     *this = KAEventPrivate(kcalEvent);
3506     if (mDisplaying)
3507     {
3508         // Retrieve the original event's unique ID
3509         setCategory(CalEvent::ACTIVE);
3510         resourceId   = mResourceId;
3511         mResourceId  = -1;
3512         showDefer    = mDisplayingDefer;
3513         showEdit     = mDisplayingEdit;
3514         mDisplaying  = false;
3515         --mAlarmCount;
3516     }
3517 }
3518 
3519 /******************************************************************************
3520 * Return the original alarm which the displaying alarm refers to.
3521 * Note that the caller is responsible for ensuring that the event was a
3522 * displaying event, since this is normally called after
3523 * reinstateFromDisplaying(), which clears mDisplaying.
3524 */
3525 KAAlarm KAEvent::convertDisplayingAlarm() const
3526 {
3527     KAAlarm al = alarm(KAAlarm::Type::Displaying);
3528     KAAlarm::Private* const al_d = al.d;
3529     const int displayingFlags = d->mDisplayingFlags;
3530     if (displayingFlags & REPEAT_AT_LOGIN)
3531     {
3532         al_d->mRepeatAtLogin = true;
3533         al_d->mType = KAAlarm::Type::AtLogin;
3534     }
3535     else if (displayingFlags & KAEventPrivate::DEFERRAL)
3536     {
3537         al_d->mDeferred = true;
3538         al_d->mTimedDeferral = (displayingFlags & KAEventPrivate::TIMED_FLAG);
3539         al_d->mType = (displayingFlags & KAEventPrivate::REMINDER) ? KAAlarm::Type::DeferredReminder : KAAlarm::Type::Deferred;
3540     }
3541     else if (displayingFlags & KAEventPrivate::REMINDER)
3542         al_d->mType = KAAlarm::Type::Reminder;
3543     else
3544         al_d->mType = KAAlarm::Type::Main;
3545     return al;
3546 }
3547 
3548 bool KAEvent::displaying() const
3549 {
3550     return d->mDisplaying;
3551 }
3552 
3553 /******************************************************************************
3554 * Return the alarm of the specified type.
3555 */
3556 KAAlarm KAEvent::alarm(KAAlarm::Type t) const
3557 {
3558     return d->alarm(t);
3559 }
3560 
3561 KAAlarm KAEventPrivate::alarm(KAAlarm::Type type) const
3562 {
3563     checkRecur();     // ensure recurrence/repetition data is consistent
3564     KAAlarm al;       // this sets type to Type::Invalid
3565     KAAlarm::Private* const al_d = al.d;
3566     if (mAlarmCount)
3567     {
3568         al_d->mActionType    = static_cast<KAAlarm::Action>(mActionSubType);
3569         al_d->mRepeatAtLogin = false;
3570         al_d->mDeferred      = false;
3571         switch (type)
3572         {
3573             case KAAlarm::Type::Main:
3574                 if (!mMainExpired)
3575                 {
3576                     al_d->mType             = KAAlarm::Type::Main;
3577                     al_d->mNextMainDateTime = mNextMainDateTime;
3578                     al_d->mRepetition       = mRepetition;
3579                     al_d->mNextRepeat       = mNextRepeat;
3580                 }
3581                 break;
3582             case KAAlarm::Type::Reminder:
3583                 if (mReminderActive == ReminderType::Active)
3584                 {
3585                     al_d->mType = KAAlarm::Type::Reminder;
3586                     if (mReminderMinutes < 0)
3587                         al_d->mNextMainDateTime = mReminderAfterTime;
3588                     else if (mReminderOnceOnly)
3589                         al_d->mNextMainDateTime = mStartDateTime.addMins(-mReminderMinutes);
3590                     else
3591                         al_d->mNextMainDateTime = mNextMainDateTime.addMins(-mReminderMinutes);
3592                 }
3593                 break;
3594             case KAAlarm::Type::DeferredReminder:
3595                 if (mDeferral != DeferType::Reminder)
3596                     break;
3597                 [[fallthrough]]; // fall through to Deferred
3598             case KAAlarm::Type::Deferred:
3599                 if (mDeferral != DeferType::None)
3600                 {
3601                     al_d->mType             = (mDeferral == DeferType::Reminder) ? KAAlarm::Type::DeferredReminder : KAAlarm::Type::Deferred;
3602                     al_d->mNextMainDateTime = mDeferralTime;
3603                     al_d->mDeferred         = true;
3604                     al_d->mTimedDeferral    = !mDeferralTime.isDateOnly();
3605                 }
3606                 break;
3607             case KAAlarm::Type::AtLogin:
3608                 if (mRepeatAtLogin)
3609                 {
3610                     al_d->mType             = KAAlarm::Type::AtLogin;
3611                     al_d->mNextMainDateTime = mAtLoginDateTime;
3612                     al_d->mRepeatAtLogin    = true;
3613                 }
3614                 break;
3615             case KAAlarm::Type::Displaying:
3616                 if (mDisplaying)
3617                 {
3618                     al_d->mType             = KAAlarm::Type::Displaying;
3619                     al_d->mNextMainDateTime = mDisplayingTime;
3620                 }
3621                 break;
3622             case KAAlarm::Type::Invalid:
3623             default:
3624                 break;
3625         }
3626     }
3627     return al;
3628 }
3629 
3630 /******************************************************************************
3631 * Return the main alarm for the event.
3632 * If the main alarm does not exist, one of the subsidiary ones is returned if
3633 * possible.
3634 * N.B. a repeat-at-login alarm can only be returned if it has been read from/
3635 * written to the calendar file.
3636 */
3637 KAAlarm KAEvent::firstAlarm() const
3638 {
3639     return d->firstAlarm();
3640 }
3641 
3642 KAAlarm KAEventPrivate::firstAlarm() const
3643 {
3644     if (mAlarmCount)
3645     {
3646         if (!mMainExpired)
3647             return alarm(KAAlarm::Type::Main);
3648         return nextAlarm(KAAlarm::Type::Main);
3649     }
3650     return {};
3651 }
3652 
3653 /******************************************************************************
3654 * Return the next alarm for the event, after the specified alarm.
3655 * N.B. a repeat-at-login alarm can only be returned if it has been read from/
3656 * written to the calendar file.
3657 */
3658 KAAlarm KAEvent::nextAlarm(const KAAlarm& previousAlarm) const
3659 {
3660     return d->nextAlarm(previousAlarm.type());
3661 }
3662 
3663 KAAlarm KAEvent::nextAlarm(KAAlarm::Type previousType) const
3664 {
3665     return d->nextAlarm(previousType);
3666 }
3667 
3668 KAAlarm KAEventPrivate::nextAlarm(KAAlarm::Type previousType) const
3669 {
3670     switch (previousType)
3671     {
3672         case KAAlarm::Type::Main:
3673             if (mReminderActive == ReminderType::Active)
3674                 return alarm(KAAlarm::Type::Reminder);
3675             [[fallthrough]]; // fall through to Reminder
3676         case KAAlarm::Type::Reminder:
3677             // There can only be one deferral alarm
3678             if (mDeferral == DeferType::Reminder)
3679                 return alarm(KAAlarm::Type::DeferredReminder);
3680             if (mDeferral == DeferType::Normal)
3681                 return alarm(KAAlarm::Type::Deferred);
3682             [[fallthrough]]; // fall through to Deferred
3683         case KAAlarm::Type::DeferredReminder:
3684         case KAAlarm::Type::Deferred:
3685             if (mRepeatAtLogin)
3686                 return alarm(KAAlarm::Type::AtLogin);
3687             [[fallthrough]]; // fall through to AtLogin
3688         case KAAlarm::Type::AtLogin:
3689             if (mDisplaying)
3690                 return alarm(KAAlarm::Type::Displaying);
3691             [[fallthrough]]; // fall through to Displaying
3692         case KAAlarm::Type::Displaying:
3693         case KAAlarm::Type::Invalid:
3694             // fall through to default
3695         default:
3696             break;
3697     }
3698     return {};
3699 }
3700 
3701 int KAEvent::alarmCount() const
3702 {
3703     return d->mAlarmCount;
3704 }
3705 
3706 /******************************************************************************
3707 * Remove the alarm of the specified type from the event.
3708 * This must only be called to remove an alarm which has expired, not to
3709 * reconfigure the event.
3710 */
3711 void KAEvent::removeExpiredAlarm(KAAlarm::Type type)
3712 {
3713     d->removeExpiredAlarm(type);
3714 }
3715 
3716 void KAEventPrivate::removeExpiredAlarm(KAAlarm::Type type)
3717 {
3718     const int count = mAlarmCount;
3719     switch (type)
3720     {
3721         case KAAlarm::Type::Main:
3722             if (mReminderActive == ReminderType::None  ||  mReminderMinutes > 0)
3723             {
3724                 mAlarmCount = 0;    // removing main alarm - also remove subsidiary alarms
3725                 break;
3726             }
3727             // There is a reminder after the main alarm - retain the
3728             // reminder and remove other subsidiary alarms.
3729             mMainExpired = true;    // mark the alarm as expired now
3730             --mAlarmCount;
3731             set_deferral(DeferType::None);
3732             if (mDisplaying)
3733             {
3734                 mDisplaying = false;
3735                 --mAlarmCount;
3736             }
3737             [[fallthrough]]; // fall through to AtLogin
3738         case KAAlarm::Type::AtLogin:
3739             if (mRepeatAtLogin)
3740             {
3741                 // Remove the at-login alarm, but keep a note of it for archiving purposes
3742                 mArchiveRepeatAtLogin = true;
3743                 mRepeatAtLogin = false;
3744                 --mAlarmCount;
3745             }
3746             break;
3747         case KAAlarm::Type::Reminder:
3748             // Remove any reminder alarm, but keep a note of it for archiving purposes
3749             // and for restoration after the next recurrence.
3750             activate_reminder(false);
3751             break;
3752         case KAAlarm::Type::DeferredReminder:
3753         case KAAlarm::Type::Deferred:
3754             set_deferral(DeferType::None);
3755             break;
3756         case KAAlarm::Type::Displaying:
3757             if (mDisplaying)
3758             {
3759                 mDisplaying = false;
3760                 --mAlarmCount;
3761             }
3762             break;
3763         case KAAlarm::Type::Invalid:
3764         default:
3765             break;
3766     }
3767     if (mAlarmCount != count)
3768         mTriggerChanged = true;
3769 }
3770 
3771 /******************************************************************************
3772 * Compare this instance with another.
3773 */
3774 bool KAEvent::compare(const KAEvent& other, Comparison comparison) const
3775 {
3776     return d->compare(*other.d, comparison);
3777 }
3778 bool KAEventPrivate::compare(const KAEventPrivate& other, KAEvent::Comparison comparison) const
3779 {
3780     if (comparison & KAEvent::Compare::Id)
3781     {
3782         if (mEventID != other.mEventID)
3783             return false;
3784     }
3785     if (mCategory         != other.mCategory
3786     ||  mActionSubType    != other.mActionSubType
3787     ||  mDisplaying       != other.mDisplaying
3788     ||  mName             != other.mName
3789     ||  mText             != other.mText
3790     ||  mStartDateTime    != other.mStartDateTime
3791     ||  mLateCancel       != other.mLateCancel
3792     ||  mCopyToKOrganizer != other.mCopyToKOrganizer
3793     ||  mWakeFromSuspend  != other.mWakeFromSuspend
3794     ||  mCompatibility    != other.mCompatibility
3795     ||  mEnabled          != other.mEnabled
3796     ||  mReadOnly         != other.mReadOnly)
3797         return false;
3798     if (mRecurrence)
3799     {
3800         if (!other.mRecurrence
3801         ||  *mRecurrence     != *other.mRecurrence
3802         ||  mExcludeHolidays != other.mExcludeHolidays
3803         ||  mWorkTimeOnly    != other.mWorkTimeOnly
3804         ||  mRepetition      != mRepetition)
3805             return false;
3806     }
3807     else
3808     {
3809         if (other.mRecurrence
3810         ||  mRepeatAtLogin        != other.mRepeatAtLogin
3811         ||  mArchiveRepeatAtLogin != other.mArchiveRepeatAtLogin
3812         ||  (mRepeatAtLogin  &&  mAtLoginDateTime != other.mAtLoginDateTime))
3813             return false;
3814     }
3815     if (mDisplaying)
3816     {
3817         if (mDisplayingTime  !=  other.mDisplayingTime
3818         ||  mDisplayingFlags !=  other.mDisplayingFlags
3819         ||  mDisplayingDefer !=  other.mDisplayingDefer
3820         ||  mDisplayingEdit  !=  other.mDisplayingEdit)
3821             return false;
3822     }
3823     if (comparison & KAEvent::Compare::ICalendar)
3824     {
3825         if (mCreatedDateTime  != other.mCreatedDateTime
3826         ||  mCustomProperties != other.mCustomProperties
3827         ||  mRevision         != other.mRevision)
3828             return false;
3829     }
3830     if (comparison & KAEvent::Compare::UserSettable)
3831     {
3832         if (mResourceId != other.mResourceId)
3833             return false;
3834     }
3835     if (comparison & KAEvent::Compare::CurrentState)
3836     {
3837         if (mNextMainDateTime != other.mNextMainDateTime
3838         ||  mMainExpired      != other.mMainExpired
3839         ||  (mRepetition  &&  mNextRepeat != other.mNextRepeat))
3840             return false;
3841     }
3842     switch (mCategory)
3843     {
3844         case CalEvent::ACTIVE:
3845             if (mArchive != other.mArchive)
3846                 return false;
3847             break;
3848         case CalEvent::TEMPLATE:
3849             if (mTemplateAfterTime != other.mTemplateAfterTime)
3850                 return false;
3851             break;
3852         default:
3853             break;
3854     }
3855 
3856     switch (mActionSubType)
3857     {
3858         case KAEvent::SubAction::Command:
3859             if (mCommandScript != other.mCommandScript || mCommandXterm != other.mCommandXterm || mCommandDisplay != other.mCommandDisplay
3860             ||  mCommandError != other.mCommandError || mCommandHideError != other.mCommandHideError || mLogFile != other.mLogFile)
3861                 return false;
3862             if (!mCommandDisplay)
3863                 break;
3864             [[fallthrough]]; // fall through to Message
3865         case KAEvent::SubAction::File:
3866         case KAEvent::SubAction::Message:
3867             if (mReminderMinutes      != other.mReminderMinutes
3868             ||  mBgColour             != other.mBgColour
3869             ||  mFgColour             != other.mFgColour
3870             ||  mUseDefaultFont       != other.mUseDefaultFont
3871             ||  (!mUseDefaultFont  &&  mFont != other.mFont)
3872             ||  mLateCancel           != other.mLateCancel
3873             ||  (mLateCancel  &&  mAutoClose != other.mAutoClose)
3874             ||  mDeferDefaultMinutes  != other.mDeferDefaultMinutes
3875             ||  (mDeferDefaultMinutes  &&  mDeferDefaultDateOnly != other.mDeferDefaultDateOnly)
3876             ||  mPreAction            != other.mPreAction
3877             ||  mPostAction           != other.mPostAction
3878             ||  mExtraActionOptions   != other.mExtraActionOptions
3879             ||  mCommandError         != other.mCommandError
3880             ||  mConfirmAck           != other.mConfirmAck
3881             ||  mNotify               != other.mNotify
3882             ||  mEmailId              != other.mEmailId
3883             ||  mBeep                 != other.mBeep
3884             ||  mSpeak                != other.mSpeak
3885             ||  mAudioFile            != other.mAudioFile)
3886                 return false;
3887             if (mReminderMinutes)
3888             {
3889                 if (mReminderOnceOnly != other.mReminderOnceOnly)
3890                     return false;
3891                 if (comparison & KAEvent::Compare::CurrentState)
3892                 {
3893                     if (mReminderActive != other.mReminderActive
3894                     ||  (mReminderActive != ReminderType::None  &&  mReminderAfterTime != other.mReminderAfterTime))
3895                         return false;
3896                 }
3897             }
3898             if (comparison & KAEvent::Compare::CurrentState)
3899             {
3900                 if (mDeferral != other.mDeferral
3901                 ||  (mDeferral != DeferType::None  &&  mDeferralTime != other.mDeferralTime))
3902                     return false;
3903             }
3904             if (mAudioFile.isEmpty())
3905                 break;
3906             [[fallthrough]]; // fall through to Audio
3907         case KAEvent::SubAction::Audio:
3908             if (mRepeatSoundPause != other.mRepeatSoundPause)
3909                 return false;
3910             if (mSoundVolume >= 0)
3911             {
3912                 if (mSoundVolume != other.mSoundVolume)
3913                     return false;
3914                 if (mFadeVolume >= 0)
3915                 {
3916                     if (mFadeVolume  != other.mFadeVolume
3917                     ||  mFadeSeconds != other.mFadeSeconds)
3918                         return false;
3919                 }
3920                 else if (other.mFadeVolume >= 0)
3921                     return false;
3922             }
3923             else if (other.mSoundVolume >= 0)
3924                 return false;
3925             break;
3926         case KAEvent::SubAction::Email:
3927             if (mEmailFromIdentity != other.mEmailFromIdentity
3928             ||  mEmailAddresses    != other.mEmailAddresses
3929             ||  mEmailSubject      != other.mEmailSubject
3930             ||  mEmailAttachments  != other.mEmailAttachments
3931             ||  mEmailBcc          != other.mEmailBcc)
3932                 return false;
3933             break;
3934     }
3935     return true;
3936 }
3937 
3938 void KAEvent::startChanges()
3939 {
3940     d->startChanges();
3941 }
3942 
3943 /******************************************************************************
3944 * Indicate that changes to the instance are complete.
3945 * This allows trigger times to be recalculated if any changes have occurred.
3946 */
3947 void KAEvent::endChanges()
3948 {
3949     d->endChanges();
3950 }
3951 
3952 void KAEventPrivate::endChanges()
3953 {
3954     if (mChangeCount > 0)
3955         --mChangeCount;
3956 }
3957 
3958 /******************************************************************************
3959 * Return a list of pointers to KAEvent objects.
3960 */
3961 KAEvent::List KAEvent::ptrList(QList<KAEvent>& objList)
3962 {
3963     KAEvent::List ptrs;
3964     const int count = objList.count();
3965     ptrs.reserve(count);
3966     for (int i = 0; i < count; ++i)
3967         ptrs += &objList[i];
3968     return ptrs;
3969 }
3970 
3971 void KAEvent::dumpDebug() const
3972 {
3973 #ifndef KDE_NO_DEBUG_OUTPUT
3974     d->dumpDebug();
3975 #endif
3976 }
3977 
3978 #ifndef KDE_NO_DEBUG_OUTPUT
3979 void KAEventPrivate::dumpDebug() const
3980 {
3981     qCDebug(KALARMCAL_LOG) << "KAEvent dump:";
3982     qCDebug(KALARMCAL_LOG) << "-- mEventID:" << mEventID;
3983     qCDebug(KALARMCAL_LOG) << "-- mActionSubType:" << (mActionSubType == KAEvent::SubAction::Message ? "MESSAGE" : mActionSubType == KAEvent::SubAction::File ? "FILE" : mActionSubType == KAEvent::SubAction::Command ? "COMMAND" : mActionSubType == KAEvent::SubAction::Email ? "EMAIL" : mActionSubType == KAEvent::SubAction::Audio ? "AUDIO" : "??");
3984     qCDebug(KALARMCAL_LOG) << "-- mNextMainDateTime:" << mNextMainDateTime.toString();
3985     qCDebug(KALARMCAL_LOG) << "-- mCommandError:" << mCommandError;
3986     qCDebug(KALARMCAL_LOG) << "-- mAllTrigger:" << mAllTrigger.toString();
3987     qCDebug(KALARMCAL_LOG) << "-- mMainTrigger:" << mMainTrigger.toString();
3988     qCDebug(KALARMCAL_LOG) << "-- mAllWorkTrigger:" << mAllWorkTrigger.toString();
3989     qCDebug(KALARMCAL_LOG) << "-- mMainWorkTrigger:" << mMainWorkTrigger.toString();
3990     qCDebug(KALARMCAL_LOG) << "-- mCategory:" << mCategory;
3991     qCDebug(KALARMCAL_LOG) << "-- mName:" << mName;
3992     if (mCategory == CalEvent::TEMPLATE)
3993         qCDebug(KALARMCAL_LOG) << "-- mTemplateAfterTime:" << mTemplateAfterTime;
3994     qCDebug(KALARMCAL_LOG) << "-- mText:" << mText;
3995     if (mActionSubType == KAEvent::SubAction::Message
3996     ||  mActionSubType == KAEvent::SubAction::File
3997     ||  (mActionSubType == KAEvent::SubAction::Command && mCommandDisplay))
3998     {
3999         if (mCommandDisplay)
4000             qCDebug(KALARMCAL_LOG) << "-- mCommandScript:" << mCommandScript;
4001         qCDebug(KALARMCAL_LOG) << "-- mBgColour:" << mBgColour.name();
4002         qCDebug(KALARMCAL_LOG) << "-- mFgColour:" << mFgColour.name();
4003         qCDebug(KALARMCAL_LOG) << "-- mUseDefaultFont:" << mUseDefaultFont;
4004         if (!mUseDefaultFont)
4005             qCDebug(KALARMCAL_LOG) << "-- mFont:" << mFont.toString();
4006         qCDebug(KALARMCAL_LOG) << "-- mSpeak:" << mSpeak;
4007         qCDebug(KALARMCAL_LOG) << "-- mAudioFile:" << mAudioFile;
4008         qCDebug(KALARMCAL_LOG) << "-- mPreAction:" << mPreAction;
4009         qCDebug(KALARMCAL_LOG) << "-- mExecPreActOnDeferral:" << (mExtraActionOptions & KAEvent::ExecPreActOnDeferral);
4010         qCDebug(KALARMCAL_LOG) << "-- mCancelOnPreActErr:" << (mExtraActionOptions & KAEvent::CancelOnPreActError);
4011         qCDebug(KALARMCAL_LOG) << "-- mDontShowPreActErr:" << (mExtraActionOptions & KAEvent::DontShowPreActError);
4012         qCDebug(KALARMCAL_LOG) << "-- mPostAction:" << mPostAction;
4013         qCDebug(KALARMCAL_LOG) << "-- mLateCancel:" << mLateCancel;
4014         qCDebug(KALARMCAL_LOG) << "-- mAutoClose:" << mAutoClose;
4015         qCDebug(KALARMCAL_LOG) << "-- mNotify:" << mNotify;
4016     }
4017     else if (mActionSubType == KAEvent::SubAction::Command)
4018     {
4019         qCDebug(KALARMCAL_LOG) << "-- mCommandScript:" << mCommandScript;
4020         qCDebug(KALARMCAL_LOG) << "-- mCommandXterm:" << mCommandXterm;
4021         qCDebug(KALARMCAL_LOG) << "-- mCommandDisplay:" << mCommandDisplay;
4022         qCDebug(KALARMCAL_LOG) << "-- mCommandHideError:" << mCommandHideError;
4023         qCDebug(KALARMCAL_LOG) << "-- mLogFile:" << mLogFile;
4024     }
4025     else if (mActionSubType == KAEvent::SubAction::Email)
4026     {
4027         qCDebug(KALARMCAL_LOG) << "-- mEmail: FromKMail:" << mEmailFromIdentity;
4028         qCDebug(KALARMCAL_LOG) << "--         Addresses:" << mEmailAddresses.join(QStringLiteral(","));
4029         qCDebug(KALARMCAL_LOG) << "--         Subject:" << mEmailSubject;
4030         qCDebug(KALARMCAL_LOG) << "--         Attachments:" << mEmailAttachments.join(QLatin1Char(','));
4031         qCDebug(KALARMCAL_LOG) << "--         Bcc:" << mEmailBcc;
4032     }
4033     else if (mActionSubType == KAEvent::SubAction::Audio)
4034         qCDebug(KALARMCAL_LOG) << "-- mAudioFile:" << mAudioFile;
4035     qCDebug(KALARMCAL_LOG) << "-- mBeep:" << mBeep;
4036     if (mActionSubType == KAEvent::SubAction::Audio  ||  !mAudioFile.isEmpty())
4037     {
4038         if (mSoundVolume >= 0)
4039         {
4040             qCDebug(KALARMCAL_LOG) << "-- mSoundVolume:" << mSoundVolume;
4041             if (mFadeVolume >= 0)
4042             {
4043                 qCDebug(KALARMCAL_LOG) << "-- mFadeVolume:" << mFadeVolume;
4044                 qCDebug(KALARMCAL_LOG) << "-- mFadeSeconds:" << mFadeSeconds;
4045             }
4046             else
4047                 qCDebug(KALARMCAL_LOG) << "-- mFadeVolume:-:";
4048         }
4049         else
4050             qCDebug(KALARMCAL_LOG) << "-- mSoundVolume:-:";
4051         qCDebug(KALARMCAL_LOG) << "-- mRepeatSoundPause:" << mRepeatSoundPause;
4052     }
4053     qCDebug(KALARMCAL_LOG) << "-- mEmailId:" << mEmailId;
4054     qCDebug(KALARMCAL_LOG) << "-- mCopyToKOrganizer:" << mCopyToKOrganizer;
4055     qCDebug(KALARMCAL_LOG) << "-- mWakeFromSuspend:" << mWakeFromSuspend;
4056     qCDebug(KALARMCAL_LOG) << "-- mExcludeHolidays:" << mExcludeHolidays;
4057     qCDebug(KALARMCAL_LOG) << "-- mWorkTimeOnly:" << mWorkTimeOnly;
4058     qCDebug(KALARMCAL_LOG) << "-- mStartDateTime:" << mStartDateTime.toString();
4059 //     qCDebug(KALARMCAL_LOG) << "-- mCreatedDateTime:" << mCreatedDateTime;
4060     qCDebug(KALARMCAL_LOG) << "-- mRepeatAtLogin:" << mRepeatAtLogin;
4061 //     if (mRepeatAtLogin)
4062 //         qCDebug(KALARMCAL_LOG) << "-- mAtLoginDateTime:" << mAtLoginDateTime;
4063     qCDebug(KALARMCAL_LOG) << "-- mArchiveRepeatAtLogin:" << mArchiveRepeatAtLogin;
4064     qCDebug(KALARMCAL_LOG) << "-- mConfirmAck:" << mConfirmAck;
4065     qCDebug(KALARMCAL_LOG) << "-- mEnabled:" << mEnabled;
4066     qCDebug(KALARMCAL_LOG) << "-- mResourceId:" << mResourceId;
4067     qCDebug(KALARMCAL_LOG) << "-- mCompatibility:" << mCompatibility;
4068     qCDebug(KALARMCAL_LOG) << "-- mReadOnly:" << mReadOnly;
4069     if (mReminderMinutes)
4070     {
4071         qCDebug(KALARMCAL_LOG) << "-- mReminderMinutes:" << mReminderMinutes;
4072         qCDebug(KALARMCAL_LOG) << "-- mReminderActive:" << (mReminderActive == ReminderType::Active ? "active" : mReminderActive == ReminderType::Hidden ? "hidden" : "no");
4073         qCDebug(KALARMCAL_LOG) << "-- mReminderOnceOnly:" << mReminderOnceOnly;
4074     }
4075     if (mDeferral != DeferType::None)
4076     {
4077         qCDebug(KALARMCAL_LOG) << "-- mDeferral:" << (mDeferral == DeferType::Normal ? "normal" : "reminder");
4078         qCDebug(KALARMCAL_LOG) << "-- mDeferralTime:" << mDeferralTime.toString();
4079     }
4080     qCDebug(KALARMCAL_LOG) << "-- mDeferDefaultMinutes:" << mDeferDefaultMinutes;
4081     if (mDeferDefaultMinutes)
4082         qCDebug(KALARMCAL_LOG) << "-- mDeferDefaultDateOnly:" << mDeferDefaultDateOnly;
4083     if (mDisplaying)
4084     {
4085         qCDebug(KALARMCAL_LOG) << "-- mDisplayingTime:" << mDisplayingTime.toString();
4086         qCDebug(KALARMCAL_LOG) << "-- mDisplayingFlags:" << mDisplayingFlags;
4087         qCDebug(KALARMCAL_LOG) << "-- mDisplayingDefer:" << mDisplayingDefer;
4088         qCDebug(KALARMCAL_LOG) << "-- mDisplayingEdit:" << mDisplayingEdit;
4089     }
4090     qCDebug(KALARMCAL_LOG) << "-- mRevision:" << mRevision;
4091     qCDebug(KALARMCAL_LOG) << "-- mRecurrence:" << mRecurrence;
4092     if (!mRepetition)
4093         qCDebug(KALARMCAL_LOG) << "-- mRepetition: 0";
4094     else if (mRepetition.isDaily())
4095         qCDebug(KALARMCAL_LOG) << "-- mRepetition: count:" << mRepetition.count() << ", interval:" << mRepetition.intervalDays() << "days";
4096     else
4097         qCDebug(KALARMCAL_LOG) << "-- mRepetition: count:" << mRepetition.count() << ", interval:" << mRepetition.intervalMinutes() << "minutes";
4098     qCDebug(KALARMCAL_LOG) << "-- mNextRepeat:" << mNextRepeat;
4099     qCDebug(KALARMCAL_LOG) << "-- mAlarmCount:" << mAlarmCount;
4100     qCDebug(KALARMCAL_LOG) << "-- mMainExpired:" << mMainExpired;
4101     qCDebug(KALARMCAL_LOG) << "-- mDisplaying:" << mDisplaying;
4102     qCDebug(KALARMCAL_LOG) << "KAEvent dump end";
4103 }
4104 #endif
4105 
4106 /******************************************************************************
4107 * Fetch the start and next date/time for a KCalendarCore::Event.
4108 * Reply = next main date/time.
4109 */
4110 DateTime KAEventPrivate::readDateTime(const Event::Ptr& event, bool localZone, bool dateOnly, DateTime& start)
4111 {
4112     start = DateTime(event->dtStart());
4113     if (dateOnly)
4114     {
4115         // A date-only event is indicated by the X-KDE-KALARM-FLAGS:DATE property, not
4116         // by a date-only start date/time (for the reasons given in updateKCalEvent()).
4117         start.setDateOnly(true);
4118     }
4119     if (localZone)
4120     {
4121         // The local system time zone is indicated by the X-KDE-KALARM-FLAGS:LOCAL
4122         // property, because QDateTime values with time spec Qt::LocalTime are not
4123         // stored correctly in the calendar file.
4124         start.setTimeSpec(KADateTime::LocalZone);
4125     }
4126     DateTime next = start;
4127     const int SZ_YEAR  = 4;                           // number of digits in year value
4128     const int SZ_MONTH = 2;                           // number of digits in month value
4129     const int SZ_DAY   = 2;                           // number of digits in day value
4130     const int SZ_DATE  = SZ_YEAR + SZ_MONTH + SZ_DAY; // total size of date value
4131     const int IX_TIME  = SZ_DATE + 1;                 // offset to time value
4132     const int SZ_HOUR  = 2;                           // number of digits in hour value
4133     const int SZ_MIN   = 2;                           // number of digits in minute value
4134     const int SZ_SEC   = 2;                           // number of digits in second value
4135     const int SZ_TIME  = SZ_HOUR + SZ_MIN + SZ_SEC;   // total size of time value
4136     const QString prop = event->customProperty(KACalendar::APPNAME, KAEventPrivate::NEXT_RECUR_PROPERTY);
4137     if (prop.length() >= SZ_DATE)
4138     {
4139         // The next due recurrence time is specified
4140         const QDate d(QStringView(prop).left(SZ_YEAR).toInt(),
4141                       QStringView(prop).mid(SZ_YEAR, SZ_MONTH).toInt(),
4142                       QStringView(prop).mid(SZ_YEAR + SZ_MONTH, SZ_DAY).toInt());
4143         if (d.isValid())
4144         {
4145             if (dateOnly  &&  prop.length() == SZ_DATE)
4146                 next.setDate(d);
4147             else if (!dateOnly  &&  prop.length() == IX_TIME + SZ_TIME  &&  prop[SZ_DATE] == QLatin1Char('T'))
4148             {
4149                 const QTime t(QStringView(prop).mid(IX_TIME, SZ_HOUR).toInt(),
4150                               QStringView(prop).mid(IX_TIME + SZ_HOUR, SZ_MIN).toInt(),
4151                               QStringView(prop).mid(IX_TIME + SZ_HOUR + SZ_MIN, SZ_SEC).toInt());
4152                 if (t.isValid())
4153                 {
4154                     next.setDate(d);
4155                     next.setTime(t);
4156                 }
4157             }
4158             if (next < start)
4159                 next = start;    // ensure next recurrence time is valid
4160         }
4161     }
4162     return next;
4163 }
4164 
4165 /******************************************************************************
4166 * Parse the alarms for a KCalendarCore::Event.
4167 * Reply = map of alarm data, indexed by KAAlarm::Type
4168 */
4169 void KAEventPrivate::readAlarms(const Event::Ptr& event, AlarmMap* alarmMap, bool cmdDisplay)
4170 {
4171     const Alarm::List alarms = event->alarms();
4172 
4173     // Check if it's an audio event with no display alarm
4174     bool audioOnly = false;
4175     for (const Alarm::Ptr& alarm : alarms)
4176     {
4177         bool done = false;
4178         switch (alarm->type())
4179         {
4180             case Alarm::Display:
4181             case Alarm::Procedure:
4182                 audioOnly = false;
4183                 done = true;   // exit from the 'for' loop
4184                 break;
4185             case Alarm::Audio:
4186                 audioOnly = true;
4187                 break;
4188             default:
4189                 break;
4190         }
4191         if (done)
4192             break;
4193     }
4194 
4195     for (const Alarm::Ptr& alarm : alarms)
4196     {
4197         // Parse the next alarm's text
4198         AlarmData data;
4199         readAlarm(alarm, data, audioOnly, cmdDisplay);
4200         if (data.type != INVALID_ALARM)
4201             alarmMap->insert(data.type, data);
4202     }
4203 }
4204 
4205 /******************************************************************************
4206 * Parse a KCalendarCore::Alarm.
4207 * If 'audioMain' is true, the event contains an audio alarm but no display alarm.
4208 * Reply = alarm ID (sequence number)
4209 */
4210 void KAEventPrivate::readAlarm(const Alarm::Ptr& alarm, AlarmData& data, bool audioMain, bool cmdDisplay)
4211 {
4212     // Parse the next alarm's text
4213     data.alarm            = alarm;
4214     data.displayingFlags  = 0;
4215     data.isEmailText      = false;
4216     data.speak            = false;
4217     data.hiddenReminder   = false;
4218     data.timedDeferral    = false;
4219     data.nextRepeat       = 0;
4220     data.repeatSoundPause = -1;
4221     if (alarm->repeatCount())
4222     {
4223         bool ok;
4224         const QString property = alarm->customProperty(KACalendar::APPNAME, KAEventPrivate::NEXT_REPEAT_PROPERTY);
4225         int n = static_cast<int>(property.toUInt(&ok));
4226         if (ok)
4227             data.nextRepeat = n;
4228     }
4229     QString property = alarm->customProperty(KACalendar::APPNAME, KAEventPrivate::FLAGS_PROPERTY);
4230     const QStringList flags = property.split(KAEventPrivate::SC, Qt::SkipEmptyParts);
4231     switch (alarm->type())
4232     {
4233         case Alarm::Procedure:
4234             data.action        = KAAlarm::Action::Command;
4235             data.cleanText     = alarm->programFile();
4236             data.commandScript = data.cleanText.isEmpty();   // blank command indicates a script
4237             if (!alarm->programArguments().isEmpty())
4238             {
4239                 if (!data.commandScript)
4240                     data.cleanText += QLatin1Char(' ');
4241                 data.cleanText += alarm->programArguments();
4242             }
4243             data.extraActionOptions = {};
4244             if (flags.contains(KAEventPrivate::EXEC_ON_DEFERRAL_FLAG))
4245                 data.extraActionOptions |= KAEvent::ExecPreActOnDeferral;
4246             if (flags.contains(KAEventPrivate::CANCEL_ON_ERROR_FLAG))
4247                 data.extraActionOptions |= KAEvent::CancelOnPreActError;
4248             if (flags.contains(KAEventPrivate::DONT_SHOW_ERROR_FLAG))
4249                 data.extraActionOptions |= KAEvent::DontShowPreActError;
4250             if (!cmdDisplay)
4251                 break;
4252             [[fallthrough]]; // fall through to Display
4253         case Alarm::Display:
4254         {
4255             if (alarm->type() == Alarm::Display)
4256             {
4257                 data.action    = KAAlarm::Action::Message;
4258                 data.cleanText = AlarmText::fromCalendarText(alarm->text(), data.isEmailText);
4259             }
4260             const QString prop = alarm->customProperty(KACalendar::APPNAME, KAEventPrivate::FONT_COLOUR_PROPERTY);
4261             const QStringList list = prop.split(QLatin1Char(';'), Qt::KeepEmptyParts);
4262             data.bgColour = QColor(255, 255, 255);   // white
4263             data.fgColour = QColor(0, 0, 0);         // black
4264             const int n = list.count();
4265             if (n > 0)
4266             {
4267                 if (!list[0].isEmpty())
4268                 {
4269                     QColor c(list[0]);
4270                     if (c.isValid())
4271                         data.bgColour = c;
4272                 }
4273                 if (n > 1  &&  !list[1].isEmpty())
4274                 {
4275                     QColor c(list[1]);
4276                     if (c.isValid())
4277                         data.fgColour = c;
4278                 }
4279             }
4280             data.defaultFont = (n <= 2 || list[2].isEmpty());
4281             if (!data.defaultFont)
4282                 data.font.fromString(list[2]);
4283             break;
4284         }
4285         case Alarm::Email:
4286         {
4287             data.action    = KAAlarm::Action::Email;
4288             data.cleanText = alarm->mailText();
4289             const int i = flags.indexOf(KAEventPrivate::EMAIL_ID_FLAG);
4290             data.emailFromId = (i >= 0  &&  i + 1 < flags.count()) ? flags[i + 1].toUInt() : 0;
4291             break;
4292         }
4293         case Alarm::Audio:
4294         {
4295             data.action      = KAAlarm::Action::Audio;
4296             data.cleanText   = alarm->audioFile();
4297             data.repeatSoundPause = (alarm->repeatCount() == -2) ? alarm->snoozeTime().asSeconds()
4298                                   : (alarm->repeatCount() == -1) ? 0 : -1;
4299             data.soundVolume = -1;
4300             data.fadeVolume  = -1;
4301             data.fadeSeconds = 0;
4302             QString prop = alarm->customProperty(KACalendar::APPNAME, KAEventPrivate::VOLUME_PROPERTY);
4303             if (!prop.isEmpty())
4304             {
4305                 bool ok;
4306                 const QStringList list = prop.split(QLatin1Char(';'), Qt::KeepEmptyParts);
4307                 data.soundVolume = list[0].toFloat(&ok);
4308                 if (!ok  ||  data.soundVolume > 1.0f)
4309                     data.soundVolume = -1;
4310                 if (data.soundVolume >= 0  &&  list.count() >= 3)
4311                 {
4312                     float fadeVolume = list[1].toFloat(&ok);
4313                     int   fadeSecs = 0;
4314                     if (ok)
4315                         fadeSecs = static_cast<int>(list[2].toUInt(&ok));
4316                     if (ok  &&  fadeVolume >= 0  &&  fadeVolume <= 1.0f  &&  fadeSecs > 0)
4317                     {
4318                         data.fadeVolume  = fadeVolume;
4319                         data.fadeSeconds = fadeSecs;
4320                     }
4321                 }
4322             }
4323             if (!audioMain)
4324             {
4325                 data.type  = AUDIO_ALARM;
4326                 data.speak = flags.contains(KAEventPrivate::SPEAK_FLAG);
4327                 return;
4328             }
4329             break;
4330         }
4331         case Alarm::Invalid:
4332             data.type = INVALID_ALARM;
4333             return;
4334     }
4335 
4336     bool atLogin      = false;
4337     bool reminder     = false;
4338     bool deferral     = false;
4339     bool dateDeferral = false;
4340     bool repeatSound  = false;
4341     data.type = MAIN_ALARM;
4342     property = alarm->customProperty(KACalendar::APPNAME, KAEventPrivate::TYPE_PROPERTY);
4343     const QStringList types = property.split(QLatin1Char(','), Qt::SkipEmptyParts);
4344     for (int i = 0, end = types.count();  i < end;  ++i)
4345     {
4346         const QString type = types[i];
4347         if (type == KAEventPrivate::AT_LOGIN_TYPE)
4348             atLogin = true;
4349         else if (type == KAEventPrivate::FILE_TYPE  &&  data.action == KAAlarm::Action::Message)
4350             data.action = KAAlarm::Action::File;
4351         else if (type == KAEventPrivate::REMINDER_TYPE)
4352             reminder = true;
4353         else if (type == KAEventPrivate::TIME_DEFERRAL_TYPE)
4354             deferral = true;
4355         else if (type == KAEventPrivate::DATE_DEFERRAL_TYPE)
4356             dateDeferral = deferral = true;
4357         else if (type == KAEventPrivate::DISPLAYING_TYPE)
4358             data.type = DISPLAYING_ALARM;
4359         else if (type == KAEventPrivate::PRE_ACTION_TYPE  &&  data.action == KAAlarm::Action::Command)
4360             data.type = PRE_ACTION_ALARM;
4361         else if (type == KAEventPrivate::POST_ACTION_TYPE  &&  data.action == KAAlarm::Action::Command)
4362             data.type = POST_ACTION_ALARM;
4363         else if (type == KAEventPrivate::SOUND_REPEAT_TYPE  &&  data.action == KAAlarm::Action::Audio)
4364         {
4365             repeatSound = true;
4366             if (i + 1 < end)
4367             {
4368                 bool ok;
4369                 uint n = types[i + 1].toUInt(&ok);
4370                 if (ok)
4371                 {
4372                     data.repeatSoundPause = n;
4373                     ++i;
4374                 }
4375             }
4376         }
4377     }
4378     if (repeatSound && data.repeatSoundPause < 0)
4379         data.repeatSoundPause = 0;
4380     else if (!repeatSound)
4381         data.repeatSoundPause = -1;
4382 
4383     if (reminder)
4384     {
4385         if (data.type == MAIN_ALARM)
4386         {
4387             data.type = deferral ? DEFERRED_REMINDER_ALARM : REMINDER_ALARM;
4388             data.timedDeferral = (deferral && !dateDeferral);
4389             if (data.type == REMINDER_ALARM
4390             &&  flags.contains(KAEventPrivate::HIDDEN_REMINDER_FLAG))
4391                 data.hiddenReminder = true;
4392         }
4393         else if (data.type == DISPLAYING_ALARM)
4394             data.displayingFlags = dateDeferral ? REMINDER | DATE_DEFERRAL
4395                                      : deferral ? REMINDER | TIME_DEFERRAL : REMINDER;
4396     }
4397     else if (deferral)
4398     {
4399         if (data.type == MAIN_ALARM)
4400         {
4401             data.type = DEFERRED_ALARM;
4402             data.timedDeferral = !dateDeferral;
4403         }
4404         else if (data.type == DISPLAYING_ALARM)
4405             data.displayingFlags = dateDeferral ? DATE_DEFERRAL : TIME_DEFERRAL;
4406     }
4407     if (atLogin)
4408     {
4409         if (data.type == MAIN_ALARM)
4410             data.type = AT_LOGIN_ALARM;
4411         else if (data.type == DISPLAYING_ALARM)
4412             data.displayingFlags = KAEvent::REPEAT_AT_LOGIN;
4413     }
4414 //qCDebug(KALARMCAL_LOG)<<"text="<<alarm->text()<<", time="<<alarm->time().toString()<<", valid time="<<alarm->time().isValid();
4415 }
4416 
4417 inline void KAEventPrivate::set_deferral(DeferType type)
4418 {
4419     if (type != DeferType::None)
4420     {
4421         if (mDeferral == DeferType::None)
4422             ++mAlarmCount;
4423     }
4424     else
4425     {
4426         if (mDeferral != DeferType::None)
4427             --mAlarmCount;
4428     }
4429     mDeferral = type;
4430 }
4431 
4432 /******************************************************************************
4433 * Calculate the next trigger times of the alarm.
4434 * This should only be called when changes have actually occurred which might
4435 * affect the event's trigger times.
4436 * mMainTrigger is set to the next scheduled recurrence/sub-repetition
4437 * mAllTrigger is the same as mMainTrigger, but takes account of reminders and
4438 *             deferred reminders.
4439 * mMainWorkTrigger is set to the next scheduled recurrence/sub-repetition
4440 *                  which occurs in working hours, if working-time-only is set.
4441 * mAllWorkTrigger is the same as mMainWorkTrigger, but takes account of reminders.
4442 */
4443 void KAEventPrivate::calcTriggerTimes() const
4444 {
4445     if (mChangeCount)
4446         return;
4447 #pragma message("May need to set date-only alarms to after start-of-day time in working-time checks")
4448     bool recurs = (checkRecur() != KARecurrence::NO_RECUR);
4449     if ((recurs  &&  mWorkTimeOnly  &&  mWorkTimeOnly != mWorkTimeIndex)
4450     ||  (recurs  &&  mExcludeHolidays  &&  mExcludeHolidayRegion != mHolidays->regionCode())
4451     ||  (mStartDateTime.isDateOnly()  &&  mTriggerStartOfDay != DateTime::startOfDay()))
4452     {
4453         // It's a work time alarm, and work days/times have changed, or
4454         // it excludes holidays, and the holidays definition has changed.
4455         mTriggerChanged = true;
4456     }
4457     else if (!mTriggerChanged)
4458         return;
4459 
4460     mTriggerChanged = false;
4461     mTriggerStartOfDay = DateTime::startOfDay();   // note start of day time used in calculation
4462     if (recurs  &&  mWorkTimeOnly)
4463         mWorkTimeOnly = mWorkTimeIndex;    // note which work time definition was used in calculation
4464     if (recurs  &&  mExcludeHolidays)
4465         mExcludeHolidayRegion = mHolidays->regionCode();  // note which holiday definition was used in calculation
4466     bool excludeHolidays = mExcludeHolidays && !mExcludeHolidayRegion.isEmpty();
4467 
4468     mMainTrigger = mainDateTime(true);   // next recurrence or sub-repetition
4469     mAllTrigger = (mDeferral == DeferType::Reminder)        ? mDeferralTime
4470                 : (mReminderActive != ReminderType::Active) ? mMainTrigger
4471                 : (mReminderMinutes < 0)                    ? mReminderAfterTime
4472                 :                                             mMainTrigger.addMins(-mReminderMinutes);
4473     // If only-during-working-time is set and it recurs, it won't actually trigger
4474     // unless it falls during working hours.
4475     if ((!mWorkTimeOnly && !excludeHolidays)
4476     ||  !recurs
4477     ||  !excludedByWorkTimeOrHoliday(mMainTrigger.kDateTime()))
4478     {
4479         // It only occurs once, or it complies with any working hours/holiday
4480         // restrictions.
4481         mMainWorkTrigger = mMainTrigger;
4482         mAllWorkTrigger  = mAllTrigger;
4483     }
4484     else if (mWorkTimeOnly)
4485     {
4486         // The alarm is restricted to working hours.
4487         // Finding the next occurrence during working hours can sometimes take a long time,
4488         // so mark the next actual trigger as invalid until the calculation completes.
4489         // Note that reminders are only triggered if the main alarm is during working time.
4490         if (!excludeHolidays)
4491         {
4492             // There are no holiday restrictions.
4493             calcNextWorkingTime(mMainTrigger);
4494         }
4495         else if (mHolidays->isValid())
4496         {
4497             // Holidays are excluded.
4498             DateTime nextTrigger = mMainTrigger;
4499             KADateTime kdt;
4500             for (int i = 0;  i < 20;  ++i)
4501             {
4502                 calcNextWorkingTime(nextTrigger);
4503                 if (!mHolidays->isHoliday(mMainWorkTrigger.date()))
4504                     return;    // found a non-holiday occurrence
4505                 kdt = mMainWorkTrigger.effectiveKDateTime();
4506                 kdt.setTime(QTime(23, 59, 59));
4507                 const KAEvent::OccurType type = nextOccurrence(kdt, nextTrigger, KAEvent::Repeats::Return);
4508                 if (!nextTrigger.isValid())
4509                     break;
4510                 if (!excludedByWorkTimeOrHoliday(nextTrigger.kDateTime()))
4511                 {
4512                     const int reminder = (mReminderMinutes > 0) ? mReminderMinutes : 0;   // only interested in reminders BEFORE the alarm
4513                     mMainWorkTrigger = nextTrigger;
4514                     mAllWorkTrigger = (type & KAEvent::OccurType::Repeat) ? mMainWorkTrigger : mMainWorkTrigger.addMins(-reminder);
4515                     return;   // found a non-holiday occurrence
4516                 }
4517             }
4518             mMainWorkTrigger = mAllWorkTrigger = DateTime();
4519         }
4520     }
4521     else if (excludeHolidays  &&  mHolidays->isValid())
4522     {
4523         // Holidays are excluded.
4524         DateTime nextTrigger = mMainTrigger;
4525         KADateTime kdt;
4526         for (int i = 0;  i < 20;  ++i)
4527         {
4528             kdt = nextTrigger.effectiveKDateTime();
4529             kdt.setTime(QTime(23, 59, 59));
4530             const KAEvent::OccurType type = nextOccurrence(kdt, nextTrigger, KAEvent::Repeats::Return);
4531             if (!nextTrigger.isValid())
4532                 break;
4533             if (!mHolidays->isHoliday(nextTrigger.date()))
4534             {
4535                 const int reminder = (mReminderMinutes > 0) ? mReminderMinutes : 0;   // only interested in reminders BEFORE the alarm
4536                 mMainWorkTrigger = nextTrigger;
4537                 mAllWorkTrigger = (type & KAEvent::OccurType::Repeat) ? mMainWorkTrigger : mMainWorkTrigger.addMins(-reminder);
4538                 return;   // found a non-holiday occurrence
4539             }
4540         }
4541         mMainWorkTrigger = mAllWorkTrigger = DateTime();
4542     }
4543 }
4544 
4545 /******************************************************************************
4546 * Return the time of the next scheduled occurrence of the event during working
4547 * hours, for an alarm which is restricted to working hours.
4548 * On entry, 'nextTrigger' = the next recurrence or repetition (as returned by
4549 * mainDateTime(true) ).
4550 */
4551 void KAEventPrivate::calcNextWorkingTime(const DateTime& nextTrigger) const
4552 {
4553     qCDebug(KALARMCAL_LOG) << "next=" << nextTrigger.kDateTime().toString(QStringLiteral("%Y-%m-%d %H:%M"));
4554 
4555     // Convert next trigger time to working day time spec to enable working time evaluation.
4556     DateTime nextTriggerWT = nextTrigger.toTimeSpec(mWorkDayTimeSpec);
4557 
4558     mMainWorkTrigger = mAllWorkTrigger = DateTime();
4559 
4560     if (!mWorkDays.count(true))
4561         return;    // no working days are defined
4562     const KARecurrence::Type recurType = checkRecur();
4563     KADateTime kdt = nextTriggerWT.effectiveKDateTime();
4564     const int reminder = (mReminderMinutes > 0) ? mReminderMinutes : 0;   // only interested in reminders BEFORE the alarm
4565     // Check if it always falls on the same day(s) of the week.
4566     const RecurrenceRule* rrule = mRecurrence->defaultRRuleConst();
4567     if (!rrule)
4568         return;    // no recurrence rule!
4569     unsigned allDaysMask = 0x7F;  // mask bits for all days of week
4570     const QList<RecurrenceRule::WDayPos> pos = rrule->byDays();
4571     const int nDayPos = pos.count();  // number of day positions
4572     if (nDayPos)
4573     {
4574         bool noWorkPos = true;  // true if no recurrence day position is working day
4575         allDaysMask = 0;
4576         for (const RecurrenceRule::WDayPos& p : pos)
4577         {
4578             const int day = p.day() - 1;  // Monday = 0
4579             if (mWorkDays.testBit(day))
4580                 noWorkPos = false;    // found a working day occurrence
4581             allDaysMask |= 1 << day;
4582         }
4583         // Check whether it ever occurs on a working day.
4584         // Note that this check is only guaranteed to work if the alarm's time
4585         // spec is the same as the working day time spec, since otherwise there
4586         // is a possibility that trigger dates could change when trigger times
4587         // are converted to the working day time spec.
4588         if (noWorkPos  &&  !mRepetition  &&  nextTrigger.timeSpec() == mWorkDayTimeSpec)
4589             return;    // never occurs on a working day
4590     }
4591     DateTime newdt;
4592 
4593     if (mStartDateTime.isDateOnly())
4594     {
4595         // It's a date-only alarm.
4596         // Sub-repetitions also have to be date-only.
4597         const int repeatFreq = mRepetition.intervalDays();
4598         const bool weeklyRepeat = mRepetition && !(repeatFreq % 7);
4599         const Duration interval = mRecurrence->regularInterval();
4600         if ((!interval.isNull()  &&  !(interval.asDays() % 7))
4601         ||  nDayPos == 1)
4602         {
4603             // It recurs on the same day each week
4604             if (!mRepetition || weeklyRepeat)
4605                 return;    // any repetitions are also weekly
4606 
4607             // It's a weekly recurrence with a non-weekly sub-repetition.
4608             // Check one cycle of repetitions for the next one that lands
4609             // on a working day.
4610             KADateTime dt(nextTriggerWT.kDateTime().addDays(1));
4611             dt.setTime(QTime(0, 0, 0));
4612             previousOccurrence(dt, newdt, false);
4613             if (!newdt.isValid())
4614                 return;    // this should never happen
4615             kdt = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
4616             const int day = kdt.date().dayOfWeek() - 1;   // Monday = 0
4617             for (int repeatNum = mNextRepeat + 1;  ;  ++repeatNum)
4618             {
4619                 if (repeatNum > mRepetition.count())
4620                     repeatNum = 0;
4621                 if (repeatNum == mNextRepeat)
4622                     break;
4623                 if (!repeatNum)
4624                 {
4625                     nextOccurrence(newdt.kDateTime(), newdt, KAEvent::Repeats::Ignore);
4626                     if (mWorkDays.testBit(day))
4627                     {
4628                         newdt = newdt.toTimeSpec(nextTrigger.timeSpec());
4629                         mMainWorkTrigger = newdt;
4630                         mAllWorkTrigger  = mMainWorkTrigger.addMins(-reminder);
4631                         return;
4632                     }
4633                     kdt = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
4634                 }
4635                 else
4636                 {
4637                     const int inc = repeatFreq * repeatNum;
4638                     if (mWorkDays.testBit((day + inc) % 7))
4639                     {
4640                         kdt = kdt.addDays(inc);
4641                         kdt.setDateOnly(true);
4642                         kdt = kdt.toTimeSpec(nextTrigger.timeSpec());
4643                         mMainWorkTrigger = mAllWorkTrigger = kdt;
4644                         return;
4645                     }
4646                 }
4647             }
4648             return;
4649         }
4650         if (!mRepetition  ||  weeklyRepeat)
4651         {
4652             // It's a date-only alarm with either no sub-repetition or a
4653             // sub-repetition which always falls on the same day of the week
4654             // as the recurrence (if any).
4655             unsigned days = 0;
4656             for (; ;)
4657             {
4658                 kdt.setTime(QTime(23, 59, 59));
4659                 nextOccurrence(kdt, newdt, KAEvent::Repeats::Ignore);
4660                 if (!newdt.isValid())
4661                     return;
4662                 kdt = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
4663                 const int day = kdt.date().dayOfWeek() - 1;
4664                 if (mWorkDays.testBit(day))
4665                     break;    // found a working day occurrence
4666                 // Prevent indefinite looping (which should never happen anyway)
4667                 if ((days & allDaysMask) == allDaysMask)
4668                     return;    // found a recurrence on every possible day of the week!?!
4669                 days |= 1 << day;
4670             }
4671             kdt.setDateOnly(true);
4672             kdt = kdt.toTimeSpec(nextTrigger.timeSpec());
4673             mMainWorkTrigger = kdt;
4674             mAllWorkTrigger  = kdt.addSecs(-60 * reminder);
4675             return;
4676         }
4677 
4678         // It's a date-only alarm which recurs on different days of the week,
4679         // as does the sub-repetition.
4680         // Find the previous recurrence (as opposed to sub-repetition)
4681         unsigned days = 1 << (kdt.date().dayOfWeek() - 1);
4682         KADateTime dt(nextTriggerWT.kDateTime().addDays(1));
4683         dt.setTime(QTime(0, 0, 0));
4684         previousOccurrence(dt, newdt, false);
4685         if (!newdt.isValid())
4686             return;    // this should never happen
4687         kdt = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
4688         int day = kdt.date().dayOfWeek() - 1;   // Monday = 0
4689         for (int repeatNum = mNextRepeat;  ;  repeatNum = 0)
4690         {
4691             while (++repeatNum <= mRepetition.count())
4692             {
4693                 const int inc = repeatFreq * repeatNum;
4694                 if (mWorkDays.testBit((day + inc) % 7))
4695                 {
4696                     kdt = kdt.addDays(inc);
4697                     kdt.setDateOnly(true);
4698                     kdt = kdt.toTimeSpec(nextTrigger.timeSpec());
4699                     mMainWorkTrigger = mAllWorkTrigger = kdt;
4700                     return;
4701                 }
4702                 if ((days & allDaysMask) == allDaysMask)
4703                     return;    // found an occurrence on every possible day of the week!?!
4704                 days |= 1 << day;
4705             }
4706             nextOccurrence(kdt, newdt, KAEvent::Repeats::Ignore);
4707             if (!newdt.isValid())
4708                 return;
4709             kdt = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
4710             day = kdt.date().dayOfWeek() - 1;
4711             if (mWorkDays.testBit(day))
4712             {
4713                 kdt.setDateOnly(true);
4714                 kdt = kdt.toTimeSpec(nextTrigger.timeSpec());
4715                 mMainWorkTrigger = kdt;
4716                 mAllWorkTrigger  = kdt.addSecs(-60 * reminder);
4717                 return;
4718             }
4719             if ((days & allDaysMask) == allDaysMask)
4720                 return;    // found an occurrence on every possible day of the week!?!
4721             days |= 1 << day;
4722         }
4723         return;
4724     }
4725 
4726     // It's a date-time alarm.
4727 
4728     /* Check whether the recurrence or sub-repetition occurs at the same time
4729      * every day. Note that because of seasonal time changes, a recurrence
4730      * defined in terms of minutes will vary its time of day even if its value
4731      * is a multiple of a day (24*60 minutes). Sub-repetitions are considered
4732      * to repeat at the same time of day regardless of time changes if they
4733      * are multiples of a day, which doesn't strictly conform to the iCalendar
4734      * format because this only allows their interval to be recorded in seconds.
4735      */
4736     const bool recurTimeVaries = (recurType == KARecurrence::MINUTELY);
4737     const bool repeatTimeVaries = (mRepetition  &&  !mRepetition.isDaily());
4738 
4739     if (!recurTimeVaries  &&  !repeatTimeVaries)
4740     {
4741         // The alarm always occurs at the same time of day.
4742         // Check whether it can ever occur during working hours.
4743         if (!mayOccurDailyDuringWork(kdt))
4744             return;    // never occurs during working hours
4745 
4746         // Find the next working day it occurs on
4747         bool repetition = false;
4748         unsigned days = 0;
4749         for (; ;)
4750         {
4751             const KAEvent::OccurType type = nextOccurrence(kdt, newdt, KAEvent::Repeats::Return);
4752             if (!newdt.isValid())
4753                 return;
4754             repetition = (type & KAEvent::OccurType::Repeat);
4755             kdt = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
4756             const int day = kdt.date().dayOfWeek() - 1;
4757             if (mWorkDays.testBit(day))
4758                 break;    // found a working day occurrence
4759             // Prevent indefinite looping (which should never happen anyway)
4760             if (!repetition)
4761             {
4762                 if ((days & allDaysMask) == allDaysMask)
4763                     return;    // found a recurrence on every possible day of the week!?!
4764                 days |= 1 << day;
4765             }
4766         }
4767         mMainWorkTrigger = nextTriggerWT;
4768         mMainWorkTrigger.setDate(kdt.date());
4769         mMainWorkTrigger = mMainWorkTrigger.toTimeSpec(nextTrigger.timeSpec());
4770         mAllWorkTrigger = repetition ? mMainWorkTrigger : mMainWorkTrigger.addMins(-reminder);
4771         return;
4772     }
4773 
4774     // The alarm occurs at different times of day.
4775     // We may need to check for a full annual cycle of seasonal time changes, in
4776     // case it only occurs during working hours after a time change.
4777     const QTimeZone tz = kdt.timeZone();
4778     // Get time zone transitions for the next 10 years.
4779     const QDateTime endTransitionsTime = QDateTime::currentDateTimeUtc().addYears(10);
4780     const QTimeZone::OffsetDataList tzTransitions = tz.transitions(mStartDateTime.qDateTime(), endTransitionsTime);
4781 
4782     if (recurTimeVaries)
4783     {
4784         /* The alarm recurs at regular clock intervals, at different times of day.
4785          * Note that for this type of recurrence, it's necessary to avoid the
4786          * performance overhead of Recurrence class calls since these can in the
4787          * worst case cause the program to hang for a significant length of time.
4788          * In this case, we can calculate the next recurrence by simply adding the
4789          * recurrence interval, since KAlarm offers no facility to regularly miss
4790          * recurrences. (But exception dates/times need to be taken into account.)
4791          */
4792         KADateTime kdtRecur;
4793         int repeatFreq = 0;
4794         int repeatNum = 0;
4795         if (mRepetition)
4796         {
4797             // It's a repetition inside a recurrence, each of which occurs
4798             // at different times of day (bearing in mind that the repetition
4799             // may occur at daily intervals after each recurrence).
4800             // Find the previous recurrence (as opposed to sub-repetition)
4801             repeatFreq = mRepetition.intervalSeconds();
4802             previousOccurrence(kdt.addSecs(1), newdt, false);
4803             if (!newdt.isValid())
4804                 return;    // this should never happen
4805             kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
4806             repeatNum = kdtRecur.secsTo(kdt) / repeatFreq;
4807             kdt = kdtRecur.addSecs(repeatNum * repeatFreq);
4808         }
4809         else
4810         {
4811             // There is no sub-repetition.
4812             // (N.B. Sub-repetitions can't exist without a recurrence.)
4813             // Check until the original time wraps round, but ensure that
4814             // if there are seasonal time changes, that all other subsequent
4815             // time offsets within the next year are checked.
4816             // This does not guarantee to find the next working time,
4817             // particularly if there are exceptions, but it's a
4818             // reasonable try.
4819             kdtRecur = kdt;
4820         }
4821         QTime firstTime = kdtRecur.time();
4822         int firstOffset = kdtRecur.utcOffset();
4823         int currentOffset = firstOffset;
4824         int dayRecur = kdtRecur.date().dayOfWeek() - 1;   // Monday = 0
4825         int firstDay = dayRecur;
4826         QDate finalDate;
4827         const bool subdaily = (repeatFreq < 24 * 3600);
4828 //        int period = mRecurrence->frequency() % (24*60);  // it is by definition a MINUTELY recurrence
4829 //        int limit = (24*60 + period - 1) / period;  // number of times until recurrence wraps round
4830         int transitionIndex = -1;
4831         for (int n = 0;  n < 7 * 24 * 60;  ++n)
4832         {
4833             if (mRepetition)
4834             {
4835                 // Check the sub-repetitions for this recurrence
4836                 for (; ;)
4837                 {
4838                     // Find the repeat count to the next start of the working day
4839                     const int inc = subdaily ? nextWorkRepetition(kdt) : 1;
4840                     repeatNum += inc;
4841                     if (repeatNum > mRepetition.count())
4842                         break;
4843                     kdt = kdt.addSecs(inc * repeatFreq);
4844                     const QTime t = kdt.time();
4845                     if (t >= mWorkDayStart  &&  t < mWorkDayEnd)
4846                     {
4847                         if (mWorkDays.testBit(kdt.date().dayOfWeek() - 1))
4848                         {
4849                             kdt = kdt.toTimeSpec(nextTrigger.timeSpec());
4850                             mMainWorkTrigger = mAllWorkTrigger = kdt;
4851                             return;
4852                         }
4853                     }
4854                 }
4855                 repeatNum = 0;
4856             }
4857             nextOccurrence(kdtRecur, newdt, KAEvent::Repeats::Ignore);
4858             if (!newdt.isValid())
4859                 return;
4860             kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
4861             dayRecur = kdtRecur.date().dayOfWeek() - 1;   // Monday = 0
4862             const QTime t = kdtRecur.time();
4863             if (t >= mWorkDayStart  &&  t < mWorkDayEnd)
4864             {
4865                 if (mWorkDays.testBit(dayRecur))
4866                 {
4867                     kdtRecur = kdtRecur.toTimeSpec(nextTrigger.timeSpec());
4868                     mMainWorkTrigger = kdtRecur;
4869                     mAllWorkTrigger  = kdtRecur.addSecs(-60 * reminder);
4870                     return;
4871                 }
4872             }
4873             if (kdtRecur.utcOffset() != currentOffset)
4874                 currentOffset = kdtRecur.utcOffset();
4875             if (t == firstTime  &&  dayRecur == firstDay  &&  currentOffset == firstOffset)
4876             {
4877                 // We've wrapped round to the starting day and time.
4878                 // If there are seasonal time changes, check for up
4879                 // to the next year in other time offsets in case the
4880                 // alarm occurs inside working hours then.
4881                 if (!finalDate.isValid())
4882                     finalDate = kdtRecur.date();
4883                 const int i = KAEventPrivate::transitionIndex(kdtRecur.toUtc().qDateTime(), tzTransitions);
4884                 if (i < 0)
4885                     return;
4886                 if (i > transitionIndex)
4887                     transitionIndex = i;
4888                 if (++transitionIndex >= tzTransitions.count())
4889                     return;
4890                 previousOccurrence(KADateTime(tzTransitions[transitionIndex].atUtc), newdt, false);
4891                 kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
4892                 if (finalDate.daysTo(kdtRecur.date()) > 365)
4893                     return;
4894                 firstTime = kdtRecur.time();
4895                 firstOffset = kdtRecur.utcOffset();
4896                 currentOffset = firstOffset;
4897                 firstDay = kdtRecur.date().dayOfWeek() - 1;
4898             }
4899             kdt = kdtRecur;
4900         }
4901 //qCDebug(KALARMCAL_LOG)<<"-----exit loop: count="<<limit<<endl;
4902         return;   // too many iterations
4903     }
4904 
4905     if (repeatTimeVaries)
4906     {
4907         /* There's a sub-repetition which occurs at different times of
4908          * day, inside a recurrence which occurs at the same time of day.
4909          * We potentially need to check recurrences starting on each day.
4910          * Then, it is still possible that a working time sub-repetition
4911          * could occur immediately after a seasonal time change.
4912          */
4913         // Find the previous recurrence (as opposed to sub-repetition)
4914         const int repeatFreq = mRepetition.intervalSeconds();
4915         previousOccurrence(kdt.addSecs(1), newdt, false);
4916         if (!newdt.isValid())
4917             return;    // this should never happen
4918         KADateTime kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
4919         const bool recurDuringWork = (kdtRecur.time() >= mWorkDayStart  &&  kdtRecur.time() < mWorkDayEnd);
4920 
4921         // Use the previous recurrence as a base for checking whether
4922         // our tests have wrapped round to the same time/day of week.
4923         const bool subdaily = (repeatFreq < 24 * 3600);
4924         unsigned days = 0;
4925         bool checkTimeChangeOnly = false;
4926         int transitionIndex = -1;
4927         for (int limit = 10;  --limit >= 0;)
4928         {
4929             // Check the next seasonal time change (for an arbitrary 10 times,
4930             // even though that might not guarantee the correct result)
4931             QDate dateRecur = kdtRecur.date();
4932             int repeatNum = kdtRecur.secsTo(kdt) / repeatFreq;
4933             kdt = kdtRecur.addSecs(repeatNum * repeatFreq);
4934 
4935             // Find the next recurrence, which sets the limit on possible sub-repetitions.
4936             // Note that for a monthly recurrence, for example, a sub-repetition could
4937             // be defined which is longer than the recurrence interval in short months.
4938             // In these cases, the sub-repetition is truncated by the following
4939             // recurrence.
4940             nextOccurrence(kdtRecur, newdt, KAEvent::Repeats::Ignore);
4941             KADateTime kdtNextRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
4942 
4943             int repeatsToCheck = mRepetition.count();
4944             int repeatsDuringWork = 0;  // 0=unknown, 1=does, -1=never
4945             for (; ;)
4946             {
4947                 // Check the sub-repetitions for this recurrence
4948                 if (repeatsDuringWork >= 0)
4949                 {
4950                     for (; ;)
4951                     {
4952                         // Find the repeat count to the next start of the working day
4953                         int inc = subdaily ? nextWorkRepetition(kdt) : 1;
4954                         repeatNum += inc;
4955                         const bool pastEnd = (repeatNum > mRepetition.count());
4956                         if (pastEnd)
4957                             inc -= repeatNum - mRepetition.count();
4958                         repeatsToCheck -= inc;
4959                         kdt = kdt.addSecs(inc * repeatFreq);
4960                         if (kdtNextRecur.isValid()  &&  kdt >= kdtNextRecur)
4961                         {
4962                             // This sub-repetition is past the next recurrence,
4963                             // so start the check again from the next recurrence.
4964                             repeatsToCheck = mRepetition.count();
4965                             break;
4966                         }
4967                         if (pastEnd)
4968                             break;
4969                         const QTime t = kdt.time();
4970                         if (t >= mWorkDayStart  &&  t < mWorkDayEnd)
4971                         {
4972                             if (mWorkDays.testBit(kdt.date().dayOfWeek() - 1))
4973                             {
4974                                 kdt = kdt.toTimeSpec(nextTrigger.timeSpec());
4975                                 mMainWorkTrigger = mAllWorkTrigger = kdt;
4976                                 return;
4977                             }
4978                             repeatsDuringWork = 1;
4979                         }
4980                         else if (!repeatsDuringWork  &&  repeatsToCheck <= 0)
4981                         {
4982                             // Sub-repetitions never occur during working hours
4983                             repeatsDuringWork = -1;
4984                             break;
4985                         }
4986                     }
4987                 }
4988                 repeatNum = 0;
4989                 if (repeatsDuringWork < 0  &&  !recurDuringWork)
4990                     break;    // it never occurs during working hours
4991 
4992                 // Check the next recurrence
4993                 if (!kdtNextRecur.isValid())
4994                     return;
4995                 if (checkTimeChangeOnly  || (days & allDaysMask) == allDaysMask)
4996                     break;    // found a recurrence on every possible day of the week!?!
4997                 kdtRecur = kdtNextRecur;
4998                 nextOccurrence(kdtRecur, newdt, KAEvent::Repeats::Ignore);
4999                 kdtNextRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
5000                 dateRecur = kdtRecur.date();
5001                 const int dayRecur = dateRecur.dayOfWeek() - 1;   // Monday = 0
5002                 if (recurDuringWork  &&  mWorkDays.testBit(dayRecur))
5003                 {
5004                     kdtRecur = kdtRecur.toTimeSpec(nextTrigger.timeSpec());
5005                     mMainWorkTrigger = kdtRecur;
5006                     mAllWorkTrigger  = kdtRecur.addSecs(-60 * reminder);
5007                     return;
5008                 }
5009                 days |= 1 << dayRecur;
5010                 kdt = kdtRecur;
5011             }
5012 
5013             // Find the next recurrence before a seasonal time change,
5014             // and ensure the time change is after the last one processed.
5015             checkTimeChangeOnly = true;
5016             const int i = KAEventPrivate::transitionIndex(kdtRecur.toUtc().qDateTime(), tzTransitions);
5017             if (i < 0)
5018                 return;
5019             if (i > transitionIndex)
5020                 transitionIndex = i;
5021             if (++transitionIndex >= tzTransitions.count())
5022                 return;
5023             kdt = KADateTime(tzTransitions[transitionIndex].atUtc);
5024             previousOccurrence(kdt, newdt, false);
5025             kdt = kdt.toTimeSpec(mWorkDayTimeSpec);
5026             kdtRecur = newdt.toTimeSpec(mWorkDayTimeSpec).effectiveKDateTime();
5027         }
5028         return;  // not found - give up
5029     }
5030 }
5031 
5032 /******************************************************************************
5033 * Find the repeat count to the next start of a working day.
5034 * This allows for possible daylight saving time changes during the repetition.
5035 * Use for repetitions which occur at different times of day.
5036 */
5037 int KAEventPrivate::nextWorkRepetition(const KADateTime& pre) const
5038 {
5039     Q_ASSERT(pre.timeSpec() == mWorkDayTimeSpec);
5040 
5041     KADateTime nextWork(pre);
5042     if (pre.time() < mWorkDayStart)
5043         nextWork.setTime(mWorkDayStart);
5044     else
5045     {
5046         const int preDay = pre.date().dayOfWeek() - 1;   // Monday = 0
5047         for (int n = 1;  ;  ++n)
5048         {
5049             if (n >= 7)
5050                 return mRepetition.count() + 1;    // should never happen
5051             if (mWorkDays.testBit((preDay + n) % 7))
5052             {
5053                 nextWork = nextWork.addDays(n);
5054                 nextWork.setTime(mWorkDayStart);
5055                 break;
5056             }
5057         }
5058     }
5059     return (pre.secsTo(nextWork) - 1) / mRepetition.intervalSeconds() + 1;
5060 }
5061 
5062 /******************************************************************************
5063 * Check whether an alarm which recurs at the same time of day can possibly
5064 * occur during working hours.
5065 * This does not determine whether it actually does, but rather whether it could
5066 * potentially given enough repetitions.
5067 * Reply = false if it can never occur during working hours, true if it might.
5068 */
5069 bool KAEventPrivate::mayOccurDailyDuringWork(const KADateTime& kdt) const
5070 {
5071     Q_ASSERT(kdt.timeSpec() == mWorkDayTimeSpec);
5072 
5073     if (!kdt.isDateOnly()
5074     &&  (kdt.time() < mWorkDayStart || kdt.time() >= mWorkDayEnd))
5075         return false;    // its time is outside working hours
5076     // Check if it always occurs on the same day of the week
5077     const Duration interval = mRecurrence->regularInterval();
5078     if (!interval.isNull()  &&  interval.isDaily()  &&  !(interval.asDays() % 7))
5079     {
5080         // It recurs weekly
5081         if (!mRepetition  || (mRepetition.isDaily() && !(mRepetition.intervalDays() % 7)))
5082             return false;    // any repetitions are also weekly
5083         // Repetitions are daily. Check if any occur on working days
5084         // by checking the first recurrence and up to 6 repetitions.
5085         int day = mRecurrence->startDateTime().toTimeSpec(mWorkDayTimeSpec).date().dayOfWeek() - 1;   // Monday = 0
5086         const int repeatDays = mRepetition.intervalDays();
5087         const int maxRepeat = (mRepetition.count() < 6) ? mRepetition.count() : 6;
5088         for (int i = 0;  !mWorkDays.testBit(day);  ++i, day = (day + repeatDays) % 7)
5089         {
5090             if (i >= maxRepeat)
5091                 return false;    // no working day occurrences
5092         }
5093     }
5094     return true;
5095 }
5096 
5097 /******************************************************************************
5098 * Set the specified alarm to be an audio alarm with the given file name.
5099 */
5100 void KAEventPrivate::setAudioAlarm(const Alarm::Ptr& alarm) const
5101 {
5102     alarm->setAudioAlarm(mAudioFile);  // empty for a beep or for speaking
5103     if (mSoundVolume >= 0)
5104         alarm->setCustomProperty(KACalendar::APPNAME, VOLUME_PROPERTY,
5105                                  QStringLiteral("%1;%2;%3").arg(QString::number(mSoundVolume, 'f', 2), QString::number(mFadeVolume, 'f', 2), QString::number(mFadeSeconds)));
5106 }
5107 
5108 /******************************************************************************
5109 * Get the date/time of the next recurrence of the event, after the specified
5110 * date/time.
5111 * 'result' = date/time of next occurrence, or invalid date/time if none.
5112 */
5113 KAEvent::OccurType KAEventPrivate::nextRecurrence(const KADateTime& preDateTime, DateTime& result) const
5114 {
5115     const KADateTime recurStart = mRecurrence->startDateTime();
5116     KADateTime pre = preDateTime.toTimeSpec(mStartDateTime.timeSpec());
5117     if (mStartDateTime.isDateOnly()  &&  !pre.isDateOnly()  &&  pre.time() < DateTime::startOfDay())
5118     {
5119         pre = pre.addDays(-1);    // today's recurrence (if today recurs) is still to come
5120         pre.setTime(DateTime::startOfDay());
5121     }
5122     const KADateTime dt = mRecurrence->getNextDateTime(pre);
5123     result = dt;
5124     result.setDateOnly(mStartDateTime.isDateOnly());
5125     if (!dt.isValid())
5126         return KAEvent::OccurType::None;
5127     if (dt == recurStart)
5128         return KAEvent::OccurType::FirstOrOnly;
5129     if (mRecurrence->duration() >= 0  &&  dt == mRecurrence->endDateTime())
5130         return KAEvent::OccurType::LastRecur;
5131     return result.isDateOnly() ? KAEvent::OccurType::RecurDate : KAEvent::OccurType::RecurDateTime;
5132 }
5133 
5134 /******************************************************************************
5135 * Validate the event's recurrence data, correcting any inconsistencies (which
5136 * should never occur!).
5137 * Reply = recurrence period type.
5138 */
5139 KARecurrence::Type KAEventPrivate::checkRecur() const
5140 {
5141     if (mRecurrence)
5142     {
5143         KARecurrence::Type type = mRecurrence->type();
5144         switch (type)
5145         {
5146             case KARecurrence::MINUTELY:     // hourly
5147             case KARecurrence::DAILY:        // daily
5148             case KARecurrence::WEEKLY:       // weekly on multiple days of week
5149             case KARecurrence::MONTHLY_DAY:  // monthly on multiple dates in month
5150             case KARecurrence::MONTHLY_POS:  // monthly on multiple nth day of week
5151             case KARecurrence::ANNUAL_DATE:  // annually on multiple months (day of month = start date)
5152             case KARecurrence::ANNUAL_POS:   // annually on multiple nth day of week in multiple months
5153                 return type;
5154             default:
5155                 if (mRecurrence)
5156                     const_cast<KAEventPrivate*>(this)->clearRecur();    // this shouldn't ever be necessary!!
5157                 break;
5158         }
5159     }
5160     if (mRepetition)
5161         const_cast<KAEventPrivate*>(this)->clearRecur();    // this shouldn't ever be necessary!!
5162     return KARecurrence::NO_RECUR;
5163 }
5164 
5165 /******************************************************************************
5166 * If the calendar was written by a previous version of KAlarm, do any
5167 * necessary format conversions on the events to ensure that when the calendar
5168 * is saved, no information is lost or corrupted.
5169 * Reply = true if any conversions were done.
5170 */
5171 bool KAEvent::convertKCalEvents(const Calendar::Ptr& calendar, int calendarVersion)
5172 {
5173     // KAlarm pre-0.9 codes held in the alarm's DESCRIPTION property
5174     static const QChar   SEPARATOR        = QLatin1Char(';');
5175     static const QChar   LATE_CANCEL_CODE = QLatin1Char('C');
5176     static const QChar   AT_LOGIN_CODE    = QLatin1Char('L');   // subsidiary alarm at every login
5177     static const QChar   DEFERRAL_CODE    = QLatin1Char('D');   // extra deferred alarm
5178     static const QString TEXT_PREFIX      = QStringLiteral("TEXT:");
5179     static const QString FILE_PREFIX      = QStringLiteral("FILE:");
5180     static const QString COMMAND_PREFIX   = QStringLiteral("CMD:");
5181 
5182     // KAlarm pre-0.9.2 codes held in the event's CATEGORY property
5183     static const QString BEEP_CATEGORY    = QStringLiteral("BEEP");
5184 
5185     // KAlarm pre-1.1.1 LATECANCEL category with no parameter
5186     static const QString LATE_CANCEL_CAT = QStringLiteral("LATECANCEL");
5187 
5188     // KAlarm pre-1.3.0 TMPLDEFTIME category with no parameter
5189     static const QString TEMPL_DEF_TIME_CAT = QStringLiteral("TMPLDEFTIME");
5190 
5191     // KAlarm pre-1.3.1 XTERM category
5192     static const QString EXEC_IN_XTERM_CAT  = QStringLiteral("XTERM");
5193 
5194     // KAlarm pre-1.9.0 categories
5195     static const QString DATE_ONLY_CATEGORY        = QStringLiteral("DATE");
5196     static const QString EMAIL_BCC_CATEGORY        = QStringLiteral("BCC");
5197     static const QString CONFIRM_ACK_CATEGORY      = QStringLiteral("ACKCONF");
5198     static const QString KORGANIZER_CATEGORY       = QStringLiteral("KORG");
5199     static const QString DEFER_CATEGORY            = QStringLiteral("DEFER;");
5200     static const QString ARCHIVE_CATEGORY          = QStringLiteral("SAVE");
5201     static const QString ARCHIVE_CATEGORIES        = QStringLiteral("SAVE:");
5202     static const QString LATE_CANCEL_CATEGORY      = QStringLiteral("LATECANCEL;");
5203     static const QString AUTO_CLOSE_CATEGORY       = QStringLiteral("LATECLOSE;");
5204     static const QString TEMPL_AFTER_TIME_CATEGORY = QStringLiteral("TMPLAFTTIME;");
5205     static const QString KMAIL_SERNUM_CATEGORY     = QStringLiteral("KMAIL:");
5206     static const QString LOG_CATEGORY              = QStringLiteral("LOG:");
5207 
5208     // KAlarm pre-1.5.0/1.9.9 properties
5209     static const QByteArray KMAIL_ID_PROPERTY("KMAILID");    // X-KDE-KALARM-KMAILID property
5210 
5211     // KAlarm pre-2.6.0 properties
5212     static const QByteArray ARCHIVE_PROPERTY("ARCHIVE");     // X-KDE-KALARM-ARCHIVE property
5213     static const QString ARCHIVE_REMINDER_ONCE_TYPE = QStringLiteral("ONCE");
5214     static const QString REMINDER_ONCE_TYPE         = QStringLiteral("REMINDER_ONCE");
5215     static const QByteArray EMAIL_ID_PROPERTY("EMAILID");         // X-KDE-KALARM-EMAILID property
5216     static const QByteArray SPEAK_PROPERTY("SPEAK");              // X-KDE-KALARM-SPEAK property
5217     static const QByteArray CANCEL_ON_ERROR_PROPERTY("ERRCANCEL");// X-KDE-KALARM-ERRCANCEL property
5218     static const QByteArray DONT_SHOW_ERROR_PROPERTY("ERRNOSHOW");// X-KDE-KALARM-ERRNOSHOW property
5219 
5220     bool adjustSummerTime = false;
5221     if (calendarVersion == -Version(0, 5, 7))
5222     {
5223         // The calendar file was written by the KDE 3.0.0 version of KAlarm 0.5.7.
5224         // Summer time was ignored when converting to UTC.
5225         calendarVersion = -calendarVersion;
5226         adjustSummerTime = true;
5227     }
5228 
5229     if (calendarVersion >= currentCalendarVersion())
5230         return false;
5231 
5232     qCDebug(KALARMCAL_LOG) << "Adjusting version" << calendarVersion;
5233     const bool pre_0_7    = (calendarVersion < Version(0, 7, 0));
5234     const bool pre_0_9    = (calendarVersion < Version(0, 9, 0));
5235     const bool pre_0_9_2  = (calendarVersion < Version(0, 9, 2));
5236     const bool pre_1_1_1  = (calendarVersion < Version(1, 1, 1));
5237     const bool pre_1_2_1  = (calendarVersion < Version(1, 2, 1));
5238     const bool pre_1_3_0  = (calendarVersion < Version(1, 3, 0));
5239     const bool pre_1_3_1  = (calendarVersion < Version(1, 3, 1));
5240     const bool pre_1_4_14 = (calendarVersion < Version(1, 4, 14));
5241     const bool pre_1_5_0  = (calendarVersion < Version(1, 5, 0));
5242     const bool pre_1_9_0  = (calendarVersion < Version(1, 9, 0));
5243     const bool pre_1_9_2  = (calendarVersion < Version(1, 9, 2));
5244     const bool pre_1_9_7  = (calendarVersion < Version(1, 9, 7));
5245     const bool pre_1_9_9  = (calendarVersion < Version(1, 9, 9));
5246     const bool pre_1_9_10 = (calendarVersion < Version(1, 9, 10));
5247     const bool pre_2_2_9  = (calendarVersion < Version(2, 2, 9));
5248     const bool pre_2_3_0  = (calendarVersion < Version(2, 3, 0));
5249     const bool pre_2_3_2  = (calendarVersion < Version(2, 3, 2));
5250     const bool pre_2_7_0  = (calendarVersion < Version(2, 7, 0));
5251     Q_ASSERT(currentCalendarVersion() == Version(2, 7, 0));
5252 
5253     const QTimeZone localZone = QTimeZone::systemTimeZone();
5254 
5255     bool converted = false;
5256     const Event::List events = calendar->rawEvents();
5257     for (Event::Ptr event : events)
5258     {
5259         const Alarm::List alarms = event->alarms();
5260         if (alarms.isEmpty())
5261             continue;    // KAlarm isn't interested in events without alarms
5262         event->startUpdates();   // prevent multiple update notifications
5263         const bool readOnly = event->isReadOnly();
5264         if (readOnly)
5265             event->setReadOnly(false);
5266         QStringList cats = event->categories();
5267         bool addLateCancel = false;
5268         QStringList flags;
5269 
5270         if (pre_0_7  &&  event->allDay())
5271         {
5272             // It's a KAlarm pre-0.7 calendar file.
5273             // Ensure that when the calendar is saved, the alarm time isn't lost.
5274             event->setAllDay(false);
5275         }
5276 
5277         if (pre_0_9)
5278         {
5279             /*
5280              * It's a KAlarm pre-0.9 calendar file.
5281              * All alarms were of type DISPLAY. Instead of the X-KDE-KALARM-TYPE
5282              * alarm property, characteristics were stored as a prefix to the
5283              * alarm DESCRIPTION property, as follows:
5284              *   SEQNO;[FLAGS];TYPE:TEXT
5285              * where
5286              *   SEQNO = sequence number of alarm within the event
5287              *   FLAGS = C for late-cancel, L for repeat-at-login, D for deferral
5288              *   TYPE = TEXT or FILE or CMD
5289              *   TEXT = message text, file name/URL or command
5290              */
5291             for (Alarm::Ptr alarm : alarms)
5292             {
5293                 bool atLogin    = false;
5294                 bool deferral   = false;
5295                 bool lateCancel = false;
5296                 KAAlarm::Action action = KAAlarm::Action::Message;
5297                 const QString txt = alarm->text();
5298                 const int length = txt.length();
5299                 int i = 0;
5300                 if (txt[0].isDigit())
5301                 {
5302                     while (++i < length && txt[i].isDigit()) {}
5303                     if (i < length  &&  txt[i++] == SEPARATOR)
5304                     {
5305                         while (i < length)
5306                         {
5307                             const QChar ch = txt[i++];
5308                             if (ch == SEPARATOR)
5309                                 break;
5310                             if (ch == LATE_CANCEL_CODE)
5311                                 lateCancel = true;
5312                             else if (ch == AT_LOGIN_CODE)
5313                                 atLogin = true;
5314                             else if (ch == DEFERRAL_CODE)
5315                                 deferral = true;
5316                         }
5317                     }
5318                     else
5319                         i = 0;    // invalid prefix
5320                 }
5321                 if (txt.indexOf(TEXT_PREFIX, i) == i)
5322                     i += TEXT_PREFIX.length();
5323                 else if (txt.indexOf(FILE_PREFIX, i) == i)
5324                 {
5325                     action = KAAlarm::Action::File;
5326                     i += FILE_PREFIX.length();
5327                 }
5328                 else if (txt.indexOf(COMMAND_PREFIX, i) == i)
5329                 {
5330                     action = KAAlarm::Action::Command;
5331                     i += COMMAND_PREFIX.length();
5332                 }
5333                 else
5334                     i = 0;
5335                 const QString altxt = txt.mid(i);
5336 
5337                 QStringList types;
5338                 switch (action)
5339                 {
5340                     case KAAlarm::Action::File:
5341                         types += KAEventPrivate::FILE_TYPE;
5342                         [[fallthrough]]; // fall through to Message
5343                     case KAAlarm::Action::Message:
5344                         alarm->setDisplayAlarm(altxt);
5345                         break;
5346                     case KAAlarm::Action::Command:
5347                         setProcedureAlarm(alarm, altxt);
5348                         break;
5349                     case KAAlarm::Action::Email:     // email alarms were introduced in KAlarm 0.9
5350                     case KAAlarm::Action::Audio:     // audio alarms (with no display) were introduced in KAlarm 2.3.2
5351                         break;
5352                 }
5353                 if (atLogin)
5354                 {
5355                     types += KAEventPrivate::AT_LOGIN_TYPE;
5356                     lateCancel = false;
5357                 }
5358                 else if (deferral)
5359                     types += KAEventPrivate::TIME_DEFERRAL_TYPE;
5360                 if (lateCancel)
5361                     addLateCancel = true;
5362                 if (types.count() > 0)
5363                     alarm->setCustomProperty(KACalendar::APPNAME, KAEventPrivate::TYPE_PROPERTY, types.join(QLatin1Char(',')));
5364 
5365                 if (pre_0_7  &&  alarm->repeatCount() > 0  &&  alarm->snoozeTime().value() > 0)
5366                 {
5367                     // It's a KAlarm pre-0.7 calendar file.
5368                     // Minutely recurrences were stored differently.
5369                     Recurrence* recur = event->recurrence();
5370                     if (recur  &&  recur->recurs())
5371                     {
5372                         recur->setMinutely(alarm->snoozeTime().asSeconds() / 60);
5373                         recur->setDuration(alarm->repeatCount() + 1);
5374                         alarm->setRepeatCount(0);
5375                         alarm->setSnoozeTime(0);
5376                     }
5377                 }
5378 
5379                 if (adjustSummerTime)
5380                 {
5381                     // The calendar file was written by the KDE 3.0.0 version of KAlarm 0.5.7.
5382                     // Summer time was ignored when converting to UTC.
5383                     KADateTime dt(alarm->time());
5384                     const qint64 t64 = dt.toSecsSinceEpoch();
5385                     const time_t t = (static_cast<quint64>(t64) >= uint(-1)) ? uint(-1) : static_cast<uint>(t64);
5386                     const struct tm* dtm = localtime(&t);
5387                     if (dtm->tm_isdst)
5388                     {
5389                         dt = dt.addSecs(-3600);
5390                         alarm->setTime(dt.qDateTime());
5391                     }
5392                 }
5393             }
5394         }
5395 
5396         if (pre_0_9_2)
5397         {
5398             /*
5399              * It's a KAlarm pre-0.9.2 calendar file.
5400              * For the archive calendar, set the CREATED time to the DTEND value.
5401              * Convert date-only DTSTART to date/time, and add category "DATE".
5402              * Set the DTEND time to the DTSTART time.
5403              * Convert all alarm times to DTSTART offsets.
5404              * For display alarms, convert the first unlabelled category to an
5405              * X-KDE-KALARM-FONTCOLOR property.
5406              * Convert BEEP category into an audio alarm with no audio file.
5407              */
5408             if (CalEvent::status(event) == CalEvent::ARCHIVED)
5409                 event->setCreated(event->dtEnd());
5410             QDateTime start = event->dtStart();
5411             if (event->allDay())
5412             {
5413                 start.setTime(QTime(0, 0));
5414                 flags += KAEventPrivate::DATE_ONLY_FLAG;
5415             }
5416             event->setDtEnd(QDateTime());
5417 
5418             for (Alarm::Ptr alarm : alarms)
5419                 alarm->setStartOffset(start.secsTo(alarm->time()));
5420 
5421             if (!cats.isEmpty())
5422             {
5423                 for (const Alarm::Ptr& alarm : alarms)
5424                 {     //clazy:exclude=range-loop   Can't use reference because 'alarms' is const
5425                     if (alarm->type() == Alarm::Display)
5426                     {
5427                         alarm->setCustomProperty(KACalendar::APPNAME, KAEventPrivate::FONT_COLOUR_PROPERTY,
5428                                                  QStringLiteral("%1;;").arg(cats.at(0)));
5429                     }
5430                 }
5431                 cats.removeAt(0);
5432             }
5433 
5434             {
5435                 int i = cats.indexOf(BEEP_CATEGORY);
5436                 if (i >= 0)
5437                 {
5438                     cats.removeAt(i);
5439 
5440                     Alarm::Ptr alarm = event->newAlarm();
5441                     alarm->setEnabled(true);
5442                     alarm->setAudioAlarm();
5443                     QDateTime dt = event->dtStart();    // default
5444 
5445                     // Parse and order the alarms to know which one's date/time to use
5446                     KAEventPrivate::AlarmMap alarmMap;
5447                     KAEventPrivate::readAlarms(event, &alarmMap);
5448                     KAEventPrivate::AlarmMap::ConstIterator it = alarmMap.constBegin();
5449                     if (it != alarmMap.constEnd())
5450                     {
5451                         dt = it.value().alarm->time();
5452                         break;
5453                     }
5454                     alarm->setStartOffset(start.secsTo(dt));
5455                     break;
5456                 }
5457             }
5458         }
5459 
5460         if (pre_1_1_1)
5461         {
5462             /*
5463              * It's a KAlarm pre-1.1.1 calendar file.
5464              * Convert simple LATECANCEL category to LATECANCEL:n where n = minutes late.
5465              */
5466             int i;
5467             while ((i = cats.indexOf(LATE_CANCEL_CAT)) >= 0)
5468             {
5469                 cats.removeAt(i);
5470                 addLateCancel = true;
5471             }
5472         }
5473 
5474         if (pre_1_2_1)
5475         {
5476             /*
5477              * It's a KAlarm pre-1.2.1 calendar file.
5478              * Convert email display alarms from translated to untranslated header prefixes.
5479              */
5480             for (Alarm::Ptr alarm : alarms)
5481             {
5482                 if (alarm->type() == Alarm::Display)
5483                 {
5484                     const QString oldtext = alarm->text();
5485                     const QString newtext = AlarmText::toCalendarText(oldtext);
5486                     if (oldtext != newtext)
5487                         alarm->setDisplayAlarm(newtext);
5488                 }
5489             }
5490         }
5491 
5492         if (pre_1_3_0)
5493         {
5494             /*
5495              * It's a KAlarm pre-1.3.0 calendar file.
5496              * Convert simple TMPLDEFTIME category to TMPLAFTTIME:n where n = minutes after.
5497              */
5498             int i;
5499             while ((i = cats.indexOf(TEMPL_DEF_TIME_CAT)) >= 0)
5500             {
5501                 cats.removeAt(i);
5502                 (flags += KAEventPrivate::TEMPL_AFTER_TIME_FLAG) += QStringLiteral("0");
5503             }
5504         }
5505 
5506         if (pre_1_3_1)
5507         {
5508             /*
5509              * It's a KAlarm pre-1.3.1 calendar file.
5510              * Convert simple XTERM category to LOG:xterm:
5511              */
5512             int i;
5513             while ((i = cats.indexOf(EXEC_IN_XTERM_CAT)) >= 0)
5514             {
5515                 cats.removeAt(i);
5516                 event->setCustomProperty(KACalendar::APPNAME, KAEventPrivate::LOG_PROPERTY, KAEventPrivate::xtermURL);
5517             }
5518         }
5519 
5520         if (pre_1_9_0)
5521         {
5522             /*
5523              * It's a KAlarm pre-1.9 calendar file.
5524              * Add the X-KDE-KALARM-STATUS custom property.
5525              * Convert KAlarm categories to custom fields.
5526              */
5527             CalEvent::setStatus(event, CalEvent::status(event));
5528             for (int i = 0;  i < cats.count();)
5529             {
5530                 const QString cat = cats.at(i);
5531                 if (cat == DATE_ONLY_CATEGORY)
5532                     flags += KAEventPrivate::DATE_ONLY_FLAG;
5533                 else if (cat == CONFIRM_ACK_CATEGORY)
5534                     flags += KAEventPrivate::CONFIRM_ACK_FLAG;
5535                 else if (cat == EMAIL_BCC_CATEGORY)
5536                     flags += KAEventPrivate::EMAIL_BCC_FLAG;
5537                 else if (cat == KORGANIZER_CATEGORY)
5538                     flags += KAEventPrivate::KORGANIZER_FLAG;
5539                 else if (cat.startsWith(DEFER_CATEGORY))
5540                     (flags += KAEventPrivate::DEFER_FLAG) += cat.mid(DEFER_CATEGORY.length());
5541                 else if (cat.startsWith(TEMPL_AFTER_TIME_CATEGORY))
5542                     (flags += KAEventPrivate::TEMPL_AFTER_TIME_FLAG) += cat.mid(TEMPL_AFTER_TIME_CATEGORY.length());
5543                 else if (cat.startsWith(LATE_CANCEL_CATEGORY))
5544                     (flags += KAEventPrivate::LATE_CANCEL_FLAG) += cat.mid(LATE_CANCEL_CATEGORY.length());
5545                 else if (cat.startsWith(AUTO_CLOSE_CATEGORY))
5546                     (flags += KAEventPrivate::AUTO_CLOSE_FLAG) += cat.mid(AUTO_CLOSE_CATEGORY.length());
5547                 else if (cat.startsWith(KMAIL_SERNUM_CATEGORY))
5548                     (flags += KAEventPrivate::KMAIL_ITEM_FLAG) += cat.mid(KMAIL_SERNUM_CATEGORY.length());
5549                 else if (cat == ARCHIVE_CATEGORY)
5550                     event->setCustomProperty(KACalendar::APPNAME, ARCHIVE_PROPERTY, QStringLiteral("0"));
5551                 else if (cat.startsWith(ARCHIVE_CATEGORIES))
5552                     event->setCustomProperty(KACalendar::APPNAME, ARCHIVE_PROPERTY, cat.mid(ARCHIVE_CATEGORIES.length()));
5553                 else if (cat.startsWith(LOG_CATEGORY))
5554                     event->setCustomProperty(KACalendar::APPNAME, KAEventPrivate::LOG_PROPERTY, cat.mid(LOG_CATEGORY.length()));
5555                 else
5556                 {
5557                     ++i;   // Not a KAlarm category, so leave it
5558                     continue;
5559                 }
5560                 cats.removeAt(i);
5561             }
5562         }
5563 
5564         if (pre_1_9_2)
5565         {
5566             /*
5567              * It's a KAlarm pre-1.9.2 calendar file.
5568              * Convert from clock time to the local system time zone.
5569              */
5570             event->shiftTimes(localZone, localZone);
5571             converted = true;
5572         }
5573 
5574         if (addLateCancel)
5575             (flags += KAEventPrivate::LATE_CANCEL_FLAG) += QStringLiteral("1");
5576         if (!flags.isEmpty())
5577             event->setCustomProperty(KACalendar::APPNAME, KAEventPrivate::FLAGS_PROPERTY, flags.join(KAEventPrivate::SC));
5578         event->setCategories(cats);
5579 
5580         if ((pre_1_4_14  || (pre_1_9_7 && !pre_1_9_0))
5581         &&  event->recurrence()  &&  event->recurrence()->recurs())
5582         {
5583             /*
5584              * It's a KAlarm pre-1.4.14 or KAlarm 1.9 series pre-1.9.7 calendar file.
5585              * For recurring events, convert the main alarm offset to an absolute
5586              * time in the X-KDE-KALARM-NEXTRECUR property, and set main alarm
5587              * offsets to zero, and convert deferral alarm offsets to be relative to
5588              * the next recurrence.
5589              */
5590             const QStringList flagsProp = event->customProperty(KACalendar::APPNAME, KAEventPrivate::FLAGS_PROPERTY).split(KAEventPrivate::SC, Qt::SkipEmptyParts);
5591             const bool dateOnly = flagsProp.contains(KAEventPrivate::DATE_ONLY_FLAG);
5592             KADateTime startDateTime(event->dtStart());
5593             if (dateOnly)
5594                 startDateTime.setDateOnly(true);
5595             // Convert the main alarm and get the next main trigger time from it
5596             KADateTime nextMainDateTime;
5597             bool mainExpired = true;
5598             for (Alarm::Ptr alarm : alarms)
5599             {
5600                 if (!alarm->hasStartOffset())
5601                     continue;
5602                 // Find whether the alarm triggers at the same time as the main
5603                 // alarm, in which case its offset needs to be set to 0. The
5604                 // following trigger with the main alarm:
5605                 //  - Additional audio alarm
5606                 //  - PRE_ACTION_TYPE
5607                 //  - POST_ACTION_TYPE
5608                 //  - DISPLAYING_TYPE
5609                 bool mainAlarm = true;
5610                 QString property = alarm->customProperty(KACalendar::APPNAME, KAEventPrivate::TYPE_PROPERTY);
5611                 const QStringList types = property.split(QLatin1Char(','), Qt::SkipEmptyParts);
5612                 for (const QString& type : types)
5613                 {
5614                     if (type == KAEventPrivate::AT_LOGIN_TYPE
5615                     ||  type == KAEventPrivate::TIME_DEFERRAL_TYPE
5616                     ||  type == KAEventPrivate::DATE_DEFERRAL_TYPE
5617                     ||  type == KAEventPrivate::REMINDER_TYPE
5618                     ||  type == REMINDER_ONCE_TYPE)
5619                     {
5620                         mainAlarm = false;
5621                         break;
5622                     }
5623                 }
5624                 if (mainAlarm)
5625                 {
5626                     if (mainExpired)
5627                     {
5628                         // All main alarms are supposed to be at the same time, so
5629                         // don't readjust the event's time for subsequent main alarms.
5630                         mainExpired = false;
5631                         nextMainDateTime = KADateTime(alarm->time());
5632                         nextMainDateTime.setDateOnly(dateOnly);
5633                         nextMainDateTime = nextMainDateTime.toTimeSpec(startDateTime);
5634                         if (nextMainDateTime != startDateTime)
5635                         {
5636                             QDateTime dt = nextMainDateTime.qDateTime();
5637                             event->setCustomProperty(KACalendar::APPNAME, KAEventPrivate::NEXT_RECUR_PROPERTY,
5638                                                      QLocale::c().toString(dt, dateOnly ? QStringLiteral("yyyyMMdd") : QStringLiteral("yyyyMMddThhmmss")));
5639                         }
5640                     }
5641                     alarm->setStartOffset(0);
5642                     converted = true;
5643                 }
5644             }
5645             int adjustment;
5646             if (mainExpired)
5647             {
5648                 // It's an expired recurrence.
5649                 // Set the alarm offset relative to the first actual occurrence
5650                 // (taking account of possible exceptions).
5651                 KADateTime dt(event->recurrence()->getNextDateTime(startDateTime.qDateTime().addDays(-1)));
5652                 dt.setDateOnly(dateOnly);
5653                 adjustment = startDateTime.secsTo(dt);
5654             }
5655             else
5656                 adjustment = startDateTime.secsTo(nextMainDateTime);
5657             if (adjustment)
5658             {
5659                 // Convert deferred alarms
5660                 for (Alarm::Ptr alarm : alarms)
5661                 {
5662                     if (!alarm->hasStartOffset())
5663                         continue;
5664                     const QString property = alarm->customProperty(KACalendar::APPNAME, KAEventPrivate::TYPE_PROPERTY);
5665                     const QStringList types = property.split(QLatin1Char(','), Qt::SkipEmptyParts);
5666                     for (const QString& type : types)
5667                     {
5668                         if (type == KAEventPrivate::TIME_DEFERRAL_TYPE
5669                         ||  type == KAEventPrivate::DATE_DEFERRAL_TYPE)
5670                         {
5671                             alarm->setStartOffset(alarm->startOffset().asSeconds() - adjustment);
5672                             converted = true;
5673                             break;
5674                         }
5675                     }
5676                 }
5677             }
5678         }
5679 
5680         if (pre_1_5_0  || (pre_1_9_9 && !pre_1_9_0))
5681         {
5682             /*
5683              * It's a KAlarm pre-1.5.0 or KAlarm 1.9 series pre-1.9.9 calendar file.
5684              * Convert email identity names to uoids.
5685              */
5686             for (const Alarm::Ptr& alarm : alarms)
5687             {
5688                 const QString name = alarm->customProperty(KACalendar::APPNAME, KMAIL_ID_PROPERTY);
5689                 if (name.isEmpty())
5690                     continue;
5691                 const uint id = Identities::identityUoid(name);
5692                 if (id)
5693                     alarm->setCustomProperty(KACalendar::APPNAME, EMAIL_ID_PROPERTY, QString::number(id));
5694                 alarm->removeCustomProperty(KACalendar::APPNAME, KMAIL_ID_PROPERTY);
5695                 converted = true;
5696             }
5697         }
5698 
5699         if (pre_1_9_10)
5700         {
5701             /*
5702              * It's a KAlarm pre-1.9.10 calendar file.
5703              * Convert simple repetitions without a recurrence, to a recurrence.
5704              */
5705             if (KAEventPrivate::convertRepetition(event))
5706                 converted = true;
5707         }
5708 
5709         if (pre_2_2_9  || (pre_2_3_2 && !pre_2_3_0))
5710         {
5711             /*
5712              * It's a KAlarm pre-2.2.9 or KAlarm 2.3 series pre-2.3.2 calendar file.
5713              * Set the time in the calendar for all date-only alarms to 00:00.
5714              */
5715             if (KAEventPrivate::convertStartOfDay(event))
5716                 converted = true;
5717         }
5718 
5719         if (pre_2_7_0)
5720         {
5721             /*
5722              * It's a KAlarm pre-2.7.0 calendar file.
5723              * Archive and at-login flags were stored in event's ARCHIVE property when the main alarm had expired.
5724              * Reminder parameters were stored in event's ARCHIVE property when no reminder was pending.
5725              * Negative reminder periods (i.e. alarm offset > 0) were invalid, so convert to 0.
5726              * Now store reminder information in FLAGS property, whether reminder is pending or not.
5727              * Move EMAILID, SPEAK, ERRCANCEL and ERRNOSHOW alarm properties into new FLAGS property.
5728              */
5729             bool flagsValid = false;
5730             QStringList preFlags;
5731             QString reminder;
5732             bool    reminderOnce = false;
5733             const QString prop = event->customProperty(KACalendar::APPNAME, ARCHIVE_PROPERTY);
5734             if (!prop.isEmpty())
5735             {
5736                 // Convert the event's ARCHIVE property to parameters in the FLAGS property
5737                 preFlags = event->customProperty(KACalendar::APPNAME, KAEventPrivate::FLAGS_PROPERTY).split(KAEventPrivate::SC, Qt::SkipEmptyParts);
5738                 preFlags << KAEventPrivate::ARCHIVE_FLAG;
5739                 flagsValid = true;
5740                 if (prop != QLatin1String("0"))
5741                 { // "0" was a dummy parameter if no others were present
5742                     // It's the archive property containing a reminder time and/or repeat-at-login flag.
5743                     // This was present when no reminder/at-login alarm was pending.
5744                     const QStringList list = prop.split(KAEventPrivate::SC, Qt::SkipEmptyParts);
5745                     for (const QString& pr : list)
5746                     {
5747                         if (pr == KAEventPrivate::AT_LOGIN_TYPE)
5748                             preFlags << KAEventPrivate::AT_LOGIN_TYPE;
5749                         else if (pr == ARCHIVE_REMINDER_ONCE_TYPE)
5750                             reminderOnce = true;
5751                         else if (!pr.isEmpty()  &&  !pr.startsWith(QChar::fromLatin1('-')))
5752                             reminder = pr;
5753                     }
5754                 }
5755                 event->setCustomProperty(KACalendar::APPNAME, KAEventPrivate::FLAGS_PROPERTY, preFlags.join(KAEventPrivate::SC));
5756                 event->removeCustomProperty(KACalendar::APPNAME, ARCHIVE_PROPERTY);
5757             }
5758 
5759             for (Alarm::Ptr alarm : alarms)
5760             {
5761                 // Convert EMAILID, SPEAK, ERRCANCEL, ERRNOSHOW properties
5762                 QStringList alflags;
5763                 QString property = alarm->customProperty(KACalendar::APPNAME, EMAIL_ID_PROPERTY);
5764                 if (!property.isEmpty())
5765                 {
5766                     alflags << KAEventPrivate::EMAIL_ID_FLAG << property;
5767                     alarm->removeCustomProperty(KACalendar::APPNAME, EMAIL_ID_PROPERTY);
5768                 }
5769                 if (!alarm->customProperty(KACalendar::APPNAME, SPEAK_PROPERTY).isEmpty())
5770                 {
5771                     alflags << KAEventPrivate::SPEAK_FLAG;
5772                     alarm->removeCustomProperty(KACalendar::APPNAME, SPEAK_PROPERTY);
5773                 }
5774                 if (!alarm->customProperty(KACalendar::APPNAME, CANCEL_ON_ERROR_PROPERTY).isEmpty())
5775                 {
5776                     alflags << KAEventPrivate::CANCEL_ON_ERROR_FLAG;
5777                     alarm->removeCustomProperty(KACalendar::APPNAME, CANCEL_ON_ERROR_PROPERTY);
5778                 }
5779                 if (!alarm->customProperty(KACalendar::APPNAME, DONT_SHOW_ERROR_PROPERTY).isEmpty())
5780                 {
5781                     alflags << KAEventPrivate::DONT_SHOW_ERROR_FLAG;
5782                     alarm->removeCustomProperty(KACalendar::APPNAME, DONT_SHOW_ERROR_PROPERTY);
5783                 }
5784                 if (!alflags.isEmpty())
5785                     alarm->setCustomProperty(KACalendar::APPNAME, KAEventPrivate::FLAGS_PROPERTY, alflags.join(KAEventPrivate::SC));
5786 
5787                 // Invalidate negative reminder periods in alarms
5788                 if (!alarm->hasStartOffset())
5789                     continue;
5790                 property = alarm->customProperty(KACalendar::APPNAME, KAEventPrivate::TYPE_PROPERTY);
5791                 QStringList types = property.split(QChar::fromLatin1(','), Qt::SkipEmptyParts);
5792                 const int r = types.indexOf(REMINDER_ONCE_TYPE);
5793                 if (r >= 0)
5794                 {
5795                     // Move reminder-once indicator from the alarm to the event's FLAGS property
5796                     types[r] = KAEventPrivate::REMINDER_TYPE;
5797                     alarm->setCustomProperty(KACalendar::APPNAME, KAEventPrivate::TYPE_PROPERTY, types.join(QChar::fromLatin1(',')));
5798                     reminderOnce = true;
5799                 }
5800                 if (r >= 0  ||  types.contains(KAEventPrivate::REMINDER_TYPE))
5801                 {
5802                     // The alarm is a reminder alarm
5803                     const int offset = alarm->startOffset().asSeconds();
5804                     if (offset > 0)
5805                     {
5806                         alarm->setStartOffset(0);
5807                         converted = true;
5808                     }
5809                     else if (offset < 0)
5810                         reminder = reminderToString(offset / 60);
5811                 }
5812             }
5813             if (!reminder.isEmpty())
5814             {
5815                 // Write reminder parameters into the event's FLAGS property
5816                 if (!flagsValid)
5817                     preFlags = event->customProperty(KACalendar::APPNAME, KAEventPrivate::FLAGS_PROPERTY).split(KAEventPrivate::SC, Qt::SkipEmptyParts);
5818                 if (!preFlags.contains(KAEventPrivate::REMINDER_TYPE))
5819                 {
5820                     preFlags += KAEventPrivate::REMINDER_TYPE;
5821                     if (reminderOnce)
5822                         preFlags += KAEventPrivate::REMINDER_ONCE_FLAG;
5823                     preFlags += reminder;
5824                 }
5825             }
5826         }
5827 
5828         if (readOnly)
5829             event->setReadOnly(true);
5830         event->endUpdates();     // finally issue an update notification
5831     }
5832     return converted;
5833 }
5834 
5835 /******************************************************************************
5836 * Set the time for a date-only event to 00:00.
5837 * Reply = true if the event was updated.
5838 */
5839 bool KAEventPrivate::convertStartOfDay(const Event::Ptr& event)
5840 {
5841     bool changed = false;
5842     const QTime midnight(0, 0);
5843     const QStringList flags = event->customProperty(KACalendar::APPNAME, KAEventPrivate::FLAGS_PROPERTY).split(KAEventPrivate::SC, Qt::SkipEmptyParts);
5844     if (flags.contains(KAEventPrivate::DATE_ONLY_FLAG))
5845     {
5846         // It's an untimed event, so fix it
5847         const QDateTime oldDt = event->dtStart();
5848         const int adjustment = oldDt.time().secsTo(midnight);
5849         if (adjustment)
5850         {
5851             event->setDtStart(QDateTime(oldDt.date(), midnight, oldDt.timeSpec()));
5852             int deferralOffset = 0;
5853             AlarmMap alarmMap;
5854             readAlarms(event, &alarmMap);
5855             for (AlarmMap::ConstIterator it = alarmMap.constBegin();  it != alarmMap.constEnd();  ++it)
5856             {
5857                 const AlarmData& data = it.value();
5858                 if (!data.alarm->hasStartOffset())
5859                     continue;
5860                 if (data.timedDeferral)
5861                 {
5862                     // Found a timed deferral alarm, so adjust the offset
5863                     deferralOffset = data.alarm->startOffset().asSeconds();
5864                     const_cast<Alarm*>(data.alarm.get())->setStartOffset(deferralOffset - adjustment);
5865                 }
5866                 else if (data.type == AUDIO_ALARM
5867                      &&  data.alarm->startOffset().asSeconds() == deferralOffset)
5868                 {
5869                     // Audio alarm is set for the same time as the above deferral alarm
5870                     const_cast<Alarm*>(data.alarm.get())->setStartOffset(deferralOffset - adjustment);
5871                 }
5872             }
5873             changed = true;
5874         }
5875     }
5876     else
5877     {
5878         // It's a timed event. Fix any untimed alarms.
5879         bool foundDeferral = false;
5880         int deferralOffset = 0;
5881         int newDeferralOffset = 0;
5882         DateTime start;
5883         const KADateTime nextMainDateTime = readDateTime(event, false, false, start).kDateTime();
5884         AlarmMap alarmMap;
5885         readAlarms(event, &alarmMap);
5886         for (AlarmMap::ConstIterator it = alarmMap.constBegin();  it != alarmMap.constEnd();  ++it)
5887         {
5888             const AlarmData& data = it.value();
5889             if (!data.alarm->hasStartOffset())
5890                 continue;
5891             if ((data.type & DEFERRED_ALARM)  &&  !data.timedDeferral)
5892             {
5893                 // Found a date-only deferral alarm, so adjust its time
5894                 QDateTime altime = data.alarm->startOffset().end(nextMainDateTime.qDateTime());
5895                 altime.setTime(midnight);
5896                 deferralOffset = data.alarm->startOffset().asSeconds();
5897                 newDeferralOffset = event->dtStart().secsTo(altime);
5898                 const_cast<Alarm*>(data.alarm.get())->setStartOffset(newDeferralOffset);
5899                 foundDeferral = true;
5900                 changed = true;
5901             }
5902             else if (foundDeferral
5903                  &&  data.type == AUDIO_ALARM
5904                  &&  data.alarm->startOffset().asSeconds() == deferralOffset)
5905             {
5906                 // Audio alarm is set for the same time as the above deferral alarm
5907                 const_cast<Alarm*>(data.alarm.get())->setStartOffset(newDeferralOffset);
5908                 changed = true;
5909             }
5910         }
5911     }
5912     return changed;
5913 }
5914 
5915 /******************************************************************************
5916 * Convert simple repetitions in an event without a recurrence, to a
5917 * recurrence. Repetitions which are an exact multiple of 24 hours are converted
5918 * to daily recurrences; else they are converted to minutely recurrences. Note
5919 * that daily and minutely recurrences produce different results when they span
5920 * a daylight saving time change.
5921 * Reply = true if any conversions were done.
5922 */
5923 bool KAEventPrivate::convertRepetition(const Event::Ptr& event)
5924 {
5925     const Alarm::List alarms = event->alarms();
5926     if (alarms.isEmpty())
5927         return false;
5928     Recurrence* recur = event->recurrence();   // guaranteed to return non-null
5929     if (recur->recurs())
5930         return false;
5931     bool converted = false;
5932     const bool readOnly = event->isReadOnly();
5933     for (Alarm::Ptr alarm : alarms)
5934     {
5935         if (alarm->repeatCount() > 0  &&  alarm->snoozeTime().value() > 0)
5936         {
5937             if (!converted)
5938             {
5939                 event->startUpdates();   // prevent multiple update notifications
5940                 if (readOnly)
5941                     event->setReadOnly(false);
5942                 if ((alarm->snoozeTime().asSeconds() % (24 * 3600)) != 0)
5943                     recur->setMinutely(alarm->snoozeTime().asSeconds() / 60);
5944                 else
5945                     recur->setDaily(alarm->snoozeTime().asDays());
5946                 recur->setDuration(alarm->repeatCount() + 1);
5947                 converted = true;
5948             }
5949             alarm->setRepeatCount(0);
5950             alarm->setSnoozeTime(0);
5951         }
5952     }
5953     if (converted)
5954     {
5955         if (readOnly)
5956             event->setReadOnly(true);
5957         event->endUpdates();     // finally issue an update notification
5958     }
5959     return converted;
5960 }
5961 
5962 /*=============================================================================
5963 = Class KAAlarm
5964 = Corresponds to a single KCalendarCore::Alarm instance.
5965 =============================================================================*/
5966 
5967 KAAlarm::KAAlarm()
5968     : d(new Private)
5969 {
5970 }
5971 
5972 KAAlarm::Private::Private() = default;
5973 
5974 KAAlarm::KAAlarm(const KAAlarm& other)
5975     : d(new Private(*other.d))
5976 {
5977 }
5978 
5979 KAAlarm::~KAAlarm()
5980 {
5981     delete d;
5982 }
5983 
5984 KAAlarm& KAAlarm::operator=(const KAAlarm& other)
5985 {
5986     if (&other != this)
5987         *d = *other.d;
5988     return *this;
5989 }
5990 
5991 KAAlarm::Action KAAlarm::action() const
5992 {
5993     return d->mActionType;
5994 }
5995 
5996 bool KAAlarm::isValid() const
5997 {
5998     return d->mType != Type::Invalid;
5999 }
6000 
6001 KAAlarm::Type KAAlarm::type() const
6002 {
6003     return d->mType;
6004 }
6005 
6006 DateTime KAAlarm::dateTime(bool withRepeats) const
6007 {
6008     return (withRepeats && d->mNextRepeat && d->mRepetition)
6009            ? DateTime(d->mRepetition.duration(d->mNextRepeat).end(d->mNextMainDateTime.qDateTime()))
6010            : d->mNextMainDateTime;
6011 }
6012 
6013 QDate KAAlarm::date() const
6014 {
6015     return d->mNextMainDateTime.date();
6016 }
6017 
6018 QTime KAAlarm::time() const
6019 {
6020     return d->mNextMainDateTime.effectiveTime();
6021 }
6022 
6023 bool KAAlarm::repeatAtLogin() const
6024 {
6025     return d->mRepeatAtLogin;
6026 }
6027 
6028 bool KAAlarm::isReminder() const
6029 {
6030     return d->mType == Type::Reminder;
6031 }
6032 
6033 bool KAAlarm::deferred() const
6034 {
6035     return d->mDeferred;
6036 }
6037 
6038 bool KAAlarm::timedDeferral() const
6039 {
6040     return d->mDeferred && d->mTimedDeferral;
6041 }
6042 
6043 void KAAlarm::setTime(const DateTime& dt)
6044 {
6045     d->mNextMainDateTime = dt;
6046 }
6047 
6048 void KAAlarm::setTime(const KADateTime& dt)
6049 {
6050     d->mNextMainDateTime = dt;
6051 }
6052 
6053 #ifdef KDE_NO_DEBUG_OUTPUT
6054 const char* KAAlarm::debugType(Type)
6055 {
6056     return "";
6057 }
6058 #else
6059 const char* KAAlarm::debugType(Type type)
6060 {
6061     switch (type)
6062     {
6063         case Type::Main:              return "MAIN";
6064         case Type::Reminder:          return "REMINDER";
6065         case Type::Deferred:          return "DEFERRED";
6066         case Type::DeferredReminder:  return "DEFERRED_REMINDER";
6067         case Type::AtLogin:           return "LOGIN";
6068         case Type::Displaying:        return "DISPLAYING";
6069         default:                      return "INVALID";
6070     }
6071 }
6072 #endif
6073 
6074 /*=============================================================================
6075 = Class EmailAddressList
6076 =============================================================================*/
6077 
6078 /******************************************************************************
6079 * Sets the list of email addresses, removing any empty addresses.
6080 * Reply = false if empty addresses were found.
6081 */
6082 EmailAddressList& EmailAddressList::operator=(const Person::List& addresses)
6083 {
6084     clear();
6085     for (const Person& addr : addresses)
6086     {
6087         if (!addr.email().isEmpty())
6088             append(addr);
6089     }
6090     return *this;
6091 }
6092 
6093 /******************************************************************************
6094 * Return the email address list as a string list of email addresses.
6095 */
6096 EmailAddressList::operator QStringList() const
6097 {
6098     int cnt = count();
6099     QStringList list;
6100     list.reserve(cnt);
6101     for (int p = 0;  p < cnt;  ++p)
6102         list += address(p);
6103     return list;
6104 }
6105 
6106 /******************************************************************************
6107 * Return the email address list as a string, each address being delimited by
6108 * the specified separator string.
6109 */
6110 QString EmailAddressList::join(const QString& separator) const
6111 {
6112     QString result;
6113     bool first = true;
6114     for (int p = 0, end = count();  p < end;  ++p)
6115     {
6116         if (first)
6117             first = false;
6118         else
6119             result += separator;
6120         result += address(p);
6121     }
6122     return result;
6123 }
6124 
6125 /******************************************************************************
6126 * Convert one item into an email address, including name.
6127 */
6128 QString EmailAddressList::address(int index) const
6129 {
6130     if (index < 0  ||  index > count())
6131         return {};
6132     QString result;
6133     bool quote = false;
6134     const Person& person = (*this)[index];
6135     const QString name = person.name();
6136     if (!name.isEmpty())
6137     {
6138         // Need to enclose the name in quotes if it has any special characters
6139         for (int i = 0, len = name.length();  i < len;  ++i)
6140         {
6141             const QChar ch = name[i];
6142             if (!ch.isLetterOrNumber())
6143             {
6144                 quote = true;
6145                 result += QLatin1Char('\"');
6146                 break;
6147             }
6148         }
6149         result += (*this)[index].name();
6150         result += (quote ? QLatin1String("\" <") : QLatin1String(" <"));
6151         quote = true;    // need angle brackets round email address
6152     }
6153 
6154     result += person.email();
6155     if (quote)
6156         result += QLatin1Char('>');
6157     return result;
6158 }
6159 
6160 /******************************************************************************
6161 * Return a list of the pure email addresses, excluding names.
6162 */
6163 QStringList EmailAddressList::pureAddresses() const
6164 {
6165     int cnt = count();
6166     QStringList list;
6167     list.reserve(cnt);
6168     for (int p = 0;  p < cnt;  ++p)
6169         list += at(p).email();
6170     return list;
6171 }
6172 
6173 /******************************************************************************
6174 * Return a list of the pure email addresses, excluding names, as a string.
6175 */
6176 QString EmailAddressList::pureAddresses(const QString& separator) const
6177 {
6178     QString result;
6179     bool first = true;
6180     for (int p = 0, end = count();  p < end;  ++p)
6181     {
6182         if (first)
6183             first = false;
6184         else
6185             result += separator;
6186         result += at(p).email();
6187     }
6188     return result;
6189 }
6190 
6191 /*=============================================================================
6192 = Static functions
6193 =============================================================================*/
6194 
6195 /******************************************************************************
6196 * Set the specified alarm to be a procedure alarm with the given command line.
6197 * The command line is first split into its program file and arguments before
6198 * initialising the alarm.
6199 */
6200 static void setProcedureAlarm(const Alarm::Ptr& alarm, const QString& commandLine)
6201 {
6202     //TODO: cater for environment variables prefixed to command
6203     QString command;
6204     QString arguments;
6205     QChar quoteChar;
6206     bool quoted = false;
6207     const uint posMax = commandLine.length();
6208     uint pos;
6209     for (pos = 0;  pos < posMax;  ++pos)
6210     {
6211         const QChar ch = commandLine[pos];
6212         if (quoted)
6213         {
6214             if (ch == quoteChar)
6215             {
6216                 ++pos;    // omit the quote character
6217                 break;
6218             }
6219             command += ch;
6220         }
6221         else
6222         {
6223             bool done = false;
6224             switch (ch.toLatin1())
6225             {
6226                 case ' ':
6227                 case ';':
6228                 case '|':
6229                 case '<':
6230                 case '>':
6231                     done = !command.isEmpty();
6232                     break;
6233                 case '\'':
6234                 case '"':
6235                     if (command.isEmpty())
6236                     {
6237                         // Start of a quoted string. Omit the quote character.
6238                         quoted = true;
6239                         quoteChar = ch;
6240                         break;
6241                     }
6242                     [[fallthrough]]; // fall through to default
6243                 default:
6244                     command += ch;
6245                     break;
6246                 }
6247                 if (done)
6248                     break;
6249         }
6250     }
6251 
6252     // Skip any spaces after the command
6253     for (; pos < posMax && commandLine[pos] == QLatin1Char(' '); ++pos) {}
6254     arguments = commandLine.mid(pos);
6255 
6256     alarm->setProcedureAlarm(command, arguments);
6257 }
6258 
6259 /******************************************************************************
6260 * Converts a reminder interval into a parameter string for the
6261 * X-KDE-KALARM-FLAGS property.
6262 */
6263 QString reminderToString(int minutes)
6264 {
6265     char unit = 'M';
6266     int count = abs(minutes);
6267     if (count % 1440 == 0)
6268     {
6269         unit = 'D';
6270         count /= 1440;
6271     }
6272     else if (count % 60 == 0)
6273     {
6274         unit = 'H';
6275         count /= 60;
6276     }
6277     if (minutes < 0)
6278         count = -count;
6279     return QStringLiteral("%1%2").arg(count).arg(unit);
6280 }
6281 
6282 } // namespace KAlarmCal
6283 
6284 // vim: et sw=4: