diff --git a/Changelog b/Changelog index c42e52a2..df20d9e6 100644 --- a/Changelog +++ b/Changelog @@ -1,1180 +1,1183 @@ KAlarm Change Log +=== Version 2.15.0 --- 11 April 2020 === ++ Refactor AlarmCalendar to split out resources and display calendars. + === Version 2.14.0 (KDE Applications 20.04) --- 27 March 2020 === + Warn user if archiving but no default archived alarms calendar is set. + Fix some error messages not being displayed. + Refactor to use generic resource classes (part 2). === Version 2.13.3 (KDE Applications 19.12.3) --- 20 February 2020 === + Fix failure of command line options requiring calendar access [KDE Bug 417108] === Version 2.13.2 (KDE Applications 19.12.2) --- 9 January 2020 === + Add Show/Hide Menubar menu option; change New Email Alarm shortcut to Ctrl-L. === Version 2.13.1 (KDE Applications 19.12.1) --- 30 December 2019 === + Make defer dialogue accessible when a full screen window is active [KDE Bug 414383] + Only show 'Cancel Deferral' in defer dialogue if a deferral is already active. === Version 2.13.0 (KDE Applications 19.12) --- 19 November 2019 === + Fix user not always being prompted to update new resource if in old format. + Terminate application after executing 'kalarm --list'. + Fix alarm type column being too wide in alarm template list. + Fix failure to display image when alarm is configured to display an image file. + Fix failure to set no-autostart for non-KDE desktops, if no autostart directory exists. + Refactor to use generic resource classes (part 1). === Version 2.12.8 (KDE Applications 19.08.3) --- 16 October 2019 === + Fix error on redo of an active alarm deletion. + Archive repeat-at-login alarms if previously triggered, when they are deleted. + Fix layout of defer alarm dialogue. + Make user settings changes take effect immediately (fixes regression introduced in 2.10.11). === Version 2.12.7 (KDE Applications 19.08.2) --- 7 October 2019 === + Show correct read-only status of an alarm in its context menu. + Fix errors deleting and reactivating alarms (regression introduced in 2.12.5). + Fix error on undo of an active alarm deletion. + Don't trigger repeat-at-login alarms when they are edited or imported. === Version 2.12.6 (KDE Applications 19.08.1) --- 26 August 2019 === + Fix crash sometimes when a resource is enabled [KDE Bug 410596] + Fix D-Bus alarm creation failing if time zone is omitted from start time [KDE Bug 411296] + Fix command line options which don't work if KAlarm not already running: --edit, --list, --triggerEvent, --cancelEvent. === Version 2.12.5 (KDE Applications 19.08) --- 26 July 2019 === + Enable alarm list columns to be hidden using context menu on list header [KDE Bug 397093] + Fix regression introduced in version 2.12.0: Show time zone abbreviation in message window if alarm time has non-local time zone. + If only one writable archived alarm calendar exists, automatically set it as the default. + Don't allow user to create a new resource using same calendar file as an existing resource. + Remove duplicate resources (i.e. which use the same calendar file) at startup [KDE Bug 403124] + Fix drag and drop of emails from KMail, and KMail button in message window. + Improve drag and drop of events and todos from KOrganizer. === Version 2.12.4 (KDE Applications 19.04.3) --- 4 July 2019 === + Fix calendar resource dialogue not configuring resource correctly [KDE Bug 407882] + Fix calendar resource dialogue creating new resources unusable until restart [KDE Bug 407882] + Enable resource after creating with the calendar resource dialogue [KDE Bug 407882] + Fix colour and alarm type columns being too wide in alarm list. === Version 2.12.3 (KDE Applications 19.04.2) --- 23 May 2019 === * Fix calendar configuration dialogue not appearing. * Fix errors creating calendar resources on first run of KAlarm [KDE Bug 407544] * Display alarm message windows within current screen in multi-head systems. === Version 2.12.2 (KDE Applications 18.08.2) --- 27 September 2018 === * Fix Defer button being disabled for recurring alarms [KDE Bug 398658] === Version 2.12.1 (KDE Applications 18.08.1) --- 18 August 2018 === * Align and right adjust 'Time to' column values in main window [KDE Bug 397130] * Remove seconds values from Time column (erroneously added in 2.12.0). === Version 2.12.0 (KDE Applications 18.08) --- 29 July 2018 === * Use KAlarmCal::KADateTime instead of deprecated KDateTime. * Remove 'clock time' option, in favour of local system time zone. * Fix times being truncated and showing ellipsis in main window [KDE Bug 365257] * Fix evaluation of work days. * Fix reminder-once alarms not being correctly loaded from calendar file. * Fix some regressions introduced in version 2.11.0, including: Make global shortcuts available. Default sound file selection dialogue to the system sound files directory. === Version 2.11.16 (KDE Applications 17.04.1) --- 15 April 2017 === * Fix option text for using default email address from KMail/System Settings [KDE Bug 378722] === Version 2.11.15 (KDE Applications 17.04) --- 15 January 2017 === * Report if terminal for command alarms is not configured. * Don't allow 'auto-hide in system tray' on Unity desktop [KDE Bug 373848] === Version 2.11.14 --- 19 February 2017 === * Fix not showing main window if activated again while already running with --tray [KDE Bug 374520] * Fix --help, --version and option errors not being reported if KAlarm is already running. * Make command options --edit-new-* work [KDE Bug 376209] === Version 2.11.13 (KDE Applications 16.12.2) --- 29 January 2017 === * Fix system tray icon used for "some alarms disabled" * Improved system tray icons (requires Plasma 5.9) [KDE Bug 362631] * Don't show misleading "Failed to update alarm" if command alarm fails [KDE Bug 375615] === Version 2.11.12 (KDE Applications 16.12.1) --- 1 January 2017 === * Fix Export Alarms file save error [KDE Bug 374337] * Fix arrow/page up/down keys not working in date edit control (needs KDE Frameworks 5.30) [KDE Bug 373886] === Version 2.11.11 (KDE Applications 16.12.0) --- 16 November 2016 === * Fix crash on exit [KDE Bug 372223] === Version 2.11.10 (KDE Applications 16.08.3) --- 31 October 2016 === * Fix default calendar files not being created on first run [KDE Bug 362962] * Fix crash when a second instance of KAlarm is started [KDE Bug 371628] * Don't output error messages about temporary files in directory calendar [KDE Bug 370627] === Version 2.11.9 (KDE Applications 16.08.1) --- 18 August 2016 === * Prevent KAlarm autostarting on non-KDE desktops if it has never been run [KDE Bug 366562] === Version 2.11.8 (KDE Applications 16.08.0) --- 13 July 2016 === * Use the default time format in alarm list and system tray status popup [KAlarm Forum: https://forum.kde.org/viewtopic.php?f=229&t=133788] === Version 2.11.7 (KDE Applications 16.04.3) --- 11 June 2016 === * Always use current setting for email sender address when sending emails [KDE Bug 359163] === Version 2.11.6 (KDE Applications 16.04.1) --- 20 April 2016 === * Prevent KAlarm autostarting on non-KDE desktops if start-at-login is disabled [KAlarm Forum: https://forum.kde.org/viewtopic.php?f=229&t=131410] === Version 2.11.5 (KDE Applications 16.04.0) --- 13 April 2016 === * Fix alarm times out by an hour in daylight savings time (needs kcalcore 16.04) [KDE Bug 336738] * Don't show spurious extra calendar after adding new calendar [KDE Bug 361543] * Fix crash when adding new calendar [KDE Bugs 361539, 361717] === Version 2.11.4 (KDE Applications 15.12.3) --- 1 February 2016 === * Fix reminder time edit being covered by 'in advance' combo [KDE Bug 357018] * Fix crash after editing an alarm, if spell check is enabled [KDE Bug 356048] * Fix occasional crash on startup [KDE Bug 358217] * Fix specification on command line of a reminder after the alarm. * Fix deferral time of date-only recurring alarms [KDE Bug 346060] * Fix frequency edit field missing from recurrence editor. === Version 2.11.3 (KDE Applications 15.08.3) --- 4 November 2015 === * Re-enable use of sendmail for email alarms. * Fix conversion error in sub-repetition interval from command line. === Version 2.11.2 (KDE Applications 15.08.2) --- 24 September 2015 === * Enable typing into New Alarm dialogue while alarm is displayed (Unity desktop) [KDE Bug 352889] === Version 2.11.1 (KDE Applications 15.08.1) --- 1 September 2015 === * Fix conversion error in sub-repetition value from command line or D-Bus command. === Version 2.11.0 (KDE Applications 15.08.0) --- 30 July 2015 === * Use KDE Frameworks. * Disable use of sendmail for email alarms, due to removal from Akonadi. === Version 2.10.12 (KDE 4.14.2) --- 30 September 2014 === * Make New Audio Alarm dialogue use sound file repeat preference setting. === Version 2.10.11 (KDE 4.14.0) --- 12 August 2014 === * [Akonadi] Fix alarms not being redisplayed after Akonadi server restarts (requires kdepimlibs 4.14.0) [KDE Bug 336942] === Version 2.10.10 (KDE 4.13.2) --- 10 May 2014 === * [Akonadi] Fix no Defer button in alarm windows restored after login [KDE Bug 334334] * Fix display of duplicate alarm windows after login. === Version 2.10.9 (KDE 4.13.1) --- 4 May 2014 === * [Akonadi] Fix no Defer button in alarm windows restored after crash [KDE Bug 334334] === Version 2.10.8 (KDE 4.12.5) --- 18 April 2014 === * [Akonadi] Fix wrong startup message about no writable active alarm calendar. * [Akonadi] Fix setting Akonadi resource read-only making it unusable (requires kdepim-runtime 4.12.5) [KDE Bug 332889] === Version 2.10.7 (KDE 4.12.4, 4.13.0) --- 21 March 2014 === * [Akonadi] Fix deletion of alarm copies from KOrganiser not working. * Fix crash after session restoration has nothing to restore [KDE Bug 331719] * Prevent data in birthday import dialogue being editable. === Version 2.10.6 (KDE 4.11.1) --- 27 August 2013 === * [Akonadi] Fix error saving template when closing Edit Template dialogue [KDE Bug 323965] === Version 2.10.5 (KDE 4.11.0) --- 3 August 2013 === * Fix memory leak whenever the edit dialogue is closed. * Fix auto-close alarms not displaying when KAlarm defaults to UTC time zone. * Fix display alarm deferral limit when KAlarm defaults to UTC time zone. === Version 2.10.4 (KDE 4.11 beta2) --- 15 June 2013 === * Show startup warning if no writable active alarm calendar is enabled [KDE Bug 316338] === Version 2.10.3 (KDE 4.10.5) --- 15 June 2013 === * Fix sound repetition pause not working in audio alarms [KDE Bug 319261] * Fix Stop Play button being left enabled after closing alarm window. === Version 2.10.2 (KDE 4.10.4) --- 4 May 2013 === * [Akonadi] Fix infinite loop on shutdown if display alarms are active [KDE Bug 317806] === Version 2.10.1 (KDE 4.10.0) --- 10 December 2012 === * [Akonadi] Fix memory leak when an alarm message window is displayed. * [Akonadi] Fix memory leak on alarm edit. === Version 2.10.0 (KDE 4.10 beta1)--- 13 November 2012 === * Add --list command line option to list scheduled alarms to stdout. * Add 'list' D-Bus command to return list of scheduled alarms. * [Akonadi] Wait until calendars are populated before using them at startup. === Version 2.9.3 (KDE 4.9.4) --- 13 November 2012 === * [Akonadi] Fix alarm list not sorting new alarms when calendar is enabled [KDE Bug 306178] === Version 2.9.2 (KDE 4.9.1) --- 22 August 2012 === * Fix Quit not working in system tray icon context menu. * [KResources] Fix KAlarm button not highlighting the alarm in the main window [KDE Bug 266082] === Version 2.9.1 (KDE 4.9.0) --- 7 July 2012 === * Add option to execute a pre-alarm action before deferred alarms. * Provide options to auto-hide system tray icon when no alarms are due. * Store KAlarm version and backend in config file. === Version 2.8.6 (KDE 4.8.5) --- 14 July 2012 === * [Akonadi] Don't display calendars which have no Akonadi resource. * [Akonadi resources] Fix resource if config is missing. * [Akonadi resources] Make resource work if location is set by path OR URL. * Fix crash when closing alarm window for alarm which plays audio file. * Fix "server did not accept the sender address" errors sending emails [KDE Bug 301946] === Version 2.8.5 (KDE 4.8.4) --- 6 June 2012 === * [Akonadi] Warn user and disable KAlarm if Akonadi fails to run [KDE Bug 300083] * [Akonadi] Fix crash when saving new alarm [KDE Bug Bug 300376] === Version 2.8.3 (KDE 4.8.3) --- 22 April 2012 === * Store KAlarm version and backend in config file. * Use the last selected sound file picker directory as the default next time. === Version 2.8.2 (KDE 4.8.2) --- 29 March 2012 === * [Akonadi] Fix error saving changed alarms when closing Edit Alarm dialogue. * [Akonadi] Show old-format calendars in read-only colour in calendar list. * [KResources] Fail cleanly if calendar resources fail to open [KDE Bug 296383] * Prevent multiple email success messages after Try used in Edit Alarm dialogue. === Version 2.8.1 (KDE 4.8.1) --- 19 February 2012 === * [Akonadi] Don't give option to save new alarms in old format calendars. * [Akonadi] Prevent duplicate prompts to update format of new calendar resource. * [Akonadi] Automatically disable duplicated calendar resources [KDE Bug 293193] * [Akonadi] Fix errors when creating default calendar resources [KDE Bug 293208] * [Akonadi] Prevent multiple standard calendars for any alarm type. * [Akonadi] Fix various crashes. * Output cmake error if Akonadi option incompatible with kdepimlibs/kalarmcal. === Version 2.8.0 (KDE 4.8.0) --- 16 January 2012 === * Use Akonadi as the default calendar access method. * Use configurable colours and KDE colour scheme for calendar list. * Allow user to stop playback after clicking Try in audio alarm edit dialogue. === Version 2.7.5 (KDE 4.7.4) --- 23 November 2011 === * Fix crash due to audio thread not being correctly deleted. === Version 2.7.4 (KDE 4.7.1) --- 28 August 2011 === * Fix crash when last recurrence of late-cancel alarm triggers too late. * Fix conversion of pre-version 1.4.14 subsidiary alarms. * Fix new alarm not being scheduled after editing alarm from alarm window. * Don't do search if invalid regular expression is entered in Find dialogue. * Don't prevent interaction with alarm windows when a prompt or warning message window is displayed [using KDE 4.7.1 or later]. * Only reset visible tab in multi-tab settings sections when Defaults is clicked in Configuration dialogue, and Current tab option is selected. * Disable command output option for display alarms in edit alarm dialogue if user not authorised to run shell commands. * Always output "not authorised" error message if unauthorised user tries to run shell commands. === Version 2.7.3 --- 26 July 2011 === * Fix crash when Wake From Suspend dialogue is shown with no alarm selected. === Version 2.7.2 --- 15 July 2011 === * Fix KAlarm not quitting when no visible windows or system tray icon remain. * Cancel wake-from-suspend if alarm is disabled, or if all alarms are disabled. * Various improvements and bug fixes to Wake From Suspend dialogue. * In calendar list show calendar colours by text background, not coloured square. * In alarm list show multi-line tooltip for alarm text when appropriate. === Version 2.7.1 (KDE 4.7.0) --- 6 July 2011 === * Make wake-from-suspend schedule a time-from-now, to make it work correctly on systems whose hardware clock is out of sync with the system clock. * Include Content-Transfer-Encoding header in emails to allow correct display. === Version 2.7.0 --- 9 May 2011 === * Add option to set a reminder AFTER the main alarm. * Add option to wake computer from suspend when a selected alarm is triggered. * Add command line option to disable alarm monitoring. * Replace EMAILID, SPEAK, ERRCANCEL, ERRNOSHOW calendar properties with FLAGS property parameters. === Version 2.6.3 --- 27 April 2011 === * Add option to not notify execution errors for pre-alarm actions. * Set environment variable KALARM_UID to event UID for pre- & post-alarm actions. * Warn user if only UTC time zone is available (if ktimezoned not installed). * Don't reactivate start-at-login without prompting, after user switches it off, except if KAlarm is session restored. * Show error message and set read-only if location is blank for new resource. * Fix crash on some systems when New Alarm dialogue is displayed from system tray icon menu. * Fix KAlarm button in alarm window not always showing main window and not highlighting the alarm in the main window. * Move New Alarm From Template action into New alarm menu to simplify toolbar. === Version 2.4.11 (KDEPIM 4.4.11) --- 16 April 2011 === * Fix bad borders round left hand buttons of time spinboxes in Oxygen style. * Fix initialisation of library global statics. * Ensure sound volume is not out of range when reading from calendar. * Fix New Alarm dialogue from system tray menu restoring other windows. === Version 2.4.10 (KDEPIM 4.4.8) --- 2 December 2010 === * Fix KAlarm showing in system tray at login when configured not to show in tray. * Fix working-time-only alarms not triggering if KAlarm is started up outside working hours, after the last trigger time during working hours was missed. * Don't quit if no window is visible when 'show in system tray' is deselected. * Disable Defer button in new message window when deferral limit has been reached. * Fix reminder time shown when editing a non-recurring alarm's deferred reminder. * Fix conversion of pre-version 1.9.10 non-recurring alarms with simple repetition. * Make disabled system tray icon more distinguishable for colour blind users. === Version 2.4.9 (KDEPIM 4.4.7) --- 19 October 2010 === * Fix crash if alarm triggers while its deletion confirmation prompt is visible. * Fix crash when Try button is clicked while creating new display alarm. * Fix crash on KAlarm exit. * Fix possible crash when enabling individual alarms. * Prevent long file name from expanding the width of file display alarm window. * Allow pre- & post-alarm actions for alarms whose text is generated by a command. * Combine 4 New Alarm icons in toolbar, to fix icon texts not fitting into width. === Version 2.4.8 (KDEPIM 4.4.6) --- 4 September 2010 === * Fix crash when a reminder alarm is being redisplayed. * Fix possible crash: on alarm deletion, always update next alarm to trigger. * Fix Sound File selection dialogue Play button not playing any sound. * Always show current storage location choice in Configuration dialogue. * Fix inability to leave file name blank in audio alarm templates. * Fix changes to volume not enabling OK button when editing an audio alarm template with no audio file specified. === Version 2.4.7 (KDE 4.4.5) --- 3 June 2010 === * Fix inability to defer non-recurring alarms. * Fix crash when selecting calendar type in calendar selector, if text widths and selector width are "exactly wrong". * Fix loss of time zone specification for date only alarms when converting a pre-2.3.2 calendar, if start-of-day time in calendar is not midnight. * Enable alarm edit dialogue Time Zone button in read-only mode. === Version 2.4.6 (KDE 4.4.4) --- 20 May 2010 === * Fix alarm edit dialog not saving changes when invoked from alarm message window's Edit button. * Fix main window close action not working when system tray icon is not shown. === Version 2.4.5 (KDE 4.4.3) --- 7 April 2010 === * Fix audio files playing silently when no volume level has been specified. === Version 2.4.4 (KDE 4.4.2) --- 17 March 2010 === * Fix display alarm whose text is generated by a command and which has an audio file, being converted into an audio-only alarm when reloaded. === Version 2.4.3 (KDE 4.4.1) --- 21 February 2010 === * Disable resource calendars which contain only wrong alarm types. === Version 2.4.2 (KDE 4.4.0) --- 30 January 2010 === * Fix non-ASCII text being corrupted in emails sent by KAlarm. * Show error message if selected email identity has no email address. === Version 2.4.1 (KDE 4.4.0 RC1) --- 8 December 2009 === * Fix date-only recurring alarms triggering repeatedly at high frequency. === Version 2.4.0 --- 24 November 2009 === * New audio alarm option, without displaying alarm window. * Add configuration setting for event duration for alarms copied to KOrganizer. * Provide 'any time' option in Defer Alarm dialogue, for date-only alarms. * Use KDE system settings to determine default working days in the week. * Improve organisation of main menu. * If dual screens, show alarm in other screen if any full screen window exists. * Fix recurring date-only alarm triggering repeatedly and eating up CPU, if the start-of-day time is after midnight and the alarm is due, but current UTC time of day is earlier than the start-of-day time of day in the alarm's time zone. * Update date-only alarm trigger times when user changes the start-of-day time. * Don't write start-of-day time into calendar, to avoid clashes if it is shared. * Don't waste processing time calculating next trigger time for archived alarms. * Disable 'New Alarm from Template' action when no alarm templates exist. * Interpret '~' (i.e. home directory) properly in entered file names. * Fix crash if calendar formats are updated at login, during session restoration. * Fix crash if editing alarm from alarm window Edit button, and window changes from reminder to normal, or window changes from at-login to final at-login trigger time, or window auto-closes. * Prevent infinite loop if NEXTRECUR time in alarm is before alarm start time. * Fix error saving the alarm after editing a repeat-at-login alarm. * Don't set reminder/late-cancel/show-in-KOrganizer when saving repeat-at-login alarms. * Improve error feedback in sound file selection. * Prevent sound file configuration dialogue closing after showing error message. === Version 2.3.0 --- 10 July 2009 === * Alarm edit: warn user if entered start time needs adjustment to fit recurrence. * Command alarm edit: show error message if no command/script has been entered. * Allow use of other command line options with --edit-new-* to initialise edit dialogue options. * Improve detection of conflicting command line options. === Version 2.2.4 --- 23 June 2009 === * Alarm edit: keep existing display file name if file select dialogue cancelled. * Guard against crashes if KAlarm quits while a modal dialogue is open. * Fix crash creating alarm from command line, if KAlarm not already running. * Fix --reminder-once command line option being treated same as --reminder. === Version 2.2.3 --- 14 June 2009 === * Fix crash when more than one alarm with audio is displayed simultaneously. === Version 2.2.2 --- 10 June 2009 === * Fix email alarms sending multiple mails, when sent by KMail. * Fix crash when closing remote calendars. === Version 2.2.1 --- 25 May 2009 === * Include new handbook translation: Ukrainian. === Version 2.2.0 --- 29 April 2009 === * Provide facility to export alarms to a new calendar file. * Provide option to spread alarm and error messages over screen. * Show command execution error indication for alarms in main window alarm list. * Add configuration setting for default deferral time in Defer Alarm dialogue. * Accept drag and drop of Todo entries to create a new alarm. === Version 2.1.8 (KDE 4.2.4) --- 25 May 2009 === * Fix crash on exit from birthday import dialogue. * Fix crash when an alarm is open for edit when its last occurrence triggers, and the edit is then saved. * Fix another possible crash when KAlarm quits. * Don't show time in alarm list for date-only alarms without time zone (e.g. those created by Import Birthdays). === Version 2.1.7 (KDE 4.2.3) --- 29 April 2009 === * Fix recurring alarms being missed when deferred to earlier than next due alarm, when next due alarm is earlier than the next recurrence. * Fix crash at startup if a non-recurring cancel-if-late alarm has been missed. * Fix speech mode not working when alarm messages are displayed. * Fix KAlarm hanging sometimes while trying to play an audio file. * Fix crash when KAlarm quits. * Fix memory leak with undo/redo. === Version 2.1.6 (KDE 4.2.2) --- 18 March 2009 === * Fix memory leaks. * Fix crash when KAlarm quits. === Version 2.1.5 (KDE 4.2.1) --- 7 February 2009 === * Disable inapplicable alarm types in alarm edit dialogue Load Template list. * Prevent multiple identical error messages being displayed for the same alarm. * Fix possible crash on alarm refresh, or removal or disabling of a resource. === Version 2.1.4 (KDE 4.2) --- 18 January 2009 === * Prevent corrupt alarms if deferral reinstates from archived alarm instead of from the displaying calendar. * Ignore events in calendar without usable alarms (which prevents them getting stuck in the alarm list, and fixes high CPU usage). * Show error message when New Template selected but no writable resource exists. * Fix crash when iCalendar item is dragged and dropped onto KAlarm. * Make New Alarm shortcuts work. * Fix alarms not being saved if created by drag-and-drop but not edited further. === Version 2.1.3 (KDE 4.2 RC1) --- 5 January 2009 === * Fix invalid alarm remaining in calendar when pre-alarm action failure message is acknowledged before the alarm is deferred. === Version 2.1.2 --- 27 December 2008 === * New KAlarm icon. * Distinguish disabled from enabled alarm colour when highlighted in alarm list. * Ensure alarm windows show on top of full-screen windows. * Fix crash if KAlarm is activated again while restoring from previous session. * Fix kalarmautostart crash on logout while kalarmautostart is still running. * Fix click on system tray icon not showing main window if 'Show in system tray' configuration setting deselected. === Version 2.1.1 (KDE 4.2 beta2) --- 8 December 2008 === * Allow global shortcuts for New Alarm actions. * Fix failure to update alarms in KOrganizer when Kontact is running but Kontact's calendar component is not loaded. * Fix toolbar configuration being lost after quitting KAlarm. === Version 2.1.0 (KDE 4.2 beta1) --- 13 November 2008 === * Add option to exclude holidays from recurring alarms. * Provide More/Less Options button in edit alarm dialogue. * Improve Configuration dialogue layout, split pages into tabs. * Show separate toolbar buttons for new display, command and email alarms. * Show 'Time Zone' button instead of time zone selection controls when using default time zone. * Set file display alarm font & colour in same way as for text display alarms. * Set default reminder time units according to how long until alarm is due. === Version 2.0.6 (KDE 4.1.3) --- 22 October 2008 === * Fix alarms not triggering correctly after laptop wakes from hibernation. * Fix inability to change or cancel alarm deferral times. * Prevent defer dialogue date being set outside the allowed range. * Set background colour for file display alarm text. * Don't wrap lines in file display alarm message windows. * Fix addition and deletion of alarms to KOrganizer. === Version 2.0.5 --- 27 September 2008 === * Fix very high CPU usage by KAlarm when there are alarms with sub-repetitions, or deferrals, with periods greater than 1 week. Fix requires kdepimlibs 4.1.3. === Version 2.0.4 (KDE 4.1.2)--- 24 September 2008 === * Add work-time-only parameter for D-Bus calls to create new alarms. === Version 2.0.3 --- 7 September 2008 === * Double click accepts selected template in pick list. * Make text in edit alarm dialogue change colour when foreground colour changed. * Replace colour combo boxes by buttons which display standard KDE colour picker. === Version 2.0.2 (KDE 4.1.1) --- 27 August 2008 === * Show alarm text entry fields in the current alarm message colours. * Show background colour selector for file display alarms. * Set KDE sound files directory as default for picking sound files. * Fix width of buttons containing only an icon. * Change Control Center references to System Settings. * Fix formatting of file display alarms for non-HTML text files. * Fix crash when birthday dialogue is opened more than once. * Prevent quitting when main window is closed but system tray icon is visible. === Version 2.0.2 --- 4 August 2008 === * Set KDE sound files directory as default for picking sound files. * Fix width of buttons containing only an icon. * Change Control Center references to System Settings. === Version 2.0.1 (KDE 4.1) --- 17 July 2008 === * Double click in template dialogue list activates template edit dialogue. * Fix KAlarm quitting on closing message window when no main window visible. * Fix KAlarm crashing when quitting. === Version 2.0.0 --- 7 July 2008 === * New facility to use multiple alarm calendar resources. * Add facility to select time zone for alarm times. * Handle summer/winter time changes correctly. * New option to trigger a recurring alarm only during working hours. * Add option for display alarm text to be generated by a command. * Provide "Don't show again for this alarm" option for command error messages. * Alarm edit dialogue layout improvements. * Make alarm edit and preferences dialogues scrollable if too high for screen. * Choose new alarm/template type from menu instead of in alarm edit dialogue. * Add option to show alarm windows in centre of screen, with buttons initially disabled to prevent accidental acknowledgement. * Remove alarm daemon (kalarmd) and do alarm monitoring in KAlarm itself. * Remove --handleEvent command line option. * Use custom properties instead of CATEGORIES in calendar events for KAlarm data. * Don't discard non-KAlarm custom event properties when editing alarms. * Use kconf_update to convert old config file settings. * Change numeric codes in config file to strings for long-term maintainability. * Rename Defaults section options in config file. * Fix detection of yearly February 29th recurrences on Feb 28th or Mar 1st. === Version 1.5.3 --- 16 June 2008 === * In New From Template menu, show list of template names in sorted order. * Fix recurrence count being lost when using alarm templates. * Prevent invalid negative values appearing in 'Time from now' edit field. * Fix time shown in alarm edit dialogue for recurring alarms. * Fix recurrence count shown in alarm edit dialogue once alarm has triggered. * Fix Find not working with a new search text after a failed search. * Display correct error message when a search fails. * Prevent user changing font/colour dialogue when editing read-only alarms. === Version 1.5.2 --- 13 February 2008 === * Prevent repetition duration error message when saving alarm which never recurs. === Version 1.5.1 (KDE 3.5.9) --- 13 February 2008 === * Fix inability to set up sub-repetitions for simple yearly recurrences. === Version 1.5.0 --- 23 January 2008 === * Replace simple repetitions with recurrence sub-repetitions, to save confusion. * Add option to enter reminder times in minutes, in addition to hours/minutes. * Replace alarm edit dialogue background colour selector with font/colour sample. * Store email unique IDs instead of names in email alarms to prevent problems if email IDs are renamed. * Fix error "Sender verify failed (in reply to RCPT TO command)" using sendmail on some systems, by adding envelope sender address to emails. * Fix OpenSolaris build error. === Version 1.4.21 --- 19 December 2007 === * Remember last used main window show/hide options instead of setting them in Preferences dialogue. * Make the Menu key work in the alarm list. * Fix crash when saving preferences, if 'xterm' is not installed in the system. * Prevent multiple identical error messages being displayed for the same alarm. === Version 1.4.20 --- 18 November 2007 === * Fix deferral of non-recurring alarms not working. * Fix loss of reminder details in archive when alarm has had a reminder deferred. * Fix inability to reactivate deleted alarms which still have repetitions to go. * Fix incorrect interpretation of --late-cancel weekly parameter on command line. === Version 1.4.19 --- 11 November 2007 === * Fix KAlarm hanging and freezing the system for a while, especially on startup. * Fix next occurrence time set after editing alarm, when it's a sub-repetition. * Prevent error messages while typing date value, until user finishes entering it. === Version 1.4.18 --- 2 November 2007 === * Fix failure to trigger some recurring date-only alarms (e.g. after suspend-resume). * Fix date-only alarms triggering every minute from midnight to start-of-day time. * Simplify recurrence text shown in alarm edit dialogue Alarm tab when possible. * Prevent error after browsing for command log file, due to file:// prefix. === Version 1.4.17 (KDE 3.5.8) --- 8 October 2007 === * Allow time-from-now values up to 999 hours to be entered. * Fix incorrect email headers resulting in failure to send some emails. === Version 1.4.16a --- 12 September 2007 === * Fix failure to retrieve font and colour settings for display alarms. === Version 1.4.16 --- 10 September 2007 === * Attempt to fix failure to retrieve font and colour settings for display alarms. * Disable reminder etc. controls for at-login recurrence in alarm edit dialogue. === Version 1.4.15 --- 7 September 2007 === * Fix deferrals of recurring alarms not triggering correctly. * Fix failure to archive details of repetitions within a recurrence. * Enable/disable "Show expired alarms" action when preferences change. === Version 1.4.14 --- 5 August 2007 === * Fix handling of exception dates in recurrences. * In sound file dialogue change Play button to a Stop button while playing a file. === Version 1.4.13 --- 18 May 2007 === * Fix time value in templates not being stored. * Expand time spin boxes to make room for all digits. * Make Preferences dialogue non-modal. === Version 1.4.12 (KDE 3.5.7) --- 11 May 2007 === * Display advance reminders for each occurrence of recurring alarms. * Fix Undo of deletion of active alarms. * Disable simple repetition controls if repetitions can't fit between recurrences. * Make the system tray tooltip take account of alarm repetitions. * Show repetition & special action status by button states in alarm edit dialogue. * Fix reminder alarms displaying very big numbers for how long until alarm is due. * Fix KMail omitting attachments from email alarms (if KMail is the email client). === Version 1.4.11 --- 16 April 2007 === * Prevent pre-alarm actions being executed multiple times when alarm is triggered. * Prevent alarm daemon triggering alarms multiple times. * Only execute pre-alarm actions once (not for reminders or deferrals). * Only execute post-alarm actions once when alarm is finally acknowledged (after any deferrals), and not after reminders. * Show file name as a tooltip on sound type combo box when "file" is selected. === Version 1.4.10 --- 3 March 2007 === * Add play button to sound file selection dialogue. * Prevent simple repetitions triggering again when KAlarm is restarted. * Fix recurring alarms being triggered on exception days. * Fix start-of-day time being ignored for date-only alarms. * Disable Defer button in new message window when deferral limit has been reached. * Fix failure to save "Execute in terminal window" option in Preferences dialogue. * Ensure up-to-date menus are displayed if user has a customised toolbar. === Version 1.4.9 (KDE 3.5.6) --- 3 January 2007 === * Minor changes. === Version 1.4.8 --- 28 December 2006 === * Fix Find always using first search text entered even after entering a new one. === Version 1.4.7 --- 14 December 2006 === * Fix crash saving Preferences dialogue (due to command alarm terminal setting). === Version 1.4.6 --- 30 November 2006 === * Fix crash if an alarm triggers while user is deleting it. * Fix "Start alarm monitoring at login" value shown in preferences dialogue. * Fix deselecting "Start alarm monitoring at login" when daemon not running. * Fix editing of 29th February alarm options for non-leap years. * Tidy up preferences dialogue Run mode options. * Tidy up alarm edit/preferences dialogue sound type options into a combo box. * Add context help for sound file fade options. === Version 1.4.5 (KDE 3.5.5) --- 29 September 2006 === * Improve alarm edit dialogue layout (Reminder controls moved to below Time box). === Version 1.4.4 --- 11 July 2006 === * Use an alarm's previous deferral time interval as default for its next deferral. === Version 1.4.3 (KDE 3.5.4) --- 11 July 2006 === * Add facility to import alarms from other calendar files. * Fix Defer dialog time interval maximum to match maximum date/time value. * Fix crash when a deferred expired recurring alarm is edited from message window. * Fix crash when a message is redisplayed after login. * Prevent inapplicable 'Unable to speak' error when alarm redisplayed after login. * Save main window column order changes to use on restart (except message column). === Version 1.3.10 (KDE 3.5.3) --- 22 May 2006 === * Add DCOP calls and command line options to display the edit alarm dialogue. * Add Select All and Deselect actions & shortcuts for import birthdays list. * Make system tray icon appear in non-KDE window managers. * Output error message if deleting copy of alarm from KOrganizer fails. * Fix corruption of alarms displayed at logout and then deferred after login. * Fix reminder time not being saved in alarm templates. * Fix erroneous date adjustment of start of recurrence when saving alarm. * Fix crash when --play command line option is used, if compiled without aRts. * Don't show disabled alarms in system tray tooltip alarm list. === Version 1.3.9 (KDE 3.5.2) --- 7 March 2006 === * Notify daemon by DCOP that alarm has been processed: to prevent alarm loss, and to prevent defunct kalarm processes when run mode is on-demand. * Add Select All and Deselect actions & shortcuts for alarm and template lists. === Version 1.3.8 --- 24 January 2006 === * Fix kalarmd hang when triggering late alarm and KAlarm run mode is on-demand. === Version 1.3.7 --- 22 January 2006 === * Fix column widths when main window is resized, if columns have been reordered. === Version 1.3.6 (KDE 3.5.1) --- 10 January 2006 === * Make autoclose of message windows work. * Fix New From Template not creating alarm if template contents are not changed. * Ensure that day and month names translations are independent of locale calendar. * Display alarm message windows within current screen in multi-head systems. * Reduce size of Preferences dialog to fit in 1024x768 screen. === Version 1.3.5 --- 14 December 2005 === * Fix email attachments being forgotten when saving alarms. * Fix toolbar configuration being lost after quitting KAlarm. === Version 1.3.4 (KDE 3.5) --- 30 October 2005 === * Fix incorrect recurrence frequency in Alarm Edit dialogue's Alarm tab. === Version 1.3.3 --- 22 September 2005 === * Add day-of-week selection to daily recurrence dialog. === Version 1.3.2 (KDE 3.5 beta 1) --- 10 September 2005 === * Add option to show alarms in KOrganizer's active calendar. * Add option for email text alarms to locate the email in KMail. * When email alarm triggers and KMail isn't running, start KMail and send mail automatically instead of opening KMail composer window. * Provide per-alarm option for yearly February 29th recurrences. * Wait longer (20 seconds) before reporting alarm daemon registration failed. * Minimise KMix window if KMix is started by KAlarm when displaying a message. * Fix Plastik style 'enabled' indication for time spinbox left-hand buttons. * Prevent message windows always being full screen after a big message is shown. * Prevent message windows being initially larger than the desktop. * Prevent message windows initially overlapping the KDE panel. * Prevent session restoration displaying main windows which should be hidden. * Fix alarms getting stuck if due during a daylight savings clock change. * Change --volume command line option short form to -V (-v is used by --version). * Fix reported shell errors when output from command alarm is discarded. * Use 'KAlarm' untranslated in calendar product ID, to cater for locale changes. === Version 1.3.1 --- 30 May 2005 === * Add Undo/Redo facility for alarm edit/creation/deletion/reactivation. * Add text search facility. * Add option to speak alarm messages (if speech synthesis is installed). * Add command line option --speak. * Add 'New alarm from template' menu option and toolbar button. * Add 'Time from now' option in alarm templates. * Add fade option for playing sound files. * Add option to log command alarm output to a file. * Add Edit button to alarm message window to allow the alarm to be edited. * Enable drag and drop of alarms to other applications. * Email drag-and-drop from KMail (KDE >= 3.5) now presets alarm edit dialog with full From/To/Cc/Subject headers and body text. === Version 1.2.8 (KDE 3.4.1) --- 9 May 2005 === * Fix failure to enable "Reminder for first recurrence only" checkbox. === Version 1.2.7 --- 20 April 2005 === * Use a sensible default for terminal window command in Preferences dialog. * Validate terminal window command entered in Preferences dialog. * Fix date range no longer being validated in Defer dialog. * Don't ignore Sound setting in Preferences dialog Edit tab. * Reset sound volume (if it was set) as soon as audio file playing is complete. * Don't start KMix when an alarm is displayed if no sound volume is specified. * Add command script and execute-in-terminal options to DCOP interface. === Version 1.2.6 (KDE 3.4) --- 22 February 2005 === * Pop up message windows far from cursor to avoid accidental acknowledgement. * Start KMix if not already running, for setting alarm sound level. * Fix alarms not triggering if IDs are duplicated in different calendar files. * Improve validation when reading configuration file values. === Version 1.2.5 (KDE 3.4 beta2) --- 21 January 2005 === * Prevent multiple "Failed to start Alarm Daemon" error messages at startup. * Fix missing left border for time spinboxes in Plastik style. === Version 1.2.4 (KDE 3.4 beta1) --- 9 January 2005 === * Provide option to enter a script for a command alarm, instead of a command line. * Add option to run command alarms in terminal windows. * Accept drag and drop of KAddressBook entries to alarm edit dialog email fields. * Drag and drop now inserts text where appropriate, rather than replacing it. * Display correct controls after loading a template in alarm edit dialog. === Version 1.2.3 --- 7 December 2004 === * Put alarm type icons in a separate, sortable, column in alarm list. * Align times in alarm list. * Fix crash when the last recurrence of an alarm is reached. * Fix random limit on expired alarm discard time if stepping with spinbox buttons. * Fix dialog layouts for right-to-left languages. * Fix time spin box layout for right-to-left languages. === Version 1.2.2 --- 27 November 2004 === * Make alarm daemon (kalarmd) exclusive to KAlarm. * Move control options for alarm daemon into KAlarm preferences dialog. * Allow user to specify the late-cancellation period for an alarm. * Add option to automatically close window after late-cancellation period. * Add facility to enable and disable individual alarms. * Add simple repetition facility, including repetition within a recurrence. * Add option to pick a KMail identity to use as sender of email alarms. * Add option to copy emails sent via sendmail, to KMail sent-mail folder. * Show scheduled times, not reminder times, in alarm list and system tray tooltip. * Make time edit controls use 12-hour clock when that is the user's default. * Also fill in alarm edit dialog email fields when email is dropped onto KAlarm. * New revised DCOP request interface (old interface still kept for compatibility). * Make detection of email message display alarms independent of language. * Use KMix whenever possible to set hardware sound volume. * Limit range of entered date/time to valid values in deferral dialogue. * Prevent kalarm failing to register with kalarmd except when really necessary. * Fix time-to-alarm column in main window not always updating every minute. === Version 1.1.7 (KDE 3.3.2) --- 27 November 2004 === * Fix KAlarm button on message windows to make it always display main window. * Show scheduled times, not reminder times, in alarm list and system tray tooltip. * Fix time-to-alarm column in main window not always updating every minute. === Version 1.1.6 (KDE 3.3.1) --- 30 September 2004 === * Prevent crash, and output error message, if menu creation fails. * Unsuppress Quit warning message box if default answer is Cancel quit. * Prevent blind copy to self of email alarms via KMail when bcc is deselected. === Version 1.1.5 --- 1 September 2004 === * Show erroneous control in alarm edit dialog when an error message is displayed. * Make alarm edit dialog always appear on current desktop. * Make weekly/monthly/yearly recurrences scheduled from command line correspond correctly to the start date. * Fix start date for monthly/yearly recurrences scheduled from the command line. * Fix DCOP triggerEvent() call to not reschedule alarm if it isn't due yet. === Version 1.1.4 --- 21 August 2004 === * Fix errors when altering or cancelling deferrals of expired recurrences. === Version 1.1.3 (KDE 3.3) --- 28 July 2004 === * Fix dialog sizing the first time KAlarm is run. === Version 1.1.2 (KDE 3.3 beta2) --- 11 July 2004 === * Fix hangup in interactions with alarm daemon introduced in version 1.1.1. * Only tick Alarms Enabled menu items once alarms have actually been enabled. * Fix build for "./configure --without-arts". === Version 1.1.1 (KDE 3.3 beta1) --- 20 June 2004 === * Output error message and disable alarms if can't register with alarm daemon. * Exit if error in alarm calendar name configuration. * Fix bug where sound file is selected even when Cancel is pressed. === Version 1.1.0 --- 1 June 2004 === * Add facility to define alarm templates. * Add facility to specify pre- and post-alarm shell command actions. * Add option to play sound file repeatedly until alarm window is closed. * Add volume control for playing sound file. * Add 'stop sound' button to alarm message window when sound file is played. * Rename command line option --sound to --play, add option --play-repeat. * Add command line option --volume. * Add 'Configure Shortcuts' and 'Configure Toolbars' menu options in main window. * After creating/editing alarm, prompt to re-enable alarms if currently disabled. * Middle mouse button over system tray icon displays new alarm dialog. * Add option to display a reminder once only before the first alarm recurrence. * Display time-to-alarm in reminder message window. * For message texts which are truncated in main window, show full text in tooltip. * Allow time of day to be entered in format HHMM in time spin boxes. * Allow hour to be omitted when colon format time is entered in time spin boxes. * Add "Don't ask again" option to alarm deletion confirmation prompt. * Prevent expired alarm calendar purges clashing with other alarm actions. * Fix initial recurrence date/time for weekly/monthly/yearly recurrences. * Fix yearly recurrences of the last day in the month. * Disable yearly recurrence's month checkboxes depending on selected day of month. * Update which time columns are displayed in alarm list when Preferences change. * Don't store audio/reminder details in email/command alarms. * Don't store email details in message/file/command alarms. * Don't close message windows when quit is selected. * Fix "Warn before quitting" configuration option. * Don't redisplay error message windows on session restoration. * Remove obsolete --displayEvent command line option (replaced by --triggerEvent). * Remove obsolete pre-version 0.7 DCOP calls. === Version 1.0.7 --- 2 May 2004 === * Fix scheduleCommand() and scheduleEmail() DCOP handling. * Make KAlarm build for "./configure --without-arts". * Fix email body text not being saved in email alarms. * Fix loss of --exec command line arguments. * Remove wasted vertical space from message windows. === Version 1.0.6 (KDE 3.2.2) --- 26 March 2004 === * Make the Quit menu item in main window quit the program. * Update time entry field after editing as soon as mouse cursor leaves it. * Cancel deferral if reminder is set before it, to prevent it becoming stuck. * Prevent undeleted recurring alarms being triggered immediately. * Don't allow alarms to be undeleted if they are completely expired. === Version 1.0.5 (KDE 3.2.1) --- 24 February 2004 === * Fix whatsThis text on bottom row of alarm list. === Version 1.0.4 --- 22 February 2004 === * Fix freeze at login when multiple alarms trigger. * Show all audio file types in sound file chooser dialogue. === Version 1.0.3 --- 15 February 2004 === * Prevent email alarms from being sent if no 'From' address is configured. * Omit 'Bcc' when sending email alarms if no 'Bcc' address is configured. * Fix freeze when starting the alarm daemon. * Fix memory leaks displaying dialogs. * Fix scheduleCommand() and scheduleEmail() DCOP handling. * Fix errors saving expired alarm calendar. === Version 1.0.2 (KDE 3.2) --- 29 January 2004 === * Prevent editing alarm and saving without changes from deleting the alarm. === Version 1.0.1 --- 4 January 2004 === * Fix failure to see alarms if KAlarm is reactivated while restoring session. === Version 1.0.0 --- 7 December 2003 === * Allow entered start date for timed recurrence events to be earlier than now. * Prevent attempted entry of recurrence end date earlier than start date or today. * Fix error displaying time of expired repeat-at-login alarms. * Fix memory leak when sending emails with attachments. * Fix error trying to send emails with very small attachments. * Eliminate duplicate reload-calendar calls to alarm daemon. === Version 0.9.6 (KDE 3.2 beta1) --- 7 November 2003 === * Add option to choose foreground colour for alarm messages. * Create new alarm by dragging KMail email onto main window or system tray icon. * Set initial recurrence defaults to correspond to alarm start date. * Add option for how February 29th recurrences are handled in non-leap years. * Monthly/yearly recurrence edit: adhere to user preference for start day of week. * Eliminate multiple confirmation prompts when deleting multiple alarms. * Eliminate duplicate alarms in system tray tooltip. * Fix crash after reporting error opening calendar file. * Fix wrong status in system tray icon if KAlarm starts up with alarms disabled. * Fix wrong number of days in Time-to-alarm column in main window. * Fix omission of deferred alarms from system tray tooltip. === Version 0.9.5 --- 3 September 2003 === * Add option for non-modal alarm message windows. * Add option to display a notification when an email alarm queues an email. * Emails via KMail are sent without opening composer window, if KMail is running. * Provide separate configuration for 'From' and 'Bcc' addresses for email alarms. * Add exceptions to recurrence specification. * Add multiple month selection to yearly recurrence. * Add day of month selection in yearly recurrence. * Add last day of month option in monthly and yearly recurrences. * Add 2nd - 5th last week of month options in monthly and yearly recurrences. * Add filename completion to file and command alarm edit fields. * Display alarms-disabled indication in system tray tooltip. * Enable file alarms to display image files. * Fix file alarms not dislaying some text files, and improve HTML file display. * Fix loss of changes to attachment list after editing email alarms. * Fix wrong recurrence end date being displayed when editing an existing alarm. === Version 0.9.4 --- 3 July 2003 === * Add time-to-alarm display option to main alarm list. * Add option to list next 24 hours' alarms in system tray tooltip. * Create new alarm by dragging text or URL onto main window or system tray icon. * Display reasons for failure to send an email. * Allow editing of the list of message colours. * Edit new alarm by context menu or double click on white space in alarm list. * Add show expired alarms option to preferences dialog. * Display HTML files correctly in file display alarms. === Version 0.9.3 --- 4 March 2003 === * Add preferences option to set default sound file for the Edit Alarm dialog. * Fix display of "Invalid date" message before Edit Alarm dialog displays. === Version 0.9.2 --- 28 February 2003 === * Option to set font for individual alarm messages. * Allow multiple alarm selection in the main window. * KAlarm icon in alarm message window selects the alarm in the main window. * In Edit Alarm dialog, move all recurrence edit controls into Recurrence tab. * Add quit warning message option to preferences dialog. * Add "New Alarm" option to system tray context menu. * Disallow command alarms when KDE is running in kiosk mode. * Revised storage of beep, font, colour and program arguments in calendar file. * Always save alarms in iCalendar format (but vCalendar may still be read). * Add reminder, recurrence and font parameters to DCOP calls. * Fix failure to enable alarms when running in on-demand mode. === Version 0.9.1 --- 16 January 2003 === * Add option to set advance reminders for display alarms. * In run-in-system-tray mode, warn that alarms will be disabled before quitting. * Fix monthly and yearly recurrences on nth Monday etc. of the month. * Fix yearly recurrences on February 29th. * Fix recurrence start times stored in expired calendar file. * Fix extra empty events being stored in expired calendar file. === Version 0.9.0 --- 3 January 2003 === * Add facility to import birthdays from KAddressBook * Add option to send an email instead of displaying an alarm message. * Add option to store and view expired alarms. * Add copy, view and undelete actions (as applicable) for the selected alarm. * In alarm message window, message text can be copied to clipboard using mouse. * Allow message text to be scrolled in alarm message window if too big to fit. * Shift key with left mouse button steps time edit arrows by 5 minutes/6 hours. * Report failure to run command alarm (bash, ksh shells only). * Retain repeat-at-login status on alarm deferral. * Restore alarm messages which were displayed before KAlarm was killed or crashed. * Store alarm data in the calendar file in a more standard way. * Alarm message defer dialog: update recurrence deferral time limit in real time. * Weekly recurrence edit: adhere to user preference for start day of week. * Use standard action icons. === Version 0.8.5 (KDE 3.1.1) --- 21 February 2003 === * Fix monthly and yearly recurrences on nth Monday etc. of the month. * Fix yearly recurrences on February 29th. * Fix failure to enable alarms when running in on-demand mode. === Version 0.8.4 (KDE 3.1) --- 8 January 2003 === * Make KAlarm icon in message window bring main window to current desktop. * Fix detection of KDE desktop. * Fix entry of yearly recurrences on a specified date in the year. === Version 0.8.3 --- 9 November 2002 === * Fix no system tray icon being displayed. * Fix multiple system tray icons being displayed. * Fix alarms being missed after changing "Disable alarms when not running" status. === Version 0.8.2 --- 2 November 2002 === * Fix audio files not playing. === Version 0.8.1 --- 1 November 2002 === * Adhere to KDE single/double click setting when clicking on alarm list. * Fix possible loss of alarms if KAlarm has previously used another calendar file. * Fix coordination between "At time" and "After time" values when they change. * Always remove alarm deferral even when next recurrence triggers instead. * When alarm triggers, replace any existing repeat-at-login alarm message window. * Fix deselection of Sound not working after selecting a sound file. * Fix display of hour spin buttons in time edit spin boxes. * Prevent time edit spin box buttons from selecting the text. * Clean up previous alarm list highlight properly when a new alarm is selected. * Set sensible initial focus when edit alarm dialog pages are displayed. * Fix Quit duplicate entry in system tray context menu. === Version 0.8 (KDE 3.1 beta2) --- 16 September 2002 === * Move recurrence edit to separate tab in alarm dialog (now fits 800x600 display). * Add accelerator keys in dialogs. * Provide date picker for entering dates. === Version 0.7.5 --- 1 September 2002 === * Add preferences options to choose default settings for the Edit Alarm dialog. * Fix right-to-left character sets not being displayed in message edit control. * Make "Help -> Report Bug" use the KDE bug system (bug #43250). * Fix session restoration not occurring. === Version 0.7.4 (KDE 3.1 beta1) --- 5 August 2002 === * Add option to prompt for confirmation on alarm deletion. * Add option to prompt for confirmation on alarm acknowedgement. * Display KAlarm handbook Preferences section when Help clicked in config dialog. * Correctly adjust wrong summer times stored by version 0.5.7 (KDE 3.0.0). === Version 0.7.3 --- 24 July 2002 === * Fix loss of alarm times after saving pre-version 0.7 calendar file. * Fix main alarm list display of hours or hours/minutes repeat interval. * Display KAlarm handbook when Help clicked in configuration dialog. === Version 0.7.2 --- 2 July 2002 === * Fix reading wrong alarm times from pre-version 0.7 calendar file. * Partially fix loss of alarm times after saving pre-version 0.7 calendar file. === Version 0.7.1 --- 29 June 2002 === * Prevent duplicate message windows from being displayed. * Make Close button on message window not the default button to reduce chance of accidental acknowledgement. * Fix non-ASCII message texts being saved as question marks. * Fix memory leak with recurrences. === Version 0.7.0 --- 15 June 2002 === * Add option to play audio file when message is displayed. * Add daily, weekly, monthly, annual recurrences. * Allow deferring only up to next scheduled repetition time. * Don't defer repetitions when an alarm is deferred. * Make regular repetition and repeat-at-login mutually exclusive. * Double click on alarm in main window opens alarm edit dialog. * Change Reset Daemon menu option to Refresh Alarms. * Save and restore window sizes. === Version 0.6.4 --- 8 May 2002 === * Make click on system tray icon always bring KAlarm to top on current desktop. * Fix alarms not being triggered (depending on time zone). === Version 0.6.0 --- 8 March 2002 === * Add option to execute a command instead of displaying an alarm message. * Add Try button to alarm message edit dialog. * Add icons in the alarm list to indicate each alarm's type. * Display error message if a file to be displayed is not a text file. * Reduce chance of lost late-cancel alarms when daemon check interval is reduced. * Rename command line option --displayEvent to --triggerEvent. * Rename DCOP function displayMessage() to triggerEvent(). * Rename DCOP function cancelMessage() to cancelEvent(). === Version 0.5.8 (KDE 3.0.5A) --- 23 November 2002 === * Fix detection of KDE desktop. === Version 0.5.8 (KDE 3.0.5) --- 4 October 2002 === * Fix possible loss of alarms if KAlarm has previously used another calendar file. === Version 0.5.8 (KDE 3.0.4) --- 18 August 2002 === * Make "Help -> Report Bug" use the KDE bug system (bug #43250). * Fix right-to-left character sets not being displayed in message edit control. === Version 0.5.8 (KDE 3.0.3) --- 5 August 2002 === * Adjust wrong summer times stored by version 0.5.7 (KDE 3.0.0). * Display KAlarm handbook when Help clicked in configuration dialog. * Make Close button on message window not the default button to reduce chance of accidental acknowledgement. * Fix session restoration often not occurring at login. === Version 0.5.7 (KDE 3.0.1) --- 9 May 2002 === * Use local time for alarm times instead of using a time zone. * Make click on system tray icon always bring KAlarm to top on current desktop. === Version 0.5.7 (KDE 3.0) --- 17 March 2002 === * Show system tray icon on deferring command line-initiated message (run-in- system-tray mode). * Associate main window with system tray icon when displayed from message window. * Don't start KAlarm at login, until it has been run for the first time. * Add startup notification to kalarm.desktop. * Prevent open main window from cancelling KDE session shutdown. * Fix failure to display messages after daemon is restarted (run-on-demand mode). * Fix possible failure to display command line-initiated message. * Fix crash in some circumstances on changing run mode to run-on-demand. * Fix crash on clicking KAlarm icon in command line-initiated message window. * Fix crash on deferring alarm in command line-initiated message window. * Fix duplication of repeat-at-login alarms at login. * Fix error displaying text file messages. === Version 0.5.4 --- 7 February 2002 === * Fix extra window being displayed in session restoration. === Version 0.5.2 --- 31 January 2002 === * Fix session restore crash if in 'run continuously in system tray' mode. === Version 0.5.1 --- 30 January 2002 === * Change configuration defaults. === Version 0.5 --- 29 January 2002 === * Incorporate system tray icon into KAlarm, add --tray option. * Add 'run continuously in system tray' operating mode. * Don't use alarm daemon GUI application. * Add enable/disable alarms option to main window menu. * Add show/hide system tray icon option to main window menu. * Add toolbar. * Rename alarm dialog Set Alarm button to OK. * Rename message window OK button to Close. * Remove keyboard accelerator for Reset Daemon. * Fix magnified system tray icon. * Include README, etc. files in installation. === Version 0.4 --- 22 December 2001 === * Modify to use split alarm daemon/alarm daemon GUI. * Prevent a command line error exiting all open KAlarm windows. * Ensure the program exits after starting with --stop or --reset options. === Version 0.3.5 --- 5 December 2001 === * Add option to repeat alarms at login. * Add context help button to main window and message window. * Fix occasional crash on displaying non-repeating alarms. * Fix possible failure to display alarms at login. * Fix blank title bar when main window restored at login. * Fix alarms not deleted from main window when displayed at login. * Fix handling of zero-length calendar file. * Improve error messages. * Make documentation files installation dependent on KDE version. === Version 0.3.1 --- 20 November 2001 === * Fix build fault when using ./configure --enable-final === Version 0.3 --- 4 November 2001 === * Add option to display a file's contents instead of specifying a message. * Add dialog option to set an alarm's time as an interval from the current time. * Add defer option to alarm message window. * Provide button in alarm message window to activate KAlarm. * Make dialogs modal only for their parent window. === Version 0.2 --- 20 October 2001 === * Implement repeating alarms. * Add extra pair of arrow buttons to time spinbox to change the hour. * Fix sorting by colour column. * Better What's This? texts for the main window. * Remove -r, -s short options (use --reset, --stop instead). === Version 0.1.1 --- 1 September 2001 === * Fix documentation not being created by build. === Version 0.1 --- 31 August 2001 === * Initial release. diff --git a/src/alarmcalendar.cpp b/src/alarmcalendar.cpp index c73573a2..920df0a3 100644 --- a/src/alarmcalendar.cpp +++ b/src/alarmcalendar.cpp @@ -1,1259 +1,1325 @@ /* * alarmcalendar.cpp - KAlarm calendar file access * Program: kalarm * Copyright © 2001-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "alarmcalendar.h" #include "kalarm.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "preferences.h" #include "resources/datamodel.h" #include "resources/resources.h" #include "lib/messagebox.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace KCalendarCore; using namespace KAlarmCal; static const QString displayCalendarName = QStringLiteral("displaying.ics"); -static const ResourceId DISPLAY_COL_ID = -1; // resource ID used for displaying calendar +static const ResourceId DISPLAY_RES_ID = -1; // resource ID used for displaying calendar -AlarmCalendar* AlarmCalendar::mResourcesCalendar = nullptr; -AlarmCalendar* AlarmCalendar::mDisplayCalendar = nullptr; +ResourcesCalendar* ResourcesCalendar::mInstance = nullptr; +DisplayCalendar* DisplayCalendar::mInstance = nullptr; /****************************************************************************** -* Initialise the alarm calendars, and ensure that their file names are different. -* There are 2 calendars: -* 1) A resources calendar containing the active alarms, archived alarms and -* alarm templates; -* 2) A user-specific one which contains details of alarms which are currently -* being displayed to that user and which have not yet been acknowledged; -* Reply = true if success, false if calendar name error. +* Initialise backend calendar access. */ -bool AlarmCalendar::initialiseCalendars() +void AlarmCalendar::initialise() { - QDir dir; - dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); - const QString displayCal = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1Char('/') + displayCalendarName; - DataModel::initialise(); Preferences::setBackend(Preferences::Akonadi); Preferences::self()->save(); - mResourcesCalendar = new AlarmCalendar(); - mDisplayCalendar = new AlarmCalendar(displayCal, CalEvent::DISPLAYING); KACalendar::setProductId(KALARM_NAME, KALARM_VERSION); CalFormat::setApplication(QStringLiteral(KALARM_NAME), QString::fromLatin1(KACalendar::icalProductId())); - return true; } /****************************************************************************** -* Terminate access to all calendars. +* Initialise the resource alarm calendars, and ensure that their file names are +* different. The resources calendar contains the active alarms, archived alarms +* and alarm templates; +*/ +void ResourcesCalendar::initialise() +{ + AlarmCalendar::initialise(); + mInstance = new ResourcesCalendar(); +} + +/****************************************************************************** +* Initialise the display alarm calendar. +* It is user-specific, and contains details of alarms which are currently being +* displayed to that user and which have not yet been acknowledged; */ -void AlarmCalendar::terminateCalendars() +void DisplayCalendar::initialise() { - delete mResourcesCalendar; - mResourcesCalendar = nullptr; - delete mDisplayCalendar; - mDisplayCalendar = nullptr; + QDir dir; + dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); + const QString displayCal = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1Char('/') + displayCalendarName; + mInstance = new DisplayCalendar(displayCal); +} + +/****************************************************************************** +* Terminate access to the resource calendars. +*/ +void ResourcesCalendar::terminate() +{ + delete mInstance; + mInstance = nullptr; +} + +/****************************************************************************** +* Terminate access to the display calendar. +*/ +void DisplayCalendar::terminate() +{ + delete mInstance; + mInstance = nullptr; } /****************************************************************************** * Return the display calendar, opening it first if necessary. */ -AlarmCalendar* AlarmCalendar::displayCalendarOpen() +DisplayCalendar* DisplayCalendar::instanceOpen() { - if (mDisplayCalendar->open()) - return mDisplayCalendar; - qCCritical(KALARM_LOG) << "AlarmCalendar::displayCalendarOpen: Open error"; + if (mInstance->open()) + return mInstance; + qCCritical(KALARM_LOG) << "DisplayCalendar::instanceOpen: Open error"; return nullptr; } /****************************************************************************** * Find and return the event with the specified ID. * The calendar searched is determined by the calendar identifier in the ID. */ -KAEvent* AlarmCalendar::getEvent(const EventId& eventId) +KAEvent* ResourcesCalendar::getEvent(const EventId& eventId) { if (eventId.eventId().isEmpty()) return nullptr; - return mResourcesCalendar->event(eventId); + return mInstance->event(eventId); } /****************************************************************************** * Constructor for the resources calendar. */ AlarmCalendar::AlarmCalendar() - : mCalType(RESOURCES) - , mEventType(CalEvent::EMPTY) +{ +} + +/****************************************************************************** +* Constructor for the resources calendar. +*/ +ResourcesCalendar::ResourcesCalendar() + : AlarmCalendar() { Resources* resources = Resources::instance(); - connect(resources, &Resources::resourceAdded, this, &AlarmCalendar::slotResourceAdded); - connect(resources, &Resources::eventsAdded, this, &AlarmCalendar::slotEventsAdded); - connect(resources, &Resources::eventsToBeRemoved, this, &AlarmCalendar::slotEventsToBeRemoved); - connect(resources, &Resources::eventUpdated, this, &AlarmCalendar::slotEventUpdated); - connect(resources, &Resources::resourcesPopulated, this, &AlarmCalendar::slotResourcesPopulated); - connect(resources, &Resources::settingsChanged, this, &AlarmCalendar::slotResourceSettingsChanged); + connect(resources, &Resources::resourceAdded, this, &ResourcesCalendar::slotResourceAdded); + connect(resources, &Resources::eventsAdded, this, &ResourcesCalendar::slotEventsAdded); + connect(resources, &Resources::eventsToBeRemoved, this, &ResourcesCalendar::slotEventsToBeRemoved); + connect(resources, &Resources::eventUpdated, this, &ResourcesCalendar::slotEventUpdated); + connect(resources, &Resources::resourcesPopulated, this, &ResourcesCalendar::slotResourcesPopulated); + connect(resources, &Resources::settingsChanged, this, &ResourcesCalendar::slotResourceSettingsChanged); // Fetch events from all resources which already exist. QVector allResources = Resources::enabledResources(); for (Resource& resource : allResources) slotResourceAdded(resource); } /****************************************************************************** * Constructor for the display calendar file. */ -AlarmCalendar::AlarmCalendar(const QString& path, CalEvent::Type type) - : mDisplayCalPath(path) +DisplayCalendar::DisplayCalendar(const QString& path) + : AlarmCalendar() + , mDisplayCalPath(path) , mDisplayICalPath(path) - , mEventType(type) { - Q_ASSERT(type == CalEvent::DISPLAYING); - mDisplayICalPath.replace(QStringLiteral("\\.vcs$"), QStringLiteral(".ics")); mCalType = (mDisplayCalPath == mDisplayICalPath) ? LOCAL_ICAL : LOCAL_VCAL; // is the calendar in ICal or VCal format? } -AlarmCalendar::~AlarmCalendar() +ResourcesCalendar::~ResourcesCalendar() { close(); } -/****************************************************************************** -* Check whether the calendar is open. -*/ -bool AlarmCalendar::isOpen() +DisplayCalendar::~DisplayCalendar() { - return mOpen; + close(); } /****************************************************************************** * Open the calendar if not already open, and load it into memory. */ -bool AlarmCalendar::open() +bool DisplayCalendar::open() { if (isOpen()) return true; - if (mCalType == RESOURCES) + + // Open the display calendar. + qCDebug(KALARM_LOG) << "DisplayCalendar::open:" << mDisplayCalPath; + if (!mCalendarStorage) { - mOpen = true; + MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); + mCalendarStorage = FileStorage::Ptr(new FileStorage(calendar, mDisplayCalPath)); } - else - { - // Open the display calendar. - qCDebug(KALARM_LOG) << "AlarmCalendar::open:" << mDisplayCalPath; - if (!mDisplayCalStorage) - { - MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); - mDisplayCalStorage = FileStorage::Ptr(new FileStorage(calendar, mDisplayCalPath)); - } - // Check for file's existence, assuming that it does exist when uncertain, - // to avoid overwriting it. - QFileInfo fi(mDisplayCalPath); - if (!fi.exists() || !fi.isFile() || load() == 0) - { - // The calendar file doesn't yet exist, or it's zero length, so create a new one - if (saveCal(mDisplayICalPath)) - load(); - } + // Check for file's existence, assuming that it does exist when uncertain, + // to avoid overwriting it. + QFileInfo fi(mDisplayCalPath); + if (!fi.exists() || !fi.isFile() || load() == 0) + { + // The calendar file doesn't yet exist, or it's zero length, so create a new one + if (saveCal(mDisplayICalPath)) + load(); } + if (!mOpen) { - mDisplayCalStorage->calendar().clear(); - mDisplayCalStorage.clear(); + mCalendarStorage->calendar().clear(); + mCalendarStorage.clear(); } return isOpen(); } /****************************************************************************** * Load the calendar into memory. * Reply = 1 if success * = 0 if zero-length file exists. * = -1 if failure to load calendar file * = -2 if instance uninitialised. */ -int AlarmCalendar::load() +int DisplayCalendar::load() { - if (mCalType != RESOURCES) + // Load the display calendar. + if (!mCalendarStorage) + return -2; + + qCDebug(KALARM_LOG) << "DisplayCalendar::load:" << mDisplayCalPath; + if (!mCalendarStorage->load()) { - // Load the display calendar. - if (!mDisplayCalStorage) - return -2; + // Load error. Check if the file is zero length + QFileInfo fi(mDisplayCalPath); + if (fi.exists() && !fi.size()) + return 0; // file is zero length - qCDebug(KALARM_LOG) << "AlarmCalendar::load:" << mDisplayCalPath; - if (!mDisplayCalStorage->load()) - { - // Load error. Check if the file is zero length - QFileInfo fi(mDisplayCalPath); - if (fi.exists() && !fi.size()) - return 0; // file is zero length - - qCCritical(KALARM_LOG) << "AlarmCalendar::load: Error loading calendar file '" << mDisplayCalPath <<"'"; - KAMessageBox::error(MainWindow::mainMainWindow(), - xi18nc("@info", "Error loading calendar:%1Please fix or delete the file.", mDisplayCalPath)); - // load() could have partially populated the calendar, so clear it out - mDisplayCalStorage->calendar()->close(); - mDisplayCalStorage->calendar().clear(); - mDisplayCalStorage.clear(); - mOpen = false; - return -1; - } - QString versionString; - KACalendar::updateVersion(mDisplayCalStorage, versionString); // convert events to current KAlarm format for when calendar is saved - updateDisplayKAEvents(); + qCCritical(KALARM_LOG) << "DisplayCalendar::load: Error loading calendar file '" << mDisplayCalPath <<"'"; + KAMessageBox::error(MainWindow::mainMainWindow(), + xi18nc("@info", "Error loading calendar:%1Please fix or delete the file.", mDisplayCalPath)); + // load() could have partially populated the calendar, so clear it out + mCalendarStorage->calendar()->close(); + mCalendarStorage->calendar().clear(); + mCalendarStorage.clear(); + mOpen = false; + return -1; } + QString versionString; + KACalendar::updateVersion(mCalendarStorage, versionString); // convert events to current KAlarm format for when calendar is saved + updateKAEvents(); + mOpen = true; return 1; } +/****************************************************************************** +* Reload the calendar resources into memory. +*/ +bool ResourcesCalendar::reload() +{ + qCDebug(KALARM_LOG) << "ResourcesCalendar::reload"; + bool ok = true; + QVector resources = Resources::enabledResources(); + for (Resource& resource : resources) + ok = ok && resource.reload(); + return ok; +} + /****************************************************************************** * Reload the calendar file into memory. */ -bool AlarmCalendar::reload() +bool DisplayCalendar::reload() { - if (mCalType == RESOURCES) - return true; - if (!mDisplayCalStorage) + if (!mCalendarStorage) return false; - qCDebug(KALARM_LOG) << "AlarmCalendar::reload:" << mDisplayCalPath; + qCDebug(KALARM_LOG) << "DisplayCalendar::reload:" << mDisplayCalPath; close(); return open(); } +/****************************************************************************** +* Save the calendar. +*/ +bool ResourcesCalendar::save() +{ + bool ok = true; + // Get all enabled, writable resources. + QVector resources = Resources::enabledResources(CalEvent::EMPTY, true); + for (Resource& resource : resources) + ok = ok && resource.save(); + return ok; +} + +/****************************************************************************** +* Save the calendar. +*/ +bool DisplayCalendar::save() +{ + return saveCal(); +} + /****************************************************************************** * Save the calendar from memory to file. * If a filename is specified, create a new calendar file. */ -bool AlarmCalendar::saveCal(const QString& newFile) +bool DisplayCalendar::saveCal(const QString& newFile) { - if (mCalType == RESOURCES) - return true; - if (!mDisplayCalStorage) + if (!mCalendarStorage) return false; if (!mOpen && newFile.isEmpty()) return false; - qCDebug(KALARM_LOG) << "AlarmCalendar::saveCal:" << "\"" << newFile << "\"," << mEventType; + qCDebug(KALARM_LOG) << "DisplayCalendar::saveCal:" << "\"" << newFile; QString saveFilename = newFile.isEmpty() ? mDisplayCalPath : newFile; if (mCalType == LOCAL_VCAL && newFile.isNull()) saveFilename = mDisplayICalPath; - mDisplayCalStorage->setFileName(saveFilename); - mDisplayCalStorage->setSaveFormat(new ICalFormat); - if (!mDisplayCalStorage->save()) + mCalendarStorage->setFileName(saveFilename); + mCalendarStorage->setSaveFormat(new ICalFormat); + if (!mCalendarStorage->save()) { - qCCritical(KALARM_LOG) << "AlarmCalendar::saveCal: Saving" << saveFilename << "failed."; + qCCritical(KALARM_LOG) << "DisplayCalendar::saveCal: Saving" << saveFilename << "failed."; KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "Failed to save calendar to %1", mDisplayICalPath)); return false; } if (mCalType == LOCAL_VCAL) { // The file was in vCalendar format, but has now been saved in iCalendar format. mDisplayCalPath = mDisplayICalPath; mCalType = LOCAL_ICAL; } - Q_EMIT calendarSaved(this); - - mUpdateSave = false; return true; } /****************************************************************************** * Close display calendar file at program exit. */ -void AlarmCalendar::close() +void ResourcesCalendar::close() { - if (mCalType != RESOURCES) + // Resource map should be empty, but just in case... + while (!mResourceMap.isEmpty()) + removeKAEvents(mResourceMap.begin().key(), true, CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE | CalEvent::DISPLAYING); +} + +/****************************************************************************** +* Close display calendar file at program exit. +*/ +void DisplayCalendar::close() +{ + if (mCalendarStorage) { - if (mDisplayCalStorage) - { - mDisplayCalStorage->calendar()->close(); - mDisplayCalStorage->calendar().clear(); - mDisplayCalStorage.clear(); - } + mCalendarStorage->calendar()->close(); + mCalendarStorage->calendar().clear(); + mCalendarStorage.clear(); } // Flag as closed now to prevent removeKAEvents() doing silly things // when it's called again mOpen = false; // Resource map should be empty, but just in case... while (!mResourceMap.isEmpty()) - removeKAEvents(mResourceMap.begin().key(), true, CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE | CalEvent::DISPLAYING); + removeKAEvents(mResourceMap.begin().key(), CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE | CalEvent::DISPLAYING); } /****************************************************************************** * Create a KAEvent instance corresponding to each KCalendarCore::Event in the * display calendar, and store them in the event map in place of the old set. * Called after the display calendar has completed loading. */ -void AlarmCalendar::updateDisplayKAEvents() +void DisplayCalendar::updateKAEvents() { - if (mCalType == RESOURCES) - return; - qCDebug(KALARM_LOG) << "AlarmCalendar::updateDisplayKAEvents"; - const ResourceId key = DISPLAY_COL_ID; + qCDebug(KALARM_LOG) << "DisplayCalendar::updateKAEvents"; + const ResourceId key = DISPLAY_RES_ID; KAEvent::List& events = mResourceMap[key]; for (KAEvent* event : events) { mEventMap.remove(EventId(key, event->id())); delete event; } events.clear(); - mEarliestAlarm[key] = nullptr; - Calendar::Ptr cal = mDisplayCalStorage->calendar(); + Calendar::Ptr cal = mCalendarStorage->calendar(); if (!cal) return; const Event::List kcalevents = cal->rawEvents(); for (Event::Ptr kcalevent : kcalevents) { if (kcalevent->alarms().isEmpty()) continue; // ignore events without alarms KAEvent* event = new KAEvent(kcalevent); if (!event->isValid()) { - qCWarning(KALARM_LOG) << "AlarmCalendar::updateDisplayKAEvents: Ignoring unusable event" << kcalevent->uid(); + qCWarning(KALARM_LOG) << "DisplayCalendar::updateKAEvents: Ignoring unusable event" << kcalevent->uid(); delete event; continue; // ignore events without usable alarms } event->setResourceId(key); events += event; mEventMap[EventId(key, kcalevent->uid())] = event; } } /****************************************************************************** * Delete a calendar and all its KAEvent instances of specified alarm types from * the lists. * Called after the calendar is deleted or alarm types have been disabled, or * the AlarmCalendar is closed. */ -void AlarmCalendar::removeKAEvents(ResourceId key, bool closing, CalEvent::Types types) +bool AlarmCalendar::removeKAEvents(ResourceId key, CalEvent::Types types) { bool removed = false; ResourceMap::Iterator rit = mResourceMap.find(key); if (rit != mResourceMap.end()) { KAEvent::List retained; KAEvent::List& events = rit.value(); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = events[i]; bool remove = (event->resourceId() != key); if (remove) { - if (key != DISPLAY_COL_ID) + if (key != DISPLAY_RES_ID) qCCritical(KALARM_LOG) << "AlarmCalendar::removeKAEvents: Event" << event->id() << ", resource" << event->resourceId() << "Indexed under resource" << key; } else remove = event->category() & types; if (remove) { mEventMap.remove(EventId(key, event->id())); delete event; removed = true; } else retained.push_back(event); } if (retained.empty()) mResourceMap.erase(rit); else events.swap(retained); } - if (removed) + return removed; +} + +bool ResourcesCalendar::removeKAEvents(ResourceId key, bool closing, CalEvent::Types types) +{ + if (!AlarmCalendar::removeKAEvents(key, types)) + return false; + + mEarliestAlarm.remove(key); + // Emit signal only if we're not in the process of closing the calendar + if (!closing) { - mEarliestAlarm.remove(key); - // Emit signal only if we're not in the process of closing the calendar - if (!closing && mOpen) - { - Q_EMIT earliestAlarmChanged(); - if (mHaveDisabledAlarms) - checkForDisabledAlarms(); - } + Q_EMIT earliestAlarmChanged(); + if (mHaveDisabledAlarms) + checkForDisabledAlarms(); } + return true; } /****************************************************************************** * Called when the enabled or read-only status of a resource has changed. * If the resource is now disabled, remove its events from the calendar. */ -void AlarmCalendar::slotResourceSettingsChanged(Resource& resource, ResourceType::Changes change) +void ResourcesCalendar::slotResourceSettingsChanged(Resource& resource, ResourceType::Changes change) { if (change & ResourceType::Enabled) { if (resource.isValid()) { // For each alarm type which has been disabled, remove the // resource's events from the map, but not from the resource. const CalEvent::Types enabled = resource.enabledTypes(); const CalEvent::Types disabled = ~enabled & (CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE); removeKAEvents(resource.id(), false, disabled); // For each alarm type which has been enabled, add the resource's // events to the map. if (enabled != CalEvent::EMPTY) slotEventsAdded(resource, resource.events()); } } } /****************************************************************************** * Called when all resources have been populated for the first time. */ -void AlarmCalendar::slotResourcesPopulated() +void ResourcesCalendar::slotResourcesPopulated() { // Now that all calendars have been processed, all repeat-at-login alarms // will have been triggered. Prevent any new or updated repeat-at-login // alarms (e.g. when they are edited by the user) triggering from now on. mIgnoreAtLogin = true; } /****************************************************************************** * Called when a resource has been added. * Add its KAEvent instances to those held by AlarmCalendar. * All events must have their resource ID set. */ -void AlarmCalendar::slotResourceAdded(Resource& resource) +void ResourcesCalendar::slotResourceAdded(Resource& resource) { slotEventsAdded(resource, resource.events()); } /****************************************************************************** * Called when events have been added to a resource. * Add corresponding KAEvent instances to those held by AlarmCalendar. * All events must have their resource ID set. */ -void AlarmCalendar::slotEventsAdded(Resource& resource, const QList& events) +void ResourcesCalendar::slotEventsAdded(Resource& resource, const QList& events) { for (const KAEvent& event : events) slotEventUpdated(resource, event); } /****************************************************************************** * Called when an event has been changed in a resource. * Change the corresponding KAEvent instance held by AlarmCalendar. * The event must have its resource ID set. */ -void AlarmCalendar::slotEventUpdated(Resource& resource, const KAEvent& event) +void ResourcesCalendar::slotEventUpdated(Resource& resource, const KAEvent& event) { bool added = true; bool updated = false; KAEventMap::Iterator it = mEventMap.find(EventId(event)); if (it != mEventMap.end()) { // The event ID already exists - remove the existing event first KAEvent* storedEvent = it.value(); if (event.category() == storedEvent->category()) { // The existing event is the same type - update it in place *storedEvent = event; addNewEvent(resource, storedEvent, true); updated = true; } else delete storedEvent; added = false; } if (!updated) addNewEvent(resource, new KAEvent(event)); if (event.category() == CalEvent::ACTIVE) { bool enabled = event.enabled(); checkForDisabledAlarms(!enabled, enabled); if (!mIgnoreAtLogin && added && enabled && event.repeatAtLogin()) Q_EMIT atLoginEventAdded(event); } } /****************************************************************************** * Called when events are about to be removed from a resource. * Remove the corresponding KAEvent instances held by AlarmCalendar. */ -void AlarmCalendar::slotEventsToBeRemoved(Resource& resource, const QList& events) +void ResourcesCalendar::slotEventsToBeRemoved(Resource& resource, const QList& events) { for (const KAEvent& event : events) { if (mEventMap.contains(EventId(event))) deleteEventInternal(event, resource, false); } } -/****************************************************************************** -* Flag the start of a group of calendar update calls. -* The purpose is to avoid multiple calendar saves during a group of operations. -*/ -void AlarmCalendar::startUpdate() -{ - ++mUpdateCount; -} - -/****************************************************************************** -* Flag the end of a group of calendar update calls. -* The calendar is saved if appropriate. -*/ -bool AlarmCalendar::endUpdate() -{ - if (mUpdateCount > 0) - --mUpdateCount; - if (!mUpdateCount) - { - if (mUpdateSave) - return saveCal(); - } - return true; -} - -/****************************************************************************** -* Save the calendar, or flag it for saving if in a group of calendar update calls. -* Note that this method has no effect for resources calendars. -*/ -bool AlarmCalendar::save() -{ - if (mUpdateCount) - { - mUpdateSave = true; - return true; - } - else - return saveCal(); -} - /****************************************************************************** * This method must only be called from the main KAlarm queue processing loop, * to prevent asynchronous calendar operations interfering with one another. * * Purge a list of archived events from the calendar. */ -void AlarmCalendar::purgeEvents(const KAEvent::List& events) +void ResourcesCalendar::purgeEvents(const KAEvent::List& events) { for (const KAEvent* event : events) { deleteEventInternal(*event); } if (mHaveDisabledAlarms) checkForDisabledAlarms(); - saveCal(); } /****************************************************************************** * Add the specified event to the calendar. * If it is an active event and 'useEventID' is false, a new event ID is -* created. In all other cases, the event ID is taken from 'event' (if non-null). -* 'event' is updated with the actual event ID. +* created. In all other cases, the event ID is taken from 'evnt' (if non-null). +* 'evnt' is updated with the actual event ID. * The event is added to 'resource' if specified; otherwise the default resource * is used or the user is prompted, depending on policy. If 'noPrompt' is true, * the user will not be prompted so that if no default resource is defined, the * function will fail. -* Reply = true if 'event' was written to the calendar, in which case (not -* Akonadi) ownership of 'event' is taken by the calendar. 'event' -* is updated. -* = false if an error occurred, in which case 'event' is unchanged. +* Reply = true if 'evnt' was written to the calendar. 'evnt' is updated. +* = false if an error occurred, in which case 'evnt' is unchanged. */ -bool AlarmCalendar::addEvent(KAEvent& evnt, QWidget* promptParent, bool useEventID, Resource* resourceptr, bool noPrompt, bool* cancelled) +bool ResourcesCalendar::addEvent(KAEvent& evnt, QWidget* promptParent, bool useEventID, Resource* resourceptr, bool noPrompt, bool* cancelled) { if (cancelled) *cancelled = false; - if (!mOpen) - return false; - // Check that the event type is valid for the calendar Resource nullresource; Resource& resource(resourceptr ? *resourceptr : nullresource); - qCDebug(KALARM_LOG) << "AlarmCalendar::addEvent:" << evnt.id() << ", resource" << resource.displayId(); + qCDebug(KALARM_LOG) << "ResourcesCalendar::addEvent:" << evnt.id() << ", resource" << resource.displayId(); + + // Check that the event type is valid for the calendar const CalEvent::Type type = evnt.category(); - if (type != mEventType) + switch (type) { - switch (type) - { - case CalEvent::ACTIVE: - case CalEvent::ARCHIVED: - case CalEvent::TEMPLATE: - if (mEventType == CalEvent::EMPTY) - break; - // fall through to default - Q_FALLTHROUGH(); - default: - return false; - } + case CalEvent::ACTIVE: + case CalEvent::ARCHIVED: + case CalEvent::TEMPLATE: + break; + default: + return false; } - ResourceId key = resource.id(); - Event::Ptr kcalEvent((mCalType == RESOURCES) ? (Event*)nullptr : new Event); + const ResourceId key = resource.id(); KAEvent* event = new KAEvent(evnt); QString id = event->id(); if (type == CalEvent::ACTIVE) { if (id.isEmpty()) useEventID = false; else if (!useEventID) id.clear(); } else useEventID = true; if (id.isEmpty()) - id = (mCalType == RESOURCES) ? CalFormat::createUniqueId() : kcalEvent->uid(); + id = CalFormat::createUniqueId(); if (useEventID) - { - id = CalEvent::uid(id, type); - if (kcalEvent) - kcalEvent->setUid(id); - } + id = CalEvent::uid(id, type); // include the alarm type tag in the ID event->setEventId(id); + bool ok = false; bool remove = false; - if (mCalType == RESOURCES) + Resource res; + if (resource.isEnabled(type)) + res = resource; + else { - Resource res; - if (resource.isEnabled(type)) - res = resource; - else + res = Resources::destination(type, promptParent, noPrompt, cancelled); + if (!res.isValid()) { - res = Resources::destination(type, promptParent, noPrompt, cancelled); - if (!res.isValid()) - { - const char* typeStr = (type == CalEvent::ACTIVE) ? "Active alarm" : (type == CalEvent::ARCHIVED) ? "Archived alarm" : "alarm Template"; - qCWarning(KALARM_LOG) << "AlarmCalendar::addEvent: Error! Cannot create" << typeStr << "(No default calendar is defined)"; - } - } - if (res.isValid()) - { - // Don't add event to mEventMap yet - its Akonadi item id is not yet known. - // It will be added once it is inserted into AkonadiDataModel. - ok = res.addEvent(*event); - remove = ok; // if success, delete the local event instance on exit - if (ok && type == CalEvent::ACTIVE && !event->enabled()) - checkForDisabledAlarms(true, false); + const char* typeStr = (type == CalEvent::ACTIVE) ? "Active alarm" : (type == CalEvent::ARCHIVED) ? "Archived alarm" : "alarm Template"; + qCWarning(KALARM_LOG) << "ResourcesCalendar::addEvent: Error! Cannot create" << typeStr << "(No default calendar is defined)"; } } - else + if (res.isValid()) { - // It's the display calendar - event->updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); - key = DISPLAY_COL_ID; - if (!mEventMap.contains(EventId(key, event->id()))) - { - addNewEvent(Resource(), event); - ok = mDisplayCalStorage->calendar()->addEvent(kcalEvent); - remove = !ok; - } + // Don't add event to mEventMap yet - its Akonadi item id is not yet known. + // It will be added once it is inserted into AkonadiDataModel. + ok = res.addEvent(*event); + remove = ok; // if success, delete the local event instance on exit + if (ok && type == CalEvent::ACTIVE && !event->enabled()) + checkForDisabledAlarms(true, false); } if (!ok) { if (remove) { // Adding to mCalendar failed, so undo AlarmCalendar::addEvent() mEventMap.remove(EventId(key, event->id())); KAEvent::List& events = mResourceMap[key]; int i = events.indexOf(event); if (i >= 0) events.remove(i); if (mEarliestAlarm[key] == event) findEarliestAlarm(key); } delete event; return false; } evnt = *event; if (remove) delete event; return true; } +/****************************************************************************** +* Add the specified event to the calendar. +* Reply = true if 'evnt' was written to the calendar. 'evnt' is updated. +* = false if an error occurred, in which case 'evnt' is unchanged. +*/ +bool DisplayCalendar::addEvent(KAEvent& evnt) +{ + if (!mOpen) + return false; + qCDebug(KALARM_LOG) << "DisplayCalendar::addEvent:" << evnt.id(); + // Check that the event type is valid for the calendar + const CalEvent::Type type = evnt.category(); + if (type != CalEvent::DISPLAYING) + return false; + + Event::Ptr kcalEvent(new Event); + KAEvent* event = new KAEvent(evnt); + QString id = event->id(); + if (id.isEmpty()) + id = kcalEvent->uid(); + id = CalEvent::uid(id, type); // include the alarm type tag in the ID + kcalEvent->setUid(id); + event->setEventId(id); + event->updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); + + bool ok = false; + bool remove = false; + const ResourceId key = DISPLAY_RES_ID; + if (!mEventMap.contains(EventId(key, event->id()))) + { + addNewEvent(Resource(), event); + ok = mCalendarStorage->calendar()->addEvent(kcalEvent); + remove = !ok; + } + if (!ok) + { + if (remove) + { + // Adding to mCalendar failed, so undo AlarmCalendar::addEvent() + mEventMap.remove(EventId(key, event->id())); + KAEvent::List& events = mResourceMap[key]; + int i = events.indexOf(event); + if (i >= 0) + events.remove(i); + } + delete event; + return false; + } + evnt = *event; + if (remove) + delete event; + return true; +} /****************************************************************************** * Internal method to add an already checked event to the calendar. * mEventMap takes ownership of the KAEvent. * If 'replace' is true, an existing event is being updated (NOTE: its category() * must remain the same). */ void AlarmCalendar::addNewEvent(const Resource& resource, KAEvent* event, bool replace) { const ResourceId key = resource.id(); event->setResourceId(key); if (!replace) { mResourceMap[key] += event; mEventMap[EventId(key, event->id())] = event; } +} + +/****************************************************************************** +* Internal method to add an already checked event to the calendar. +* mEventMap takes ownership of the KAEvent. +* If 'replace' is true, an existing event is being updated (NOTE: its category() +* must remain the same). +*/ +void ResourcesCalendar::addNewEvent(const Resource& resource, KAEvent* event, bool replace) +{ + AlarmCalendar::addNewEvent(resource, event, replace); + if ((resource.alarmTypes() & CalEvent::ACTIVE) && event->category() == CalEvent::ACTIVE) { // Update the earliest alarm to trigger + const ResourceId key = resource.id(); const KAEvent* earliest = mEarliestAlarm.value(key, (KAEvent*)nullptr); if (replace && earliest == event) findEarliestAlarm(key); else { const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); if (dt.isValid() && (!earliest || dt < earliest->nextTrigger(KAEvent::ALL_TRIGGER))) { mEarliestAlarm[key] = event; Q_EMIT earliestAlarmChanged(); } } } } /****************************************************************************** * Modify the specified event in the calendar with its new contents. * The new event must have a different event ID from the old one. * It is assumed to be of the same event type as the old one (active, etc.) -* Reply = true if 'newEvent' was written to the calendar, in which case (not -* Akonadi) ownership of 'newEvent' is taken by the calendar. -* 'newEvent' is updated. +* Reply = true if 'newEvent' was written to the calendar. 'newEvent' is updated. * = false if an error occurred, in which case 'newEvent' is unchanged. */ -bool AlarmCalendar::modifyEvent(const EventId& oldEventId, KAEvent& newEvent) +bool ResourcesCalendar::modifyEvent(const EventId& oldEventId, KAEvent& newEvent) { const EventId newId(oldEventId.resourceId(), newEvent.id()); - qCDebug(KALARM_LOG) << "AlarmCalendar::modifyEvent:" << oldEventId << "->" << newId; + qCDebug(KALARM_LOG) << "ResourcesCalendar::modifyEvent:" << oldEventId << "->" << newId; bool noNewId = newId.isEmpty(); if (!noNewId && oldEventId == newId) { - qCCritical(KALARM_LOG) << "AlarmCalendar::modifyEvent: Same IDs"; - return false; - } - if (!mOpen) + qCCritical(KALARM_LOG) << "ResourcesCalendar::modifyEvent: Same IDs"; return false; - if (mCalType == RESOURCES) - { - // Set the event's ID, and update the old event in the resources calendar. - const KAEvent* storedEvent = event(oldEventId); - if (!storedEvent) - { - qCCritical(KALARM_LOG) << "AlarmCalendar::modifyEvent: Old event not found"; - return false; - } - if (noNewId) - newEvent.setEventId(CalFormat::createUniqueId()); - Resource resource = Resources::resource(oldEventId.resourceId()); - if (!resource.isValid()) - return false; - // Don't add new event to mEventMap yet - its Akonadi item id is not yet known - if (!resource.addEvent(newEvent)) - return false; - // Note: deleteEventInternal() will delete storedEvent before using the - // event parameter, so need to pass a copy as the parameter. - deleteEventInternal(KAEvent(*storedEvent), resource); - if (mHaveDisabledAlarms) - checkForDisabledAlarms(); } - else + + // Set the event's ID, and update the old event in the resources calendar. + const KAEvent* storedEvent = event(oldEventId); + if (!storedEvent) { - // This functionality isn't needed for the display calendar. - // The calendar would take ownership of newEvent. + qCCritical(KALARM_LOG) << "ResourcesCalendar::modifyEvent: Old event not found"; return false; } + if (noNewId) + newEvent.setEventId(CalFormat::createUniqueId()); + Resource resource = Resources::resource(oldEventId.resourceId()); + if (!resource.isValid()) + return false; + // Don't add new event to mEventMap yet - its Akonadi item id is not yet known + if (!resource.addEvent(newEvent)) + return false; + // Note: deleteEventInternal() will delete storedEvent before using the + // event parameter, so need to pass a copy as the parameter. + deleteEventInternal(KAEvent(*storedEvent), resource); + if (mHaveDisabledAlarms) + checkForDisabledAlarms(); return true; } /****************************************************************************** * Update the specified event in the calendar with its new contents. * The event retains the same ID. The event must be in the resource calendar. * Reply = event which has been updated * = 0 if error. */ -KAEvent* AlarmCalendar::updateEvent(const KAEvent& evnt) +KAEvent* ResourcesCalendar::updateEvent(const KAEvent& evnt) { - return updateEvent(&evnt); -} -KAEvent* AlarmCalendar::updateEvent(const KAEvent* evnt) -{ - if (!mOpen || mCalType != RESOURCES) - return nullptr; - KAEvent* kaevnt = event(EventId(*evnt)); + KAEvent* kaevnt = event(EventId(evnt)); if (kaevnt) { - Resource resource = Resources::resourceForEvent(evnt->id()); - if (resource.updateEvent(*evnt)) + Resource resource = Resources::resourceForEvent(evnt.id()); + if (resource.updateEvent(evnt)) { - *kaevnt = *evnt; + *kaevnt = evnt; return kaevnt; } } - qCDebug(KALARM_LOG) << "AlarmCalendar::updateEvent: error"; + qCDebug(KALARM_LOG) << "ResourcesCalendar::updateEvent: error"; return nullptr; } /****************************************************************************** * Delete the specified event from the resource calendar, if it exists. * The calendar is then optionally saved. */ -bool AlarmCalendar::deleteEvent(const KAEvent& event, bool saveit) +bool ResourcesCalendar::deleteEvent(const KAEvent& event, bool saveit) { - if (mOpen && mCalType == RESOURCES) - { - const CalEvent::Type status = deleteEventInternal(event); - if (mHaveDisabledAlarms) - checkForDisabledAlarms(); - if (status != CalEvent::EMPTY) - { - if (saveit) - return save(); - return true; - } - } - return false; + Q_UNUSED(saveit); + + const CalEvent::Type status = deleteEventInternal(event); + if (mHaveDisabledAlarms) + checkForDisabledAlarms(); + return status != CalEvent::EMPTY; } /****************************************************************************** * Delete the specified event from the calendar, if it exists. * The calendar is then optionally saved. */ -bool AlarmCalendar::deleteDisplayEvent(const QString& eventID, bool saveit) +bool DisplayCalendar::deleteEvent(const QString& eventID, bool saveit) { - if (mOpen && mCalType != RESOURCES) + if (mOpen) { + Event::Ptr kcalEvent; + if (mCalendarStorage) + kcalEvent = mCalendarStorage->calendar()->event(eventID); // display calendar + Resource resource; - const CalEvent::Type status = deleteEventInternal(eventID, KAEvent(), resource, false); - if (mHaveDisabledAlarms) - checkForDisabledAlarms(); + deleteEventBase(eventID, resource); + + CalEvent::Type status = CalEvent::EMPTY; + if (kcalEvent) + { + status = CalEvent::status(kcalEvent); + mCalendarStorage->calendar()->deleteEvent(kcalEvent); + } + if (status != CalEvent::EMPTY) { if (saveit) return save(); return true; } } return false; } /****************************************************************************** * Internal method to delete the specified event from the calendar and lists. * Reply = event status, if it was found in the resource calendar/calendar * resource or local calendar * = CalEvent::EMPTY otherwise. */ -CalEvent::Type AlarmCalendar::deleteEventInternal(const KAEvent& event, bool deleteFromResources) +CalEvent::Type ResourcesCalendar::deleteEventInternal(const KAEvent& event, bool deleteFromResources) { Resource resource = Resources::resource(event.resourceId()); if (!resource.isValid()) return CalEvent::EMPTY; return deleteEventInternal(event.id(), event, resource, deleteFromResources); } -CalEvent::Type AlarmCalendar::deleteEventInternal(const KAEvent& event, Resource& resource, bool deleteFromResources) +CalEvent::Type ResourcesCalendar::deleteEventInternal(const KAEvent& event, Resource& resource, bool deleteFromResources) { if (!resource.isValid()) return CalEvent::EMPTY; if (event.resourceId() != resource.id()) { - qCCritical(KALARM_LOG) << "AlarmCalendar::deleteEventInternal: Event" << event.id() << ": resource" << event.resourceId() << "differs from 'resource'" << resource.id(); + qCCritical(KALARM_LOG) << "ResourcesCalendar::deleteEventInternal: Event" << event.id() << ": resource" << event.resourceId() << "differs from 'resource'" << resource.id(); return CalEvent::EMPTY; } return deleteEventInternal(event.id(), event, resource, deleteFromResources); } -CalEvent::Type AlarmCalendar::deleteEventInternal(const QString& eventID, const KAEvent& event, Resource& resource, bool deleteFromResources) +CalEvent::Type ResourcesCalendar::deleteEventInternal(const QString& eventID, const KAEvent& event, Resource& resource, bool deleteFromResources) { // Make a copy of the KAEvent and the ID QString, since the supplied // references might be destructed when the event is deleted below. const QString id = eventID; const KAEvent paramEvent = event; - Event::Ptr kcalEvent; - if (mDisplayCalStorage) - kcalEvent = mDisplayCalStorage->calendar()->event(id); // display calendar + const ResourceId key = resource.id(); - KAEventMap::Iterator it = mEventMap.find(EventId(key, id)); - if (it != mEventMap.end()) + KAEvent* ev = deleteEventBase(eventID, resource); + if (ev) { - KAEvent* ev = it.value(); - mEventMap.erase(it); - KAEvent::List& events = mResourceMap[key]; - int i = events.indexOf(ev); - if (i >= 0) - events.remove(i); - delete ev; if (mEarliestAlarm[key] == ev) findEarliestAlarm(resource); } else { for (EarliestMap::Iterator eit = mEarliestAlarm.begin(); eit != mEarliestAlarm.end(); ++eit) { - KAEvent* ev = eit.value(); + ev = eit.value(); if (ev && ev->id() == id) { findEarliestAlarm(eit.key()); break; } } } + + CalEvent::Type status = CalEvent::EMPTY; - if (kcalEvent) - { - // It's a display calendar event - status = CalEvent::status(kcalEvent); - mDisplayCalStorage->calendar()->deleteEvent(kcalEvent); - } - else if (deleteFromResources) + if (deleteFromResources) { // Delete from the resources calendar CalEvent::Type s = paramEvent.category(); if (resource.deleteEvent(paramEvent)) status = s; } return status; } +/****************************************************************************** +* Internal method to delete the specified event from the calendar and lists. +* Reply = event which was deleted, or null if not found. +*/ +KAEvent* AlarmCalendar::deleteEventBase(const QString& eventID, Resource& resource) +{ + // Make a copy of the ID QString, since the supplied reference might be + // destructed when the event is deleted below. + const QString id = eventID; + + const ResourceId key = resource.id(); + KAEventMap::Iterator it = mEventMap.find(EventId(key, id)); + if (it == mEventMap.end()) + return nullptr; + + KAEvent* ev = it.value(); + mEventMap.erase(it); + KAEvent::List& events = mResourceMap[key]; + int i = events.indexOf(ev); + if (i >= 0) + events.remove(i); + delete ev; + return ev; +} + +/****************************************************************************** +* Return the event with the specified ID. +* If 'findUniqueId' is true, and the resource ID is invalid, if there is a +* unique event with the given ID, it will be returned. +*/ +KAEvent* AlarmCalendar::event(const EventId& uniqueID) +{ + if (!isValid()) + return nullptr; + KAEventMap::ConstIterator it = mEventMap.constFind(uniqueID); + if (it == mEventMap.constEnd()) + return nullptr; + return it.value(); +} /****************************************************************************** * Return the event with the specified ID. * If 'findUniqueId' is true, and the resource ID is invalid, if there is a * unique event with the given ID, it will be returned. */ -KAEvent* AlarmCalendar::event(const EventId& uniqueID, bool findUniqueId) +KAEvent* ResourcesCalendar::event(const EventId& uniqueID, bool findUniqueId) { if (!isValid()) return nullptr; - const QString eventId = uniqueID.eventId(); if (uniqueID.resourceId() == -1 && findUniqueId) { // The resource isn't known, but use the event ID if it is unique among // all resources. + const QString eventId = uniqueID.eventId(); const KAEvent::List list = events(eventId); if (list.count() > 1) { - qCWarning(KALARM_LOG) << "AlarmCalendar::event: Multiple events found with ID" << eventId; + qCWarning(KALARM_LOG) << "ResourcesCalendar::event: Multiple events found with ID" << eventId; return nullptr; } if (list.isEmpty()) return nullptr; return list[0]; } KAEventMap::ConstIterator it = mEventMap.constFind(uniqueID); if (it == mEventMap.constEnd()) return nullptr; return it.value(); } /****************************************************************************** * Find the alarm template with the specified name. * Reply = 0 if not found. */ -KAEvent* AlarmCalendar::templateEvent(const QString& templateName) +KAEvent* ResourcesCalendar::templateEvent(const QString& templateName) { if (templateName.isEmpty()) return nullptr; const KAEvent::List eventlist = events(CalEvent::TEMPLATE); for (KAEvent* event : eventlist) { if (event->templateName() == templateName) return event; } return nullptr; } /****************************************************************************** * Return all events with the specified ID, from all calendars. */ -KAEvent::List AlarmCalendar::events(const QString& uniqueId) const +KAEvent::List ResourcesCalendar::events(const QString& uniqueId) const { KAEvent::List list; - if (mCalType == RESOURCES && isValid()) + if (isValid()) { for (ResourceMap::ConstIterator rit = mResourceMap.constBegin(); rit != mResourceMap.constEnd(); ++rit) { const ResourceId id = rit.key(); KAEventMap::ConstIterator it = mEventMap.constFind(EventId(id, uniqueId)); if (it != mEventMap.constEnd()) list += it.value(); } } return list; } +KAEvent::List ResourcesCalendar::events(const Resource& resource, CalEvent::Types type) const +{ + return AlarmCalendar::events(type, resource); +} + +KAEvent::List ResourcesCalendar::events(CalEvent::Types type) const +{ + Resource resource; + return AlarmCalendar::events(type, resource); +} + + /****************************************************************************** * Return all events in the calendar which contain alarms. * Optionally the event type can be filtered, using an OR of event types. */ -KAEvent::List AlarmCalendar::events(const Resource& resource, CalEvent::Types type) const +KAEvent::List DisplayCalendar::events(CalEvent::Types type) const +{ + if (!mCalendarStorage) + return KAEvent::List(); + Resource resource; + return AlarmCalendar::events(type, resource); +} + +KAEvent::List AlarmCalendar::events(CalEvent::Types type, const Resource& resource) const { KAEvent::List list; - if (mCalType != RESOURCES && (!mDisplayCalStorage || resource.isValid())) - return list; if (resource.isValid()) { const ResourceId key = resource.id(); ResourceMap::ConstIterator rit = mResourceMap.constFind(key); if (rit == mResourceMap.constEnd()) return list; const KAEvent::List events = rit.value(); if (type == CalEvent::EMPTY) return events; for (KAEvent* const event : events) if (type & event->category()) list += event; } else { for (ResourceMap::ConstIterator rit = mResourceMap.constBegin(); rit != mResourceMap.constEnd(); ++rit) { const KAEvent::List events = rit.value(); if (type == CalEvent::EMPTY) list += events; else { for (KAEvent* const event : events) if (type & event->category()) list += event; } } } return list; } /****************************************************************************** * Return the event with the specified ID. * This method is for the display calendar only. */ -Event::Ptr AlarmCalendar::kcalEvent(const QString& uniqueID) +Event::Ptr DisplayCalendar::kcalEvent(const QString& uniqueID) { - Q_ASSERT(mCalType != RESOURCES); // only allowed for display calendar - if (!mDisplayCalStorage) + if (!mCalendarStorage) return Event::Ptr(); - return mDisplayCalStorage->calendar()->event(uniqueID); + return mCalendarStorage->calendar()->event(uniqueID); } /****************************************************************************** * Return all events in the calendar which contain usable alarms. * This method is for the display calendar only. * Optionally the event type can be filtered, using an OR of event types. */ -Event::List AlarmCalendar::kcalEvents(CalEvent::Type type) +Event::List DisplayCalendar::kcalEvents(CalEvent::Type type) { Event::List list; - Q_ASSERT(mCalType != RESOURCES); // only allowed for display calendar - if (!mDisplayCalStorage) + if (!mCalendarStorage) return list; - list = mDisplayCalStorage->calendar()->rawEvents(); + list = mCalendarStorage->calendar()->rawEvents(); for (int i = 0; i < list.count(); ) { Event::Ptr event = list.at(i); if (event->alarms().isEmpty() || (type != CalEvent::EMPTY && !(type & CalEvent::status(event))) || !KAEvent(event).isValid()) list.remove(i); else ++i; } return list; } /****************************************************************************** * Return whether an event is read-only. * Display calendar events are always returned as read-only. */ -bool AlarmCalendar::eventReadOnly(const QString& eventId) const +bool ResourcesCalendar::eventReadOnly(const QString& eventId) const { - if (mCalType != RESOURCES) - return true; KAEvent event; const Resource resource = Resources::resourceForEvent(eventId, event); return !event.isValid() || event.isReadOnly() || !resource.isWritable(event.category()); //TODO || compatibility(event) != KACalendar::Current; } /****************************************************************************** * Called when an alarm's enabled status has changed. */ -void AlarmCalendar::disabledChanged(const KAEvent* event) +void ResourcesCalendar::disabledChanged(const KAEvent* event) { if (event->category() == CalEvent::ACTIVE) { bool status = event->enabled(); checkForDisabledAlarms(!status, status); } } /****************************************************************************** * Check whether there are any individual disabled alarms, following an alarm * creation or modification. Must only be called for an ACTIVE alarm. */ -void AlarmCalendar::checkForDisabledAlarms(bool oldEnabled, bool newEnabled) +void ResourcesCalendar::checkForDisabledAlarms(bool oldEnabled, bool newEnabled) { - if (mCalType == RESOURCES && newEnabled != oldEnabled) + if (newEnabled != oldEnabled) { if (newEnabled && mHaveDisabledAlarms) checkForDisabledAlarms(); else if (!newEnabled && !mHaveDisabledAlarms) { mHaveDisabledAlarms = true; Q_EMIT haveDisabledAlarmsChanged(true); } } } /****************************************************************************** * Check whether there are any individual disabled alarms. */ -void AlarmCalendar::checkForDisabledAlarms() +void ResourcesCalendar::checkForDisabledAlarms() { - if (mCalType != RESOURCES) - return; bool disabled = false; const KAEvent::List eventlist = events(CalEvent::ACTIVE); for (const KAEvent* const event : eventlist) { if (!event->enabled()) { disabled = true; break; } } if (disabled != mHaveDisabledAlarms) { mHaveDisabledAlarms = disabled; Q_EMIT haveDisabledAlarmsChanged(disabled); } } /****************************************************************************** * Find and note the active alarm with the earliest trigger time for a calendar. */ -void AlarmCalendar::findEarliestAlarm(const Resource& resource) +void ResourcesCalendar::findEarliestAlarm(const Resource& resource) { - if (mCalType != RESOURCES) - return; if (!(resource.alarmTypes() & CalEvent::ACTIVE)) return; findEarliestAlarm(resource.id()); } -void AlarmCalendar::findEarliestAlarm(ResourceId key) +void ResourcesCalendar::findEarliestAlarm(ResourceId key) { EarliestMap::Iterator eit = mEarliestAlarm.find(key); if (eit != mEarliestAlarm.end()) eit.value() = nullptr; - if (mCalType != RESOURCES - || key < 0) + if (key < 0) return; ResourceMap::ConstIterator rit = mResourceMap.constFind(key); if (rit == mResourceMap.constEnd()) return; const KAEvent::List& events = rit.value(); KAEvent* earliest = nullptr; KADateTime earliestTime; for (KAEvent* event : events) { if (event->category() != CalEvent::ACTIVE || mPendingAlarms.contains(event->id())) continue; const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); if (dt.isValid() && (!earliest || dt < earliestTime)) { earliestTime = dt; earliest = event; } } mEarliestAlarm[key] = earliest; Q_EMIT earliestAlarmChanged(); } /****************************************************************************** * Return the active alarm with the earliest trigger time. * Reply = 0 if none. */ -KAEvent* AlarmCalendar::earliestAlarm() const +KAEvent* ResourcesCalendar::earliestAlarm() const { KAEvent* earliest = nullptr; KADateTime earliestTime; for (EarliestMap::ConstIterator eit = mEarliestAlarm.constBegin(); eit != mEarliestAlarm.constEnd(); ++eit) { KAEvent* event = eit.value(); if (!event) continue; const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); if (dt.isValid() && (!earliest || dt < earliestTime)) { earliestTime = dt; earliest = event; } } return earliest; } /****************************************************************************** * Note that an alarm which has triggered is now being processed. While pending, * it will be ignored for the purposes of finding the earliest trigger time. */ -void AlarmCalendar::setAlarmPending(KAEvent* event, bool pending) +void ResourcesCalendar::setAlarmPending(KAEvent* event, bool pending) { const QString id = event->id(); bool wasPending = mPendingAlarms.contains(id); - qCDebug(KALARM_LOG) << "AlarmCalendar::setAlarmPending:" << id << "," << pending << "(was" << wasPending << ")"; + qCDebug(KALARM_LOG) << "ResourcesCalendar::setAlarmPending:" << id << "," << pending << "(was" << wasPending << ")"; if (pending) { if (wasPending) return; mPendingAlarms += id; } else { if (!wasPending) return; mPendingAlarms.remove(id); } // Now update the earliest alarm to trigger for its calendar findEarliestAlarm(Resources::resourceForEvent(event->id())); } /****************************************************************************** * Called when the user changes the start-of-day time. * Adjust the start times of all date-only alarms' recurrences. */ void AlarmCalendar::adjustStartOfDay() { if (!isValid()) return; for (ResourceMap::ConstIterator rit = mResourceMap.constBegin(); rit != mResourceMap.constEnd(); ++rit) KAEvent::adjustStartOfDay(rit.value()); } // vim: et sw=4: diff --git a/src/alarmcalendar.h b/src/alarmcalendar.h index 43e36dd9..ed14696c 100644 --- a/src/alarmcalendar.h +++ b/src/alarmcalendar.h @@ -1,142 +1,181 @@ /* * alarmcalendar.h - KAlarm calendar file access * Program: kalarm * Copyright © 2001-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef ALARMCALENDAR_H #define ALARMCALENDAR_H #include "eventid.h" #include "resources/resource.h" #include #include #include #include #include #include using namespace KAlarmCal; -/** Provides read and write access to calendar files and resources. +/** Base class to provide read and write access to calendar files and resources. * Either vCalendar or iCalendar files may be read, but the calendar is saved * only in iCalendar format to avoid information loss. */ class AlarmCalendar : public QObject { - Q_OBJECT - public: - ~AlarmCalendar() override; - bool open(); - int load(); - bool reload(); - bool save(); - void close(); - void startUpdate(); - bool endUpdate(); - KAEvent* earliestAlarm() const; - void setAlarmPending(KAEvent*, bool pending = true); - bool haveDisabledAlarms() const { return mHaveDisabledAlarms; } - void disabledChanged(const KAEvent*); - KAEvent* event(const EventId& uniqueId, bool findUniqueId = false); - KAEvent* templateEvent(const QString& templateName); - KAEvent::List events(const QString& uniqueId) const; - KAEvent::List events(CalEvent::Types s = CalEvent::EMPTY) const { return events(Resource(), s); } - KAEvent::List events(const Resource&, CalEvent::Types = CalEvent::EMPTY) const; - KCalendarCore::Event::Ptr kcalEvent(const QString& uniqueID); // display calendar only - KCalendarCore::Event::List kcalEvents(CalEvent::Type s = CalEvent::EMPTY); // display calendar only - bool eventReadOnly(const QString& eventId) const; - bool addEvent(KAEvent&, QWidget* promptparent = nullptr, bool useEventID = false, Resource* = nullptr, bool noPrompt = false, bool* cancelled = nullptr); - bool modifyEvent(const EventId& oldEventId, KAEvent& newEvent); - KAEvent* updateEvent(const KAEvent&); - KAEvent* updateEvent(const KAEvent*); - bool deleteEvent(const KAEvent&, bool save = false); - bool deleteDisplayEvent(const QString& eventID, bool save = false); - void purgeEvents(const KAEvent::List&); - bool isOpen(); - void adjustStartOfDay(); - - static bool initialiseCalendars(); - static void terminateCalendars(); - static AlarmCalendar* resources() { return mResourcesCalendar; } - static AlarmCalendar* displayCalendar() { return mDisplayCalendar; } - static AlarmCalendar* displayCalendarOpen(); - static KAEvent* getEvent(const EventId&); - - Q_SIGNALS: - void earliestAlarmChanged(); - void haveDisabledAlarmsChanged(bool haveDisabled); - void atLoginEventAdded(const KAEvent&); - void calendarSaved(AlarmCalendar*); - - private Q_SLOTS: - void slotResourceSettingsChanged(Resource&, ResourceType::Changes); - void slotResourcesPopulated(); - void slotResourceAdded(Resource&); - void slotEventsAdded(Resource&, const QList&); - void slotEventsToBeRemoved(Resource&, const QList&); - void slotEventUpdated(Resource&, const KAEvent&); - private: - enum CalType { RESOURCES, LOCAL_ICAL, LOCAL_VCAL }; - typedef QMap ResourceMap; // id = invalid for display calendar - typedef QMap EarliestMap; - typedef QHash KAEventMap; // indexed by resource and event UID - - AlarmCalendar(); - AlarmCalendar(const QString& file, CalEvent::Type); - bool saveCal(const QString& newFile = QString()); - bool isValid() const { return mCalType == RESOURCES || mDisplayCalStorage; } - void addNewEvent(const Resource&, KAEvent*, bool replace = false); - CalEvent::Type deleteEventInternal(const KAEvent&, bool deleteFromResources = true); - CalEvent::Type deleteEventInternal(const KAEvent&, Resource&, bool deleteFromResources = true); - CalEvent::Type deleteEventInternal(const QString& eventID, const KAEvent&, Resource&, - bool deleteFromResources = true); - void updateDisplayKAEvents(); - void removeKAEvents(ResourceId, bool closing = false, CalEvent::Types = CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE); - void findEarliestAlarm(const Resource&); - void findEarliestAlarm(ResourceId); //deprecated - void checkForDisabledAlarms(); - void checkForDisabledAlarms(bool oldEnabled, bool newEnabled); - - static AlarmCalendar* mResourcesCalendar; // the calendar resources - static AlarmCalendar* mDisplayCalendar; // the display calendar - - ResourceMap mResourceMap; - KAEventMap mEventMap; // lookup of all events by UID - EarliestMap mEarliestAlarm; // alarm with earliest trigger time, by resource - QSet mPendingAlarms; // IDs of alarms which are currently being processed after triggering - KCalendarCore::FileStorage::Ptr mDisplayCalStorage; // for display calendar; null if resources calendar - QString mDisplayCalPath; // path of display calendar file - QString mDisplayICalPath; // path of display iCalendar file - CalType mCalType; // what type of calendar mCalendar is (resources/ical/vcal) - CalEvent::Type mEventType; // what type of events the calendar file is for - bool mOpen {false}; // true if the calendar file is open - bool mIgnoreAtLogin {false}; // ignore new/updated repeat-at-login alarms - int mUpdateCount {0}; // nesting level of group of calendar update calls - bool mUpdateSave {false}; // save() was called while mUpdateCount > 0 - bool mHaveDisabledAlarms {false}; // there is at least one individually disabled alarm - - using QObject::event; // prevent "hidden" warning + Q_OBJECT +public: + KAEvent* event(const EventId& uniqueId); + bool addEvent(KAEvent&, QWidget* promptparent = nullptr, bool useEventID = false, Resource* = nullptr, bool noPrompt = false, bool* cancelled = nullptr); + void adjustStartOfDay(); + + static void initialise(); + +protected: + typedef QMap ResourceMap; // id = invalid for display calendar + typedef QHash KAEventMap; // indexed by resource and event UID + + AlarmCalendar(); + virtual bool isValid() const = 0; + KAEvent::List events(CalEvent::Types, const Resource&) const; + virtual void addNewEvent(const Resource&, KAEvent*, bool replace = false); + KAEvent* deleteEventBase(const QString& eventID, Resource&); + bool removeKAEvents(ResourceId, CalEvent::Types); + + ResourceMap mResourceMap; + KAEventMap mEventMap; // lookup of all events by UID + + using QObject::event; // prevent "hidden" warning +}; + +/** Provides read and write access to resource calendars. + */ +class ResourcesCalendar : public AlarmCalendar +{ + Q_OBJECT +public: + ~ResourcesCalendar() override; + bool reload(); + bool save(); + void close(); + KAEvent* earliestAlarm() const; + void setAlarmPending(KAEvent*, bool pending = true); + bool haveDisabledAlarms() const { return mHaveDisabledAlarms; } + void disabledChanged(const KAEvent*); + using AlarmCalendar::event; + KAEvent* event(const EventId& uniqueId, bool findUniqueId); + KAEvent* templateEvent(const QString& templateName); + KAEvent::List events(const QString& uniqueId) const; + KAEvent::List events(const Resource&, CalEvent::Types = CalEvent::EMPTY) const; + KAEvent::List events(CalEvent::Types s = CalEvent::EMPTY) const; + bool eventReadOnly(const QString& eventId) const; + bool addEvent(KAEvent&, QWidget* promptparent = nullptr, bool useEventID = false, Resource* = nullptr, bool noPrompt = false, bool* cancelled = nullptr); + bool modifyEvent(const EventId& oldEventId, KAEvent& newEvent); + KAEvent* updateEvent(const KAEvent&); + bool deleteEvent(const KAEvent&, bool save = false); + void purgeEvents(const KAEvent::List&); + + static void initialise(); + static void terminate(); + static ResourcesCalendar* instance() { return mInstance; } + static KAEvent* getEvent(const EventId&); + +Q_SIGNALS: + void earliestAlarmChanged(); + void haveDisabledAlarmsChanged(bool haveDisabled); + void atLoginEventAdded(const KAEvent&); + +private Q_SLOTS: + void slotResourceSettingsChanged(Resource&, ResourceType::Changes); + void slotResourcesPopulated(); + void slotResourceAdded(Resource&); + void slotEventsAdded(Resource&, const QList&); + void slotEventsToBeRemoved(Resource&, const QList&); + void slotEventUpdated(Resource&, const KAEvent&); +private: + typedef QMap EarliestMap; + + ResourcesCalendar(); + bool isValid() const override { return true; } + void addNewEvent(const Resource&, KAEvent*, bool replace = false) override; + CalEvent::Type deleteEventInternal(const KAEvent&, bool deleteFromResources = true); + CalEvent::Type deleteEventInternal(const KAEvent&, Resource&, bool deleteFromResources = true); + CalEvent::Type deleteEventInternal(const QString& eventID, const KAEvent&, Resource&, + bool deleteFromResources = true); + bool removeKAEvents(ResourceId, bool closing = false, CalEvent::Types = CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE); + void findEarliestAlarm(const Resource&); + void findEarliestAlarm(ResourceId); + void checkForDisabledAlarms(); + void checkForDisabledAlarms(bool oldEnabled, bool newEnabled); + + static ResourcesCalendar* mInstance; // the unique instance + + EarliestMap mEarliestAlarm; // alarm with earliest trigger time, by resource + QSet mPendingAlarms; // IDs of alarms which are currently being processed after triggering + bool mIgnoreAtLogin {false}; // ignore new/updated repeat-at-login alarms + bool mHaveDisabledAlarms {false}; // there is at least one individually disabled alarm +}; + +/** Provides read and write access to the display calendar. + */ +class DisplayCalendar : public AlarmCalendar +{ + Q_OBJECT +public: + ~DisplayCalendar() override; + bool open(); + int load(); + bool reload(); + bool save(); + void close(); + KAEvent::List events(CalEvent::Types = CalEvent::EMPTY) const; + KCalendarCore::Event::Ptr kcalEvent(const QString& uniqueID); + KCalendarCore::Event::List kcalEvents(CalEvent::Type s = CalEvent::EMPTY); + bool addEvent(KAEvent&); + bool deleteEvent(const QString& eventID, bool save = false); + bool isOpen() const { return mOpen; } + + static void initialise(); + static void terminate(); + static DisplayCalendar* instance() { return mInstance; } + static DisplayCalendar* instanceOpen(); + +private: + enum CalType { LOCAL_ICAL, LOCAL_VCAL }; + + explicit DisplayCalendar(const QString& file); + bool saveCal(const QString& newFile = QString()); + bool isValid() const override { return mCalendarStorage; } + void updateKAEvents(); + + static DisplayCalendar* mInstance; // the unique instance + + KCalendarCore::FileStorage::Ptr mCalendarStorage; + QString mDisplayCalPath; // path of display calendar file + QString mDisplayICalPath; // path of display iCalendar file + CalType mCalType; // what type of calendar mCalendar is (ical/vcal) + bool mOpen {false}; // true if the calendar file is open }; #endif // ALARMCALENDAR_H // vim: et sw=4: diff --git a/src/birthdaymodel.cpp b/src/birthdaymodel.cpp index 64a6e88d..bcd04a9c 100644 --- a/src/birthdaymodel.cpp +++ b/src/birthdaymodel.cpp @@ -1,129 +1,129 @@ /* * birthdaymodel.cpp - model class for birthdays from address book * Program: kalarm * Copyright © 2009 by Tobias Koenig - * Copyright © 2007-2019 David Jarvie + * Copyright © 2007-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "birthdaymodel.h" #include "alarmcalendar.h" #include #include #include #include #include #include #include using namespace KAlarmCal; BirthdayModel* BirthdayModel::mInstance = nullptr; BirthdayModel::BirthdayModel(Akonadi::ChangeRecorder* recorder) : Akonadi::ContactsTreeModel(recorder) { setColumns({FullName, Birthday}); } BirthdayModel::~BirthdayModel() { if (this == mInstance) mInstance = nullptr; } BirthdayModel* BirthdayModel::instance() { if (!mInstance) { Akonadi::Session* session = new Akonadi::Session("KAlarm::BirthdayModelSession"); Akonadi::ItemFetchScope scope; scope.fetchFullPayload(true); scope.fetchAttribute(); Akonadi::ChangeRecorder* recorder = new Akonadi::ChangeRecorder; recorder->setSession(session); recorder->fetchCollection(true); recorder->setItemFetchScope(scope); recorder->setCollectionMonitored(Akonadi::Collection::root()); recorder->setMimeTypeMonitored(KContacts::Addressee::mimeType(), true); mInstance = new BirthdayModel(recorder); } return mInstance; } QVariant BirthdayModel::entityData(const Akonadi::Item& item, int column, int role) const { if (columns().at(column) == Birthday && role == Qt::DisplayRole) { const QDate date = Akonadi::ContactsTreeModel::entityData(item, column, DateRole).toDate(); if (date.isValid()) return QLocale().toString(date, QLocale::ShortFormat); } return Akonadi::ContactsTreeModel::entityData(item, column, role); } /*============================================================================*/ BirthdaySortModel::BirthdaySortModel(QObject* parent) : QSortFilterProxyModel(parent) { } void BirthdaySortModel::setPrefixSuffix(const QString& prefix, const QString& suffix) { mContactsWithAlarm.clear(); mPrefix = prefix; mSuffix = suffix; KAEvent event; - const KAEvent::List events = AlarmCalendar::resources()->events(CalEvent::ACTIVE); + const KAEvent::List events = ResourcesCalendar::instance()->events(CalEvent::ACTIVE); for (KAEvent* event : events) { if (event->actionSubType() == KAEvent::MESSAGE && event->recurType() == KARecurrence::ANNUAL_DATE && (prefix.isEmpty() || event->message().startsWith(prefix))) mContactsWithAlarm.append(event->message()); } invalidateFilter(); } bool BirthdaySortModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { const QModelIndex nameIndex = sourceModel()->index(sourceRow, 0, sourceParent); const QModelIndex birthdayIndex = sourceModel()->index(sourceRow, 1, sourceParent); // If the birthday is invalid, the second column is empty if (birthdayIndex.data(Qt::DisplayRole).toString().isEmpty()) return false; const QString text = mPrefix + nameIndex.data(Qt::DisplayRole).toString() + mSuffix; if (mContactsWithAlarm.contains(text)) return false; return true; } // vim: et sw=4: diff --git a/src/deferdlg.cpp b/src/deferdlg.cpp index 2b0bc44c..ab1d9be5 100644 --- a/src/deferdlg.cpp +++ b/src/deferdlg.cpp @@ -1,186 +1,186 @@ /* * deferdlg.cpp - dialog to defer an alarm * Program: kalarm - * Copyright © 2002-2019 David Jarvie + * Copyright © 2002-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "deferdlg.h" #include "alarmcalendar.h" #include "alarmtimewidget.h" #include "functions.h" #include "kalarmapp.h" #include "lib/messagebox.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include /****************************************************************************** * Constructor. * If 'cancelButton' is true, the Cancel Deferral button will be shown to allow * any existing deferral to be cancelled. */ DeferAlarmDlg::DeferAlarmDlg(const DateTime& initialDT, bool anyTimeOption, bool cancelButton, QWidget* parent) : QDialog(parent) { setWindowModality(Qt::WindowModal); setWindowTitle(i18nc("@title:window", "Defer Alarm")); QVBoxLayout* layout = new QVBoxLayout(this); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTimeWidget = new AlarmTimeWidget((anyTimeOption ? AlarmTimeWidget::DEFER_ANY_TIME : AlarmTimeWidget::DEFER_TIME), this); mTimeWidget->setDateTime(initialDT); mTimeWidget->setMinDateTimeIsCurrent(); connect(mTimeWidget, &AlarmTimeWidget::pastMax, this, &DeferAlarmDlg::slotPastLimit); layout->addWidget(mTimeWidget); layout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mButtonBox = new QDialogButtonBox(this); layout->addWidget(mButtonBox); QPushButton* okButton = mButtonBox->addButton(QDialogButtonBox::Ok); okButton->setWhatsThis(i18nc("@info:whatsthis", "Defer the alarm until the specified time.")); mButtonBox->addButton(QDialogButtonBox::Cancel); connect(mButtonBox, &QDialogButtonBox::accepted, this, &DeferAlarmDlg::slotOk); connect(mButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); if (cancelButton) { QPushButton* deferButton = mButtonBox->addButton(i18nc("@action:button", "Cancel Deferral"), QDialogButtonBox::ActionRole); deferButton->setWhatsThis(i18nc("@info:whatsthis", "Cancel the deferred alarm. This does not affect future recurrences.")); connect(mButtonBox, &QDialogButtonBox::clicked, [this, deferButton](QAbstractButton* btn) { if (btn == deferButton) slotCancelDeferral(); }); } } /****************************************************************************** * Called when the OK button is clicked. */ void DeferAlarmDlg::slotOk() { mAlarmDateTime = mTimeWidget->getDateTime(&mDeferMinutes); if (!mAlarmDateTime.isValid()) return; KAEvent::DeferLimitType limitType = KAEvent::LIMIT_NONE; DateTime endTime; if (!mLimitEventId.isEmpty()) { // Get the event being deferred - const KAEvent* event = AlarmCalendar::getEvent(mLimitEventId); + const KAEvent* event = ResourcesCalendar::getEvent(mLimitEventId); if (event) endTime = event->deferralLimit(&limitType); } else { endTime = mLimitDateTime; limitType = mLimitDateTime.isValid() ? KAEvent::LIMIT_MAIN : KAEvent::LIMIT_NONE; } if (endTime.isValid() && mAlarmDateTime > endTime) { QString text; switch (limitType) { case KAEvent::LIMIT_REPETITION: text = i18nc("@info", "Cannot defer past the alarm's next sub-repetition (currently %1)", endTime.formatLocale()); break; case KAEvent::LIMIT_RECURRENCE: text = i18nc("@info", "Cannot defer past the alarm's next recurrence (currently %1)", endTime.formatLocale()); break; case KAEvent::LIMIT_REMINDER: text = i18nc("@info", "Cannot defer past the alarm's next reminder (currently %1)", endTime.formatLocale()); break; case KAEvent::LIMIT_MAIN: text = i18nc("@info", "Cannot defer reminder past the main alarm time (%1)", endTime.formatLocale()); break; case KAEvent::LIMIT_NONE: break; // can't happen with a valid endTime } KAMessageBox::sorry(this, text); } else accept(); } /****************************************************************************** * Select the 'Time from now' radio button and preset its value. */ void DeferAlarmDlg::setDeferMinutes(int minutes) { mTimeWidget->selectTimeFromNow(minutes); } /****************************************************************************** * Called the maximum date/time for the date/time edit widget has been passed. */ void DeferAlarmDlg::slotPastLimit() { mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(false); } /****************************************************************************** * Set the time limit for deferral based on the next occurrence of the alarm * with the specified ID. */ void DeferAlarmDlg::setLimit(const DateTime& limit) { mLimitEventId.clear(); mLimitDateTime = limit; mTimeWidget->setMaxDateTime(mLimitDateTime); } /****************************************************************************** * Set the time limit for deferral based on the next occurrence of the alarm * with the specified ID. */ DateTime DeferAlarmDlg::setLimit(const KAEvent& event) { Q_ASSERT(event.collectionId() >= 0); mLimitEventId = EventId(event); - const KAEvent* evnt = AlarmCalendar::getEvent(mLimitEventId); + const KAEvent* evnt = ResourcesCalendar::getEvent(mLimitEventId); mLimitDateTime = evnt ? evnt->deferralLimit() : DateTime(); mTimeWidget->setMaxDateTime(mLimitDateTime); return mLimitDateTime; } /****************************************************************************** * Called when the Cancel Deferral button is clicked. */ void DeferAlarmDlg::slotCancelDeferral() { mAlarmDateTime = DateTime(); accept(); } // vim: et sw=4: diff --git a/src/editdlg.cpp b/src/editdlg.cpp index ea13f4c6..cea5616e 100644 --- a/src/editdlg.cpp +++ b/src/editdlg.cpp @@ -1,1478 +1,1478 @@ /* * editdlg.cpp - dialog to create or modify an alarm or alarm template * Program: kalarm - * Copyright © 2001-2019 David Jarvie + * Copyright © 2001-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "editdlg.h" #include "editdlg_p.h" #include "editdlgtypes.h" #include "alarmcalendar.h" #include "alarmtimewidget.h" #include "deferdlg.h" #include "functions.h" #include "kalarmapp.h" #include "latecancel.h" #include "mainwindow.h" #include "preferences.h" #include "recurrenceedit.h" #include "reminder.h" #include "resources/datamodel.h" #include "resources/resources.h" #include "lib/autoqpointer.h" #include "lib/buttongroup.h" #include "lib/checkbox.h" #include "lib/config.h" #include "lib/lineedit.h" #include "lib/messagebox.h" #include "lib/packedlayout.h" #include "lib/radiobutton.h" #include "lib/shellprocess.h" #include "lib/spinbox.h" #include "lib/stackedwidgets.h" #include "templatepickdlg.h" #include "lib/timeedit.h" #include "lib/timespinbox.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KCal; using namespace KAlarmCal; static const char EDIT_DIALOG_NAME[] = "EditDialog"; static const char TEMPLATE_DIALOG_NAME[] = "EditTemplateDialog"; static const char EDIT_MORE_GROUP[] = "ShowOpts"; static const char EDIT_MORE_KEY[] = "EditMore"; static const int maxDelayTime = 99*60 + 59; // < 100 hours inline QString recurText(const KAEvent& event) { QString r; if (event.repetition()) r = QStringLiteral("%1 / %2").arg(event.recurrenceText()).arg(event.repetitionText()); else r = event.recurrenceText(); return i18nc("@title:tab", "Recurrence - [%1]", r); } QList EditAlarmDlg::mWindowList; // Collect these widget labels together to ensure consistent wording and // translations across different modules. QString EditAlarmDlg::i18n_chk_ShowInKOrganizer() { return i18nc("@option:check", "Show in KOrganizer"); } EditAlarmDlg* EditAlarmDlg::create(bool Template, Type type, QWidget* parent, GetResourceType getResource) { qCDebug(KALARM_LOG) << "EditAlarmDlg::create"; switch (type) { case DISPLAY: return new EditDisplayAlarmDlg(Template, parent, getResource); case COMMAND: return new EditCommandAlarmDlg(Template, parent, getResource); case EMAIL: return new EditEmailAlarmDlg(Template, parent, getResource); case AUDIO: return new EditAudioAlarmDlg(Template, parent, getResource); default: break; } return nullptr; } EditAlarmDlg* EditAlarmDlg::create(bool Template, const KAEvent* event, bool newAlarm, QWidget* parent, GetResourceType getResource, bool readOnly) { switch (event->actionTypes()) { case KAEvent::ACT_COMMAND: return new EditCommandAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); case KAEvent::ACT_DISPLAY_COMMAND: case KAEvent::ACT_DISPLAY: return new EditDisplayAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); case KAEvent::ACT_EMAIL: return new EditEmailAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); case KAEvent::ACT_AUDIO: return new EditAudioAlarmDlg(Template, event, newAlarm, parent, getResource, readOnly); default: break; } return nullptr; } /****************************************************************************** * Constructor. * Parameters: * Template = true to edit/create an alarm template * = false to edit/create an alarm. * event != to initialise the dialog to show the specified event's data. */ EditAlarmDlg::EditAlarmDlg(bool Template, KAEvent::SubAction action, QWidget* parent, GetResourceType getResource) : QDialog(parent) , mAlarmType(action) , mTemplate(Template) , mNewAlarm(true) , mDesiredReadOnly(false) , mReadOnly(false) { init(nullptr, getResource); mWindowList.append(this); } EditAlarmDlg::EditAlarmDlg(bool Template, const KAEvent* event, bool newAlarm, QWidget* parent, GetResourceType getResource, bool readOnly) : QDialog(parent) , mAlarmType(event->actionSubType()) , mEventId(newAlarm ? QString() : event->id()) , mTemplate(Template) , mNewAlarm(newAlarm) , mDesiredReadOnly(readOnly) , mReadOnly(readOnly) { init(event, getResource); mWindowList.append(this); } void EditAlarmDlg::init(const KAEvent* event, GetResourceType getResource) { switch (getResource) { case RES_USE_EVENT_ID: if (event) { mResourceEventId = event->id(); mUseResourceEventId = true; break; } Q_FALLTHROUGH(); // fall through to RES_PROMPT case RES_PROMPT: mResourceEventId.clear(); mUseResourceEventId = true; break; case RES_IGNORE: default: mResourceEventId.clear(); mUseResourceEventId = false; break; } } void EditAlarmDlg::init(const KAEvent* event) { setObjectName(mTemplate ? QStringLiteral("TemplEditDlg") : QStringLiteral("EditDlg")); // used by LikeBack QString caption; if (mReadOnly) caption = mTemplate ? i18nc("@title:window", "Alarm Template [read-only]") : event->expired() ? i18nc("@title:window", "Archived Alarm [read-only]") : i18nc("@title:window", "Alarm [read-only]"); else caption = type_caption(); setWindowTitle(caption); // Create button box now so that types can work with it, but don't insert // it into layout just yet mButtonBox = new QDialogButtonBox(this); if (mReadOnly) { mButtonBox->addButton(QDialogButtonBox::Cancel); mTryButton = mButtonBox->addButton(i18nc("@action:button", "Try"), QDialogButtonBox::ActionRole); mMoreLessButton = mButtonBox->addButton(QDialogButtonBox::RestoreDefaults); } else if (mTemplate) { mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); mTryButton = mButtonBox->addButton(i18nc("@action:button", "Try"), QDialogButtonBox::ActionRole); mMoreLessButton = mButtonBox->addButton(QDialogButtonBox::RestoreDefaults); } else { mButtonBox->addButton(QDialogButtonBox::Ok); mButtonBox->addButton(QDialogButtonBox::Cancel); mTryButton = mButtonBox->addButton(i18nc("@action:button", "Try"), QDialogButtonBox::ActionRole); mLoadTemplateButton = mButtonBox->addButton(i18nc("@action:button", "Load Template..."), QDialogButtonBox::HelpRole); mMoreLessButton = mButtonBox->addButton(QDialogButtonBox::RestoreDefaults); } connect(mButtonBox, &QDialogButtonBox::clicked, this, &EditAlarmDlg::slotButtonClicked); if (mButtonBox->button(QDialogButtonBox::Ok)) mButtonBox->button(QDialogButtonBox::Ok)->setWhatsThis(i18nc("@info:whatsthis", "Schedule the alarm at the specified time.")); QVBoxLayout* mainLayout = new QVBoxLayout(this); if (mTemplate) { QFrame* frame = new QFrame; QHBoxLayout* box = new QHBoxLayout(); frame->setLayout(box); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label:textbox", "Template name:")); label->setFixedSize(label->sizeHint()); box->addWidget(label); mTemplateName = new QLineEdit(); mTemplateName->setReadOnly(mReadOnly); connect(mTemplateName, &QLineEdit::textEdited, this, &EditAlarmDlg::contentsChanged); label->setBuddy(mTemplateName); box->addWidget(mTemplateName); frame->setWhatsThis(i18nc("@info:whatsthis", "Enter the name of the alarm template")); frame->setFixedHeight(box->sizeHint().height()); mainLayout->addWidget(frame); } mTabs = new QTabWidget(this); mainLayout->addWidget(mTabs); mTabScrollGroup = new StackedScrollGroup(this, mTabs); StackedScrollWidget* mainScroll = new StackedScrollWidget(mTabScrollGroup); mTabs->addTab(mainScroll, i18nc("@title:tab", "Alarm")); mMainPageIndex = 0; PageFrame* mainPage = new PageFrame(mainScroll); mainScroll->setWidget(mainPage); // mainPage becomes the child of mainScroll connect(mainPage, &PageFrame::shown, this, &EditAlarmDlg::slotShowMainPage); QVBoxLayout* topLayout = new QVBoxLayout(mainPage); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->setContentsMargins(dcm, dcm, dcm, dcm); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Recurrence tab StackedScrollWidget* recurScroll = new StackedScrollWidget(mTabScrollGroup); mTabs->addTab(recurScroll, QString()); mRecurPageIndex = 1; QFrame* recurTab = new QFrame; QVBoxLayout* recurTabLayout = new QVBoxLayout(); recurTabLayout->setContentsMargins(dcm, dcm, dcm, dcm); recurTab->setLayout(recurTabLayout); recurScroll->setWidget(recurTab); // recurTab becomes the child of recurScroll mRecurrenceEdit = new RecurrenceEdit(mReadOnly); recurTabLayout->addWidget(mRecurrenceEdit); connect(mRecurrenceEdit, &RecurrenceEdit::shown, this, &EditAlarmDlg::slotShowRecurrenceEdit); connect(mRecurrenceEdit, &RecurrenceEdit::typeChanged, this, &EditAlarmDlg::slotRecurTypeChange); connect(mRecurrenceEdit, &RecurrenceEdit::frequencyChanged, this, &EditAlarmDlg::slotRecurFrequencyChange); connect(mRecurrenceEdit, &RecurrenceEdit::repeatNeedsInitialisation, this, &EditAlarmDlg::slotSetSubRepetition); connect(mRecurrenceEdit, &RecurrenceEdit::contentsChanged, this, &EditAlarmDlg::contentsChanged); // Controls specific to the alarm type QGroupBox* actionBox = new QGroupBox(i18nc("@title:group", "Action"), mainPage); topLayout->addWidget(actionBox, 1); QVBoxLayout* layout = new QVBoxLayout(actionBox); layout->setContentsMargins(dcm, dcm, dcm, dcm); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); type_init(actionBox, layout); if (!mTemplate) { // Deferred date/time: visible only for a deferred recurring event. mDeferGroup = new QGroupBox(i18nc("@title:group", "Deferred Alarm"), mainPage); topLayout->addWidget(mDeferGroup); QHBoxLayout* hlayout = new QHBoxLayout(mDeferGroup); hlayout->setContentsMargins(dcm, dcm, dcm, dcm); hlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label", "Deferred to:"), mDeferGroup); label->setFixedSize(label->sizeHint()); hlayout->addWidget(label); mDeferTimeLabel = new QLabel(mDeferGroup); hlayout->addWidget(mDeferTimeLabel); mDeferChangeButton = new QPushButton(i18nc("@action:button", "Change..."), mDeferGroup); mDeferChangeButton->setFixedSize(mDeferChangeButton->sizeHint()); connect(mDeferChangeButton, &QPushButton::clicked, this, &EditAlarmDlg::slotEditDeferral); mDeferChangeButton->setWhatsThis(i18nc("@info:whatsthis", "Change the alarm's deferred time, or cancel the deferral")); hlayout->addWidget(mDeferChangeButton); //?? mDeferGroup->addSpace(0); } QHBoxLayout* hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); topLayout->addLayout(hlayout); // Date and time entry if (mTemplate) { QGroupBox* templateTimeBox = new QGroupBox(i18nc("@title:group", "Time"), mainPage); topLayout->addWidget(templateTimeBox); QGridLayout* grid = new QGridLayout(templateTimeBox); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTemplateTimeGroup = new ButtonGroup(templateTimeBox); connect(mTemplateTimeGroup, &ButtonGroup::buttonSet, this, &EditAlarmDlg::slotTemplateTimeType); connect(mTemplateTimeGroup, &ButtonGroup::buttonSet, this, &EditAlarmDlg::contentsChanged); mTemplateDefaultTime = new RadioButton(i18nc("@option:radio", "Default time"), templateTimeBox); mTemplateDefaultTime->setFixedSize(mTemplateDefaultTime->sizeHint()); mTemplateDefaultTime->setReadOnly(mReadOnly); mTemplateDefaultTime->setWhatsThis(i18nc("@info:whatsthis", "Do not specify a start time for alarms based on this template. " "The normal default start time will be used.")); mTemplateTimeGroup->addButton(mTemplateDefaultTime); grid->addWidget(mTemplateDefaultTime, 0, 0, Qt::AlignLeft); QWidget* box = new QWidget(templateTimeBox); QHBoxLayout* layout = new QHBoxLayout(box); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTemplateUseTime = new RadioButton(i18nc("@option:radio", "Time:"), box); mTemplateUseTime->setFixedSize(mTemplateUseTime->sizeHint()); mTemplateUseTime->setReadOnly(mReadOnly); mTemplateUseTime->setWhatsThis(i18nc("@info:whatsthis", "Specify a start time for alarms based on this template.")); layout->addWidget(mTemplateUseTime); mTemplateTimeGroup->addButton(mTemplateUseTime); mTemplateTime = new TimeEdit(); mTemplateTime->setFixedSize(mTemplateTime->sizeHint()); mTemplateTime->setReadOnly(mReadOnly); mTemplateTime->setWhatsThis(xi18nc("@info:whatsthis", "Enter the start time for alarms based on this template.%1", TimeSpinBox::shiftWhatsThis())); connect(mTemplateTime, &TimeEdit::valueChanged, this, &EditAlarmDlg::contentsChanged); layout->addWidget(mTemplateTime); layout->addStretch(1); grid->addWidget(box, 0, 1, Qt::AlignLeft); mTemplateAnyTime = new RadioButton(i18nc("@option:radio", "Date only"), templateTimeBox); mTemplateAnyTime->setFixedSize(mTemplateAnyTime->sizeHint()); mTemplateAnyTime->setReadOnly(mReadOnly); mTemplateAnyTime->setWhatsThis(xi18nc("@info:whatsthis", "Set the Any time option for alarms based on this template.")); mTemplateTimeGroup->addButton(mTemplateAnyTime); grid->addWidget(mTemplateAnyTime, 1, 0, Qt::AlignLeft); box = new QWidget(templateTimeBox); layout = new QHBoxLayout(box); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTemplateUseTimeAfter = new RadioButton(i18nc("@option:radio", "Time from now:"), box); mTemplateUseTimeAfter->setFixedSize(mTemplateUseTimeAfter->sizeHint()); mTemplateUseTimeAfter->setReadOnly(mReadOnly); mTemplateUseTimeAfter->setWhatsThis(i18nc("@info:whatsthis", "Set alarms based on this template to start after the specified time " "interval from when the alarm is created.")); layout->addWidget(mTemplateUseTimeAfter); mTemplateTimeGroup->addButton(mTemplateUseTimeAfter); mTemplateTimeAfter = new TimeSpinBox(1, maxDelayTime); mTemplateTimeAfter->setValue(1439); mTemplateTimeAfter->setFixedSize(mTemplateTimeAfter->sizeHint()); mTemplateTimeAfter->setReadOnly(mReadOnly); connect(mTemplateTimeAfter, static_cast(&TimeSpinBox::valueChanged), this, &EditAlarmDlg::contentsChanged); mTemplateTimeAfter->setWhatsThis(xi18nc("@info:whatsthis", "%1%2", AlarmTimeWidget::i18n_TimeAfterPeriod(), TimeSpinBox::shiftWhatsThis())); layout->addWidget(mTemplateTimeAfter); box->setFixedHeight(box->sizeHint().height()); grid->addWidget(box, 1, 1, Qt::AlignLeft); hlayout->addStretch(); } else { mTimeWidget = new AlarmTimeWidget(i18nc("@title:group", "Time"), AlarmTimeWidget::AT_TIME, mainPage); connect(mTimeWidget, &AlarmTimeWidget::dateOnlyToggled, this, &EditAlarmDlg::slotAnyTimeToggled); connect(mTimeWidget, &AlarmTimeWidget::changed, this, &EditAlarmDlg::contentsChanged); topLayout->addWidget(mTimeWidget); } // Optional controls depending on More/Fewer Options button mMoreOptions = new QFrame(mainPage); mMoreOptions->setFrameStyle(QFrame::NoFrame); topLayout->addWidget(mMoreOptions); QVBoxLayout* moreLayout = new QVBoxLayout(mMoreOptions); moreLayout->setContentsMargins(0, 0, 0, 0); moreLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Reminder mReminder = createReminder(mMoreOptions); if (mReminder) { mReminder->setFixedSize(mReminder->sizeHint()); connect(mReminder, &Reminder::changed, this, &EditAlarmDlg::contentsChanged); moreLayout->addWidget(mReminder, 0, Qt::AlignLeft); if (mTimeWidget) connect(mTimeWidget, &AlarmTimeWidget::changed, mReminder, &Reminder::setDefaultUnits); } // Late cancel selector - default = allow late display mLateCancel = new LateCancelSelector(true, mMoreOptions); connect(mLateCancel, &LateCancelSelector::changed, this, &EditAlarmDlg::contentsChanged); moreLayout->addWidget(mLateCancel, 0, Qt::AlignLeft); PackedLayout* playout = new PackedLayout(Qt::AlignJustify); playout->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); moreLayout->addLayout(playout); // Acknowledgement confirmation required - default = no confirmation CheckBox* confirmAck = type_createConfirmAckCheckbox(mMoreOptions); if (confirmAck) { confirmAck->setFixedSize(confirmAck->sizeHint()); connect(confirmAck, &CheckBox::toggled, this, &EditAlarmDlg::contentsChanged); playout->addWidget(confirmAck); } if (theApp()->korganizerEnabled()) { // Show in KOrganizer checkbox mShowInKorganizer = new CheckBox(i18n_chk_ShowInKOrganizer(), mMoreOptions); mShowInKorganizer->setFixedSize(mShowInKorganizer->sizeHint()); connect(mShowInKorganizer, &CheckBox::toggled, this, &EditAlarmDlg::contentsChanged); mShowInKorganizer->setWhatsThis(i18nc("@info:whatsthis", "Check to copy the alarm into KOrganizer's calendar")); playout->addWidget(mShowInKorganizer); } mainLayout->addWidget(mButtonBox); // Hide optional controls KConfigGroup config(KSharedConfig::openConfig(), EDIT_MORE_GROUP); showOptions(config.readEntry(EDIT_MORE_KEY, false)); // Initialise the state of all controls according to the specified event, if any initValues(event); if (mTemplateName) mTemplateName->setFocus(); if (!mNewAlarm) { // Save the initial state of all controls so that we can later tell if they have changed saveState((event && (mTemplate || !event->isTemplate())) ? event : nullptr); contentsChanged(); // enable/disable OK button } // Note the current desktop so that the dialog can be shown on it. // If a main window is visible, the dialog will by KDE default always appear on its // desktop. If the user invokes the dialog via the system tray on a different desktop, // that can cause confusion. mDesktop = KWindowSystem::currentDesktop(); if (theApp()->windowFocusBroken()) { const QList children = findChildren(); for (QWidget* w : children) w->installEventFilter(this); } } EditAlarmDlg::~EditAlarmDlg() { delete mButtonBox; mButtonBox = nullptr; // prevent text edit contentsChanged() signal triggering a crash delete mSavedEvent; mWindowList.removeAll(this); } /****************************************************************************** * Return the number of instances. */ int EditAlarmDlg::instanceCount() { return mWindowList.count(); } /****************************************************************************** * Initialise the dialog controls from the specified event. */ void EditAlarmDlg::initValues(const KAEvent* event) { setReadOnly(mDesiredReadOnly); mChanged = false; mOnlyDeferred = false; mExpiredRecurrence = false; mLateCancel->showAutoClose(false); bool deferGroupVisible = false; if (event) { // Set the values to those for the specified event if (mTemplate) mTemplateName->setText(event->templateName()); bool recurs = event->recurs(); if ((recurs || event->repetition()) && !mTemplate && event->deferred()) { deferGroupVisible = true; mDeferDateTime = event->deferDateTime(); mDeferTimeLabel->setText(mDeferDateTime.formatLocale()); mDeferGroup->show(); } if (mTemplate) { // Editing a template int afterTime = event->isTemplate() ? event->templateAfterTime() : -1; bool noTime = !afterTime; bool useTime = !event->mainDateTime().isDateOnly(); RadioButton* button = noTime ? mTemplateDefaultTime : (afterTime > 0) ? mTemplateUseTimeAfter : useTime ? mTemplateUseTime : mTemplateAnyTime; button->setChecked(true); mTemplateTimeAfter->setValue(afterTime > 0 ? afterTime : 1); if (!noTime && useTime) mTemplateTime->setValue(event->mainDateTime().kDateTime().time()); else mTemplateTime->setValue(0); } else { if (event->isTemplate()) { // Initialising from an alarm template: use current date const KADateTime now = KADateTime::currentDateTime(Preferences::timeSpec()); int afterTime = event->templateAfterTime(); if (afterTime >= 0) { mTimeWidget->setDateTime(now.addSecs(afterTime * 60)); mTimeWidget->selectTimeFromNow(); } else { KADateTime dt = event->startDateTime().kDateTime(); dt.setTimeSpec(Preferences::timeSpec()); QDate d = now.date(); if (!dt.isDateOnly() && now.time() >= dt.time()) d = d.addDays(1); // alarm time has already passed, so use tomorrow dt.setDate(d); mTimeWidget->setDateTime(dt); } } else { mExpiredRecurrence = recurs && event->mainExpired(); mTimeWidget->setDateTime(recurs || event->category() == CalEvent::ARCHIVED ? event->startDateTime() : event->mainExpired() ? event->deferDateTime() : event->mainDateTime()); } } KAEvent::SubAction action = event->actionSubType(); AlarmText altext; if (event->commandScript()) altext.setScript(event->cleanText()); else altext.setText(event->cleanText()); setAction(action, altext); mLateCancel->setMinutes(event->lateCancel(), event->startDateTime().isDateOnly(), TimePeriod::HoursMinutes); if (mShowInKorganizer) mShowInKorganizer->setChecked(event->copyToKOrganizer()); type_initValues(event); mRecurrenceEdit->set(*event); // must be called after mTimeWidget is set up, to ensure correct date-only enabling mTabs->setTabText(mRecurPageIndex, recurText(*event)); } else { // Set the values to their defaults const KADateTime defaultTime = KADateTime::currentUtcDateTime().addSecs(60).toTimeSpec(Preferences::timeSpec()); if (mTemplate) { mTemplateDefaultTime->setChecked(true); mTemplateTime->setValue(0); mTemplateTimeAfter->setValue(1); } else mTimeWidget->setDateTime(defaultTime); mLateCancel->setMinutes((Preferences::defaultLateCancel() ? 1 : 0), false, TimePeriod::HoursMinutes); if (mShowInKorganizer) mShowInKorganizer->setChecked(Preferences::defaultCopyToKOrganizer()); type_initValues(nullptr); mRecurrenceEdit->setDefaults(defaultTime); // must be called after mTimeWidget is set up, to ensure correct date-only enabling slotRecurFrequencyChange(); // update the Recurrence text } if (mReminder && mTimeWidget) mReminder->setDefaultUnits(mTimeWidget->getDateTime(nullptr, false, false)); if (!deferGroupVisible && mDeferGroup) mDeferGroup->hide(); - bool empty = AlarmCalendar::resources()->events(CalEvent::TEMPLATE).isEmpty(); + bool empty = ResourcesCalendar::instance()->events(CalEvent::TEMPLATE).isEmpty(); if (mLoadTemplateButton) mLoadTemplateButton->setEnabled(!empty); } /****************************************************************************** * Initialise various values in the New Alarm dialogue. */ void EditAlarmDlg::setTime(const DateTime& start) { mTimeWidget->setDateTime(start); } void EditAlarmDlg::setRecurrence(const KARecurrence& recur, const KCalendarCore::Duration& subRepeatInterval, int subRepeatCount) { KAEvent event; event.setTime(mTimeWidget->getDateTime(nullptr, false, false)); event.setRecurrence(recur); event.setRepetition(Repetition(subRepeatInterval, subRepeatCount - 1)); mRecurrenceEdit->set(event); } void EditAlarmDlg::setRepeatAtLogin() { mRecurrenceEdit->setRepeatAtLogin(); } void EditAlarmDlg::setLateCancel(int minutes) { mLateCancel->setMinutes(minutes, mTimeWidget->getDateTime(nullptr, false, false).isDateOnly(), TimePeriod::HoursMinutes); } void EditAlarmDlg::setShowInKOrganizer(bool show) { mShowInKorganizer->setChecked(show); } /****************************************************************************** * Set the read-only status of all non-template controls. */ void EditAlarmDlg::setReadOnly(bool readOnly) { mReadOnly = readOnly; if (mTimeWidget) mTimeWidget->setReadOnly(readOnly); mLateCancel->setReadOnly(readOnly); if (mDeferChangeButton) { if (readOnly) mDeferChangeButton->hide(); else mDeferChangeButton->show(); } if (mShowInKorganizer) mShowInKorganizer->setReadOnly(readOnly); } /****************************************************************************** * Save the state of all controls. */ void EditAlarmDlg::saveState(const KAEvent* event) { delete mSavedEvent; mSavedEvent = nullptr; if (event) mSavedEvent = new KAEvent(*event); if (mTemplate) { mSavedTemplateName = mTemplateName->text(); mSavedTemplateTimeType = mTemplateTimeGroup->checkedButton(); mSavedTemplateTime = mTemplateTime->time(); mSavedTemplateAfterTime = mTemplateTimeAfter->value(); } checkText(mSavedTextFileCommandMessage, false); if (mTimeWidget) mSavedDateTime = mTimeWidget->getDateTime(nullptr, false, false); mSavedLateCancel = mLateCancel->minutes(); if (mShowInKorganizer) mSavedShowInKorganizer = mShowInKorganizer->isChecked(); mSavedRecurrenceType = mRecurrenceEdit->repeatType(); mSavedDeferTime = mDeferDateTime.kDateTime(); } /****************************************************************************** * Check whether any of the controls has changed state since the dialog was * first displayed. * Reply = true if any non-deferral controls have changed, or if it's a new event. * = false if no non-deferral controls have changed. In this case, * mOnlyDeferred indicates whether deferral controls may have changed. */ bool EditAlarmDlg::stateChanged() const { mChanged = true; mOnlyDeferred = false; if (!mSavedEvent) return true; QString textFileCommandMessage; checkText(textFileCommandMessage, false); if (mTemplate) { if (mSavedTemplateName != mTemplateName->text() || mSavedTemplateTimeType != mTemplateTimeGroup->checkedButton() || (mTemplateUseTime->isChecked() && mSavedTemplateTime != mTemplateTime->time()) || (mTemplateUseTimeAfter->isChecked() && mSavedTemplateAfterTime != mTemplateTimeAfter->value())) return true; } else { const KADateTime dt = mTimeWidget->getDateTime(nullptr, false, false); if (mSavedDateTime.timeSpec() != dt.timeSpec() || mSavedDateTime != dt) return true; } if (mSavedLateCancel != mLateCancel->minutes() || (mShowInKorganizer && mSavedShowInKorganizer != mShowInKorganizer->isChecked()) || textFileCommandMessage != mSavedTextFileCommandMessage || mSavedRecurrenceType != mRecurrenceEdit->repeatType()) return true; if (type_stateChanged()) return true; if (mRecurrenceEdit->stateChanged()) return true; if (mSavedEvent && mSavedEvent->deferred()) mOnlyDeferred = true; mChanged = false; return false; } /****************************************************************************** * Called whenever any of the controls changes state. * Enable or disable the OK button depending on whether any controls have a * different state from their initial state. */ void EditAlarmDlg::contentsChanged() { // Don't do anything if it's a new alarm or we're still initialising // (i.e. mSavedEvent null). if (mSavedEvent && mButtonBox && mButtonBox->button(QDialogButtonBox::Ok)) mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(stateChanged() || mDeferDateTime.kDateTime() != mSavedDeferTime); } /****************************************************************************** * Get the currently entered dialog data. * The data is returned in the supplied KAEvent instance. * Reply = false if the only change has been to an existing deferral. */ bool EditAlarmDlg::getEvent(KAEvent& event, Resource& resource) { resource = mResource; if (mChanged) { // It's a new event, or the edit controls have changed setEvent(event, mAlarmMessage, false); return true; } // Only the deferral time may have changed event = *mSavedEvent; if (mOnlyDeferred) { // Just modify the original event, to avoid expired recurring events // being returned as rubbish. if (mDeferDateTime.isValid()) event.defer(mDeferDateTime, event.reminderDeferral(), false); else event.cancelDefer(); } return false; } /****************************************************************************** * Extract the data in the dialog and set up a KAEvent from it. * If 'trial' is true, the event is set up for a simple one-off test, ignoring * recurrence, reminder, template etc. data. */ void EditAlarmDlg::setEvent(KAEvent& event, const QString& text, bool trial) { KADateTime dt; if (!trial) { if (!mTemplate) dt = mAlarmDateTime.effectiveKDateTime(); else if (mTemplateUseTime->isChecked()) dt = KADateTime(QDate(2000,1,1), mTemplateTime->time()); } int lateCancel = (trial || !mLateCancel->isEnabled()) ? 0 : mLateCancel->minutes(); type_setEvent(event, dt, text, lateCancel, trial); if (!trial) { if (mRecurrenceEdit->repeatType() != RecurrenceEdit::NO_RECUR) { mRecurrenceEdit->updateEvent(event, !mTemplate); const KADateTime now = KADateTime::currentDateTime(mAlarmDateTime.timeSpec()); bool dateOnly = mAlarmDateTime.isDateOnly(); if ((dateOnly && mAlarmDateTime.date() < now.date()) || (!dateOnly && mAlarmDateTime.kDateTime() < now)) { // A timed recurrence has an entered start date which has // already expired, so we must adjust the next repetition. event.setNextOccurrence(now); } mAlarmDateTime = event.startDateTime(); if (mDeferDateTime.isValid() && mDeferDateTime < mAlarmDateTime) { bool deferral = true; bool deferReminder = false; int reminder = mReminder ? mReminder->minutes() : 0; if (reminder) { DateTime remindTime = mAlarmDateTime.addMins(-reminder); if (mDeferDateTime >= remindTime) { if (remindTime > KADateTime::currentUtcDateTime()) deferral = false; // ignore deferral if it's after next reminder else if (mDeferDateTime > remindTime) deferReminder = true; // it's the reminder which is being deferred } } if (deferral) event.defer(mDeferDateTime, deferReminder, false); } } if (mTemplate) { int afterTime = mTemplateDefaultTime->isChecked() ? 0 : mTemplateUseTimeAfter->isChecked() ? mTemplateTimeAfter->value() : -1; event.setTemplate(mTemplateName->text(), afterTime); } } } /****************************************************************************** * Get the currently specified alarm flag bits. */ KAEvent::Flags EditAlarmDlg::getAlarmFlags() const { KAEvent::Flags flags{}; if (mShowInKorganizer && mShowInKorganizer->isEnabled() && mShowInKorganizer->isChecked()) flags |= KAEvent::COPY_KORGANIZER; if (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN) flags |= KAEvent::REPEAT_AT_LOGIN; if (mTemplate ? mTemplateAnyTime->isChecked() : mAlarmDateTime.isDateOnly()) flags |= KAEvent::ANY_TIME; return flags; } /****************************************************************************** * Called when the dialog is displayed. * The first time through, sets the size to the same as the last time it was * displayed. */ void EditAlarmDlg::showEvent(QShowEvent* se) { QDialog::showEvent(se); if (!mDeferGroupHeight) { if (mDeferGroup) mDeferGroupHeight = mDeferGroup->height() + style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing); QSize s; if (Config::readWindowSize(mTemplate ? TEMPLATE_DIALOG_NAME : EDIT_DIALOG_NAME, s)) { bool defer = mDeferGroup && !mDeferGroup->isHidden(); s.setHeight(s.height() + (defer ? mDeferGroupHeight : 0)); if (!defer) mTabScrollGroup->setSized(); resize(s); } } slotResize(); KWindowSystem::setOnDesktop(winId(), mDesktop); // ensure it displays on the desktop expected by the user if (theApp()->needWindowFocusFix()) { QApplication::setActiveWindow(this); QTimer::singleShot(0, this, &EditAlarmDlg::focusFixTimer); } } /****************************************************************************** * Called when the window is first shown, to ensure that it initially becomes * the active window. * This is only required on Ubuntu's Unity desktop, which doesn't transfer * keyboard focus properly between Edit Alarm Dialog windows and MessageWin * windows. */ void EditAlarmDlg::focusFixTimer() { if (theApp()->needWindowFocusFix() && QApplication::focusWidget()->window() != this) { QApplication::setActiveWindow(this); QTimer::singleShot(0, this, &EditAlarmDlg::focusFixTimer); } } /****************************************************************************** * Called to detect when the mouse is pressed anywhere inside the window. * Activates this window if a MessageWin window is also active. * This is only required on Ubuntu's Unity desktop, which doesn't transfer * keyboard focus properly between Edit Alarm Dialog windows and MessageWin * windows. */ bool EditAlarmDlg::eventFilter(QObject*, QEvent* e) { if (theApp()->needWindowFocusFix()) { if (e->type() == QEvent::MouseButtonPress) QApplication::setActiveWindow(this); } return false; } /****************************************************************************** * Called when the dialog is closed. */ void EditAlarmDlg::closeEvent(QCloseEvent* ce) { Q_EMIT rejected(); QDialog::closeEvent(ce); } /****************************************************************************** * Update the tab sizes (again) and if the resized dialog height is greater * than the minimum, resize it again. This is necessary because (a) resizing * tabs doesn't always work properly the first time, and (b) resizing to the * minimum size hint doesn't always work either. */ void EditAlarmDlg::slotResize() { QSize s = mTabScrollGroup->adjustSize(true); s = minimumSizeHint(); if (height() > s.height()) { // Resize to slightly greater than the minimum height. // This is for some unknown reason necessary, since // sometimes resizing to the minimum height fails. resize(s.width(), s.height() + 2); } } /****************************************************************************** * Called when the dialog's size has changed. * Records the new size (adjusted to ignore the optional height of the deferred * time edit widget) in the config file. */ void EditAlarmDlg::resizeEvent(QResizeEvent* re) { if (isVisible() && mDeferGroupHeight) { QSize s = re->size(); s.setHeight(s.height() - (!mDeferGroup || mDeferGroup->isHidden() ? 0 : mDeferGroupHeight)); Config::writeWindowSize(mTemplate ? TEMPLATE_DIALOG_NAME : EDIT_DIALOG_NAME, s); } QDialog::resizeEvent(re); } /****************************************************************************** * Called when any button is clicked. */ void EditAlarmDlg::slotButtonClicked(QAbstractButton* button) { if (button == mTryButton) slotTry(); else if (button == mLoadTemplateButton) slotHelp(); else if (button == mMoreLessButton) slotDefault(); else if (button == mButtonBox->button(QDialogButtonBox::Ok)) { if (validate()) accept(); } else reject(); } /****************************************************************************** * Called when the OK button is clicked. * Validate the input data. */ bool EditAlarmDlg::validate() { if (!stateChanged()) { // No changes have been made except possibly to an existing deferral if (!mOnlyDeferred) reject(); return mOnlyDeferred; } RecurrenceEdit::RepeatType recurType = mRecurrenceEdit->repeatType(); if (mTimeWidget && mTabs->currentIndex() == mRecurPageIndex && recurType == RecurrenceEdit::AT_LOGIN) mTimeWidget->setDateTime(mRecurrenceEdit->endDateTime()); bool timedRecurrence = mRecurrenceEdit->isTimedRepeatType(); // does it recur other than at login? if (mTemplate) { // Check that the template name is not blank and is unique QString errmsg; QString name = mTemplateName->text(); if (name.isEmpty()) errmsg = i18nc("@info", "You must enter a name for the alarm template"); else if (name != mSavedTemplateName) { - if (AlarmCalendar::resources()->templateEvent(name)) + if (ResourcesCalendar::instance()->templateEvent(name)) errmsg = i18nc("@info", "Template name is already in use"); } if (!errmsg.isEmpty()) { mTemplateName->setFocus(); KAMessageBox::sorry(this, errmsg); return false; } } else if (mTimeWidget) { QWidget* errWidget; mAlarmDateTime = mTimeWidget->getDateTime(nullptr, !timedRecurrence, false, &errWidget); if (errWidget) { // It's more than just an existing deferral being changed, so the time matters mTabs->setCurrentIndex(mMainPageIndex); errWidget->setFocus(); mTimeWidget->getDateTime(); // display the error message now return false; } } if (!type_validate(false)) return false; if (!mTemplate) { if (mChanged && mRecurrenceEdit->repeatType() != RecurrenceEdit::NO_RECUR) { // Check whether the start date/time must be adjusted // to match the recurrence specification. DateTime dt = mAlarmDateTime; // setEvent() changes mAlarmDateTime KAEvent event; setEvent(event, mAlarmMessage, false); mAlarmDateTime = dt; // restore KADateTime pre = dt.effectiveKDateTime(); bool dateOnly = dt.isDateOnly(); if (dateOnly) pre = pre.addDays(-1); else pre = pre.addSecs(-1); DateTime next; event.nextOccurrence(pre, next, KAEvent::IGNORE_REPETITION); if (next != dt) { QString prompt = dateOnly ? i18nc("@info The parameter is a date value", "The start date does not match the alarm's recurrence pattern, " "so it will be adjusted to the date of the next recurrence (%1).", QLocale().toString(next.date(), QLocale::ShortFormat)) : i18nc("@info The parameter is a date/time value", "The start date/time does not match the alarm's recurrence pattern, " "so it will be adjusted to the date/time of the next recurrence (%1).", QLocale().toString(next.effectiveDateTime(), QLocale::ShortFormat)); if (KAMessageBox::warningContinueCancel(this, prompt) != KMessageBox::Continue) return false; } } if (timedRecurrence) { KAEvent event; Resource res; getEvent(event, res); // this may adjust mAlarmDateTime const KADateTime now = KADateTime::currentDateTime(mAlarmDateTime.timeSpec()); bool dateOnly = mAlarmDateTime.isDateOnly(); if ((dateOnly && mAlarmDateTime.date() < now.date()) || (!dateOnly && mAlarmDateTime.kDateTime() < now)) { // A timed recurrence has an entered start date which // has already expired, so we must adjust it. if (event.nextOccurrence(now, mAlarmDateTime, KAEvent::ALLOW_FOR_REPETITION) == KAEvent::NO_OCCURRENCE) { KAMessageBox::sorry(this, i18nc("@info", "Recurrence has already expired")); return false; } if (event.workTimeOnly() && !event.nextTrigger(KAEvent::DISPLAY_TRIGGER).isValid()) { if (KAMessageBox::warningContinueCancel(this, i18nc("@info", "The alarm will never occur during working hours")) != KMessageBox::Continue) return false; } } } QString errmsg; QWidget* errWidget = mRecurrenceEdit->checkData(mAlarmDateTime.effectiveKDateTime(), errmsg); if (errWidget) { mTabs->setCurrentIndex(mRecurPageIndex); errWidget->setFocus(); KAMessageBox::sorry(this, errmsg); return false; } } if (recurType != RecurrenceEdit::NO_RECUR) { KAEvent recurEvent; int longestRecurMinutes = -1; int reminder = mReminder ? mReminder->minutes() : 0; if (reminder && !mReminder->isOnceOnly()) { mRecurrenceEdit->updateEvent(recurEvent, false); longestRecurMinutes = recurEvent.longestRecurrenceInterval().asSeconds() / 60; if (longestRecurMinutes && reminder >= longestRecurMinutes) { mTabs->setCurrentIndex(mMainPageIndex); mReminder->setFocusOnCount(); KAMessageBox::sorry(this, xi18nc("@info", "Reminder period must be less than the recurrence interval, unless %1 is checked.", Reminder::i18n_chk_FirstRecurrenceOnly())); return false; } } if (mRecurrenceEdit->subRepetition()) { if (longestRecurMinutes < 0) { mRecurrenceEdit->updateEvent(recurEvent, false); longestRecurMinutes = recurEvent.longestRecurrenceInterval().asSeconds() / 60; } if (longestRecurMinutes > 0 && recurEvent.repetition().intervalMinutes() * recurEvent.repetition().count() >= longestRecurMinutes - reminder) { KAMessageBox::sorry(this, i18nc("@info", "The duration of a repetition within the recurrence must be less than the recurrence interval minus any reminder period")); mRecurrenceEdit->activateSubRepetition(); // display the alarm repetition dialog again return false; } if (!recurEvent.repetition().isDaily() && ((mTemplate && mTemplateAnyTime->isChecked()) || (!mTemplate && mAlarmDateTime.isDateOnly()))) { KAMessageBox::sorry(this, i18nc("@info", "For a repetition within the recurrence, its period must be in units of days or weeks for a date-only alarm")); mRecurrenceEdit->activateSubRepetition(); // display the alarm repetition dialog again return false; } } } if (!checkText(mAlarmMessage)) return false; mResource = Resource(); // mUseResourceEventId = false indicates that the caller already knows // which resource to use. if (mUseResourceEventId) { if (!mResourceEventId.isEmpty()) { mResource = Resources::resourceForEvent(mResourceEventId); if (mResource.isValid()) { CalEvent::Type type = mTemplate ? CalEvent::TEMPLATE : CalEvent::ACTIVE; if (!(mResource.alarmTypes() & type)) mResource = Resource(); // event may have expired while dialog was open } } bool cancelled = false; CalEvent::Type type = mTemplate ? CalEvent::TEMPLATE : CalEvent::ACTIVE; if (!mResource.isWritable(type)) mResource = Resources::destination(type, this, false, &cancelled); if (!mResource.isValid()) { if (!cancelled) KAMessageBox::sorry(this, i18nc("@info", "You must select a calendar to save the alarm in")); return false; } } return true; } /****************************************************************************** * Called when the Try button is clicked. * Display/execute the alarm immediately for the user to check its configuration. */ void EditAlarmDlg::slotTry() { QString text; if (checkText(text)) { if (!type_validate(true)) return; KAEvent event; setEvent(event, text, true); if (!mNewAlarm && !stateChanged()) { // It's an existing alarm which hasn't been changed yet: // enable KALARM_UID environment variable to be set. event.setEventId(mEventId); } type_aboutToTry(); void* result = theApp()->execAlarm(event, event.firstAlarm(), false, false); type_executedTry(text, result); } } /****************************************************************************** * Called when the Load Template button is clicked. * Prompt to select a template and initialise the dialog with its contents. */ void EditAlarmDlg::slotHelp() { KAEvent::Actions type; switch (mAlarmType) { case KAEvent::FILE: case KAEvent::MESSAGE: type = KAEvent::ACT_DISPLAY; break; case KAEvent::COMMAND: type = KAEvent::ACT_COMMAND; break; case KAEvent::EMAIL: type = KAEvent::ACT_EMAIL; break; case KAEvent::AUDIO: type = KAEvent::ACT_AUDIO; break; default: return; } // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of EditAlarmDlg, and on return from this function). AutoQPointer dlg = new TemplatePickDlg(type, this); if (dlg->exec() == QDialog::Accepted) { KAEvent event = dlg->selectedTemplate(); initValues(&event); } } /****************************************************************************** * Called when the More Options or Fewer Options buttons are clicked. * Show/hide the optional options and swap the More/Less buttons, and save the * new setting as the default from now on. */ void EditAlarmDlg::slotDefault() { showOptions(!mShowingMore); KConfigGroup config(KSharedConfig::openConfig(), EDIT_MORE_GROUP); config.writeEntry(EDIT_MORE_KEY, mShowingMore); } /****************************************************************************** * Show/hide the optional options and swap the More/Less buttons. */ void EditAlarmDlg::showOptions(bool more) { qCDebug(KALARM_LOG) << "EditAlarmDlg::showOptions:" << (more ? "More" : "Less"); if (more) { mMoreOptions->show(); mMoreLessButton->setText(i18nc("@action:Button", "Fewer Options <<")); } else { mMoreOptions->hide(); mMoreLessButton->setText(i18nc("@action:button", "More Options >>")); } if (mTimeWidget) mTimeWidget->showMoreOptions(more); type_showOptions(more); mRecurrenceEdit->showMoreOptions(more); mShowingMore = more; QTimer::singleShot(0, this, &EditAlarmDlg::slotResize); } /****************************************************************************** * Called when the Change deferral button is clicked. */ void EditAlarmDlg::slotEditDeferral() { if (!mTimeWidget) return; bool limit = true; const Repetition repetition = mRecurrenceEdit->subRepetition(); DateTime start = mSavedEvent->recurs() ? (mExpiredRecurrence ? DateTime() : mSavedEvent->mainDateTime()) : mTimeWidget->getDateTime(nullptr, !repetition, !mExpiredRecurrence); if (!start.isValid()) { if (!mExpiredRecurrence) return; limit = false; } const KADateTime now = KADateTime::currentUtcDateTime(); if (limit) { if (repetition && start < now) { // Sub-repetition - find the time of the next one int repeatNum = repetition.isDaily() ? (start.daysTo(now) + repetition.intervalDays() - 1) / repetition.intervalDays() : (start.secsTo(now) + repetition.intervalSeconds() - 1) / repetition.intervalSeconds(); if (repeatNum > repetition.count()) { mTimeWidget->getDateTime(); // output the appropriate error message return; } start = KADateTime(repetition.duration(repeatNum).end(start.qDateTime())); } } bool deferred = mDeferDateTime.isValid(); // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of EditAlarmDlg, and on return from this function). AutoQPointer deferDlg = new DeferAlarmDlg((deferred ? mDeferDateTime : DateTime(now.addSecs(60).toTimeSpec(start.timeSpec()))), start.isDateOnly(), deferred, this); deferDlg->setObjectName(QStringLiteral("EditDeferDlg")); // used by LikeBack if (limit) { // Don't allow deferral past the next recurrence int reminder = mReminder ? mReminder->minutes() : 0; if (reminder) { DateTime remindTime = start.addMins(-reminder); if (KADateTime::currentUtcDateTime() < remindTime) start = remindTime; } deferDlg->setLimit(start.addSecs(-60)); } if (deferDlg->exec() == QDialog::Accepted) { mDeferDateTime = deferDlg->getDateTime(); mDeferTimeLabel->setText(mDeferDateTime.isValid() ? mDeferDateTime.formatLocale() : QString()); contentsChanged(); } } /****************************************************************************** * Called when the main page is shown. * Sets the focus widget to the first edit field. */ void EditAlarmDlg::slotShowMainPage() { if (!mMainPageShown) { if (mTemplateName) mTemplateName->setFocus(); mMainPageShown = true; } else { // Set scroll position to top, since it otherwise jumps randomly StackedScrollWidget* main = static_cast(mTabs->widget(0)); main->verticalScrollBar()->setValue(0); } if (mTimeWidget) { if (!mReadOnly && mRecurPageShown && mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN) mTimeWidget->setDateTime(mRecurrenceEdit->endDateTime()); if (mReadOnly || mRecurrenceEdit->isTimedRepeatType()) mTimeWidget->setMinDateTime(); // don't set a minimum date/time else mTimeWidget->setMinDateTimeIsCurrent(); // set the minimum date/time to track the clock } } /****************************************************************************** * Called when the recurrence edit page is shown. * The recurrence defaults are set to correspond to the start date. * The first time, for a new alarm, the recurrence end date is set according to * the alarm start time. */ void EditAlarmDlg::slotShowRecurrenceEdit() { mRecurPageIndex = mTabs->currentIndex(); if (!mReadOnly && !mTemplate) { mAlarmDateTime = mTimeWidget->getDateTime(nullptr, false, false); const KADateTime now = KADateTime::currentDateTime(mAlarmDateTime.timeSpec()); bool expired = (mAlarmDateTime.effectiveKDateTime() < now); if (mRecurSetDefaultEndDate) { mRecurrenceEdit->setDefaultEndDate(expired ? now.date() : mAlarmDateTime.date()); mRecurSetDefaultEndDate = false; } mRecurrenceEdit->setStartDate(mAlarmDateTime.date(), now.date()); if (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN) mRecurrenceEdit->setEndDateTime(expired ? now : mAlarmDateTime.kDateTime()); } mRecurPageShown = true; } /****************************************************************************** * Called when the recurrence type selection changes. * Enables/disables date-only alarms as appropriate. * Enables/disables controls depending on at-login setting. */ void EditAlarmDlg::slotRecurTypeChange(int repeatType) { bool atLogin = (mRecurrenceEdit->repeatType() == RecurrenceEdit::AT_LOGIN); if (!mTemplate) { bool recurs = (mRecurrenceEdit->repeatType() != RecurrenceEdit::NO_RECUR); if (mDeferGroup) mDeferGroup->setEnabled(recurs); mTimeWidget->enableAnyTime(!recurs || repeatType != RecurrenceEdit::SUBDAILY); if (atLogin) { mAlarmDateTime = mTimeWidget->getDateTime(nullptr, false, false); mRecurrenceEdit->setEndDateTime(mAlarmDateTime.kDateTime()); } if (mReminder) mReminder->enableOnceOnly(recurs && !atLogin); } if (mReminder) mReminder->setAfterOnly(atLogin); mLateCancel->setEnabled(!atLogin); if (mShowInKorganizer) mShowInKorganizer->setEnabled(!atLogin); slotRecurFrequencyChange(); } /****************************************************************************** * Called when the recurrence frequency selection changes, or the sub- * repetition interval changes. * Updates the recurrence frequency text. */ void EditAlarmDlg::slotRecurFrequencyChange() { slotSetSubRepetition(); KAEvent event; mRecurrenceEdit->updateEvent(event, false); mTabs->setTabText(mRecurPageIndex, recurText(event)); } /****************************************************************************** * Called when the Repetition within Recurrence button has been pressed to * display the sub-repetition dialog. * Alarm repetition has the following restrictions: * 1) Not allowed for a repeat-at-login alarm * 2) For a date-only alarm, the repeat interval must be a whole number of days. * 3) The overall repeat duration must be less than the recurrence interval. */ void EditAlarmDlg::slotSetSubRepetition() { bool dateOnly = mTemplate ? mTemplateAnyTime->isChecked() : mTimeWidget->anyTime(); mRecurrenceEdit->setSubRepetition((mReminder ? mReminder->minutes() : 0), dateOnly); } /****************************************************************************** * Called when one of the template time radio buttons is clicked, * to enable or disable the template time entry spin boxes. */ void EditAlarmDlg::slotTemplateTimeType(QAbstractButton*) { mTemplateTime->setEnabled(mTemplateUseTime->isChecked()); mTemplateTimeAfter->setEnabled(mTemplateUseTimeAfter->isChecked()); } /****************************************************************************** * Called when the "Any time" checkbox is toggled in the date/time widget. * Sets the advance reminder and late cancel units to days if any time is checked. */ void EditAlarmDlg::slotAnyTimeToggled(bool anyTime) { if (mReminder && mReminder->isReminder()) mReminder->setDateOnly(anyTime); mLateCancel->setDateOnly(anyTime); } bool EditAlarmDlg::dateOnly() const { return mTimeWidget ? mTimeWidget->anyTime() : mTemplateAnyTime->isChecked(); } bool EditAlarmDlg::isTimedRecurrence() const { return mRecurrenceEdit->isTimedRepeatType(); } void EditAlarmDlg::showMainPage() { mTabs->setCurrentIndex(mMainPageIndex); } #include "moc_editdlg.cpp" #include "moc_editdlg_p.cpp" // vim: et sw=4: diff --git a/src/functions.cpp b/src/functions.cpp index 2067461a..349e200f 100644 --- a/src/functions.cpp +++ b/src/functions.cpp @@ -1,1642 +1,1641 @@ /* * functions.cpp - miscellaneous functions * Program: kalarm * Copyright © 2001-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "functions.h" #include "functions_p.h" #include "akonadicollectionsearch.h" #include "alarmcalendar.h" #include "alarmtime.h" #include "editdlg.h" #include "kalarmapp.h" #include "kamail.h" #include "mainwindow.h" #include "messagewin.h" #include "preferences.h" #include "templatelistview.h" #include "templatemenuaction.h" #include "resources/datamodel.h" #include "resources/resources.h" #include "resources/eventmodel.h" #include "lib/autoqpointer.h" #include "lib/messagebox.h" #include "lib/shellprocess.h" #include "config-kalarm.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include using namespace KCalendarCore; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { bool refreshAlarmsQueued = false; struct UpdateStatusData { KAlarm::UpdateResult status; // status code and KOrganizer error message if any int warnErr; int warnKOrg; explicit UpdateStatusData(KAlarm::UpdateStatus s = KAlarm::UPDATE_OK) : status(s), warnErr(0), warnKOrg(0) {} // Set an error status and increment to number of errors to warn about void setError(KAlarm::UpdateStatus st, int errorCount = -1) { status.set(st); if (errorCount < 0) ++warnErr; else warnErr = errorCount; } // Update the error status with a KOrganizer related status void korgUpdate(const KAlarm::UpdateResult& result) { if (result.status != KAlarm::UPDATE_OK) { ++warnKOrg; if (result.status > status.status) status = result; } } }; const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail"); //const QLatin1String KMAIL_DBUS_IFACE("org.kde.kmail.kmail"); //const QLatin1String KMAIL_DBUS_WINDOW_PATH("/kmail/kmail_mainwindow_1"); const QLatin1String KORG_DBUS_SERVICE("org.kde.korganizer"); const QLatin1String KORG_DBUS_IFACE("org.kde.korganizer.Korganizer"); // D-Bus object path of KOrganizer's notification interface #define KORG_DBUS_PATH "/Korganizer" #define KORG_DBUS_LOAD_PATH "/korganizer_PimApplication" //const QLatin1String KORG_DBUS_WINDOW_PATH("/korganizer/MainWindow_1"); const QLatin1String KORG_MIME_TYPE("application/x-vnd.akonadi.calendar.event"); const QLatin1String KORGANIZER_UID("korg-"); const QLatin1String ALARM_OPTS_FILE("alarmopts"); const char* DONT_SHOW_ERRORS_GROUP = "DontShowErrors"; void editNewTemplate(EditAlarmDlg::Type, const KAEvent* preset, QWidget* parent); void displayUpdateError(QWidget* parent, KAlarm::UpdateError, const UpdateStatusData&, bool showKOrgError = true); KAlarm::UpdateResult sendToKOrganizer(const KAEvent&); KAlarm::UpdateResult deleteFromKOrganizer(const QString& eventID); KAlarm::UpdateResult runKOrganizer(); QString uidKOrganizer(const QString& eventID); } namespace KAlarm { Private* Private::mInstance = nullptr; /****************************************************************************** * Display a main window with the specified event selected. */ MainWindow* displayMainWindowSelected(const QString& eventId) { MainWindow* win = MainWindow::firstWindow(); if (!win) { if (theApp()->checkCalendar()) // ensure calendar is open { win = MainWindow::create(); win->show(); } } else { // There is already a main window, so make it the active window #pragma message("Don't hide unless necessary, since it moves the window") win->hide(); // in case it's on a different desktop win->setWindowState(win->windowState() & ~Qt::WindowMinimized); win->show(); win->raise(); win->activateWindow(); } if (win) win->selectEvent(eventId); return win; } /****************************************************************************** * Create an "Alarms Enabled/Enable Alarms" action. */ KToggleAction* createAlarmEnableAction(QObject* parent) { KToggleAction* action = new KToggleAction(i18nc("@action", "Enable &Alarms"), parent); action->setChecked(theApp()->alarmsEnabled()); QObject::connect(action, &QAction::toggled, theApp(), &KAlarmApp::setAlarmsEnabled); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::alarmEnabledToggled, action, &QAction::setChecked); return action; } /****************************************************************************** * Create a "Stop Play" action. */ QAction* createStopPlayAction(QObject* parent) { QAction* action = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-stop")), i18nc("@action", "Stop Play"), parent); action->setEnabled(MessageWin::isAudioPlaying()); QObject::connect(action, &QAction::triggered, theApp(), &KAlarmApp::stopAudio); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::audioPlaying, action, &QAction::setEnabled); return action; } /****************************************************************************** * Create a "Spread Windows" action. */ KToggleAction* createSpreadWindowsAction(QObject* parent) { KToggleAction* action = new KToggleAction(i18nc("@action", "Spread Windows"), parent); QObject::connect(action, &QAction::triggered, theApp(), &KAlarmApp::spreadWindows); // The following line ensures that all instances are kept in the same state QObject::connect(theApp(), &KAlarmApp::spreadWindowsToggled, action, &QAction::setChecked); return action; } /****************************************************************************** * Add a new active (non-archived) alarm. * Save it in the calendar file and add it to every main window instance. * Parameters: msgParent = parent widget for any calendar selection prompt or * error message. * event - is updated with the actual event ID. */ UpdateResult addEvent(KAEvent& event, Resource* resource, QWidget* msgParent, int options, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::addEvent:" << event.id(); bool cancelled = false; UpdateStatusData status; if (!theApp()->checkCalendar()) // ensure calendar is open status.status = UPDATE_FAILED; else { // Save the event details in the calendar file, and get the new event ID - AlarmCalendar* cal = AlarmCalendar::resources(); + ResourcesCalendar* cal = ResourcesCalendar::instance(); // Note that AlarmCalendar::addEvent() updates 'event'. if (!cal->addEvent(event, msgParent, (options & USE_EVENT_ID), resource, (options & NO_RESOURCE_PROMPT), &cancelled)) { status.status = UPDATE_FAILED; } else { if (!cal->save()) status.status = SAVE_FAILED; } if (status.status == UPDATE_OK) { if ((options & ALLOW_KORG_UPDATE) && event.copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(event); // tell KOrganizer to show the event status.korgUpdate(st); } } } if (status.status != UPDATE_OK && !cancelled && msgParent) displayUpdateError(msgParent, ERR_ADD, status, showKOrgErr); return status.status; } /****************************************************************************** * Add a list of new active (non-archived) alarms. * Save them in the calendar file and add them to every main window instance. * The events are updated with their actual event IDs. */ UpdateResult addEvents(QVector& events, QWidget* msgParent, bool allowKOrgUpdate, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::addEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; if (!theApp()->checkCalendar()) // ensure calendar is open status.status = UPDATE_FAILED; else { Resource resource = Resources::destination(CalEvent::ACTIVE, msgParent); if (!resource.isValid()) { qCDebug(KALARM_LOG) << "KAlarm::addEvents: No calendar"; status.status = UPDATE_FAILED; } else { - AlarmCalendar* cal = AlarmCalendar::resources(); + ResourcesCalendar* cal = ResourcesCalendar::instance(); for (int i = 0, end = events.count(); i < end; ++i) { // Save the event details in the calendar file, and get the new event ID KAEvent& event = events[i]; if (!cal->addEvent(event, msgParent, false, &resource)) { status.setError(UPDATE_ERROR); continue; } if (allowKOrgUpdate && event.copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(event); // tell KOrganizer to show the event status.korgUpdate(st); } } if (status.warnErr == events.count()) status.status = UPDATE_FAILED; else if (!cal->save()) status.setError(SAVE_FAILED, events.count()); // everything failed } } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_ADD, status, showKOrgErr); return status.status; } /****************************************************************************** * Save the event in the archived calendar and adjust every main window instance. * The event's ID is changed to an archived ID if necessary. */ bool addArchivedEvent(KAEvent& event, Resource* resourceptr) { qCDebug(KALARM_LOG) << "KAlarm::addArchivedEvent:" << event.id(); bool archiving = (event.category() == CalEvent::ACTIVE); if (archiving && !Preferences::archivedKeepDays()) return false; // expired alarms aren't being kept - AlarmCalendar* cal = AlarmCalendar::resources(); + ResourcesCalendar* cal = ResourcesCalendar::instance(); KAEvent newevent(event); KAEvent* const newev = &newevent; if (archiving) { newev->setCategory(CalEvent::ARCHIVED); // this changes the event ID newev->setCreatedDateTime(KADateTime::currentUtcDateTime()); // time stamp to control purging } // Note that archived resources are automatically saved after changes are made if (!cal->addEvent(newevent, nullptr, false, resourceptr)) return false; event = *newev; // update event ID etc. return true; } /****************************************************************************** * Add a new template. * Save it in the calendar file and add it to every template list view. * 'event' is updated with the actual event ID. * Parameters: promptParent = parent widget for any calendar selection prompt. */ UpdateResult addTemplate(KAEvent& event, Resource* resourceptr, QWidget* msgParent) { qCDebug(KALARM_LOG) << "KAlarm::addTemplate:" << event.id(); UpdateStatusData status; // Add the template to the calendar file - AlarmCalendar* cal = AlarmCalendar::resources(); + ResourcesCalendar* cal = ResourcesCalendar::instance(); KAEvent newev(event); if (!cal->addEvent(newev, msgParent, false, resourceptr)) status.status = UPDATE_FAILED; else { event = newev; // update event ID etc. if (!cal->save()) status.status = SAVE_FAILED; else { return UpdateResult(UPDATE_OK); } } if (msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, status); return status.status; } /****************************************************************************** * Modify an active (non-archived) alarm in the calendar file and in every main * window instance. * The new event must have a different event ID from the old one. */ UpdateResult modifyEvent(KAEvent& oldEvent, KAEvent& newEvent, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::modifyEvent:" << oldEvent.id(); UpdateStatusData status; if (!newEvent.isValid()) { deleteEvent(oldEvent, true); status.status = UPDATE_FAILED; } else { EventId oldId(oldEvent); if (oldEvent.copyToKOrganizer()) { // Tell KOrganizer to delete its old event. // But ignore errors, because the user could have manually // deleted it since KAlarm asked KOrganizer to set it up. deleteFromKOrganizer(oldId.eventId()); } // Update the event in the calendar file, and get the new event ID - AlarmCalendar* cal = AlarmCalendar::resources(); + ResourcesCalendar* cal = ResourcesCalendar::instance(); if (!cal->modifyEvent(oldId, newEvent)) status.status = UPDATE_FAILED; else { if (!cal->save()) status.status = SAVE_FAILED; if (status.status == UPDATE_OK) { if (newEvent.copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(newEvent); // tell KOrganizer to show the new event status.korgUpdate(st); } // Remove "Don't show error messages again" for the old alarm setDontShowErrors(oldId); } } } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_MODIFY, status, showKOrgErr); return status.status; } /****************************************************************************** * Update an active (non-archived) alarm from the calendar file and from every * main window instance. * The new event will have the same event ID as the old one. * The event is not updated in KOrganizer, since this function is called when an * existing alarm is rescheduled (due to recurrence or deferral). */ UpdateResult updateEvent(KAEvent& event, QWidget* msgParent, bool archiveOnDelete) { qCDebug(KALARM_LOG) << "KAlarm::updateEvent:" << event.id(); if (!event.isValid()) deleteEvent(event, archiveOnDelete); else { // Update the event in the calendar file. - AlarmCalendar* cal = AlarmCalendar::resources(); + ResourcesCalendar* cal = ResourcesCalendar::instance(); cal->updateEvent(event); if (!cal->save()) { if (msgParent) displayUpdateError(msgParent, ERR_ADD, UpdateStatusData(SAVE_FAILED)); return UpdateResult(SAVE_FAILED); } } return UpdateResult(UPDATE_OK); } /****************************************************************************** * Update a template in the calendar file and in every template list view. * If 'selectionView' is non-null, the selection highlight is moved to the * updated event in that listView instance. */ UpdateResult updateTemplate(KAEvent& event, QWidget* msgParent) { - AlarmCalendar* cal = AlarmCalendar::resources(); + ResourcesCalendar* cal = ResourcesCalendar::instance(); const KAEvent* newEvent = cal->updateEvent(event); UpdateStatus status = UPDATE_OK; if (!newEvent) status = UPDATE_FAILED; else if (!cal->save()) status = SAVE_FAILED; if (status != UPDATE_OK) { if (msgParent) - displayUpdateError(msgParent, ERR_TEMPLATE, UpdateStatusData(SAVE_FAILED)); + displayUpdateError(msgParent, ERR_TEMPLATE, UpdateStatusData(status)); return UpdateResult(status); } return UpdateResult(UPDATE_OK); } /****************************************************************************** * Delete alarms from the calendar file and from every main window instance. * If the events are archived, the events' IDs are changed to archived IDs if necessary. */ UpdateResult deleteEvent(KAEvent& event, bool archive, QWidget* msgParent, bool showKOrgErr) { QVector events(1, event); return deleteEvents(events, archive, msgParent, showKOrgErr); } UpdateResult deleteEvents(QVector& events, bool archive, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::deleteEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; - AlarmCalendar* cal = AlarmCalendar::resources(); + ResourcesCalendar* cal = ResourcesCalendar::instance(); bool deleteWakeFromSuspendAlarm = false; const QString wakeFromSuspendId = checkRtcWakeConfig().value(0); for (int i = 0, end = events.count(); i < end; ++i) { // Save the event details in the calendar file, and get the new event ID KAEvent* event = &events[i]; const QString id = event->id(); // Delete the event from the calendar file if (event->category() != CalEvent::ARCHIVED) { if (event->copyToKOrganizer()) { // The event was shown in KOrganizer, so tell KOrganizer to // delete it. But ignore errors, because the user could have // manually deleted it from KOrganizer since it was set up. UpdateResult st = deleteFromKOrganizer(id); status.korgUpdate(st); } if (archive && event->toBeArchived()) { KAEvent ev(*event); addArchivedEvent(ev); // this changes the event ID to an archived ID } } if (!cal->deleteEvent(*event, false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); if (id == wakeFromSuspendId) deleteWakeFromSuspendAlarm = true; // Remove "Don't show error messages again" for this alarm setDontShowErrors(EventId(*event)); } if (status.warnErr == events.count()) status.status = UPDATE_FAILED; else if (!cal->save()) // save the calendars now status.setError(SAVE_FAILED, events.count()); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_DELETE, status, showKOrgErr); // Remove any wake-from-suspend scheduled for a deleted alarm if (deleteWakeFromSuspendAlarm && !wakeFromSuspendId.isEmpty()) cancelRtcWake(msgParent, wakeFromSuspendId); return status.status; } /****************************************************************************** * Delete templates from the calendar file and from every template list view. */ UpdateResult deleteTemplates(const KAEvent::List& events, QWidget* msgParent) { int count = events.count(); qCDebug(KALARM_LOG) << "KAlarm::deleteTemplates:" << count; if (!count) return UpdateResult(UPDATE_OK); UpdateStatusData status; - AlarmCalendar* cal = AlarmCalendar::resources(); + ResourcesCalendar* cal = ResourcesCalendar::instance(); for (const KAEvent* event : events) { // Update the window lists // Delete the template from the calendar file - AlarmCalendar* cal = AlarmCalendar::resources(); if (!cal->deleteEvent(*event, false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); } if (status.warnErr == count) status.status = UPDATE_FAILED; else if (!cal->save()) // save the calendars now status.setError(SAVE_FAILED, count); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_TEMPLATE, status); return status.status; } /****************************************************************************** * Delete an alarm from the display calendar. */ void deleteDisplayEvent(const QString& eventID) { qCDebug(KALARM_LOG) << "KAlarm::deleteDisplayEvent:" << eventID; - AlarmCalendar* cal = AlarmCalendar::displayCalendarOpen(); + DisplayCalendar* cal = DisplayCalendar::instanceOpen(); if (cal) - cal->deleteDisplayEvent(eventID, true); // save calendar after deleting + cal->deleteEvent(eventID, true); // save calendar after deleting } /****************************************************************************** * Undelete archived alarms, and update every main window instance. * The archive bit is set to ensure that they get re-archived if deleted again. * Parameters: * calendar - the active alarms calendar to restore the alarms into, or null * to use the default way of determining the active alarm calendar. * ineligibleIDs - will be filled in with the IDs of any ineligible events. */ UpdateResult reactivateEvent(KAEvent& event, Resource* resourceptr, QWidget* msgParent, bool showKOrgErr) { QVector ids; QVector events(1, event); return reactivateEvents(events, ids, resourceptr, msgParent, showKOrgErr); } UpdateResult reactivateEvents(QVector& events, QVector& ineligibleIDs, Resource* resourceptr, QWidget* msgParent, bool showKOrgErr) { qCDebug(KALARM_LOG) << "KAlarm::reactivateEvents:" << events.count(); ineligibleIDs.clear(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; Resource resource; if (resourceptr) resource = *resourceptr; if (!resource.isValid()) resource = Resources::destination(CalEvent::ACTIVE, msgParent); if (!resource.isValid()) { qCDebug(KALARM_LOG) << "KAlarm::reactivateEvents: No calendar"; status.setError(UPDATE_FAILED, events.count()); } else { int count = 0; - AlarmCalendar* cal = AlarmCalendar::resources(); + ResourcesCalendar* cal = ResourcesCalendar::instance(); const KADateTime now = KADateTime::currentUtcDateTime(); for (int i = 0, end = events.count(); i < end; ++i) { // Delete the event from the archived resource KAEvent* event = &events[i]; if (event->category() != CalEvent::ARCHIVED || !event->occursAfter(now, true)) { ineligibleIDs += EventId(*event); continue; } ++count; KAEvent newevent(*event); KAEvent* const newev = &newevent; newev->setCategory(CalEvent::ACTIVE); // this changes the event ID if (newev->recurs() || newev->repetition()) newev->setNextOccurrence(now); // skip any recurrences in the past newev->setArchive(); // ensure that it gets re-archived if it is deleted // Save the event details in the calendar file. // This converts the event ID. if (!cal->addEvent(newevent, msgParent, true, &resource)) { status.setError(UPDATE_ERROR); continue; } if (newev->copyToKOrganizer()) { UpdateResult st = sendToKOrganizer(*newev); // tell KOrganizer to show the event status.korgUpdate(st); } if (cal->event(EventId(*event)) // no error if event doesn't exist in archived resource && !cal->deleteEvent(*event, false)) // don't save calendar after deleting status.setError(UPDATE_ERROR); events[i] = newevent; } if (status.warnErr == count) status.status = UPDATE_FAILED; // Save the calendars, even if all events failed, since more than one calendar was updated if (!cal->save() && status.status != UPDATE_FAILED) status.setError(SAVE_FAILED, count); } if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_REACTIVATE, status, showKOrgErr); return status.status; } /****************************************************************************** * Enable or disable alarms in the calendar file and in every main window instance. * The new events will have the same event IDs as the old ones. */ UpdateResult enableEvents(QVector& events, bool enable, QWidget* msgParent) { qCDebug(KALARM_LOG) << "KAlarm::enableEvents:" << events.count(); if (events.isEmpty()) return UpdateResult(UPDATE_OK); UpdateStatusData status; - AlarmCalendar* cal = AlarmCalendar::resources(); + ResourcesCalendar* cal = ResourcesCalendar::instance(); bool deleteWakeFromSuspendAlarm = false; const QString wakeFromSuspendId = checkRtcWakeConfig().value(0); for (int i = 0, end = events.count(); i < end; ++i) { KAEvent* event = &events[i]; if (event->category() == CalEvent::ACTIVE && enable != event->enabled()) { event->setEnabled(enable); if (!enable && event->id() == wakeFromSuspendId) deleteWakeFromSuspendAlarm = true; // Update the event in the calendar file - const KAEvent* newev = cal->updateEvent(event); + const KAEvent* newev = cal->updateEvent(*event); if (!newev) qCCritical(KALARM_LOG) << "KAlarm::enableEvents: Error updating event in calendar:" << event->id(); else { cal->disabledChanged(newev); // If we're disabling a display alarm, close any message window if (!enable && (event->actionTypes() & KAEvent::ACT_DISPLAY)) { MessageWin* win = MessageWin::findEvent(EventId(*event)); delete win; } } } } if (!cal->save()) status.setError(SAVE_FAILED, events.count()); if (status.status != UPDATE_OK && msgParent) displayUpdateError(msgParent, ERR_ADD, status); // Remove any wake-from-suspend scheduled for a disabled alarm if (deleteWakeFromSuspendAlarm && !wakeFromSuspendId.isEmpty()) cancelRtcWake(msgParent, wakeFromSuspendId); return status.status; } /****************************************************************************** * This method must only be called from the main KAlarm queue processing loop, * to prevent asynchronous calendar operations interfering with one another. * * Purge all archived events from the default archived alarm resource whose end * time is longer ago than 'purgeDays'. All events are deleted if 'purgeDays' is * zero. */ void purgeArchive(int purgeDays) { if (purgeDays < 0) return; qCDebug(KALARM_LOG) << "KAlarm::purgeArchive:" << purgeDays; const QDate cutoff = KADateTime::currentLocalDate().addDays(-purgeDays); const Resource resource = Resources::getStandard(CalEvent::ARCHIVED); if (!resource.isValid()) return; - KAEvent::List events = AlarmCalendar::resources()->events(resource); + KAEvent::List events = ResourcesCalendar::instance()->events(resource); for (int i = 0; i < events.count(); ) { if (purgeDays && events.at(i)->createdDateTime().date() >= cutoff) events.remove(i); else ++i; } if (!events.isEmpty()) - AlarmCalendar::resources()->purgeEvents(events); // delete the events and save the calendar + ResourcesCalendar::instance()->purgeEvents(events); // delete the events and save the calendar } /****************************************************************************** * Display an error message about an error when saving an event. * If 'model' is non-null, the AlarmListModel* which it points to is used; if * that is null, it is created. */ QVector getSortedActiveEvents(QObject* parent, AlarmListModel** model) { AlarmListModel* mdl = nullptr; if (!model) model = &mdl; if (!*model) { *model = DataModel::createAlarmListModel(parent); (*model)->setEventTypeFilter(CalEvent::ACTIVE); (*model)->sort(AlarmListModel::TimeColumn); } QVector result; for (int i = 0, count = (*model)->rowCount(); i < count; ++i) { const KAEvent event = (*model)->event(i); if (event.enabled() && !event.expired()) result += event; } return result; } /****************************************************************************** * Display an error message corresponding to a specified alarm update error code. */ void displayKOrgUpdateError(QWidget* parent, UpdateError code, const UpdateResult& korgError, int nAlarms) { QString errmsg; switch (code) { case ERR_ADD: case ERR_REACTIVATE: errmsg = (nAlarms > 1) ? i18nc("@info", "Unable to show alarms in KOrganizer") : i18nc("@info", "Unable to show alarm in KOrganizer"); break; case ERR_MODIFY: errmsg = i18nc("@info", "Unable to update alarm in KOrganizer"); break; case ERR_DELETE: errmsg = (nAlarms > 1) ? i18nc("@info", "Unable to delete alarms from KOrganizer") : i18nc("@info", "Unable to delete alarm from KOrganizer"); break; case ERR_TEMPLATE: return; } bool showDetail = !korgError.message.isEmpty(); QString msg; switch (korgError.status) { case UPDATE_KORG_ERRINIT: msg = xi18nc("@info", "%1(Could not start KOrganizer)", errmsg); break; case UPDATE_KORG_ERRSTART: msg = xi18nc("@info", "%1(KOrganizer not fully started)", errmsg); break; case UPDATE_KORG_ERR: msg = xi18nc("@info", "%1(Error communicating with KOrganizer)", errmsg); break; default: msg = errmsg; showDetail = false; break; } if (showDetail) KAMessageBox::detailedError(parent, msg, korgError.message); else KAMessageBox::error(parent, msg); } /****************************************************************************** * Execute a New Alarm dialog for the specified alarm type. */ void editNewAlarm(EditAlarmDlg::Type type, QWidget* parent) { execNewAlarmDlg(EditAlarmDlg::create(false, type, parent)); } /****************************************************************************** * Execute a New Alarm dialog for the specified alarm type. */ void editNewAlarm(KAEvent::SubAction action, QWidget* parent, const AlarmText* text) { bool setAction = false; EditAlarmDlg::Type type; switch (action) { case KAEvent::MESSAGE: case KAEvent::FILE: type = EditAlarmDlg::DISPLAY; setAction = true; break; case KAEvent::COMMAND: type = EditAlarmDlg::COMMAND; break; case KAEvent::EMAIL: type = EditAlarmDlg::EMAIL; break; case KAEvent::AUDIO: type = EditAlarmDlg::AUDIO; break; default: return; } EditAlarmDlg* editDlg = EditAlarmDlg::create(false, type, parent); if (setAction || text) editDlg->setAction(action, *text); execNewAlarmDlg(editDlg); } /****************************************************************************** * Execute a New Alarm dialog, optionally either presetting it to the supplied * event, or setting the action and text. */ void editNewAlarm(const KAEvent* preset, QWidget* parent) { execNewAlarmDlg(EditAlarmDlg::create(false, preset, true, parent)); } /****************************************************************************** * Common code for editNewAlarm() variants. */ void execNewAlarmDlg(EditAlarmDlg* editDlg) { // Create a PrivateNewAlarmDlg parented by editDlg. // It will be deleted when editDlg is closed. new PrivateNewAlarmDlg(editDlg); editDlg->show(); editDlg->raise(); editDlg->activateWindow(); } PrivateNewAlarmDlg::PrivateNewAlarmDlg(EditAlarmDlg* dlg) : QObject(dlg) { connect(dlg, &QDialog::accepted, this, &PrivateNewAlarmDlg::okClicked); connect(dlg, &QDialog::rejected, this, &PrivateNewAlarmDlg::cancelClicked); } /****************************************************************************** * Called when the dialogue is accepted (e.g. by clicking the OK button). * Creates the event specified in the instance's dialogue. */ void PrivateNewAlarmDlg::okClicked() { accept(static_cast(parent())); } /****************************************************************************** * Creates the event specified in a given dialogue. */ void PrivateNewAlarmDlg::accept(EditAlarmDlg* editDlg) { KAEvent event; Resource resource; editDlg->getEvent(event, resource); // Add the alarm to the displayed lists and to the calendar file const UpdateResult status = addEvent(event, &resource, editDlg); switch (status.status) { case UPDATE_FAILED: return; case UPDATE_KORG_ERR: case UPDATE_KORG_ERRINIT: case UPDATE_KORG_ERRSTART: case UPDATE_KORG_FUNCERR: displayKOrgUpdateError(editDlg, ERR_ADD, status); break; default: break; } Undo::saveAdd(event, resource); outputAlarmWarnings(editDlg, &event); editDlg->deleteLater(); } /****************************************************************************** * Called when the dialogue is rejected (e.g. by clicking the Cancel button). */ void PrivateNewAlarmDlg::cancelClicked() { static_cast(parent())->deleteLater(); } /****************************************************************************** * Display the alarm edit dialog to edit a new alarm, preset with a template. */ bool editNewAlarm(const QString& templateName, QWidget* parent) { if (!templateName.isEmpty()) { - KAEvent* templateEvent = AlarmCalendar::resources()->templateEvent(templateName); + KAEvent* templateEvent = ResourcesCalendar::instance()->templateEvent(templateName); if (templateEvent->isValid()) { editNewAlarm(templateEvent, parent); return true; } qCWarning(KALARM_LOG) << "KAlarm::editNewAlarm:" << templateName << ": template not found"; } return false; } /****************************************************************************** * Create a new template. */ void editNewTemplate(EditAlarmDlg::Type type, QWidget* parent) { ::editNewTemplate(type, nullptr, parent); } /****************************************************************************** * Create a new template, based on an existing event or template. */ void editNewTemplate(const KAEvent* preset, QWidget* parent) { ::editNewTemplate(EditAlarmDlg::Type(0), preset, parent); } /****************************************************************************** * Check the config as to whether there is a wake-on-suspend alarm pending, and * if so, delete it from the config if it has expired. * If 'checkExists' is true, the config entry will only be returned if the * event exists. * Reply = config entry: [0] = event's resource ID, * [1] = event ID, * [2] = trigger time (int64 seconds since epoch). * = empty list if none or expired. */ QStringList checkRtcWakeConfig(bool checkEventExists) { KConfigGroup config(KSharedConfig::openConfig(), "General"); const QStringList params = config.readEntry("RtcWake", QStringList()); #if KALARMCAL_VERSION >= QT_VERSION_CHECK(5,12,1) if (params.count() == 3 && params[2].toLongLong() > KADateTime::currentUtcDateTime().toSecsSinceEpoch()) #else if (params.count() == 3 && params[2].toUInt() > KADateTime::currentUtcDateTime().toTime_t()) #endif { - if (checkEventExists && !AlarmCalendar::getEvent(EventId(params[0].toLongLong(), params[1]))) + if (checkEventExists && !ResourcesCalendar::getEvent(EventId(params[0].toLongLong(), params[1]))) return QStringList(); return params; // config entry is valid } if (!params.isEmpty()) { config.deleteEntry("RtcWake"); // delete the expired config entry config.sync(); } return QStringList(); } /****************************************************************************** * Delete any wake-on-suspend alarm from the config. */ void deleteRtcWakeConfig() { KConfigGroup config(KSharedConfig::openConfig(), "General"); config.deleteEntry("RtcWake"); config.sync(); } /****************************************************************************** * Delete any wake-on-suspend alarm, optionally only for a specified event. */ void cancelRtcWake(QWidget* msgParent, const QString& eventId) { const QStringList wakeup = checkRtcWakeConfig(); if (!wakeup.isEmpty() && (eventId.isEmpty() || wakeup[0] == eventId)) { Private::instance()->mMsgParent = msgParent ? msgParent : MainWindow::mainMainWindow(); QTimer::singleShot(0, Private::instance(), &Private::cancelRtcWake); } } /****************************************************************************** * Delete any wake-on-suspend alarm. */ void Private::cancelRtcWake() { // setRtcWakeTime will only work with a parent window specified setRtcWakeTime(0, mMsgParent); deleteRtcWakeConfig(); KAMessageBox::information(mMsgParent, i18nc("info", "The scheduled Wake from Suspend has been cancelled.")); } /****************************************************************************** * Set the wakeup time for the system. * Set 'triggerTime' to zero to cancel the wakeup. * Reply = true if successful. */ bool setRtcWakeTime(unsigned triggerTime, QWidget* parent) { QVariantMap args; args[QStringLiteral("time")] = triggerTime; KAuth::Action action(QStringLiteral("org.kde.kalarm.rtcwake.settimer")); action.setHelperId(QStringLiteral("org.kde.kalarm.rtcwake")); action.setParentWidget(parent); action.setArguments(args); KAuth::ExecuteJob* job = action.execute(); if (!job->exec()) { QString errmsg = job->errorString(); qCDebug(KALARM_LOG) << "KAlarm::setRtcWakeTime: Error code=" << job->error() << errmsg; if (errmsg.isEmpty()) { int errcode = job->error(); switch (errcode) { case KAuth::ActionReply::AuthorizationDeniedError: case KAuth::ActionReply::UserCancelledError: qCDebug(KALARM_LOG) << "KAlarm::setRtcWakeTime: Authorization error:" << errcode; return false; // the user should already know about this default: break; } errmsg = i18nc("@info", "Error obtaining authorization (%1)", errcode); } KAMessageBox::information(parent, errmsg); return false; } return true; } } // namespace KAlarm namespace { /****************************************************************************** * Create a new template. * 'preset' is non-null to base it on an existing event or template; otherwise, * the alarm type is set to 'type'. */ void editNewTemplate(EditAlarmDlg::Type type, const KAEvent* preset, QWidget* parent) { if (Resources::enabledResources(CalEvent::TEMPLATE, true).isEmpty()) { KAMessageBox::sorry(parent, i18nc("@info", "You must enable a template calendar to save the template in")); return; } // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg; if (preset) editDlg = EditAlarmDlg::create(true, preset, true, parent); else editDlg = EditAlarmDlg::create(true, type, parent); if (editDlg->exec() == QDialog::Accepted) { KAEvent event; Resource resource; editDlg->getEvent(event, resource); // Add the template to the displayed lists and to the calendar file KAlarm::addTemplate(event, &resource, editDlg); Undo::saveAdd(event, resource); } } } // namespace namespace KAlarm { /****************************************************************************** * Open the Edit Alarm dialog to edit the specified alarm. * If the alarm is read-only or archived, the dialog is opened read-only. */ void editAlarm(KAEvent* event, QWidget* parent) { - if (event->expired() || AlarmCalendar::resources()->eventReadOnly(event->id())) + if (event->expired() || ResourcesCalendar::instance()->eventReadOnly(event->id())) { viewAlarm(event, parent); return; } const EventId id(*event); // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(false, event, false, parent, EditAlarmDlg::RES_USE_EVENT_ID); if (editDlg->exec() == QDialog::Accepted) { - if (!AlarmCalendar::resources()->event(id)) + if (!ResourcesCalendar::instance()->event(id)) { // Event has been deleted while the user was editing the alarm, // so treat it as a new alarm. PrivateNewAlarmDlg().accept(editDlg); return; } KAEvent newEvent; Resource resource; bool changeDeferral = !editDlg->getEvent(newEvent, resource); // Update the event in the displays and in the calendar file const Undo::Event undo(*event, resource); if (changeDeferral) { // The only change has been to an existing deferral if (updateEvent(newEvent, editDlg, true) != UPDATE_OK) // keep the same event ID return; // failed to save event } else { const UpdateResult status = modifyEvent(*event, newEvent, editDlg); if (status.status != UPDATE_OK && status.status <= UPDATE_KORG_ERR) displayKOrgUpdateError(editDlg, ERR_MODIFY, status); } Undo::saveEdit(undo, newEvent); outputAlarmWarnings(editDlg, &newEvent); } } /****************************************************************************** * Display the alarm edit dialog to edit the alarm with the specified ID. * An error occurs if the alarm is not found, if there is more than one alarm * with the same ID, or if it is read-only or expired. */ bool editAlarmById(const EventId& id, QWidget* parent) { const QString eventID(id.eventId()); - KAEvent* event = AlarmCalendar::resources()->event(id, true); + KAEvent* event = ResourcesCalendar::instance()->event(id, true); if (!event) { if (id.resourceId() != -1) qCWarning(KALARM_LOG) << "KAlarm::editAlarmById: Event ID not found, or duplicated:" << eventID; else qCWarning(KALARM_LOG) << "KAlarm::editAlarmById: Event ID not found:" << eventID; return false; } - if (AlarmCalendar::resources()->eventReadOnly(event->id())) + if (ResourcesCalendar::instance()->eventReadOnly(event->id())) { qCCritical(KALARM_LOG) << "KAlarm::editAlarmById:" << eventID << ": read-only"; return false; } switch (event->category()) { case CalEvent::ACTIVE: case CalEvent::TEMPLATE: break; default: qCCritical(KALARM_LOG) << "KAlarm::editAlarmById:" << eventID << ": event not active or template"; return false; } editAlarm(event, parent); return true; } /****************************************************************************** * Open the Edit Alarm dialog to edit the specified template. * If the template is read-only, the dialog is opened read-only. */ void editTemplate(KAEvent* event, QWidget* parent) { - if (AlarmCalendar::resources()->eventReadOnly(event->id())) + if (ResourcesCalendar::instance()->eventReadOnly(event->id())) { // The template is read-only, so make the dialogue read-only. // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(true, event, false, parent, EditAlarmDlg::RES_PROMPT, true); editDlg->exec(); return; } // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(true, event, false, parent, EditAlarmDlg::RES_USE_EVENT_ID); if (editDlg->exec() == QDialog::Accepted) { KAEvent newEvent; Resource resource; editDlg->getEvent(newEvent, resource); const QString id = event->id(); newEvent.setEventId(id); newEvent.setResourceId(event->resourceId()); // Update the event in the displays and in the calendar file const Undo::Event undo(*event, resource); updateTemplate(newEvent, editDlg); Undo::saveEdit(undo, newEvent); } } /****************************************************************************** * Open the Edit Alarm dialog to view the specified alarm (read-only). */ void viewAlarm(const KAEvent* event, QWidget* parent) { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of parent, and on return from this function). AutoQPointer editDlg = EditAlarmDlg::create(false, event, false, parent, EditAlarmDlg::RES_PROMPT, true); editDlg->exec(); } /****************************************************************************** * Called when OK is clicked in the alarm edit dialog invoked by the Edit button * in an alarm message window. * Updates the alarm calendar and closes the dialog. */ void updateEditedAlarm(EditAlarmDlg* editDlg, KAEvent& event, Resource& resource) { qCDebug(KALARM_LOG) << "KAlarm::updateEditedAlarm"; KAEvent newEvent; Resource res; editDlg->getEvent(newEvent, res); // Update the displayed lists and the calendar file UpdateResult status; - if (AlarmCalendar::resources()->event(EventId(event))) + if (ResourcesCalendar::instance()->event(EventId(event))) { // The old alarm hasn't expired yet, so replace it const Undo::Event undo(event, resource); status = modifyEvent(event, newEvent, editDlg); Undo::saveEdit(undo, newEvent); } else { // The old event has expired, so simply create a new one status = addEvent(newEvent, &resource, editDlg); Undo::saveAdd(newEvent, resource); } if (status.status != UPDATE_OK && status.status <= UPDATE_KORG_ERR) displayKOrgUpdateError(editDlg, ERR_MODIFY, status); outputAlarmWarnings(editDlg, &newEvent); editDlg->close(); } /****************************************************************************** * Returns a list of all alarm templates. * If shell commands are disabled, command alarm templates are omitted. */ KAEvent::List templateList() { KAEvent::List templates; const bool includeCmdAlarms = ShellProcess::authorised(); - const KAEvent::List events = AlarmCalendar::resources()->events(CalEvent::TEMPLATE); + const KAEvent::List events = ResourcesCalendar::instance()->events(CalEvent::TEMPLATE); for (KAEvent* event : events) { if (includeCmdAlarms || !(event->actionTypes() & KAEvent::ACT_COMMAND)) templates.append(event); } return templates; } /****************************************************************************** * To be called after an alarm has been edited. * Prompt the user to re-enable alarms if they are currently disabled, and if * it's an email alarm, warn if no 'From' email address is configured. */ void outputAlarmWarnings(QWidget* parent, const KAEvent* event) { if (event && event->actionTypes() == KAEvent::ACT_EMAIL && Preferences::emailAddress().isEmpty()) KAMessageBox::information(parent, xi18nc("@info Please set the 'From' email address...", "%1Please set it in the Configuration dialog.", KAMail::i18n_NeedFromEmailAddress())); if (!theApp()->alarmsEnabled()) { if (KAMessageBox::warningYesNo(parent, xi18nc("@info", "Alarms are currently disabled.Do you want to enable alarms now?"), QString(), KGuiItem(i18nc("@action:button", "Enable")), KGuiItem(i18nc("@action:button", "Keep Disabled")), QStringLiteral("EditEnableAlarms")) == KMessageBox::Yes) theApp()->setAlarmsEnabled(true); } } /****************************************************************************** * Reload the calendar. */ void refreshAlarms() { qCDebug(KALARM_LOG) << "KAlarm::refreshAlarms"; if (!refreshAlarmsQueued) { refreshAlarmsQueued = true; theApp()->processQueue(); } } /****************************************************************************** * This method must only be called from the main KAlarm queue processing loop, * to prevent asynchronous calendar operations interfering with one another. * * If refreshAlarms() has been called, reload the calendars. */ void refreshAlarmsIfQueued() { if (refreshAlarmsQueued) { qCDebug(KALARM_LOG) << "KAlarm::refreshAlarmsIfQueued"; - AlarmCalendar::resources()->reload(); + ResourcesCalendar::instance()->reload(); // Close any message windows for alarms which are now disabled - const KAEvent::List events = AlarmCalendar::resources()->events(CalEvent::ACTIVE); + const KAEvent::List events = ResourcesCalendar::instance()->events(CalEvent::ACTIVE); for (KAEvent* event : events) { if (!event->enabled() && (event->actionTypes() & KAEvent::ACT_DISPLAY)) { MessageWin* win = MessageWin::findEvent(EventId(*event)); delete win; } } MainWindow::refresh(); refreshAlarmsQueued = false; } } /****************************************************************************** * Start KMail if it isn't already running, optionally minimised. * Reply = reason for failure to run KMail * = null string if success. */ QString runKMail() { const QDBusReply reply = QDBusConnection::sessionBus().interface()->isServiceRegistered(KMAIL_DBUS_SERVICE); if (!reply.isValid() || !reply.value()) { // Program is not already running, so start it const QDBusReply startReply = QDBusConnection::sessionBus().interface()->startService(KMAIL_DBUS_SERVICE); if (!startReply.isValid()) { const QString errmsg = startReply.error().message(); qCCritical(KALARM_LOG) << "Couldn't start KMail (" << errmsg << ")"; return xi18nc("@info", "Unable to start KMail(%1)", errmsg); } } return QString(); } /****************************************************************************** * The "Don't show again" option for error messages is personal to the user on a * particular computer. For example, he may want to inhibit error messages only * on his laptop. So the status is not stored in the alarm calendar, but in the * user's local KAlarm data directory. ******************************************************************************/ /****************************************************************************** * Return the Don't-show-again error message tags set for a specified alarm ID. */ QStringList dontShowErrors(const EventId& eventId) { if (eventId.isEmpty()) return QStringList(); KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.resourceId()).arg(eventId.eventId()); return group.readEntry(id, QStringList()); } /****************************************************************************** * Check whether the specified Don't-show-again error message tag is set for an * alarm ID. */ bool dontShowErrors(const EventId& eventId, const QString& tag) { if (tag.isEmpty()) return false; const QStringList tags = dontShowErrors(eventId); return tags.indexOf(tag) >= 0; } /****************************************************************************** * Reset the Don't-show-again error message tags for an alarm ID. * If 'tags' is empty, the config entry is deleted. */ void setDontShowErrors(const EventId& eventId, const QStringList& tags) { if (eventId.isEmpty()) return; KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.resourceId()).arg(eventId.eventId()); if (tags.isEmpty()) group.deleteEntry(id); else group.writeEntry(id, tags); group.sync(); } /****************************************************************************** * Set the specified Don't-show-again error message tag for an alarm ID. * Existing tags are unaffected. */ void setDontShowErrors(const EventId& eventId, const QString& tag) { if (eventId.isEmpty() || tag.isEmpty()) return; KConfig config(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + ALARM_OPTS_FILE); KConfigGroup group(&config, DONT_SHOW_ERRORS_GROUP); const QString id = QStringLiteral("%1:%2").arg(eventId.resourceId()).arg(eventId.eventId()); QStringList tags = group.readEntry(id, QStringList()); if (tags.indexOf(tag) < 0) { tags += tag; group.writeEntry(id, tags); group.sync(); } } #ifndef NDEBUG /****************************************************************************** * Set up KAlarm test conditions based on environment variables. * KALARM_TIME: specifies current system time (format [[[yyyy-]mm-]dd-]hh:mm [TZ]). */ void setTestModeConditions() { const QByteArray newTime = qgetenv("KALARM_TIME"); if (!newTime.isEmpty()) { KADateTime dt; if (AlarmTime::convertTimeString(newTime, dt, KADateTime::realCurrentLocalDateTime(), true)) setSimulatedSystemTime(dt); } } /****************************************************************************** * Set the simulated system time. */ void setSimulatedSystemTime(const KADateTime& dt) { KADateTime::setSimulatedSystemTime(dt); qCDebug(KALARM_LOG) << "New time =" << qPrintable(KADateTime::currentLocalDateTime().toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))); } #endif } // namespace KAlarm namespace { /****************************************************************************** * Display an error message about an error when saving an event. */ void displayUpdateError(QWidget* parent, KAlarm::UpdateError code, const UpdateStatusData& status, bool showKOrgError) { QString errmsg; if (status.status.status > KAlarm::UPDATE_KORG_ERR) { switch (code) { case KAlarm::ERR_ADD: case KAlarm::ERR_MODIFY: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving alarms") : i18nc("@info", "Error saving alarm"); break; case KAlarm::ERR_DELETE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error deleting alarms") : i18nc("@info", "Error deleting alarm"); break; case KAlarm::ERR_REACTIVATE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving reactivated alarms") : i18nc("@info", "Error saving reactivated alarm"); break; case KAlarm::ERR_TEMPLATE: errmsg = (status.warnErr > 1) ? i18nc("@info", "Error saving alarm templates") : i18nc("@info", "Error saving alarm template"); break; } KAMessageBox::error(parent, errmsg); } else if (showKOrgError) displayKOrgUpdateError(parent, code, status.status, status.warnKOrg); } /****************************************************************************** * Tell KOrganizer to put an alarm in its calendar. * It will be held by KOrganizer as a simple event, without alarms - KAlarm * is still responsible for alarming. */ KAlarm::UpdateResult sendToKOrganizer(const KAEvent& event) { Event::Ptr kcalEvent(new KCalendarCore::Event); event.updateKCalEvent(kcalEvent, KAEvent::UID_IGNORE); // Change the event ID to avoid duplicating the same unique ID as the original event const QString uid = uidKOrganizer(event.id()); kcalEvent->setUid(uid); kcalEvent->clearAlarms(); QString userEmail; switch (event.actionTypes()) { case KAEvent::ACT_DISPLAY: case KAEvent::ACT_COMMAND: case KAEvent::ACT_DISPLAY_COMMAND: kcalEvent->setSummary(event.cleanText()); userEmail = Preferences::emailAddress(); break; case KAEvent::ACT_EMAIL: { const QString from = event.emailFromId() ? Identities::identityManager()->identityForUoid(event.emailFromId()).fullEmailAddr() : Preferences::emailAddress(); AlarmText atext; atext.setEmail(event.emailAddresses(QStringLiteral(", ")), from, QString(), QString(), event.emailSubject(), QString()); kcalEvent->setSummary(atext.displayText()); userEmail = from; break; } case KAEvent::ACT_AUDIO: kcalEvent->setSummary(event.audioFile()); break; default: break; } const Person person(QString(), userEmail); kcalEvent->setOrganizer(person); kcalEvent->setDuration(Duration(Preferences::kOrgEventDuration() * 60, Duration::Seconds)); // Translate the event into string format ICalFormat format; format.setTimeZone(Preferences::timeSpecAsZone()); const QString iCal = format.toICalString(kcalEvent); // Send the event to KOrganizer KAlarm::UpdateResult status = runKOrganizer(); // start KOrganizer if it isn't already running, and create its D-Bus interface if (status != KAlarm::UPDATE_OK) return status; QDBusInterface korgInterface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_PATH), KORG_DBUS_IFACE); const QList args{iCal}; QDBusReply reply = korgInterface.callWithArgumentList(QDBus::Block, QStringLiteral("addIncidence"), args); if (!reply.isValid()) { if (reply.error().type() == QDBusError::UnknownObject) { status = KAlarm::UPDATE_KORG_ERRSTART; qCCritical(KALARM_LOG) << "KAlarm::sendToKOrganizer: addIncidence() D-Bus error: still starting"; } else { status.set(KAlarm::UPDATE_KORG_ERR, reply.error().message()); qCCritical(KALARM_LOG) << "KAlarm::sendToKOrganizer: addIncidence(" << uid << ") D-Bus call failed:" << status.message; } } else if (!reply.value()) { status = KAlarm::UPDATE_KORG_FUNCERR; qCDebug(KALARM_LOG) << "KAlarm::sendToKOrganizer: addIncidence(" << uid << ") D-Bus call returned false"; } else qCDebug(KALARM_LOG) << "KAlarm::sendToKOrganizer:" << uid << ": success"; return status; } /****************************************************************************** * Tell KOrganizer to delete an event from its calendar. */ KAlarm::UpdateResult deleteFromKOrganizer(const QString& eventID) { const QString newID = uidKOrganizer(eventID); new AkonadiCollectionSearch(KORG_MIME_TYPE, QString(), newID, true); // this auto-deletes when complete // Ignore errors return KAlarm::UpdateResult(KAlarm::UPDATE_OK); } /****************************************************************************** * Start KOrganizer if not already running, and create its D-Bus interface. */ KAlarm::UpdateResult runKOrganizer() { KAlarm::UpdateResult status; // If Kontact is running, there is a load() method which needs to be called to // load KOrganizer into Kontact. But if KOrganizer is running independently, // the load() method doesn't exist. This call starts korganizer if needed, too. QDBusInterface iface(KORG_DBUS_SERVICE, QStringLiteral(KORG_DBUS_LOAD_PATH), QStringLiteral("org.kde.PIMUniqueApplication")); QDBusReply reply = iface.call(QStringLiteral("load")); if ((!reply.isValid() || !reply.value()) && iface.lastError().type() != QDBusError::UnknownMethod) { status.set(KAlarm::UPDATE_KORG_ERR, iface.lastError().message()); qCWarning(KALARM_LOG) << "Loading KOrganizer failed:" << status.message; return status; } return status; } /****************************************************************************** * Insert a KOrganizer string after the hyphen in the supplied event ID. */ QString uidKOrganizer(const QString& id) { if (id.startsWith(KORGANIZER_UID)) return id; QString result = id; return result.insert(0, KORGANIZER_UID); } } // namespace /****************************************************************************** * Case insensitive comparison for use by qSort(). */ bool caseInsensitiveLessThan(const QString& s1, const QString& s2) { return s1.toLower() < s2.toLower(); } // vim: et sw=4: diff --git a/src/kalarmapp.cpp b/src/kalarmapp.cpp index 994a7707..f0fa0d2a 100644 --- a/src/kalarmapp.cpp +++ b/src/kalarmapp.cpp @@ -1,2699 +1,2699 @@ /* * kalarmapp.cpp - the KAlarm application object * Program: kalarm * Copyright © 2001-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kalarmapp.h" #include "alarmcalendar.h" #include "alarmtime.h" #include "commandoptions.h" #include "dbushandler.h" #include "editdlgtypes.h" #include "functions.h" #include "kamail.h" #include "mainwindow.h" #include "messagewin.h" #include "kalarmmigrateapplication.h" #include "preferences.h" #include "prefdlg.h" #include "startdaytimer.h" #include "traywindow.h" #include "resources/datamodel.h" #include "resources/resources.h" #include "resources/eventmodel.h" #include "lib/desktop.h" #include "lib/messagebox.h" #include "lib/shellprocess.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const int AKONADI_TIMEOUT = 30; // timeout (seconds) for Akonadi collections to be populated /****************************************************************************** * Find the maximum number of seconds late which a late-cancel alarm is allowed * to be. This is calculated as the late cancel interval, plus a few seconds * leeway to cater for any timing irregularities. */ static inline int maxLateness(int lateCancel) { static const int LATENESS_LEEWAY = 5; int lc = (lateCancel >= 1) ? (lateCancel - 1)*60 : 0; return LATENESS_LEEWAY + lc; } KAlarmApp* KAlarmApp::mInstance = nullptr; int KAlarmApp::mActiveCount = 0; int KAlarmApp::mFatalError = 0; QString KAlarmApp::mFatalMessage; /****************************************************************************** * Construct the application. */ KAlarmApp::KAlarmApp(int& argc, char** argv) : QApplication(argc, argv) , mDBusHandler(new DBusHandler()) { qCDebug(KALARM_LOG) << "KAlarmApp:"; KAlarmMigrateApplication migrate; migrate.migrate(); #ifndef NDEBUG KAlarm::setTestModeConditions(); #endif setQuitOnLastWindowClosed(false); Preferences::self(); // read KAlarm configuration if (!Preferences::noAutoStart()) { // Strip out any "OnlyShowIn=KDE" list from kalarm.autostart.desktop Preferences::setNoAutoStart(false); // Enable kalarm.autostart.desktop to start KAlarm Preferences::setAutoStart(true); Preferences::self()->save(); } Preferences::connect(SIGNAL(startOfDayChanged(QTime)), this, SLOT(changeStartOfDay())); Preferences::connect(SIGNAL(workTimeChanged(QTime,QTime,QBitArray)), this, SLOT(slotWorkTimeChanged(QTime,QTime,QBitArray))); Preferences::connect(SIGNAL(holidaysChanged(KHolidays::HolidayRegion)), this, SLOT(slotHolidaysChanged(KHolidays::HolidayRegion))); Preferences::connect(SIGNAL(feb29TypeChanged(Feb29Type)), this, SLOT(slotFeb29TypeChanged(Feb29Type))); Preferences::connect(SIGNAL(showInSystemTrayChanged(bool)), this, SLOT(slotShowInSystemTrayChanged())); Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(setArchivePurgeDays())); Preferences::connect(SIGNAL(messageFontChanged(QFont)), this, SLOT(slotMessageFontChanged(QFont))); slotFeb29TypeChanged(Preferences::defaultFeb29Type()); KAEvent::setStartOfDay(Preferences::startOfDay()); KAEvent::setWorkTime(Preferences::workDays(), Preferences::workDayStart(), Preferences::workDayEnd()); KAEvent::setHolidays(Preferences::holidays()); KAEvent::setDefaultFont(Preferences::messageFont()); // Check if KOrganizer is installed const QString korg = QStringLiteral("korganizer"); mKOrganizerEnabled = !QStandardPaths::findExecutable(korg).isEmpty(); if (!mKOrganizerEnabled) { qCDebug(KALARM_LOG) << "KAlarmApp: KOrganizer options disabled (KOrganizer not found)"; } // Check if the window manager can't handle keyboard focus transfer between windows mWindowFocusBroken = (Desktop::currentIdentity() == Desktop::Unity); if (mWindowFocusBroken) { qCDebug(KALARM_LOG) << "KAlarmApp: Window keyboard focus broken"; } } /****************************************************************************** */ KAlarmApp::~KAlarmApp() { while (!mCommandProcesses.isEmpty()) { ProcData* pd = mCommandProcesses.at(0); mCommandProcesses.pop_front(); delete pd; } - AlarmCalendar::terminateCalendars(); + ResourcesCalendar::terminate(); + DisplayCalendar::terminate(); + DataModel::terminate(); } /****************************************************************************** * Return the one and only KAlarmApp instance. * If it doesn't already exist, it is created first. */ KAlarmApp* KAlarmApp::create(int& argc, char** argv) { if (!mInstance) { mInstance = new KAlarmApp(argc, argv); if (mFatalError) mInstance->quitFatal(); } return mInstance; } /****************************************************************************** * Perform initialisations which may require the constructor to have completed * and KAboutData to have been set up. */ void KAlarmApp::initialise() { if (initialiseTimerResources()) // initialise calendars and alarm timer { Resources* resources = Resources::instance(); connect(resources, &Resources::resourceAdded, this, &KAlarmApp::slotResourceAdded); connect(resources, &Resources::resourcePopulated, this, &KAlarmApp::slotResourcePopulated); connect(resources, &Resources::resourcePopulated, this, &KAlarmApp::purgeNewArchivedDefault); connect(resources, &Resources::resourcesCreated, this, &KAlarmApp::slotResourcesCreated); connect(resources, &Resources::migrationCompleted, this, &KAlarmApp::checkWritableCalendar); connect(resources, &Resources::resourcesPopulated, this, &KAlarmApp::processQueue); KConfigGroup config(KSharedConfig::openConfig(), "General"); mNoSystemTray = config.readEntry("NoSystemTray", false); mOldShowInSystemTray = wantShowInSystemTray(); DateTime::setStartOfDay(Preferences::startOfDay()); mPrefsArchivedColour = Preferences::archivedColour(); } } /****************************************************************************** * Initialise or reinitialise things which are tidied up/closed by quitIf(). * Reinitialisation can be necessary if session restoration finds nothing to * restore and starts quitting the application, but KAlarm then starts up again * before the application has exited. * Reply = true if calendars were initialised successfully, * false if they were already initialised, or if initialisation failed. */ bool KAlarmApp::initialiseTimerResources() { if (!mAlarmTimer) { mAlarmTimer = new QTimer(this); mAlarmTimer->setSingleShot(true); connect(mAlarmTimer, &QTimer::timeout, this, &KAlarmApp::checkNextDueAlarm); } - if (!AlarmCalendar::resources()) + if (!ResourcesCalendar::instance()) { qCDebug(KALARM_LOG) << "KAlarmApp::initialise: initialising calendars"; - if (AlarmCalendar::initialiseCalendars()) - { - connect(AlarmCalendar::resources(), &AlarmCalendar::earliestAlarmChanged, this, &KAlarmApp::checkNextDueAlarm); - connect(AlarmCalendar::resources(), &AlarmCalendar::atLoginEventAdded, this, &KAlarmApp::atLoginEventAdded); - return true; - } + DataModel::initialise(); + ResourcesCalendar::initialise(); + DisplayCalendar::initialise(); + connect(ResourcesCalendar::instance(), &ResourcesCalendar::earliestAlarmChanged, this, &KAlarmApp::checkNextDueAlarm); + connect(ResourcesCalendar::instance(), &ResourcesCalendar::atLoginEventAdded, this, &KAlarmApp::atLoginEventAdded); + return true; } return false; } /****************************************************************************** * Restore the saved session if required. */ bool KAlarmApp::restoreSession() { if (!isSessionRestored()) return false; if (mFatalError) { quitFatal(); return false; } // Process is being restored by session management. qCDebug(KALARM_LOG) << "KAlarmApp::restoreSession: Restoring"; ++mActiveCount; // Create the session config object now. // This is necessary since if initCheck() below causes calendars to be updated, // the session config created after that points to an invalid file, resulting // in no windows being restored followed by a later crash. KConfigGui::sessionConfig(); // When KAlarm is session restored, automatically set start-at-login to true. Preferences::self()->load(); Preferences::setAutoStart(true); Preferences::setNoAutoStart(false); Preferences::setAskAutoStart(true); // cancel any start-at-login prompt suppression Preferences::self()->save(); if (!initCheck(true)) // open the calendar file (needed for main windows), don't process queue yet { --mActiveCount; quitIf(1, true); // error opening the main calendar - quit return false; } MainWindow* trayParent = nullptr; for (int i = 1; KMainWindow::canBeRestored(i); ++i) { const QString type = KMainWindow::classNameOfToplevel(i); if (type == QLatin1String("MainWindow")) { MainWindow* win = MainWindow::create(true); win->restore(i, false); if (win->isHiddenTrayParent()) trayParent = win; else win->show(); } else if (type == QLatin1String("MessageWin")) { MessageWin* win = new MessageWin; win->restore(i, false); if (win->isValid()) { if (Resources::allCreated()) win->show(); } else delete win; } } // Try to display the system tray icon if it is configured to be shown if (trayParent || wantShowInSystemTray()) { if (!MainWindow::count()) qCWarning(KALARM_LOG) << "KAlarmApp::restoreSession: no main window to be restored!?"; else { displayTrayIcon(true, trayParent); // Occasionally for no obvious reason, the main main window is // shown when it should be hidden, so hide it just to be sure. if (trayParent) trayParent->hide(); } } --mActiveCount; if (quitIf(0)) // quit if no windows are open return false; // quitIf() can sometimes return, despite calling exit() startProcessQueue(); // start processing the execution queue return true; } /****************************************************************************** * Called for a unique QApplication when a new instance of the application is * started. * Reply: exit code (>= 0), or -1 to continue execution. * If exit code >= 0, 'outputText' holds text to output before terminating. */ void KAlarmApp::activateByDBus(const QStringList& args, const QString& workingDirectory) { activateInstance(args, workingDirectory, nullptr); } /****************************************************************************** * Called to start a new instance of the application. * Reply: exit code (>= 0), or -1 to continue execution. * If exit code >= 0, 'outputText' holds text to output before terminating. */ int KAlarmApp::activateInstance(const QStringList& args, const QString& workingDirectory, QString* outputText) { Q_UNUSED(workingDirectory) qCDebug(KALARM_LOG) << "KAlarmApp::activateInstance"; if (outputText) outputText->clear(); if (mFatalError) { quitFatal(); return 1; } // The D-Bus call to activate a subsequent instance of KAlarm may not supply // any arguments, but we need one. if (!args.isEmpty() && mActivateArg0.isEmpty()) mActivateArg0 = args[0]; QStringList fixedArgs(args); if (args.isEmpty() && !mActivateArg0.isEmpty()) fixedArgs << mActivateArg0; // Parse and interpret command line arguments. QCommandLineParser parser; KAboutData::applicationData().setupCommandLine(&parser); parser.setApplicationDescription(QApplication::applicationDisplayName()); CommandOptions* options = new CommandOptions; const QStringList newArgs = options->setOptions(&parser, fixedArgs); options->parse(); KAboutData::applicationData().processCommandLine(&parser); ++mActiveCount; int exitCode = 0; // default = success static bool firstInstance = true; bool dontRedisplay = false; CommandOptions::Command command = CommandOptions::NONE; const bool processOptions = (!firstInstance || !isSessionRestored()); if (processOptions) { options->process(); #ifndef NDEBUG if (options->simulationTime().isValid()) KAlarm::setSimulatedSystemTime(options->simulationTime()); #endif command = options->command(); if (options->disableAll()) setAlarmsEnabled(false); // disable alarm monitoring // Handle options which exit with a terminal message, before // making the application a unique application, since a // unique application won't output to the terminal if another // instance is already running. switch (command) { case CommandOptions::CMD_ERROR: if (outputText) { *outputText = options->outputText(); delete options; return 1; } mReadOnly = true; // don't need write access to calendars exitCode = 1; break; default: break; } } // Make this a unique application. KDBusService* s = new KDBusService(KDBusService::Unique, this); connect(this, &KAlarmApp::aboutToQuit, s, &KDBusService::deleteLater); connect(s, &KDBusService::activateRequested, this, &KAlarmApp::activateByDBus); if (processOptions) { switch (command) { case CommandOptions::TRIGGER_EVENT: case CommandOptions::CANCEL_EVENT: { // Display or delete the event with the specified event ID const QueuedAction action = static_cast(int((command == CommandOptions::TRIGGER_EVENT) ? QueuedAction::Trigger : QueuedAction::Cancel) | int(QueuedAction::FindId) | int(QueuedAction::Exit)); // Open the calendar, don't start processing execution queue yet, // and wait for the calendar resources to be populated. if (!initCheck(true)) exitCode = 1; else { mCommandOption = options->commandName(); mActionQueue.enqueue(ActionQEntry(action, options->eventId())); startProcessQueue(); // start processing the execution queue dontRedisplay = true; } break; } case CommandOptions::LIST: // Output a list of scheduled alarms to stdout. // Open the calendar, don't start processing execution queue yet, // and wait for all calendar resources to be populated. mReadOnly = true; // don't need write access to calendars mAlarmsEnabled = false; // prevent alarms being processed if (!initCheck(true)) exitCode = 1; else { const QueuedAction action = static_cast(int(QueuedAction::List) | int(QueuedAction::Exit)); mActionQueue.enqueue(ActionQEntry(action, EventId())); startProcessQueue(); // start processing the execution queue dontRedisplay = true; } break; case CommandOptions::EDIT: // Edit a specified existing alarm. // Open the calendar and wait for the calendar resources to be populated. if (!initCheck(false)) exitCode = 1; else { mCommandOption = options->commandName(); if (firstInstance) mEditingCmdLineAlarm = 0x10; // want to redisplay alarms if successful mActionQueue.enqueue(ActionQEntry(QueuedAction::Edit, options->eventId())); startProcessQueue(); // start processing the execution queue dontRedisplay = true; } break; case CommandOptions::EDIT_NEW: { // Edit a new alarm, and optionally preset selected values if (!initCheck()) exitCode = 1; else { EditAlarmDlg* editDlg = EditAlarmDlg::create(false, options->editType(), MainWindow::mainMainWindow()); if (options->alarmTime().isValid()) editDlg->setTime(options->alarmTime()); if (options->recurrence()) editDlg->setRecurrence(*options->recurrence(), options->subRepeatInterval(), options->subRepeatCount()); else if (options->flags() & KAEvent::REPEAT_AT_LOGIN) editDlg->setRepeatAtLogin(); editDlg->setAction(options->editAction(), AlarmText(options->text())); if (options->lateCancel()) editDlg->setLateCancel(options->lateCancel()); if (options->flags() & KAEvent::COPY_KORGANIZER) editDlg->setShowInKOrganizer(true); switch (options->editType()) { case EditAlarmDlg::DISPLAY: { // EditAlarmDlg::create() always returns EditDisplayAlarmDlg for type = DISPLAY EditDisplayAlarmDlg* dlg = qobject_cast(editDlg); if (options->fgColour().isValid()) dlg->setFgColour(options->fgColour()); if (options->bgColour().isValid()) dlg->setBgColour(options->bgColour()); if (!options->audioFile().isEmpty() || options->flags() & (KAEvent::BEEP | KAEvent::SPEAK)) { const KAEvent::Flags flags = options->flags(); const Preferences::SoundType type = (flags & KAEvent::BEEP) ? Preferences::Sound_Beep : (flags & KAEvent::SPEAK) ? Preferences::Sound_Speak : Preferences::Sound_File; dlg->setAudio(type, options->audioFile(), options->audioVolume(), (flags & KAEvent::REPEAT_SOUND ? 0 : -1)); } if (options->reminderMinutes()) dlg->setReminder(options->reminderMinutes(), (options->flags() & KAEvent::REMINDER_ONCE)); if (options->flags() & KAEvent::CONFIRM_ACK) dlg->setConfirmAck(true); if (options->flags() & KAEvent::AUTO_CLOSE) dlg->setAutoClose(true); break; } case EditAlarmDlg::COMMAND: break; case EditAlarmDlg::EMAIL: { // EditAlarmDlg::create() always returns EditEmailAlarmDlg for type = EMAIL EditEmailAlarmDlg* dlg = qobject_cast(editDlg); if (options->fromID() || !options->addressees().isEmpty() || !options->subject().isEmpty() || !options->attachments().isEmpty()) dlg->setEmailFields(options->fromID(), options->addressees(), options->subject(), options->attachments()); if (options->flags() & KAEvent::EMAIL_BCC) dlg->setBcc(true); break; } case EditAlarmDlg::AUDIO: { // EditAlarmDlg::create() always returns EditAudioAlarmDlg for type = AUDIO EditAudioAlarmDlg* dlg = qobject_cast(editDlg); if (!options->audioFile().isEmpty() || options->audioVolume() >= 0) dlg->setAudio(options->audioFile(), options->audioVolume()); break; } case EditAlarmDlg::NO_TYPE: break; } // Execute the edit dialogue. Note that if no other instance of KAlarm is // running, this new instance will not exit after the dialogue is closed. // This is deliberate, since exiting would mean that KAlarm wouldn't // trigger the new alarm. KAlarm::execNewAlarmDlg(editDlg); createOnlyMainWindow(); // prevent the application from quitting } break; } case CommandOptions::EDIT_NEW_PRESET: // Edit a new alarm, preset with a template if (!initCheck()) exitCode = 1; else { // Execute the edit dialogue. Note that if no other instance of KAlarm is // running, this new instance will not exit after the dialogue is closed. // This is deliberate, since exiting would mean that KAlarm wouldn't // trigger the new alarm. KAlarm::editNewAlarm(options->templateName()); createOnlyMainWindow(); // prevent the application from quitting } break; case CommandOptions::NEW: // Display a message or file, execute a command, or send an email setResourcesTimeout(); // set timeout for resource initialisation if (!initCheck()) exitCode = 1; else { if (!scheduleEvent(options->editAction(), options->text(), options->alarmTime(), options->lateCancel(), options->flags(), options->bgColour(), options->fgColour(), QFont(), options->audioFile(), options->audioVolume(), options->reminderMinutes(), (options->recurrence() ? *options->recurrence() : KARecurrence()), options->subRepeatInterval(), options->subRepeatCount(), options->fromID(), options->addressees(), options->subject(), options->attachments())) exitCode = 1; else createOnlyMainWindow(); // prevent the application from quitting } break; case CommandOptions::TRAY: // Display only the system tray icon if (Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable()) { if (!initCheck()) // open the calendar, start processing execution queue exitCode = 1; else { if (!displayTrayIcon(true)) exitCode = 1; } break; } Q_FALLTHROUGH(); // fall through to NONE case CommandOptions::NONE: // No arguments - run interactively & display the main window #ifndef NDEBUG if (options->simulationTime().isValid() && !firstInstance) break; // simulating time: don't open main window if already running #endif if (!initCheck()) exitCode = 1; else { if (mTrayWindow && mTrayWindow->assocMainWindow() && !mTrayWindow->assocMainWindow()->isVisible()) mTrayWindow->showAssocMainWindow(); else { MainWindow* win = MainWindow::create(); if (command == CommandOptions::TRAY) win->setWindowState(win->windowState() | Qt::WindowMinimized); win->show(); } } break; default: break; } } if (options != CommandOptions::firstInstance()) delete options; // If this is the first time through, redisplay any alarm message windows // from last time. if (firstInstance && !dontRedisplay && !exitCode) { /* First time through, so redisplay alarm message windows from last time. * But it is possible for session restoration in some circumstances to * not create any windows, in which case the alarm calendars will have * been deleted - if so, don't try to do anything. (This has been known * to happen under the Xfce desktop.) */ - if (AlarmCalendar::resources()) + if (ResourcesCalendar::instance()) { if (Resources::allCreated()) { mRedisplayAlarms = false; MessageWin::redisplayAlarms(); } else mRedisplayAlarms = true; } } --mActiveCount; firstInstance = false; // Quit the application if this was the last/only running "instance" of the program. // Executing 'return' doesn't work very well since the program continues to // run if no windows were created. if (quitIf(exitCode >= 0 ? exitCode : 0)) return exitCode; // exit this application instance return -1; // continue executing the application instance } /****************************************************************************** * Create a minimised main window if none already exists. * This prevents the application from quitting. */ void KAlarmApp::createOnlyMainWindow() { if (!MainWindow::count()) { if (Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable()) { if (displayTrayIcon(true)) return; } MainWindow* win = MainWindow::create(); win->setWindowState(Qt::WindowMinimized); win->show(); } } /****************************************************************************** * Quit the program, optionally only if there are no more "instances" running. * Reply = true if program exited. */ bool KAlarmApp::quitIf(int exitCode, bool force) { if (force) { // Quit regardless, except for message windows mQuitting = true; MainWindow::closeAll(); mQuitting = false; displayTrayIcon(false); if (MessageWin::instanceCount(true)) // ignore always-hidden windows (e.g. audio alarms) return false; } else if (mQuitting) return false; // MainWindow::closeAll() causes quitIf() to be called again else { // Quit only if there are no more "instances" running mPendingQuit = false; if (mActiveCount > 0 || MessageWin::instanceCount(true)) // ignore always-hidden windows (e.g. audio alarms) return false; const int mwcount = MainWindow::count(); MainWindow* mw = mwcount ? MainWindow::firstWindow() : nullptr; if (mwcount > 1 || (mwcount && (!mw->isHidden() || !mw->isTrayParent()))) return false; // There are no windows left except perhaps a main window which is a hidden // tray icon parent, or an always-hidden message window. if (mTrayWindow) { // There is a system tray icon. // Don't exit unless the system tray doesn't seem to exist. if (checkSystemTray()) return false; } if (!mActionQueue.isEmpty() || !mCommandProcesses.isEmpty()) { // Don't quit yet if there are outstanding actions on the execution queue mPendingQuit = true; mPendingQuitCode = exitCode; return false; } } // This was the last/only running "instance" of the program, so exit completely. // NOTE: Everything which is terminated/deleted here must where applicable // be initialised in the initialiseTimerResources() method, in case // KAlarm is started again before application exit completes! qCDebug(KALARM_LOG) << "KAlarmApp::quitIf:" << exitCode << ": quitting"; MessageWin::stopAudio(true); if (mCancelRtcWake) { KAlarm::setRtcWakeTime(0, nullptr); KAlarm::deleteRtcWakeConfig(); } delete mAlarmTimer; // prevent checking for alarms after deleting calendars mAlarmTimer = nullptr; mInitialised = false; // prevent processQueue() from running - AlarmCalendar::terminateCalendars(); + ResourcesCalendar::terminate(); + DisplayCalendar::terminate(); DataModel::terminate(); exit(exitCode); return true; // sometimes we actually get to here, despite calling exit() } /****************************************************************************** * Called when the Quit menu item is selected. * Closes the system tray window and all main windows, but does not exit the * program if other windows are still open. */ void KAlarmApp::doQuit(QWidget* parent) { qCDebug(KALARM_LOG) << "KAlarmApp::doQuit"; if (KAMessageBox::warningCancelContinue(parent, i18nc("@info", "Quitting will disable alarms (once any alarm message windows are closed)."), QString(), KStandardGuiItem::quit(), KStandardGuiItem::cancel(), Preferences::QUIT_WARN ) != KMessageBox::Continue) return; if (!KAlarm::checkRtcWakeConfig(true).isEmpty()) { // A wake-on-suspend alarm is set if (KAMessageBox::warningCancelContinue(parent, i18nc("@info", "Quitting will cancel the scheduled Wake from Suspend."), QString(), KStandardGuiItem::quit() ) != KMessageBox::Continue) return; mCancelRtcWake = true; } if (!Preferences::autoStart()) { int option = KMessageBox::No; if (!Preferences::autoStartChangedByUser()) { option = KAMessageBox::questionYesNoCancel(parent, xi18nc("@info", "Do you want to start KAlarm at login?" "(Note that alarms will be disabled if KAlarm is not started.)"), QString(), KStandardGuiItem::yes(), KStandardGuiItem::no(), KStandardGuiItem::cancel(), Preferences::ASK_AUTO_START); } switch (option) { case KMessageBox::Yes: Preferences::setAutoStart(true); Preferences::setNoAutoStart(false); break; case KMessageBox::No: Preferences::setNoAutoStart(true); break; case KMessageBox::Cancel: default: return; } Preferences::self()->save(); } quitIf(0, true); } /****************************************************************************** * Display an error message for a fatal error. Prevent further actions since * the program state is unsafe. */ void KAlarmApp::displayFatalError(const QString& message) { if (!mFatalError) { mFatalError = 1; mFatalMessage = message; if (mInstance) QTimer::singleShot(0, mInstance, &KAlarmApp::quitFatal); } } /****************************************************************************** * Quit the program, once the fatal error message has been acknowledged. */ void KAlarmApp::quitFatal() { switch (mFatalError) { case 0: case 2: return; case 1: mFatalError = 2; KMessageBox::error(nullptr, mFatalMessage); // this is an application modal window mFatalError = 3; Q_FALLTHROUGH(); // fall through to '3' case 3: if (mInstance) mInstance->quitIf(1, true); break; } QTimer::singleShot(1000, this, &KAlarmApp::quitFatal); } /****************************************************************************** * Called by the alarm timer when the next alarm is due. * Also called when the execution queue has finished processing to check for the * next alarm. */ void KAlarmApp::checkNextDueAlarm() { if (!mAlarmsEnabled) return; // Find the first alarm due - const KAEvent* nextEvent = AlarmCalendar::resources()->earliestAlarm(); + const KAEvent* nextEvent = ResourcesCalendar::instance()->earliestAlarm(); if (!nextEvent) return; // there are no alarms pending const KADateTime nextDt = nextEvent->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); const KADateTime now = KADateTime::currentDateTime(Preferences::timeSpec()); qint64 interval = now.msecsTo(nextDt); qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm: now:" << qPrintable(now.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", next:" << qPrintable(nextDt.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"))) << ", due:" << interval; if (interval <= 0) { // Queue the alarm queueAlarmId(*nextEvent); qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent->id() << ": due now"; QTimer::singleShot(0, this, &KAlarmApp::processQueue); } else { // No alarm is due yet, so set timer to wake us when it's due. // Check for integer overflow before setting timer. #pragma message("TODO: use hibernation wakeup signal") #ifndef HIBERNATION_SIGNAL /* TODO: REPLACE THIS CODE WHEN A SYSTEM NOTIFICATION SIGNAL BECOMES * AVAILABLE FOR WAKEUP FROM HIBERNATION. * Re-evaluate the next alarm time every minute, in case the * system clock jumps. The most common case when the clock jumps * is when a laptop wakes from hibernation. If timers were left to * run, they would trigger late by the length of time the system * was asleep. */ if (interval > 60000) // 1 minute interval = 60000; #endif ++interval; // ensure we don't trigger just before the minute boundary if (interval > INT_MAX) interval = INT_MAX; qCDebug(KALARM_LOG) << "KAlarmApp::checkNextDueAlarm:" << nextEvent->id() << "wait" << interval/1000 << "seconds"; mAlarmTimer->start(static_cast(interval)); } } /****************************************************************************** * Called by the alarm timer when the next alarm is due. * Also called when the execution queue has finished processing to check for the * next alarm. */ void KAlarmApp::queueAlarmId(const KAEvent& event) { const EventId id(event); for (const ActionQEntry& entry : qAsConst(mActionQueue)) { if (entry.action == QueuedAction::Handle && entry.eventId == id) return; // the alarm is already queued } mActionQueue.enqueue(ActionQEntry(QueuedAction::Handle, id)); } /****************************************************************************** * Start processing the execution queue. */ void KAlarmApp::startProcessQueue() { if (!mInitialised) { qCDebug(KALARM_LOG) << "KAlarmApp::startProcessQueue"; mInitialised = true; QTimer::singleShot(0, this, &KAlarmApp::processQueue); // process anything already queued } } /****************************************************************************** * The main processing loop for KAlarm. * All KAlarm operations involving opening or updating calendar files are called * from this loop to ensure that only one operation is active at any one time. * This precaution is necessary because KAlarm's activities are mostly * asynchronous, being in response to D-Bus calls from other programs or timer * events, any of which can be received in the middle of performing another * operation. If a calendar file is opened or updated while another calendar * operation is in progress, the program has been observed to hang, or the first * calendar call has failed with data loss - clearly unacceptable!! */ void KAlarmApp::processQueue() { if (mInitialised && !mProcessingQueue) { qCDebug(KALARM_LOG) << "KAlarmApp::processQueue"; mProcessingQueue = true; // Refresh alarms if that's been queued KAlarm::refreshAlarmsIfQueued(); // Process queued events while (!mActionQueue.isEmpty()) { ActionQEntry& entry = mActionQueue.head(); // Can't process the first action until its resource has been populated. const ResourceId id = entry.eventId.resourceId(); if ((id < 0 && !Resources::allPopulated()) || (id >= 0 && !Resources::resource(id).isPopulated())) { // If resource population has timed out, discard all queued events. if (mResourcesTimedOut) { qCCritical(KALARM_LOG) << "Error! Timeout reading calendars"; mActionQueue.clear(); } break; } // Process the first action in the queue. const bool findUniqueId = int(entry.action) & int(QueuedAction::FindId); const bool exitAfter = int(entry.action) & int(QueuedAction::Exit); const QueuedAction action = static_cast(int(entry.action) & int(QueuedAction::ActionMask)); bool ok = true; if (entry.eventId.isEmpty()) { // It's a new alarm switch (action) { case QueuedAction::Trigger: execAlarm(entry.event, entry.event.firstAlarm(), false); break; case QueuedAction::Handle: KAlarm::addEvent(entry.event, nullptr, nullptr, KAlarm::ALLOW_KORG_UPDATE | KAlarm::NO_RESOURCE_PROMPT); break; case QueuedAction::List: { const QStringList alarms = scheduledAlarmList(); for (const QString& alarm : alarms) std::cout << alarm.toUtf8().constData() << std::endl; break; } default: break; } } else { if (action == QueuedAction::Edit) { int editingCmdLineAlarm = mEditingCmdLineAlarm & 3; bool keepQueued = editingCmdLineAlarm <= 1; switch (editingCmdLineAlarm) { case 0: // Initiate editing an alarm specified on the command line. mEditingCmdLineAlarm |= 1; QTimer::singleShot(0, this, &KAlarmApp::slotEditAlarmById); break; case 1: // Currently editing the alarm. break; case 2: // The edit has completed. mEditingCmdLineAlarm = 0; break; default: break; } if (keepQueued) break; } else { ok = handleEvent(entry.eventId, action, findUniqueId); if (!ok && exitAfter) CommandOptions::printError(xi18nc("@info:shell", "%1: Event %2 not found, or not unique", mCommandOption, entry.eventId.eventId())); } } if (exitAfter) { mActionQueue.clear(); // ensure that quitIf() actually exits the program quitIf(ok ? 0 : 1); return; // quitIf() can sometimes return, despite calling exit() } mActionQueue.dequeue(); } // Purge the default archived alarms resource if it's time to do so if (mPurgeDaysQueued >= 0) { KAlarm::purgeArchive(mPurgeDaysQueued); mPurgeDaysQueued = -1; } // Now that the queue has been processed, quit if a quit was queued if (mPendingQuit) { if (quitIf(mPendingQuitCode)) return; // quitIf() can sometimes return, despite calling exit() } mProcessingQueue = false; if (!mEditingCmdLineAlarm) { // Schedule the application to be woken when the next alarm is due checkNextDueAlarm(); } } } /****************************************************************************** * Called when a repeat-at-login alarm has been added externally. * Queues the alarm for triggering. * First, cancel any scheduled reminder or deferral for it, since these will be * superseded by the new at-login trigger. */ void KAlarmApp::atLoginEventAdded(const KAEvent& event) { KAEvent ev = event; if (!cancelReminderAndDeferral(ev)) { if (mAlarmsEnabled) { mActionQueue.enqueue(ActionQEntry(QueuedAction::Handle, EventId(ev))); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); } } } /****************************************************************************** * Called when the system tray main window is closed. */ void KAlarmApp::removeWindow(TrayWindow*) { mTrayWindow = nullptr; } /****************************************************************************** * Display or close the system tray icon. */ bool KAlarmApp::displayTrayIcon(bool show, MainWindow* parent) { qCDebug(KALARM_LOG) << "KAlarmApp::displayTrayIcon"; static bool creating = false; if (show) { if (!mTrayWindow && !creating) { if (!QSystemTrayIcon::isSystemTrayAvailable()) return false; if (!MainWindow::count()) { // We have to have at least one main window to act // as parent to the system tray icon (even if the // window is hidden). creating = true; // prevent main window constructor from creating an additional tray icon parent = MainWindow::create(); creating = false; } mTrayWindow = new TrayWindow(parent ? parent : MainWindow::firstWindow()); connect(mTrayWindow, &TrayWindow::deleted, this, &KAlarmApp::trayIconToggled); Q_EMIT trayIconToggled(); if (!checkSystemTray()) quitIf(0); // exit the application if there are no open windows } } else { delete mTrayWindow; mTrayWindow = nullptr; } return true; } /****************************************************************************** * Check whether the system tray icon has been housed in the system tray. */ bool KAlarmApp::checkSystemTray() { if (!mTrayWindow) return true; if (QSystemTrayIcon::isSystemTrayAvailable() == mNoSystemTray) { qCDebug(KALARM_LOG) << "KAlarmApp::checkSystemTray: changed ->" << mNoSystemTray; mNoSystemTray = !mNoSystemTray; // Store the new setting in the config file, so that if KAlarm exits it will // restart with the correct default. KConfigGroup config(KSharedConfig::openConfig(), "General"); config.writeEntry("NoSystemTray", mNoSystemTray); config.sync(); // Update other settings slotShowInSystemTrayChanged(); } return !mNoSystemTray; } /****************************************************************************** * Return the main window associated with the system tray icon. */ MainWindow* KAlarmApp::trayMainWindow() const { return mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr; } /****************************************************************************** * Called when the show-in-system-tray preference setting has changed, to show * or hide the system tray icon. */ void KAlarmApp::slotShowInSystemTrayChanged() { const bool newShowInSysTray = wantShowInSystemTray(); if (newShowInSysTray != mOldShowInSystemTray) { // The system tray run mode has changed ++mActiveCount; // prevent the application from quitting MainWindow* win = mTrayWindow ? mTrayWindow->assocMainWindow() : nullptr; delete mTrayWindow; // remove the system tray icon if it is currently shown mTrayWindow = nullptr; mOldShowInSystemTray = newShowInSysTray; if (newShowInSysTray) { // Show the system tray icon displayTrayIcon(true); } else { // Stop showing the system tray icon if (win && win->isHidden()) { if (MainWindow::count() > 1) delete win; else { win->setWindowState(win->windowState() | Qt::WindowMinimized); win->show(); } } } --mActiveCount; } } /****************************************************************************** * Called when the start-of-day time preference setting has changed. * Change alarm times for date-only alarms. */ void KAlarmApp::changeStartOfDay() { DateTime::setStartOfDay(Preferences::startOfDay()); KAEvent::setStartOfDay(Preferences::startOfDay()); - AlarmCalendar::resources()->adjustStartOfDay(); + ResourcesCalendar::instance()->adjustStartOfDay(); } /****************************************************************************** * Called when the default alarm message font preference setting has changed. * Notify KAEvent. */ void KAlarmApp::slotMessageFontChanged(const QFont& font) { KAEvent::setDefaultFont(font); } /****************************************************************************** * Called when the working time preference settings have changed. * Notify KAEvent. */ void KAlarmApp::slotWorkTimeChanged(const QTime& start, const QTime& end, const QBitArray& days) { KAEvent::setWorkTime(days, start, end); } /****************************************************************************** * Called when the holiday region preference setting has changed. * Notify KAEvent. */ void KAlarmApp::slotHolidaysChanged(const KHolidays::HolidayRegion& holidays) { KAEvent::setHolidays(holidays); } /****************************************************************************** * Called when the date for February 29th recurrences has changed in the * preferences settings. */ void KAlarmApp::slotFeb29TypeChanged(Preferences::Feb29Type type) { KARecurrence::Feb29Type rtype; switch (type) { default: case Preferences::Feb29_None: rtype = KARecurrence::Feb29_None; break; case Preferences::Feb29_Feb28: rtype = KARecurrence::Feb29_Feb28; break; case Preferences::Feb29_Mar1: rtype = KARecurrence::Feb29_Mar1; break; } KARecurrence::setDefaultFeb29Type(rtype); } /****************************************************************************** * Return whether the program is configured to be running in the system tray. */ bool KAlarmApp::wantShowInSystemTray() const { return Preferences::showInSystemTray() && QSystemTrayIcon::isSystemTrayAvailable(); } /****************************************************************************** * Set a timeout for populating resources. */ void KAlarmApp::setResourcesTimeout() { QTimer::singleShot(AKONADI_TIMEOUT * 1000, this, &KAlarmApp::slotResourcesTimeout); } /****************************************************************************** * Called on a timeout to check whether resources have been populated. * If not, exit the program with code 1. */ void KAlarmApp::slotResourcesTimeout() { if (!Resources::allPopulated()) { // Resource population has timed out. mResourcesTimedOut = true; quitIf(1); } } /****************************************************************************** * Called when all resources have been created at startup. * Check whether there are any writable active calendars, and if not, warn the * user. * If alarms are being archived, check whether there is a default archived * calendar, and if not, warn the user. */ void KAlarmApp::slotResourcesCreated() { if (mRedisplayAlarms) { mRedisplayAlarms = false; MessageWin::redisplayAlarms(); } checkWritableCalendar(); checkArchivedCalendar(); } /****************************************************************************** * Called when all calendars have been fetched at startup, or calendar migration * has completed. * Check whether there are any writable active calendars, and if not, warn the * user. */ void KAlarmApp::checkWritableCalendar() { if (mReadOnly) return; // don't need write access to calendars if (!Resources::allCreated() || !DataModel::isMigrationComplete()) return; static bool done = false; if (done) return; done = true; qCDebug(KALARM_LOG) << "KAlarmApp::checkWritableCalendar"; // Check for, and remove, any duplicate resources, i.e. those which use the // same calendar file/directory. DataModel::removeDuplicateResources(); // Find whether there are any writable active alarm calendars const bool active = !Resources::enabledResources(CalEvent::ACTIVE, true).isEmpty(); if (!active) { qCWarning(KALARM_LOG) << "KAlarmApp::checkWritableCalendar: No writable active calendar"; KAMessageBox::information(MainWindow::mainMainWindow(), xi18nc("@info", "Alarms cannot be created or updated, because no writable active alarm calendar is enabled." "To fix this, use View | Show Calendars to check or change calendar statuses."), QString(), QStringLiteral("noWritableCal")); } } /****************************************************************************** * If alarms are being archived, check whether there is a default archived * calendar, and if not, warn the user. */ void KAlarmApp::checkArchivedCalendar() { static bool done = false; if (done) return; done = true; // If alarms are to be archived, check that the default archived alarm // calendar is writable. if (Preferences::archivedKeepDays()) { Resource standard = Resources::getStandard(CalEvent::ARCHIVED); if (!standard.isValid()) { // Schedule the display of a user prompt, without holding up // other processing. QTimer::singleShot(0, this, &KAlarmApp::promptArchivedCalendar); } } } /****************************************************************************** * Edit an alarm specified on the command line. */ void KAlarmApp::slotEditAlarmById() { qCDebug(KALARM_LOG) << "KAlarmApp::slotEditAlarmById"; ActionQEntry& entry = mActionQueue.head(); if (!KAlarm::editAlarmById(entry.eventId)) { CommandOptions::printError(xi18nc("@info:shell", "%1: Event %2 not found, or not editable", mCommandOption, entry.eventId.eventId())); mActionQueue.clear(); quitIf(1); } else { createOnlyMainWindow(); // prevent the application from quitting if (mEditingCmdLineAlarm & 0x10) { mRedisplayAlarms = false; MessageWin::redisplayAlarms(); } mEditingCmdLineAlarm = 2; // indicate edit completion QTimer::singleShot(0, this, &KAlarmApp::processQueue); } } /****************************************************************************** * If alarms are being archived, check whether there is a default archived * calendar, and if not, warn the user. */ void KAlarmApp::promptArchivedCalendar() { const bool archived = !Resources::enabledResources(CalEvent::ARCHIVED, true).isEmpty(); if (archived) { qCWarning(KALARM_LOG) << "KAlarmApp::checkArchivedCalendar: Archiving, but no writable archived calendar"; KAMessageBox::information(MainWindow::mainMainWindow(), xi18nc("@info", "Alarms are configured to be archived, but this is not possible because no writable archived alarm calendar is enabled." "To fix this, use View | Show Calendars to check or change calendar statuses."), QString(), QStringLiteral("noWritableArch")); } else { qCWarning(KALARM_LOG) << "KAlarmApp::checkArchivedCalendar: Archiving, but no standard archived calendar"; KAMessageBox::information(MainWindow::mainMainWindow(), xi18nc("@info", "Alarms are configured to be archived, but this is not possible because no archived alarm calendar is set as default." "To fix this, use View | Show Calendars, select an archived alarms calendar, and check Use as Default for Archived Alarms."), QString(), QStringLiteral("noStandardArch")); } } /****************************************************************************** * Called when a new resource has been added, to note the possible need to purge * its old alarms if it is the default archived calendar. */ void KAlarmApp::slotResourceAdded(const Resource& resource) { if (resource.alarmTypes() & CalEvent::ARCHIVED) mPendingPurges += resource.id(); } /****************************************************************************** * Called when a resource has been populated, to purge its old alarms if it is * the default archived calendar. */ void KAlarmApp::slotResourcePopulated(const Resource& resource) { if (mPendingPurges.removeAll(resource.id()) > 0) purgeNewArchivedDefault(resource); } /****************************************************************************** * Called when a new resource has been populated, or when a resource has been * set as the standard resource for its type. * If it is the default archived calendar, purge its old alarms if necessary. */ void KAlarmApp::purgeNewArchivedDefault(const Resource& resource) { if (Resources::isStandard(resource, CalEvent::ARCHIVED)) { qCDebug(KALARM_LOG) << "KAlarmApp::purgeNewArchivedDefault:" << resource.displayId() << ": standard archived..."; if (mArchivedPurgeDays >= 0) purge(mArchivedPurgeDays); else setArchivePurgeDays(); } } /****************************************************************************** * Called when the length of time to keep archived alarms changes in KAlarm's * preferences. * Set the number of days to keep archived alarms. * Alarms which are older are purged immediately, and at the start of each day. */ void KAlarmApp::setArchivePurgeDays() { const int newDays = Preferences::archivedKeepDays(); if (newDays != mArchivedPurgeDays) { const int oldDays = mArchivedPurgeDays; mArchivedPurgeDays = newDays; if (mArchivedPurgeDays <= 0) StartOfDayTimer::disconnect(this); if (mArchivedPurgeDays < 0) return; // keep indefinitely, so don't purge if (oldDays < 0 || mArchivedPurgeDays < oldDays) { // Alarms are now being kept for less long, so purge them purge(mArchivedPurgeDays); if (!mArchivedPurgeDays) return; // don't archive any alarms } // Start the purge timer to expire at the start of the next day // (using the user-defined start-of-day time). StartOfDayTimer::connect(this, SLOT(slotPurge())); } } /****************************************************************************** * Purge all archived events from the calendar whose end time is longer ago than * 'daysToKeep'. All events are deleted if 'daysToKeep' is zero. */ void KAlarmApp::purge(int daysToKeep) { if (mPurgeDaysQueued < 0 || daysToKeep < mPurgeDaysQueued) mPurgeDaysQueued = daysToKeep; // Do the purge once any other current operations are completed processQueue(); } /****************************************************************************** * Output a list of pending alarms, with their next scheduled occurrence. */ QStringList KAlarmApp::scheduledAlarmList() { QStringList alarms; const QVector events = KAlarm::getSortedActiveEvents(this); for (const KAEvent& event : events) { const KADateTime dateTime = event.nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone(); const Resource resource = Resources::resource(event.resourceId()); QString text(resource.configName() + QLatin1String(":")); text += event.id() + QLatin1Char(' ') + dateTime.toString(QStringLiteral("%Y%m%dT%H%M ")) + AlarmText::summary(event, 1); alarms << text; } return alarms; } /****************************************************************************** * Enable or disable alarm monitoring. */ void KAlarmApp::setAlarmsEnabled(bool enabled) { if (enabled != mAlarmsEnabled) { mAlarmsEnabled = enabled; Q_EMIT alarmEnabledToggled(enabled); if (!enabled) KAlarm::cancelRtcWake(nullptr); else if (!mProcessingQueue) checkNextDueAlarm(); } } /****************************************************************************** * Spread or collect alarm message and error message windows. */ void KAlarmApp::spreadWindows(bool spread) { spread = MessageWin::spread(spread); Q_EMIT spreadWindowsToggled(spread); } /****************************************************************************** * Called when the spread status of message windows changes. * Set the 'spread windows' action state. */ void KAlarmApp::setSpreadWindowsState(bool spread) { Q_EMIT spreadWindowsToggled(spread); } /****************************************************************************** * Check whether the window manager's handling of keyboard focus transfer * between application windows is broken. This is true for Ubuntu's Unity * desktop, where MessageWin windows steal keyboard focus from EditAlarmDlg * windows. */ bool KAlarmApp::windowFocusBroken() const { return mWindowFocusBroken; } /****************************************************************************** * Check whether window/keyboard focus currently needs to be fixed manually due * to the window manager not handling it correctly. This will occur if there are * both EditAlarmDlg and MessageWin windows currently active. */ bool KAlarmApp::needWindowFocusFix() const { return mWindowFocusBroken && MessageWin::instanceCount(true) && EditAlarmDlg::instanceCount(); } /****************************************************************************** * Called to schedule a new alarm, either in response to a DCOP notification or * to command line options. * Reply = true unless there was a parameter error or an error opening calendar file. */ bool KAlarmApp::scheduleEvent(KAEvent::SubAction action, const QString& text, const KADateTime& dateTime, int lateCancel, KAEvent::Flags flags, const QColor& bg, const QColor& fg, const QFont& font, const QString& audioFile, float audioVolume, int reminderMinutes, const KARecurrence& recurrence, const KCalendarCore::Duration& repeatInterval, int repeatCount, uint mailFromID, const KCalendarCore::Person::List& mailAddresses, const QString& mailSubject, const QStringList& mailAttachments) { qCDebug(KALARM_LOG) << "KAlarmApp::scheduleEvent:" << text; if (!dateTime.isValid()) return false; const KADateTime now = KADateTime::currentUtcDateTime(); if (lateCancel && dateTime < now.addSecs(-maxLateness(lateCancel))) return true; // alarm time was already archived too long ago KADateTime alarmTime = dateTime; // Round down to the nearest minute to avoid scheduling being messed up if (!dateTime.isDateOnly()) alarmTime.setTime(QTime(alarmTime.time().hour(), alarmTime.time().minute(), 0)); KAEvent event(alarmTime, text, bg, fg, font, action, lateCancel, flags, true); if (reminderMinutes) { const bool onceOnly = flags & KAEvent::REMINDER_ONCE; event.setReminder(reminderMinutes, onceOnly); } if (!audioFile.isEmpty()) event.setAudioFile(audioFile, audioVolume, -1, 0, (flags & KAEvent::REPEAT_SOUND) ? 0 : -1); if (!mailAddresses.isEmpty()) event.setEmail(mailFromID, mailAddresses, mailSubject, mailAttachments); event.setRecurrence(recurrence); event.setFirstRecurrence(); event.setRepetition(Repetition(repeatInterval, repeatCount - 1)); event.endChanges(); if (alarmTime <= now) { // Alarm is due for display already. // First execute it once without adding it to the calendar file. if (!mInitialised) mActionQueue.enqueue(ActionQEntry(event, QueuedAction::Trigger)); else execAlarm(event, event.firstAlarm(), false); // If it's a recurring alarm, reschedule it for its next occurrence if (!event.recurs() || event.setNextOccurrence(now) == KAEvent::NO_OCCURRENCE) return true; // It has recurrences in the future } // Queue the alarm for insertion into the calendar file mActionQueue.enqueue(ActionQEntry(event)); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); return true; } /****************************************************************************** * Called in response to a D-Bus request to trigger or cancel an event. * Optionally display the event. Delete the event from the calendar file and * from every main window instance. */ bool KAlarmApp::dbusHandleEvent(const EventId& eventID, QueuedAction action) { qCDebug(KALARM_LOG) << "KAlarmApp::dbusHandleEvent:" << eventID; mActionQueue.append(ActionQEntry(action, eventID)); if (mInitialised) QTimer::singleShot(0, this, &KAlarmApp::processQueue); return true; } /****************************************************************************** * Called in response to a D-Bus request to list all pending alarms. */ QString KAlarmApp::dbusList() { qCDebug(KALARM_LOG) << "KAlarmApp::dbusList"; return scheduledAlarmList().join(QLatin1Char('\n')) + QLatin1Char('\n'); } /****************************************************************************** * Either: * a) Display the event and then delete it if it has no outstanding repetitions. * b) Delete the event. * c) Reschedule the event for its next repetition. If none remain, delete it. * If the event is deleted, it is removed from the calendar file and from every * main window instance. * If 'findUniqueId' is true and 'id' does not specify a resource, all resources * will be searched for the event's unique ID. * Reply = false if event ID not found, or if more than one event with the same * ID is found. */ bool KAlarmApp::handleEvent(const EventId& id, QueuedAction action, bool findUniqueId) { Q_ASSERT(!(int(action) & ~int(QueuedAction::ActionMask))); // Delete any expired wake-on-suspend config data KAlarm::checkRtcWakeConfig(); const QString eventID(id.eventId()); - KAEvent* event = AlarmCalendar::resources()->event(id, findUniqueId); + KAEvent* event = ResourcesCalendar::instance()->event(id, findUniqueId); if (!event) { if (id.resourceId() != -1) qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found, or duplicated:" << eventID; else qCWarning(KALARM_LOG) << "KAlarmApp::handleEvent: Event ID not found:" << eventID; return false; } switch (action) { case QueuedAction::Cancel: qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << ", CANCEL"; KAlarm::deleteEvent(*event, true); break; case QueuedAction::Trigger: // handle it if it's due, else execute it regardless case QueuedAction::Handle: // handle it if it's due { const KADateTime now = KADateTime::currentUtcDateTime(); qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent:" << eventID << "," << (action==QueuedAction::Trigger?"TRIGGER:":"HANDLE:") << qPrintable(now.qDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm"))) << "UTC"; bool updateCalAndDisplay = false; bool alarmToExecuteValid = false; KAAlarm alarmToExecute; bool restart = false; // Check all the alarms in turn. // Note that the main alarm is fetched before any other alarms. for (KAAlarm alarm = event->firstAlarm(); alarm.isValid(); alarm = (restart ? event->firstAlarm() : event->nextAlarm(alarm)), restart = false) { // Check if the alarm is due yet. const KADateTime nextDT = alarm.dateTime(true).effectiveKDateTime(); const int secs = nextDT.secsTo(now); if (secs < 0) { // The alarm appears to be in the future. // Check if it's an invalid local time during a daylight // saving time shift, which has actually passed. if (alarm.dateTime().timeSpec() != KADateTime::LocalZone || nextDT > now.toTimeSpec(KADateTime::LocalZone)) { // This alarm is definitely not due yet qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not due"; continue; } } bool reschedule = false; bool rescheduleWork = false; if ((event->workTimeOnly() || event->holidaysExcluded()) && !alarm.deferred()) { // The alarm is restricted to working hours and/or non-holidays // (apart from deferrals). This needs to be re-evaluated every // time it triggers, since working hours could change. if (alarm.dateTime().isDateOnly()) { KADateTime dt(nextDT); dt.setDateOnly(true); reschedule = !event->isWorkingTime(dt); } else reschedule = !event->isWorkingTime(nextDT); rescheduleWork = reschedule; if (reschedule) qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << "at" << nextDT.qDateTime() << ": not during working hours"; } if (!reschedule && alarm.repeatAtLogin()) { // Alarm is to be displayed at every login. qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: REPEAT_AT_LOGIN"; // Check if the main alarm is already being displayed. // (We don't want to display both at the same time.) if (alarmToExecute.isValid()) continue; // Set the time to display if it's a display alarm alarm.setTime(now); } if (!reschedule && event->lateCancel()) { // Alarm is due, and it is to be cancelled if too late. qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: LATE_CANCEL"; bool cancel = false; if (alarm.dateTime().isDateOnly()) { // The alarm has no time, so cancel it if its date is too far past const int maxlate = event->lateCancel() / 1440; // maximum lateness in days KADateTime limit(DateTime(nextDT.addDays(maxlate + 1)).effectiveKDateTime()); if (now >= limit) { // It's too late to display the scheduled occurrence. // Find the last previous occurrence of the alarm. DateTime next; const KAEvent::OccurType type = event->previousOccurrence(now, next, true); switch (type & ~KAEvent::OCCURRENCE_REPEAT) { case KAEvent::FIRST_OR_ONLY_OCCURRENCE: case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: limit.setDate(next.date().addDays(maxlate + 1)); if (now >= limit) { if (type == KAEvent::LAST_RECURRENCE || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event->recurs())) cancel = true; // last occurrence (and there are no repetitions) else reschedule = true; } break; case KAEvent::NO_OCCURRENCE: default: reschedule = true; break; } } } else { // The alarm is timed. Allow it to be the permitted amount late before cancelling it. const int maxlate = maxLateness(event->lateCancel()); if (secs > maxlate) { // It's over the maximum interval late. // Find the most recent occurrence of the alarm. DateTime next; const KAEvent::OccurType type = event->previousOccurrence(now, next, true); switch (type & ~KAEvent::OCCURRENCE_REPEAT) { case KAEvent::FIRST_OR_ONLY_OCCURRENCE: case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: if (next.effectiveKDateTime().secsTo(now) > maxlate) { if (type == KAEvent::LAST_RECURRENCE || (type == KAEvent::FIRST_OR_ONLY_OCCURRENCE && !event->recurs())) cancel = true; // last occurrence (and there are no repetitions) else reschedule = true; } break; case KAEvent::NO_OCCURRENCE: default: reschedule = true; break; } } } if (cancel) { // All recurrences are finished, so cancel the event event->setArchive(); if (cancelAlarm(*event, alarm.type(), false)) return true; // event has been deleted updateCalAndDisplay = true; continue; } } if (reschedule) { // The latest repetition was too long ago, so schedule the next one switch (rescheduleAlarm(*event, alarm, false, (rescheduleWork ? nextDT : KADateTime()))) { case 1: // A working-time-only alarm has been rescheduled and the // rescheduled time is already due. Start processing the // event again. alarmToExecuteValid = false; restart = true; break; case -1: return true; // event has been deleted default: break; } updateCalAndDisplay = true; continue; } if (!alarmToExecuteValid) { qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": execute"; alarmToExecute = alarm; // note the alarm to be displayed alarmToExecuteValid = true; // only trigger one alarm for the event } else qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: Alarm" << alarm.type() << ": skip"; } // If there is an alarm to execute, do this last after rescheduling/cancelling // any others. This ensures that the updated event is only saved once to the calendar. if (alarmToExecute.isValid()) execAlarm(*event, alarmToExecute, true, !alarmToExecute.repeatAtLogin()); else { if (action == QueuedAction::Trigger) { // The alarm is to be executed regardless of whether it's due. // Only trigger one alarm from the event - we don't want multiple // identical messages, for example. const KAAlarm alarm = event->firstAlarm(); if (alarm.isValid()) execAlarm(*event, alarm, false); } if (updateCalAndDisplay) KAlarm::updateEvent(*event); // update the window lists and calendar file else if (action != QueuedAction::Trigger) { qCDebug(KALARM_LOG) << "KAlarmApp::handleEvent: No action"; } } break; } default: break; } return true; } /****************************************************************************** * Called when an alarm action has completed, to perform any post-alarm actions. */ void KAlarmApp::alarmCompleted(const KAEvent& event) { if (!event.postAction().isEmpty()) { // doShellCommand() will error if the user is not authorised to run // shell commands. const QString command = event.postAction(); qCDebug(KALARM_LOG) << "KAlarmApp::alarmCompleted:" << event.id() << ":" << command; doShellCommand(command, event, nullptr, ProcData::POST_ACTION); } } /****************************************************************************** * Reschedule the alarm for its next recurrence after now. If none remain, * delete it. If the alarm is deleted and it is the last alarm for its event, * the event is removed from the calendar file and from every main window * instance. * If 'nextDt' is valid, the event is rescheduled for the next non-working * time occurrence after that. * Reply = 1 if 'nextDt' is valid and the rescheduled event is already due * = -1 if the event has been deleted * = 0 otherwise. */ int KAlarmApp::rescheduleAlarm(KAEvent& event, const KAAlarm& alarm, bool updateCalAndDisplay, const KADateTime& nextDt) { qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: Alarm type:" << alarm.type(); int reply = 0; bool update = false; event.startChanges(); if (alarm.repeatAtLogin()) { // Leave an alarm which repeats at every login until its main alarm triggers if (!event.reminderActive() && event.reminderMinutes() < 0) { // Executing an at-login alarm: first schedule the reminder // which occurs AFTER the main alarm. event.activateReminderAfter(KADateTime::currentUtcDateTime()); } // Repeat-at-login alarms are usually unchanged after triggering. // Ensure that the archive flag (which was set in execAlarm()) is saved. update = true; } else if (alarm.isReminder() || alarm.deferred()) { // It's a reminder alarm or an extra deferred alarm, so delete it event.removeExpiredAlarm(alarm.type()); update = true; } else { // Reschedule the alarm for its next occurrence. bool cancelled = false; DateTime last = event.mainDateTime(false); // note this trigger time if (last != event.mainDateTime(true)) last = DateTime(); // but ignore sub-repetition triggers bool next = nextDt.isValid(); KADateTime next_dt = nextDt; const KADateTime now = KADateTime::currentUtcDateTime(); do { const KAEvent::OccurType type = event.setNextOccurrence(next ? next_dt : now); switch (type) { case KAEvent::NO_OCCURRENCE: // All repetitions are finished, so cancel the event qCDebug(KALARM_LOG) << "KAlarmApp::rescheduleAlarm: No occurrence"; if (event.reminderMinutes() < 0 && last.isValid() && alarm.type() != KAAlarm::AT_LOGIN_ALARM && !event.mainExpired()) { // Set the reminder which is now due after the last main alarm trigger. // Note that at-login reminders are scheduled in execAlarm(). event.activateReminderAfter(last); updateCalAndDisplay = true; } if (cancelAlarm(event, alarm.type(), updateCalAndDisplay)) return -1; break; default: if (!(type & KAEvent::OCCURRENCE_REPEAT)) break; // Next occurrence is a repeat, so fall through to recurrence handling Q_FALLTHROUGH(); case KAEvent::RECURRENCE_DATE: case KAEvent::RECURRENCE_DATE_TIME: case KAEvent::LAST_RECURRENCE: // The event is due by now and repetitions still remain, so rewrite the event if (updateCalAndDisplay) update = true; break; case KAEvent::FIRST_OR_ONLY_OCCURRENCE: // The first occurrence is still due?!?, so don't do anything break; } if (cancelled) break; if (event.deferred()) { // Just in case there's also a deferred alarm, ensure it's removed event.removeExpiredAlarm(KAAlarm::DEFERRED_ALARM); update = true; } if (next) { // The alarm is restricted to working hours and/or non-holidays. // Check if the calculated next time is valid. next_dt = event.mainDateTime(true).effectiveKDateTime(); if (event.mainDateTime(false).isDateOnly()) { KADateTime dt(next_dt); dt.setDateOnly(true); next = !event.isWorkingTime(dt); } else next = !event.isWorkingTime(next_dt); } } while (next && next_dt <= now); reply = (!cancelled && next_dt.isValid() && (next_dt <= now)) ? 1 : 0; if (event.reminderMinutes() < 0 && last.isValid() && alarm.type() != KAAlarm::AT_LOGIN_ALARM) { // Set the reminder which is now due after the last main alarm trigger. // Note that at-login reminders are scheduled in execAlarm(). event.activateReminderAfter(last); } } event.endChanges(); if (update) KAlarm::updateEvent(event); // update the window lists and calendar file return reply; } /****************************************************************************** * Delete the alarm. If it is the last alarm for its event, the event is removed * from the calendar file and from every main window instance. * Reply = true if event has been deleted. */ bool KAlarmApp::cancelAlarm(KAEvent& event, KAAlarm::Type alarmType, bool updateCalAndDisplay) { qCDebug(KALARM_LOG) << "KAlarmApp::cancelAlarm"; if (alarmType == KAAlarm::MAIN_ALARM && !event.displaying() && event.toBeArchived()) { // The event is being deleted. Save it in the archived resources first. KAEvent ev(event); KAlarm::addArchivedEvent(ev); } event.removeExpiredAlarm(alarmType); if (!event.alarmCount()) { // If it's a command alarm being executed, mark it as deleted ProcData* pd = findCommandProcess(event.id()); if (pd) pd->eventDeleted = true; // Delete it KAlarm::deleteEvent(event, false); return true; } if (updateCalAndDisplay) KAlarm::updateEvent(event); // update the window lists and calendar file return false; } /****************************************************************************** * Cancel any reminder or deferred alarms in an repeat-at-login event. * This should be called when the event is first loaded. * If there are no more alarms left in the event, the event is removed from the * calendar file and from every main window instance. * Reply = true if event has been deleted. */ bool KAlarmApp::cancelReminderAndDeferral(KAEvent& event) { return cancelAlarm(event, KAAlarm::REMINDER_ALARM, false) || cancelAlarm(event, KAAlarm::DEFERRED_REMINDER_ALARM, false) || cancelAlarm(event, KAAlarm::DEFERRED_ALARM, true); } /****************************************************************************** * Execute an alarm by displaying its message or file, or executing its command. * Reply = ShellProcess instance if a command alarm * = MessageWin if an audio alarm * != 0 if successful * = -1 if execution has not completed * = 0 if the alarm is disabled, or if an error message was output. */ void* KAlarmApp::execAlarm(KAEvent& event, const KAAlarm& alarm, bool reschedule, bool allowDefer, bool noPreAction) { if (!mAlarmsEnabled || !event.enabled()) { // The event (or all events) is disabled qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": disabled"; if (reschedule) rescheduleAlarm(event, alarm, true); return nullptr; } void* result = (void*)1; event.setArchive(); switch (alarm.action()) { case KAAlarm::COMMAND: if (!event.commandDisplay()) { // execCommandAlarm() will error if the user is not authorised // to run shell commands. result = execCommandAlarm(event, alarm); if (reschedule) rescheduleAlarm(event, alarm, true); break; } Q_FALLTHROUGH(); // fall through to MESSAGE case KAAlarm::MESSAGE: case KAAlarm::FILE: { // Display a message, file or command output, provided that the same event // isn't already being displayed MessageWin* win = MessageWin::findEvent(EventId(event)); // Find if we're changing a reminder message to the real message const bool reminder = (alarm.type() & KAAlarm::REMINDER_ALARM); const bool replaceReminder = !reminder && win && (win->alarmType() & KAAlarm::REMINDER_ALARM); if (!reminder && (!event.deferred() || (event.extraActionOptions() & KAEvent::ExecPreActOnDeferral)) && (replaceReminder || !win) && !noPreAction && !event.preAction().isEmpty()) { // It's not a reminder alarm, and it's not a deferred alarm unless the // pre-alarm action applies to deferred alarms, and there is no message // window (other than a reminder window) currently displayed for this // alarm, and we need to execute a command before displaying the new window. // // NOTE: The pre-action is not executed for a recurring alarm if an // alarm message window for a previous occurrence is still visible. // Check whether the command is already being executed for this alarm. for (const ProcData* pd : qAsConst(mCommandProcesses)) { if (pd->event->id() == event.id() && (pd->flags & ProcData::PRE_ACTION)) { qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Already executing pre-DISPLAY command"; return pd->process; // already executing - don't duplicate the action } } // doShellCommand() will error if the user is not authorised to run // shell commands. const QString command = event.preAction(); qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: Pre-DISPLAY command:" << command; const int flags = (reschedule ? ProcData::RESCHEDULE : 0) | (allowDefer ? ProcData::ALLOW_DEFER : 0); if (doShellCommand(command, event, &alarm, (flags | ProcData::PRE_ACTION))) { - AlarmCalendar::resources()->setAlarmPending(&event); + ResourcesCalendar::instance()->setAlarmPending(&event); return result; // display the message after the command completes } // Error executing command if (event.extraActionOptions() & KAEvent::CancelOnPreActError) { // Cancel the rest of the alarm execution qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm:" << event.id() << ": pre-action failed: cancelled"; if (reschedule) rescheduleAlarm(event, alarm, true); return nullptr; } // Display the message even though it failed } if (!win) { // There isn't already a message for this event const int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | (allowDefer ? 0 : MessageWin::NO_DEFER); (new MessageWin(&event, alarm, flags))->show(); } else if (replaceReminder) { // The caption needs to be changed from "Reminder" to "Message" win->cancelReminder(event, alarm); } else if (!win->hasDefer() && !alarm.repeatAtLogin()) { // It's a repeat-at-login message with no Defer button, // which has now reached its final trigger time and needs // to be replaced with a new message. win->showDefer(); win->showDateTime(event, alarm); } else { // Use the existing message window } if (win) { // Raise the existing message window and replay any sound win->repeat(alarm); // N.B. this reschedules the alarm } break; } case KAAlarm::EMAIL: { qCDebug(KALARM_LOG) << "KAlarmApp::execAlarm: EMAIL to:" << event.emailAddresses(QStringLiteral(",")); QStringList errmsgs; KAMail::JobData data(event, alarm, reschedule, (reschedule || allowDefer)); data.queued = true; int ans = KAMail::send(data, errmsgs); if (ans) { // The email has either been sent or failed - not queued if (ans < 0) result = nullptr; // failure data.queued = false; emailSent(data, errmsgs, (ans > 0)); } else { result = (void*)-1; // email has been queued } if (reschedule) rescheduleAlarm(event, alarm, true); break; } case KAAlarm::AUDIO: { // Play the sound, provided that the same event // isn't already playing MessageWin* win = MessageWin::findEvent(EventId(event)); if (!win) { // There isn't already a message for this event. const int flags = (reschedule ? 0 : MessageWin::NO_RESCHEDULE) | MessageWin::ALWAYS_HIDE; win = new MessageWin(&event, alarm, flags); } else { // There's an existing message window: replay the sound win->repeat(alarm); // N.B. this reschedules the alarm } return win; } default: return nullptr; } return result; } /****************************************************************************** * Called when sending an email has completed. */ void KAlarmApp::emailSent(KAMail::JobData& data, const QStringList& errmsgs, bool copyerr) { if (!errmsgs.isEmpty()) { // Some error occurred, although the email may have been sent successfully if (errmsgs.count() > 1) qCDebug(KALARM_LOG) << "KAlarmApp::emailSent:" << (copyerr ? "Copy error:" : "Failed:") << errmsgs[1]; MessageWin::showError(data.event, data.alarm.dateTime(), errmsgs); } else if (data.queued) Q_EMIT execAlarmSuccess(); } /****************************************************************************** * Execute the command specified in a command alarm. * To connect to the output ready signals of the process, specify a slot to be * called by supplying 'receiver' and 'slot' parameters. */ ShellProcess* KAlarmApp::execCommandAlarm(const KAEvent& event, const KAAlarm& alarm, const QObject* receiver, const char* slot) { // doShellCommand() will error if the user is not authorised to run // shell commands. const int flags = (event.commandXterm() ? ProcData::EXEC_IN_XTERM : 0) | (event.commandDisplay() ? ProcData::DISP_OUTPUT : 0); const QString command = event.cleanText(); if (event.commandScript()) { // Store the command script in a temporary file for execution qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm: Script"; const QString tmpfile = createTempScriptFile(command, false, event, alarm); if (tmpfile.isEmpty()) { setEventCommandError(event, KAEvent::CMD_ERROR); return nullptr; } return doShellCommand(tmpfile, event, &alarm, (flags | ProcData::TEMP_FILE), receiver, slot); } else { qCDebug(KALARM_LOG) << "KAlarmApp::execCommandAlarm:" << command; return doShellCommand(command, event, &alarm, flags, receiver, slot); } } /****************************************************************************** * Execute a shell command line specified by an alarm. * If the PRE_ACTION bit of 'flags' is set, the alarm will be executed via * execAlarm() once the command completes, the execAlarm() parameters being * derived from the remaining bits in 'flags'. * 'flags' must contain the bit PRE_ACTION or POST_ACTION if and only if it is * a pre- or post-alarm action respectively. * To connect to the output ready signals of the process, specify a slot to be * called by supplying 'receiver' and 'slot' parameters. * * Note that if shell access is not authorised, the attempt to run the command * will be errored. */ ShellProcess* KAlarmApp::doShellCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, const QObject* receiver, const char* slot) { qCDebug(KALARM_LOG) << "KAlarmApp::doShellCommand:" << command << "," << event.id(); QIODevice::OpenMode mode = QIODevice::WriteOnly; QString cmd; QString tmpXtermFile; if (flags & ProcData::EXEC_IN_XTERM) { // Execute the command in a terminal window. cmd = composeXTermCommand(command, event, alarm, flags, tmpXtermFile); if (cmd.isEmpty()) { qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed (no terminal selected)"; const QStringList errors{i18nc("@info", "Failed to execute command\n(no terminal selected for command alarms)")}; commandErrorMsg(nullptr, event, alarm, flags, errors); return nullptr; } } else { cmd = command; mode = QIODevice::ReadWrite; } ProcData* pd = nullptr; ShellProcess* proc = nullptr; if (!cmd.isEmpty()) { // Use ShellProcess, which automatically checks whether the user is // authorised to run shell commands. proc = new ShellProcess(cmd); proc->setEnv(QStringLiteral("KALARM_UID"), event.id(), true); proc->setOutputChannelMode(KProcess::MergedChannels); // combine stdout & stderr connect(proc, &ShellProcess::shellExited, this, &KAlarmApp::slotCommandExited); if ((flags & ProcData::DISP_OUTPUT) && receiver && slot) { connect(proc, SIGNAL(receivedStdout(ShellProcess*)), receiver, slot); connect(proc, SIGNAL(receivedStderr(ShellProcess*)), receiver, slot); } if (mode == QIODevice::ReadWrite && !event.logFile().isEmpty()) { // Output is to be appended to a log file. // Set up a logging process to write the command's output to. QString heading; if (alarm && alarm->dateTime().isValid()) { const QString dateTime = alarm->dateTime().formatLocale(); heading = QStringLiteral("\n******* KAlarm %1 *******\n").arg(dateTime); } else heading = QStringLiteral("\n******* KAlarm *******\n"); QFile logfile(event.logFile()); if (logfile.open(QIODevice::Append | QIODevice::Text)) { QTextStream out(&logfile); out << heading; logfile.close(); } proc->setStandardOutputFile(event.logFile(), QIODevice::Append); } pd = new ProcData(proc, new KAEvent(event), (alarm ? new KAAlarm(*alarm) : nullptr), flags); if (flags & ProcData::TEMP_FILE) pd->tempFiles += command; if (!tmpXtermFile.isEmpty()) pd->tempFiles += tmpXtermFile; mCommandProcesses.append(pd); if (proc->start(mode)) return proc; } // Error executing command - report it qCWarning(KALARM_LOG) << "KAlarmApp::doShellCommand: Command failed to start"; commandErrorMsg(proc, event, alarm, flags); if (pd) { mCommandProcesses.removeAt(mCommandProcesses.indexOf(pd)); delete pd; } return nullptr; } /****************************************************************************** * Compose a command line to execute the given command in a terminal window. * 'tempScriptFile' receives the name of a temporary script file which is * invoked by the command line, if applicable. * Reply = command line, or empty string if error. */ QString KAlarmApp::composeXTermCommand(const QString& command, const KAEvent& event, const KAAlarm* alarm, int flags, QString& tempScriptFile) const { qCDebug(KALARM_LOG) << "KAlarmApp::composeXTermCommand:" << command << "," << event.id(); tempScriptFile.clear(); QString cmd = Preferences::cmdXTermCommand(); if (cmd.isEmpty()) return QString(); // no terminal application is configured cmd.replace(QLatin1String("%t"), KAboutData::applicationData().displayName()); // set the terminal window title if (cmd.indexOf(QLatin1String("%C")) >= 0) { // Execute the command from a temporary script file if (flags & ProcData::TEMP_FILE) cmd.replace(QLatin1String("%C"), command); // the command is already calling a temporary file else { tempScriptFile = createTempScriptFile(command, true, event, *alarm); if (tempScriptFile.isEmpty()) return QString(); cmd.replace(QLatin1String("%C"), tempScriptFile); // %C indicates where to insert the command } } else if (cmd.indexOf(QLatin1String("%W")) >= 0) { // Execute the command from a temporary script file, // with a sleep after the command is executed tempScriptFile = createTempScriptFile(command + QLatin1String("\nsleep 86400\n"), true, event, *alarm); if (tempScriptFile.isEmpty()) return QString(); cmd.replace(QLatin1String("%W"), tempScriptFile); // %w indicates where to insert the command } else if (cmd.indexOf(QLatin1String("%w")) >= 0) { // Append a sleep to the command. // Quote the command in case it contains characters such as [>|;]. const QString exec = KShell::quoteArg(command + QLatin1String("; sleep 86400")); cmd.replace(QLatin1String("%w"), exec); // %w indicates where to insert the command string } else { // Set the command to execute. // Put it in quotes in case it contains characters such as [>|;]. const QString exec = KShell::quoteArg(command); if (cmd.indexOf(QLatin1String("%c")) >= 0) cmd.replace(QLatin1String("%c"), exec); // %c indicates where to insert the command string else cmd.append(exec); // otherwise, simply append the command string } return cmd; } /****************************************************************************** * Create a temporary script file containing the specified command string. * Reply = path of temporary file, or null string if error. */ QString KAlarmApp::createTempScriptFile(const QString& command, bool insertShell, const KAEvent& event, const KAAlarm& alarm) const { QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); // don't delete file when it is destructed if (!tmpFile.open()) qCCritical(KALARM_LOG) << "Unable to create a temporary script file"; else { tmpFile.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser); QTextStream stream(&tmpFile); if (insertShell) stream << "#!" << ShellProcess::shellPath() << "\n"; stream << command; stream.flush(); if (tmpFile.error() != QFile::NoError) qCCritical(KALARM_LOG) << "Error" << tmpFile.errorString() << " writing to temporary script file"; else return tmpFile.fileName(); } const QStringList errmsgs(i18nc("@info", "Error creating temporary script file")); MessageWin::showError(event, alarm.dateTime(), errmsgs, QStringLiteral("Script")); return QString(); } /****************************************************************************** * Called when a command alarm's execution completes. */ void KAlarmApp::slotCommandExited(ShellProcess* proc) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited"; // Find this command in the command list for (int i = 0, end = mCommandProcesses.count(); i < end; ++i) { ProcData* pd = mCommandProcesses.at(i); if (pd->process == proc) { // Found the command. Check its exit status. bool executeAlarm = pd->preAction(); const ShellProcess::Status status = proc->status(); if (status == ShellProcess::SUCCESS && !proc->exitCode()) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": SUCCESS"; clearEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE : pd->postAction() ? KAEvent::CMD_ERROR_POST : KAEvent::CMD_ERROR); } else { QString errmsg = proc->errorMessage(); if (status == ShellProcess::SUCCESS || status == ShellProcess::NOT_FOUND) qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status << ", code =" << proc->exitCode(); else qCWarning(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ":" << errmsg << "exit status =" << status; if (pd->messageBoxParent) { // Close the existing informational KMessageBox for this process const QList dialogs = pd->messageBoxParent->findChildren(); if (!dialogs.isEmpty()) delete dialogs[0]; setEventCommandError(*pd->event, pd->preAction() ? KAEvent::CMD_ERROR_PRE : pd->postAction() ? KAEvent::CMD_ERROR_POST : KAEvent::CMD_ERROR); if (!pd->tempFile()) { errmsg += QLatin1Char('\n'); errmsg += proc->command(); } KAMessageBox::error(pd->messageBoxParent, errmsg); } else commandErrorMsg(proc, *pd->event, pd->alarm, pd->flags); if (executeAlarm && (pd->event->extraActionOptions() & KAEvent::CancelOnPreActError)) { qCDebug(KALARM_LOG) << "KAlarmApp::slotCommandExited:" << pd->event->id() << ": pre-action failed: cancelled"; if (pd->reschedule()) rescheduleAlarm(*pd->event, *pd->alarm, true); executeAlarm = false; } } if (pd->preAction()) - AlarmCalendar::resources()->setAlarmPending(pd->event, false); + ResourcesCalendar::instance()->setAlarmPending(pd->event, false); if (executeAlarm) execAlarm(*pd->event, *pd->alarm, pd->reschedule(), pd->allowDefer(), true); mCommandProcesses.removeAt(i); delete pd; break; } } // If there are now no executing shell commands, quit if a quit was queued if (mPendingQuit && mCommandProcesses.isEmpty()) quitIf(mPendingQuitCode); } /****************************************************************************** * Output an error message for a shell command, and record the alarm's error status. */ void KAlarmApp::commandErrorMsg(const ShellProcess* proc, const KAEvent& event, const KAAlarm* alarm, int flags, const QStringList& errors) { KAEvent::CmdErrType cmderr; QString dontShowAgain; QStringList errmsgs = errors; if (flags & ProcData::PRE_ACTION) { if (event.extraActionOptions() & KAEvent::DontShowPreActError) return; // don't notify user of any errors for the alarm errmsgs += i18nc("@info", "Pre-alarm action:"); dontShowAgain = QStringLiteral("Pre"); cmderr = KAEvent::CMD_ERROR_PRE; } else if (flags & ProcData::POST_ACTION) { errmsgs += i18nc("@info", "Post-alarm action:"); dontShowAgain = QStringLiteral("Post"); cmderr = (event.commandError() == KAEvent::CMD_ERROR_PRE) ? KAEvent::CMD_ERROR_PRE_POST : KAEvent::CMD_ERROR_POST; } else { dontShowAgain = QStringLiteral("Exec"); cmderr = KAEvent::CMD_ERROR; } // Record the alarm's error status setEventCommandError(event, cmderr); // Display an error message if (proc) { errmsgs += proc->errorMessage(); if (!(flags & ProcData::TEMP_FILE)) errmsgs += proc->command(); dontShowAgain += QString::number(proc->status()); } MessageWin::showError(event, (alarm ? alarm->dateTime() : DateTime()), errmsgs, dontShowAgain); } /****************************************************************************** * Notes that an informational KMessageBox is displayed for this process. */ void KAlarmApp::commandMessage(ShellProcess* proc, QWidget* parent) { // Find this command in the command list for (ProcData* pd : qAsConst(mCommandProcesses)) { if (pd->process == proc) { pd->messageBoxParent = parent; break; } } } /****************************************************************************** * If this is the first time through, open the calendar file, and start * processing the execution queue. */ bool KAlarmApp::initCheck(bool calendarOnly) { static bool firstTime = true; if (firstTime) qCDebug(KALARM_LOG) << "KAlarmApp::initCheck: first time"; if (initialiseTimerResources() || firstTime) { /* Need to open the display calendar now, since otherwise if display * alarms are immediately due, they will often be processed while * MessageWin::redisplayAlarms() is executing open() (but before open() * completes), which causes problems!! */ - AlarmCalendar::displayCalendar()->open(); - - if (!AlarmCalendar::resources()->open()) - return false; + DisplayCalendar::instance()->open(); } if (firstTime) { setArchivePurgeDays(); firstTime = false; } if (!calendarOnly) startProcessQueue(); // start processing the execution queue return true; } /****************************************************************************** * Called when an audio thread starts or stops. */ void KAlarmApp::notifyAudioPlaying(bool playing) { Q_EMIT audioPlaying(playing); } /****************************************************************************** * Stop audio play. */ void KAlarmApp::stopAudio() { MessageWin::stopAudio(); } /****************************************************************************** * Set the command error for the specified alarm. */ void KAlarmApp::setEventCommandError(const KAEvent& event, KAEvent::CmdErrType err) const { ProcData* pd = findCommandProcess(event.id()); if (pd && pd->eventDeleted) return; // the alarm has been deleted, so can't set error status if (err == KAEvent::CMD_ERROR_POST && event.commandError() == KAEvent::CMD_ERROR_PRE) err = KAEvent::CMD_ERROR_PRE_POST; event.setCommandError(err); - KAEvent* ev = AlarmCalendar::resources()->event(EventId(event)); + KAEvent* ev = ResourcesCalendar::instance()->event(EventId(event)); if (ev && ev->commandError() != err) ev->setCommandError(err); Resource resource = Resources::resourceForEvent(event.id()); resource.handleCommandErrorChange(event); } /****************************************************************************** * Clear the command error for the specified alarm. */ void KAlarmApp::clearEventCommandError(const KAEvent& event, KAEvent::CmdErrType err) const { ProcData* pd = findCommandProcess(event.id()); if (pd && pd->eventDeleted) return; // the alarm has been deleted, so can't set error status KAEvent::CmdErrType newerr = static_cast(event.commandError() & ~err); event.setCommandError(newerr); - KAEvent* ev = AlarmCalendar::resources()->event(EventId(event)); + KAEvent* ev = ResourcesCalendar::instance()->event(EventId(event)); if (ev) { newerr = static_cast(ev->commandError() & ~err); ev->setCommandError(newerr); } Resource resource = Resources::resourceForEvent(event.id()); resource.handleCommandErrorChange(event); } /****************************************************************************** * Find the currently executing command process for an event ID, if any. */ KAlarmApp::ProcData* KAlarmApp::findCommandProcess(const QString& eventId) const { for (ProcData* pd : qAsConst(mCommandProcesses)) { if (pd->event->id() == eventId) return pd; } return nullptr; } KAlarmApp::ProcData::ProcData(ShellProcess* p, KAEvent* e, KAAlarm* a, int f) : process(p) , event(e) , alarm(a) , messageBoxParent(nullptr) , flags(f) , eventDeleted(false) { } KAlarmApp::ProcData::~ProcData() { while (!tempFiles.isEmpty()) { // Delete the temporary file called by the XTerm command QFile f(tempFiles.first()); f.remove(); tempFiles.removeFirst(); } delete process; delete event; delete alarm; } // vim: et sw=4: diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index dfe7de21..c69f46f5 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,1638 +1,1638 @@ /* * mainwindow.cpp - main application window * Program: kalarm * Copyright © 2001-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "mainwindow.h" #include "alarmcalendar.h" #include "alarmlistdelegate.h" #include "alarmlistview.h" #include "birthdaydlg.h" #include "functions.h" #include "kalarmapp.h" #include "kamail.h" #include "newalarmaction.h" #include "prefdlg.h" #include "preferences.h" #include "resourceselector.h" #include "templatedlg.h" #include "templatemenuaction.h" #include "templatepickdlg.h" #include "traywindow.h" #include "wakedlg.h" #include "resources/datamodel.h" #include "resources/resources.h" #include "resources/eventmodel.h" #include "lib/autoqpointer.h" #include "lib/messagebox.h" #include "lib/synchtimer.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include using namespace KCalendarCore; using namespace KCalUtils; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KAlarmCal; namespace { const QString UI_FILE(QStringLiteral("kalarmui.rc")); const char* WINDOW_NAME = "MainWindow"; const char* VIEW_GROUP = "View"; const char* SHOW_COLUMNS = "ShowColumns"; const char* SHOW_ARCHIVED_KEY = "ShowArchivedAlarms"; const char* SHOW_RESOURCES_KEY = "ShowResources"; QString undoText; QString undoTextStripped; QList undoShortcut; QString redoText; QString redoTextStripped; QList redoShortcut; } /*============================================================================= = Class: MainWindow =============================================================================*/ MainWindow::WindowList MainWindow::mWindowList; TemplateDlg* MainWindow::mTemplateDlg = nullptr; /****************************************************************************** * Construct an instance. * To avoid resize() events occurring while still opening the calendar (and * resultant crashes), the calendar is opened before constructing the instance. */ MainWindow* MainWindow::create(bool restored) { theApp()->checkCalendar(); // ensure calendar is open return new MainWindow(restored); } MainWindow::MainWindow(bool restored) : MainWindowBase(nullptr, Qt::WindowContextHelpButtonHint) { qCDebug(KALARM_LOG) << "MainWindow:"; setAttribute(Qt::WA_DeleteOnClose); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("MainWin")); // used by LikeBack setPlainCaption(KAboutData::applicationData().displayName()); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); mShowResources = config.readEntry(SHOW_RESOURCES_KEY, false); mShowArchived = config.readEntry(SHOW_ARCHIVED_KEY, false); const QList showColumns = config.readEntry(SHOW_COLUMNS, QList()); if (!restored) { KConfigGroup wconfig(KSharedConfig::openConfig(), WINDOW_NAME); mResourcesWidth = wconfig.readEntry(QStringLiteral("Splitter %1").arg(QApplication::desktop()->width()), (int)0); } setAcceptDrops(true); // allow drag-and-drop onto this window mSplitter = new QSplitter(Qt::Horizontal, this); mSplitter->setChildrenCollapsible(false); mSplitter->installEventFilter(this); setCentralWidget(mSplitter); // Create the calendar resource selector widget Akonadi::ControlGui::widgetNeedsAkonadi(this); mResourceSelector = new ResourceSelector(mSplitter); mSplitter->setStretchFactor(0, 0); // don't resize resource selector when window is resized mSplitter->setStretchFactor(1, 1); // Create the alarm list widget mListFilterModel = DataModel::createAlarmListModel(this); mListFilterModel->setEventTypeFilter(mShowArchived ? CalEvent::ACTIVE | CalEvent::ARCHIVED : CalEvent::ACTIVE); mListView = new AlarmListView(WINDOW_NAME, mSplitter); mListView->setModel(mListFilterModel); mListView->setColumnsVisible(showColumns); mListView->setItemDelegate(new AlarmListDelegate(mListView)); connect(mListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::slotSelection); connect(mListView, &AlarmListView::contextMenuRequested, this, &MainWindow::slotContextMenuRequested); connect(mListView, &AlarmListView::columnsVisibleChanged, this, &MainWindow::slotAlarmListColumnsChanged); connect(Resources::instance(), &Resources::settingsChanged, this, &MainWindow::slotCalendarStatusChanged); connect(mResourceSelector, &ResourceSelector::resized, this, &MainWindow::resourcesResized); mListView->installEventFilter(this); initActions(); setAutoSaveSettings(QLatin1String(WINDOW_NAME), true); // save toolbars, window sizes etc. mWindowList.append(this); if (mWindowList.count() == 1) { // It's the first main window if (theApp()->wantShowInSystemTray()) theApp()->displayTrayIcon(true, this); // create system tray icon for run-in-system-tray mode else if (theApp()->trayWindow()) theApp()->trayWindow()->setAssocMainWindow(this); // associate this window with the system tray icon } slotCalendarStatusChanged(); // initialise action states now that window is registered } MainWindow::~MainWindow() { qCDebug(KALARM_LOG) << "~MainWindow"; bool trayParent = isTrayParent(); // must call before removing from window list mWindowList.removeAt(mWindowList.indexOf(this)); // Prevent view updates during window destruction delete mResourceSelector; mResourceSelector = nullptr; delete mListView; mListView = nullptr; if (theApp()->trayWindow()) { if (trayParent) delete theApp()->trayWindow(); else theApp()->trayWindow()->removeWindow(this); } KSharedConfig::openConfig()->sync(); // save any new window size to disc theApp()->quitIf(); } /****************************************************************************** * Called when the QApplication::saveStateRequest() signal has been emitted. * Save settings to the session managed config file, for restoration * when the program is restored. */ void MainWindow::saveProperties(KConfigGroup& config) { config.writeEntry("HiddenTrayParent", isTrayParent() && isHidden()); config.writeEntry("ShowArchived", mShowArchived); config.writeEntry("ShowColumns", mListView->columnsVisible()); config.writeEntry("ResourcesWidth", mResourceSelector->isHidden() ? 0 : mResourceSelector->width()); } /****************************************************************************** * Read settings from the session managed config file. * This function is automatically called whenever the app is being * restored. Read in whatever was saved in saveProperties(). */ void MainWindow::readProperties(const KConfigGroup& config) { mHiddenTrayParent = config.readEntry("HiddenTrayParent", true); mShowArchived = config.readEntry("ShowArchived", false); mResourcesWidth = config.readEntry("ResourcesWidth", (int)0); mShowResources = (mResourcesWidth > 0); mListView->setColumnsVisible(config.readEntry("ShowColumns", QList())); } /****************************************************************************** * Get the main main window, i.e. the parent of the system tray icon, or if * none, the first main window to be created. Visible windows take precedence * over hidden ones. */ MainWindow* MainWindow::mainMainWindow() { MainWindow* tray = (theApp() && theApp()->trayWindow()) ? theApp()->trayWindow()->assocMainWindow() : nullptr; if (tray && tray->isVisible()) return tray; for (int i = 0, end = mWindowList.count(); i < end; ++i) if (mWindowList.at(i)->isVisible()) return mWindowList.at(i); if (tray) return tray; if (mWindowList.isEmpty()) return nullptr; return mWindowList.at(0); } /****************************************************************************** * Check whether this main window is effectively the parent of the system tray icon. */ bool MainWindow::isTrayParent() const { TrayWindow* tray = theApp()->trayWindow(); if (!tray || !QSystemTrayIcon::isSystemTrayAvailable()) return false; if (tray->assocMainWindow() == this) return true; return mWindowList.count() == 1; } /****************************************************************************** * Close all main windows. */ void MainWindow::closeAll() { while (!mWindowList.isEmpty()) delete mWindowList[0]; // N.B. the destructor removes the window from the list } /****************************************************************************** * Intercept events for the splitter widget. */ bool MainWindow::eventFilter(QObject* obj, QEvent* e) { if (obj == mSplitter) { switch (e->type()) { case QEvent::Resize: // Don't change resources size while WINDOW is being resized. // Resize event always occurs before Paint. mResizing = true; break; case QEvent::Paint: // Allow resources to be resized again mResizing = false; break; default: break; } } else if (obj == mListView) { switch (e->type()) { case QEvent::KeyPress: { QKeyEvent* ke = static_cast(e); if (ke->key() == Qt::Key_Delete && ke->modifiers() == Qt::ShiftModifier) { // Prevent Shift-Delete being processed by EventListDelegate mActionDeleteForce->trigger(); return true; } break; } default: break; } } return false; } /****************************************************************************** * Called when the window's size has changed (before it is painted). * Sets the last column in the list view to extend at least to the right hand * edge of the list view. * Records the new size in the config file. */ void MainWindow::resizeEvent(QResizeEvent* re) { // Save the window's new size only if it's the first main window MainWindowBase::resizeEvent(re); if (mResourcesWidth > 0) { QList widths; widths.append(mResourcesWidth); widths.append(width() - mResourcesWidth - mSplitter->handleWidth()); mSplitter->setSizes(widths); } } /****************************************************************************** * Called when the resources panel has been resized. * Records the new size in the config file. */ void MainWindow::resourcesResized() { if (!mShown || mResizing) return; QList widths = mSplitter->sizes(); if (widths.count() > 1) { mResourcesWidth = widths[0]; // Width is reported as non-zero when resource selector is // actually invisible, so note a zero width in these circumstances. if (mResourcesWidth <= 5) mResourcesWidth = 0; else if (mainMainWindow() == this) { KConfigGroup config(KSharedConfig::openConfig(), WINDOW_NAME); config.writeEntry(QStringLiteral("Splitter %1").arg(QApplication::desktop()->width()), mResourcesWidth); config.sync(); } } } /****************************************************************************** * Called when the window is first displayed. * Sets the last column in the list view to extend at least to the right hand * edge of the list view. */ void MainWindow::showEvent(QShowEvent* se) { if (mResourcesWidth > 0) { QList widths; widths.append(mResourcesWidth); widths.append(width() - mResourcesWidth - mSplitter->handleWidth()); mSplitter->setSizes(widths); } MainWindowBase::showEvent(se); mShown = true; } /****************************************************************************** * Display the window. */ void MainWindow::show() { MainWindowBase::show(); if (mMenuError) { // Show error message now that the main window has been displayed. // Waiting until now lets the user easily associate the message with // the main window which is faulty. KAMessageBox::error(this, xi18nc("@info", "Failure to create menus (perhaps %1 missing or corrupted)", UI_FILE)); mMenuError = false; } } /****************************************************************************** * Called after the window is hidden. */ void MainWindow::hideEvent(QHideEvent* he) { MainWindowBase::hideEvent(he); } /****************************************************************************** * Initialise the menu, toolbar and main window actions. */ void MainWindow::initActions() { KActionCollection* actions = actionCollection(); mActionTemplates = new QAction(i18nc("@action", "&Templates..."), this); actions->addAction(QStringLiteral("templates"), mActionTemplates); connect(mActionTemplates, &QAction::triggered, this, &MainWindow::slotTemplates); mActionNew = new NewAlarmAction(false, i18nc("@action", "&New"), this, actions); actions->addAction(QStringLiteral("new"), mActionNew); QAction* action = mActionNew->displayAlarmAction(QStringLiteral("newDisplay")); connect(action, &QAction::triggered, this, &MainWindow::slotNewDisplay); action = mActionNew->commandAlarmAction(QStringLiteral("newCommand")); connect(action, &QAction::triggered, this, &MainWindow::slotNewCommand); action = mActionNew->emailAlarmAction(QStringLiteral("newEmail")); connect(action, &QAction::triggered, this, &MainWindow::slotNewEmail); action = mActionNew->audioAlarmAction(QStringLiteral("newAudio")); connect(action, &QAction::triggered, this, &MainWindow::slotNewAudio); TemplateMenuAction* templateMenuAction = mActionNew->fromTemplateAlarmAction(QStringLiteral("newFromTemplate")); connect(templateMenuAction, &TemplateMenuAction::selected, this, &MainWindow::slotNewFromTemplate); mActionCreateTemplate = new QAction(i18nc("@action", "Create Tem&plate..."), this); actions->addAction(QStringLiteral("createTemplate"), mActionCreateTemplate); connect(mActionCreateTemplate, &QAction::triggered, this, &MainWindow::slotNewTemplate); mActionCopy = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18nc("@action", "&Copy..."), this); actions->addAction(QStringLiteral("copy"), mActionCopy); actions->setDefaultShortcut(mActionCopy, QKeySequence(Qt::SHIFT + Qt::Key_Insert)); connect(mActionCopy, &QAction::triggered, this, &MainWindow::slotCopy); mActionModify = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18nc("@action", "&Edit..."), this); actions->addAction(QStringLiteral("modify"), mActionModify); actions->setDefaultShortcut(mActionModify, QKeySequence(Qt::CTRL + Qt::Key_E)); connect(mActionModify, &QAction::triggered, this, &MainWindow::slotModify); mActionDelete = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action", "&Delete"), this); actions->addAction(QStringLiteral("delete"), mActionDelete); actions->setDefaultShortcut(mActionDelete, QKeySequence::Delete); connect(mActionDelete, &QAction::triggered, this, &MainWindow::slotDeleteIf); // Set up Shift-Delete as a shortcut to delete without confirmation mActionDeleteForce = new QAction(i18nc("@action", "Delete Without Confirmation"), this); actions->addAction(QStringLiteral("delete-force"), mActionDeleteForce); actions->setDefaultShortcut(mActionDeleteForce, QKeySequence::Delete + Qt::SHIFT); connect(mActionDeleteForce, &QAction::triggered, this, &MainWindow::slotDeleteForce); mActionReactivate = new QAction(i18nc("@action", "Reac&tivate"), this); actions->addAction(QStringLiteral("undelete"), mActionReactivate); actions->setDefaultShortcut(mActionReactivate, QKeySequence(Qt::CTRL + Qt::Key_R)); connect(mActionReactivate, &QAction::triggered, this, &MainWindow::slotReactivate); mActionEnable = new QAction(this); actions->addAction(QStringLiteral("disable"), mActionEnable); actions->setDefaultShortcut(mActionEnable, QKeySequence(Qt::CTRL + Qt::Key_B)); connect(mActionEnable, &QAction::triggered, this, &MainWindow::slotEnable); action = new QAction(i18nc("@action", "Wake From Suspend..."), this); actions->addAction(QStringLiteral("wakeSuspend"), action); connect(action, &QAction::triggered, this, &MainWindow::slotWakeFromSuspend); action = KAlarm::createStopPlayAction(this); actions->addAction(QStringLiteral("stopAudio"), action); KGlobalAccel::setGlobalShortcut(action, QList()); // allow user to set a global shortcut mActionShowArchived = new KToggleAction(i18nc("@action", "Show Archi&ved Alarms"), this); actions->addAction(QStringLiteral("showArchivedAlarms"), mActionShowArchived); actions->setDefaultShortcut(mActionShowArchived, QKeySequence(Qt::CTRL + Qt::Key_P)); connect(mActionShowArchived, &KToggleAction::triggered, this, &MainWindow::slotShowArchived); mActionToggleTrayIcon = new KToggleAction(i18nc("@action", "Show in System &Tray"), this); actions->addAction(QStringLiteral("showInSystemTray"), mActionToggleTrayIcon); connect(mActionToggleTrayIcon, &KToggleAction::triggered, this, &MainWindow::slotToggleTrayIcon); mActionToggleResourceSel = new KToggleAction(QIcon::fromTheme(QStringLiteral("view-choose")), i18nc("@action", "Show &Calendars"), this); actions->addAction(QStringLiteral("showResources"), mActionToggleResourceSel); connect(mActionToggleResourceSel, &KToggleAction::triggered, this, &MainWindow::slotToggleResourceSelector); mActionSpreadWindows = KAlarm::createSpreadWindowsAction(this); actions->addAction(QStringLiteral("spread"), mActionSpreadWindows); KGlobalAccel::setGlobalShortcut(mActionSpreadWindows, QList()); // allow user to set a global shortcut mActionImportAlarms = new QAction(i18nc("@action", "Import &Alarms..."), this); actions->addAction(QStringLiteral("importAlarms"), mActionImportAlarms); connect(mActionImportAlarms, &QAction::triggered, this, &MainWindow::slotImportAlarms); mActionImportBirthdays = new QAction(i18nc("@action", "Import &Birthdays..."), this); actions->addAction(QStringLiteral("importBirthdays"), mActionImportBirthdays); connect(mActionImportBirthdays, &QAction::triggered, this, &MainWindow::slotBirthdays); mActionExportAlarms = new QAction(i18nc("@action", "E&xport Selected Alarms..."), this); actions->addAction(QStringLiteral("exportAlarms"), mActionExportAlarms); connect(mActionExportAlarms, &QAction::triggered, this, &MainWindow::slotExportAlarms); mActionExport = new QAction(i18nc("@action", "E&xport..."), this); actions->addAction(QStringLiteral("export"), mActionExport); connect(mActionExport, &QAction::triggered, this, &MainWindow::slotExportAlarms); action = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("@action", "&Refresh Alarms"), this); actions->addAction(QStringLiteral("refreshAlarms"), action); connect(action, &QAction::triggered, this, &MainWindow::slotRefreshAlarms); KToggleAction* toggleAction = KAlarm::createAlarmEnableAction(this); actions->addAction(QStringLiteral("alarmsEnable"), toggleAction); if (undoText.isNull()) { // Get standard texts, etc., for Undo and Redo actions QAction* act = KStandardAction::undo(this, nullptr, actions); undoShortcut = act->shortcuts(); undoText = act->text(); undoTextStripped = KLocalizedString::removeAcceleratorMarker(undoText); delete act; act = KStandardAction::redo(this, nullptr, actions); redoShortcut = act->shortcuts(); redoText = act->text(); redoTextStripped = KLocalizedString::removeAcceleratorMarker(redoText); delete act; } mActionUndo = new KToolBarPopupAction(QIcon::fromTheme(QStringLiteral("edit-undo")), undoText, this); actions->addAction(QStringLiteral("edit_undo"), mActionUndo); actions->setDefaultShortcuts(mActionUndo, undoShortcut); connect(mActionUndo, &KToolBarPopupAction::triggered, this, &MainWindow::slotUndo); mActionRedo = new KToolBarPopupAction(QIcon::fromTheme(QStringLiteral("edit-redo")), redoText, this); actions->addAction(QStringLiteral("edit_redo"), mActionRedo); actions->setDefaultShortcuts(mActionRedo, redoShortcut); connect(mActionRedo, &KToolBarPopupAction::triggered, this, &MainWindow::slotRedo); KStandardAction::find(mListView, SLOT(slotFind()), actions); mActionFindNext = KStandardAction::findNext(mListView, SLOT(slotFindNext()), actions); mActionFindPrev = KStandardAction::findPrev(mListView, SLOT(slotFindPrev()), actions); KStandardAction::selectAll(mListView, SLOT(selectAll()), actions); KStandardAction::deselect(mListView, SLOT(clearSelection()), actions); // Quit only once the event loop is called; otherwise, the parent window will // be deleted while still processing the action, resulting in a crash. QAction* act = KStandardAction::quit(nullptr, nullptr, actions); connect(act, &QAction::triggered, this, &MainWindow::slotQuit, Qt::QueuedConnection); QAction* actionMenubar = KStandardAction::showMenubar(this, SLOT(slotShowMenubar()), actions); KStandardAction::keyBindings(this, SLOT(slotConfigureKeys()), actions); KStandardAction::configureToolbars(this, SLOT(slotConfigureToolbar()), actions); KStandardAction::preferences(this, SLOT(slotPreferences()), actions); mResourceSelector->initActions(actions); setStandardToolBarMenuEnabled(true); createGUI(UI_FILE); // Load menu and toolbar settings applyMainWindowSettings(KSharedConfig::openConfig()->group(WINDOW_NAME)); mContextMenu = static_cast(factory()->container(QStringLiteral("listContext"), this)); mActionsMenu = static_cast(factory()->container(QStringLiteral("actions"), this)); QMenu* resourceMenu = static_cast(factory()->container(QStringLiteral("resourceContext"), this)); mResourceSelector->setContextMenu(resourceMenu); mMenuError = (!mContextMenu || !mActionsMenu || !resourceMenu); connect(mActionUndo->menu(), &QMenu::aboutToShow, this, &MainWindow::slotInitUndoMenu); connect(mActionUndo->menu(), &QMenu::triggered, this, &MainWindow::slotUndoItem); connect(mActionRedo->menu(), &QMenu::aboutToShow, this, &MainWindow::slotInitRedoMenu); connect(mActionRedo->menu(), &QMenu::triggered, this, &MainWindow::slotRedoItem); connect(Undo::instance(), &Undo::changed, this, &MainWindow::slotUndoStatus); connect(mListView, &AlarmListView::findActive, this, &MainWindow::slotFindActive); Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(updateKeepArchived(int))); Preferences::connect(SIGNAL(showInSystemTrayChanged(bool)), this, SLOT(updateTrayIconAction())); connect(theApp(), &KAlarmApp::trayIconToggled, this, &MainWindow::updateTrayIconAction); // Set menu item states setEnableText(true); mActionShowArchived->setChecked(mShowArchived); if (!Preferences::archivedKeepDays()) mActionShowArchived->setEnabled(false); mActionToggleResourceSel->setChecked(mShowResources); slotToggleResourceSelector(); updateTrayIconAction(); // set the correct text for this action mActionUndo->setEnabled(Undo::haveUndo()); mActionRedo->setEnabled(Undo::haveRedo()); mActionFindNext->setEnabled(false); mActionFindPrev->setEnabled(false); mActionCopy->setEnabled(false); mActionModify->setEnabled(false); mActionDelete->setEnabled(false); mActionReactivate->setEnabled(false); mActionEnable->setEnabled(false); mActionCreateTemplate->setEnabled(false); mActionExport->setEnabled(false); const bool menuVisible = !menuBar()->isHidden(); actionMenubar->setChecked(menuVisible); Undo::emitChanged(); // set the Undo/Redo menu texts // Daemon::monitoringAlarms(); } /****************************************************************************** * Enable or disable the Templates menu item in every main window instance. */ void MainWindow::enableTemplateMenuItem(bool enable) { for (int i = 0, end = mWindowList.count(); i < end; ++i) mWindowList[i]->mActionTemplates->setEnabled(enable); } /****************************************************************************** * Refresh the alarm list in every main window instance. */ void MainWindow::refresh() { qCDebug(KALARM_LOG) << "MainWindow::refresh"; DataModel::reload(); } /****************************************************************************** * Called when the keep archived alarm setting changes in the user preferences. * Enable/disable Show archived alarms option. */ void MainWindow::updateKeepArchived(int days) { qCDebug(KALARM_LOG) << "MainWindow::updateKeepArchived:" << (bool)days; if (mShowArchived && !days) slotShowArchived(); // toggle Show Archived option setting mActionShowArchived->setEnabled(days); } /****************************************************************************** * Select an alarm in the displayed list. */ void MainWindow::selectEvent(const QString& eventId) { mListView->clearSelection(); const QModelIndex index = mListFilterModel->eventIndex(eventId); if (index.isValid()) { mListView->select(index); mListView->scrollTo(index); } } /****************************************************************************** * Return the single selected alarm in the displayed list. */ KAEvent MainWindow::selectedEvent() const { return mListView->selectedEvent(); } /****************************************************************************** * Deselect all alarms in the displayed list. */ void MainWindow::clearSelection() { mListView->clearSelection(); } /****************************************************************************** * Called when the New button is clicked to edit a new alarm to add to the list. */ void MainWindow::slotNew(EditAlarmDlg::Type type) { KAlarm::editNewAlarm(type, mListView); } /****************************************************************************** * Called when a template is selected from the New From Template popup menu. * Executes a New Alarm dialog, preset from the selected template. */ void MainWindow::slotNewFromTemplate(const KAEvent* tmplate) { KAlarm::editNewAlarm(tmplate, mListView); } /****************************************************************************** * Called when the New Template button is clicked to create a new template * based on the currently selected alarm. */ void MainWindow::slotNewTemplate() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editNewTemplate(&event, this); } /****************************************************************************** * Called when the Copy button is clicked to edit a copy of an existing alarm, * to add to the list. */ void MainWindow::slotCopy() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editNewAlarm(&event, this); } /****************************************************************************** * Called when the Modify button is clicked to edit the currently highlighted * alarm in the list. */ void MainWindow::slotModify() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editAlarm(&event, this); // edit alarm (view-only mode if archived or read-only) } /****************************************************************************** * Called when the Delete button is clicked to delete the currently highlighted * alarms in the list. */ void MainWindow::slotDelete(bool force) { QVector events = mListView->selectedEvents(); if (!force && Preferences::confirmAlarmDeletion()) { int n = events.count(); if (KAMessageBox::warningContinueCancel(this, i18ncp("@info", "Do you really want to delete the selected alarm?", "Do you really want to delete the %1 selected alarms?", n), i18ncp("@title:window", "Delete Alarm", "Delete Alarms", n), KGuiItem(i18nc("@action:button", "&Delete"), QStringLiteral("edit-delete")), KStandardGuiItem::cancel(), Preferences::CONFIRM_ALARM_DELETION) != KMessageBox::Continue) return; } // Remove any events which have just triggered, from the list to delete. Undo::EventList undos; for (int i = 0; i < events.count(); ) { Resource res = Resources::resourceForEvent(events[i].id()); if (!res.isValid()) events.remove(i); else undos.append(events[i++], res); } if (events.isEmpty()) qCDebug(KALARM_LOG) << "MainWindow::slotDelete: No alarms left to delete"; else { // Delete the events from the calendar and displays KAlarm::deleteEvents(events, true, this); Undo::saveDeletes(undos); } } /****************************************************************************** * Called when the Reactivate button is clicked to reinstate the currently * highlighted archived alarms in the list. */ void MainWindow::slotReactivate() { QVector events = mListView->selectedEvents(); mListView->clearSelection(); // Add the alarms to the displayed lists and to the calendar file Undo::EventList undos; QVector ineligibleIDs; KAlarm::reactivateEvents(events, ineligibleIDs, nullptr, this); // Create the undo list, excluding ineligible events for (int i = 0, end = events.count(); i < end; ++i) { if (!ineligibleIDs.contains(EventId(events[i]))) undos.append(events[i], Resources::resourceForEvent(events[i].id())); } Undo::saveReactivates(undos); } /****************************************************************************** * Called when the Enable/Disable button is clicked to enable or disable the * currently highlighted alarms in the list. */ void MainWindow::slotEnable() { bool enable = mActionEnableEnable; // save since changed in response to KAlarm::enableEvent() QVector events = mListView->selectedEvents(); QVector eventCopies; for (int i = 0, end = events.count(); i < end; ++i) eventCopies += events[i]; KAlarm::enableEvents(eventCopies, enable, this); slotSelection(); // update Enable/Disable action text } /****************************************************************************** * Called when the columns visible in the alarm list view have changed. */ void MainWindow::slotAlarmListColumnsChanged() { KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_COLUMNS, mListView->columnsVisible()); config.sync(); } /****************************************************************************** * Called when the Show Archived Alarms menu item is selected or deselected. */ void MainWindow::slotShowArchived() { mShowArchived = !mShowArchived; mActionShowArchived->setChecked(mShowArchived); mActionShowArchived->setToolTip(mShowArchived ? i18nc("@info:tooltip", "Hide Archived Alarms") : i18nc("@info:tooltip", "Show Archived Alarms")); mListFilterModel->setEventTypeFilter(mShowArchived ? CalEvent::ACTIVE | CalEvent::ARCHIVED : CalEvent::ACTIVE); mListView->reset(); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_ARCHIVED_KEY, mShowArchived); config.sync(); } /****************************************************************************** * Called when the Spread Windows global shortcut is selected, to spread alarm * windows so that they are all visible. */ void MainWindow::slotSpreadWindowsShortcut() { mActionSpreadWindows->trigger(); } /****************************************************************************** * Called when the Wake From Suspend menu option is selected. */ void MainWindow::slotWakeFromSuspend() { (WakeFromSuspendDlg::create(this))->show(); } /****************************************************************************** * Called when the Import Alarms menu item is selected, to merge alarms from an * external calendar into the current calendars. */ void MainWindow::slotImportAlarms() { Resource resource; Resources::importAlarms(resource, this); } /****************************************************************************** * Called when the Export Alarms menu item is selected, to export the selected * alarms to an external calendar. */ void MainWindow::slotExportAlarms() { QVector events = mListView->selectedEvents(); if (!events.isEmpty()) { const KAEvent::List evts = KAEvent::ptrList(events); Resources::exportAlarms(evts, this); } } /****************************************************************************** * Called when the Import Birthdays menu item is selected, to display birthdays * from the address book for selection as alarms. */ void MainWindow::slotBirthdays() { // Use AutoQPointer to guard against crash on application exit while // the dialogue is still open. It prevents double deletion (both on // deletion of MainWindow, and on return from this function). AutoQPointer dlg = new BirthdayDlg(this); if (dlg->exec() == QDialog::Accepted) { QVector events = dlg->events(); if (!events.isEmpty()) { mListView->clearSelection(); // Add alarm to the displayed lists and to the calendar file KAlarm::UpdateResult status = KAlarm::addEvents(events, dlg, true, true); Undo::EventList undos; for (int i = 0, end = events.count(); i < end; ++i) undos.append(events[i], Resources::resourceForEvent(events[i].id())); Undo::saveAdds(undos, i18nc("@info", "Import birthdays")); if (status != KAlarm::UPDATE_FAILED) KAlarm::outputAlarmWarnings(dlg); } } } /****************************************************************************** * Called when the Templates menu item is selected, to display the alarm * template editing dialog. */ void MainWindow::slotTemplates() { if (!mTemplateDlg) { mTemplateDlg = TemplateDlg::create(this); enableTemplateMenuItem(false); // disable menu item in all windows connect(mTemplateDlg, &QDialog::finished, this, &MainWindow::slotTemplatesEnd); mTemplateDlg->show(); } } /****************************************************************************** * Called when the alarm template editing dialog has exited. */ void MainWindow::slotTemplatesEnd() { if (mTemplateDlg) { mTemplateDlg->deleteLater(); // this deletes the dialog once it is safe to do so mTemplateDlg = nullptr; enableTemplateMenuItem(true); // re-enable menu item in all windows } } /****************************************************************************** * Called when the Display System Tray Icon menu item is selected. */ void MainWindow::slotToggleTrayIcon() { theApp()->displayTrayIcon(!theApp()->trayIconDisplayed(), this); } /****************************************************************************** * Called when the Show Resource Selector menu item is selected. */ void MainWindow::slotToggleResourceSelector() { mShowResources = mActionToggleResourceSel->isChecked(); if (mShowResources) { if (mResourcesWidth <= 0) { mResourcesWidth = mResourceSelector->sizeHint().width(); mResourceSelector->resize(mResourcesWidth, mResourceSelector->height()); QList widths = mSplitter->sizes(); if (widths.count() == 1) { int listwidth = widths[0] - mSplitter->handleWidth() - mResourcesWidth; mListView->resize(listwidth, mListView->height()); widths.append(listwidth); widths[0] = mResourcesWidth; } mSplitter->setSizes(widths); } mResourceSelector->show(); } else mResourceSelector->hide(); KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); config.writeEntry(SHOW_RESOURCES_KEY, mShowResources); config.sync(); } /****************************************************************************** * Called when an error occurs in the resource calendar, to display a message. */ void MainWindow::showErrorMessage(const QString& msg) { KAMessageBox::error(this, msg); } /****************************************************************************** * Called when the system tray icon is created or destroyed. * Set the system tray icon menu text according to whether or not the system * tray icon is currently visible. */ void MainWindow::updateTrayIconAction() { mActionToggleTrayIcon->setEnabled(QSystemTrayIcon::isSystemTrayAvailable()); mActionToggleTrayIcon->setChecked(theApp()->trayIconDisplayed()); } /****************************************************************************** * Called when the active status of Find changes. */ void MainWindow::slotFindActive(bool active) { mActionFindNext->setEnabled(active); mActionFindPrev->setEnabled(active); } /****************************************************************************** * Called when the Undo action is selected. */ void MainWindow::slotUndo() { Undo::undo(this, KLocalizedString::removeAcceleratorMarker(mActionUndo->text())); } /****************************************************************************** * Called when the Redo action is selected. */ void MainWindow::slotRedo() { Undo::redo(this, KLocalizedString::removeAcceleratorMarker(mActionRedo->text())); } /****************************************************************************** * Called when an Undo item is selected. */ void MainWindow::slotUndoItem(QAction* action) { int id = mUndoMenuIds[action]; Undo::undo(id, this, Undo::actionText(Undo::UNDO, id)); } /****************************************************************************** * Called when a Redo item is selected. */ void MainWindow::slotRedoItem(QAction* action) { int id = mUndoMenuIds[action]; Undo::redo(id, this, Undo::actionText(Undo::REDO, id)); } /****************************************************************************** * Called when the Undo menu is about to show. * Populates the menu. */ void MainWindow::slotInitUndoMenu() { initUndoMenu(mActionUndo->menu(), Undo::UNDO); } /****************************************************************************** * Called when the Redo menu is about to show. * Populates the menu. */ void MainWindow::slotInitRedoMenu() { initUndoMenu(mActionRedo->menu(), Undo::REDO); } /****************************************************************************** * Populate the undo or redo menu. */ void MainWindow::initUndoMenu(QMenu* menu, Undo::Type type) { menu->clear(); mUndoMenuIds.clear(); const QString& action = (type == Undo::UNDO) ? undoTextStripped : redoTextStripped; QList ids = Undo::ids(type); for (int i = 0, end = ids.count(); i < end; ++i) { int id = ids[i]; QString actText = Undo::actionText(type, id); QString descrip = Undo::description(type, id); QString text = descrip.isEmpty() ? i18nc("@action Undo/Redo [action]", "%1 %2", action, actText) : i18nc("@action Undo [action]: message", "%1 %2: %3", action, actText, descrip); QAction* act = menu->addAction(text); mUndoMenuIds[act] = id; } } /****************************************************************************** * Called when the status of the Undo or Redo list changes. * Change the Undo or Redo text to include the action which would be undone/redone. */ void MainWindow::slotUndoStatus(const QString& undo, const QString& redo) { if (undo.isNull()) { mActionUndo->setEnabled(false); mActionUndo->setText(undoText); } else { mActionUndo->setEnabled(true); mActionUndo->setText(QStringLiteral("%1 %2").arg(undoText, undo)); } if (redo.isNull()) { mActionRedo->setEnabled(false); mActionRedo->setText(redoText); } else { mActionRedo->setEnabled(true); mActionRedo->setText(QStringLiteral("%1 %2").arg(redoText, redo)); } } /****************************************************************************** * Called when the Refresh Alarms menu item is selected. */ void MainWindow::slotRefreshAlarms() { KAlarm::refreshAlarms(); } /****************************************************************************** * Called when the "Configure KAlarm" menu item is selected. */ void MainWindow::slotPreferences() { KAlarmPrefDlg::display(); } /****************************************************************************** * Called when the Show Menubar menu item is selected. */ void MainWindow::slotShowMenubar() { const bool visible = menuBar()->isVisible(); menuBar()->setVisible(!visible); } /****************************************************************************** * Called when the Configure Keys menu item is selected. */ void MainWindow::slotConfigureKeys() { KShortcutsDialog::configure(actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, this); } /****************************************************************************** * Called when the Configure Toolbars menu item is selected. */ void MainWindow::slotConfigureToolbar() { KConfigGroup grp(KSharedConfig::openConfig()->group(WINDOW_NAME)); saveMainWindowSettings(grp); KEditToolBar dlg(factory()); connect(&dlg, &KEditToolBar::newToolBarConfig, this, &MainWindow::slotNewToolbarConfig); dlg.exec(); } /****************************************************************************** * Called when OK or Apply is clicked in the Configure Toolbars dialog, to save * the new configuration. */ void MainWindow::slotNewToolbarConfig() { createGUI(UI_FILE); applyMainWindowSettings(KSharedConfig::openConfig()->group(WINDOW_NAME)); } /****************************************************************************** * Called when the Quit menu item is selected. * Note that this must be called by the event loop, not directly from the menu * item, since otherwise the window will be deleted while still processing the * menu, resulting in a crash. */ void MainWindow::slotQuit() { theApp()->doQuit(this); } /****************************************************************************** * Called when the user or the session manager attempts to close the window. */ void MainWindow::closeEvent(QCloseEvent* ce) { if (!qApp->isSavingSession()) { // The user (not the session manager) wants to close the window. if (isTrayParent()) { // It's the parent window of the system tray icon, so just hide // it to prevent the system tray icon closing. hide(); theApp()->quitIf(); ce->ignore(); return; } } ce->accept(); } /****************************************************************************** * Called when the drag cursor enters a main or system tray window, to accept * or reject the dragged object. */ void MainWindow::executeDragEnterEvent(QDragEnterEvent* e) { const QMimeData* data = e->mimeData(); bool accept = ICalDrag::canDecode(data) ? !e->source() // don't accept "text/calendar" objects from this application : data->hasText() || data->hasUrls() || KPIM::MailList::canDecode(data); if (accept) e->acceptProposedAction(); } /****************************************************************************** * Called when an object is dropped on the window. * If the object is recognised, the edit alarm dialog is opened appropriately. */ void MainWindow::dropEvent(QDropEvent* e) { executeDropEvent(this, e); } static QString getMailHeader(const char* header, KMime::Content& content) { KMime::Headers::Base* hd = content.headerByType(header); return hd ? hd->asUnicodeString() : QString(); } /****************************************************************************** * Called when an object is dropped on a main or system tray window, to * evaluate the action required and extract the text. */ void MainWindow::executeDropEvent(MainWindow* win, QDropEvent* e) { qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: Formats:" << e->mimeData()->formats(); const QMimeData* data = e->mimeData(); KAEvent::SubAction action = KAEvent::MESSAGE; QByteArray bytes; AlarmText alarmText; KPIM::MailList mailList; QList urls; MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); #ifndef NDEBUG QString fmts = data->formats().join(QLatin1String(", ")); qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent:" << fmts; #endif /* The order of the tests below matters, since some dropped objects * provide more than one mime type. * Don't change them without careful thought !! */ if (!(bytes = data->data(QStringLiteral("message/rfc822"))).isEmpty()) { // Email message(s). Ignore all but the first. qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: email"; KMime::Content content; content.setContent(bytes); content.parse(); QString body; if (content.textContent()) body = content.textContent()->decodedText(true, true); // strip trailing newlines & spaces unsigned long sernum = 0; if (KPIM::MailList::canDecode(data)) { // Get its KMail serial number to allow the KMail message // to be called up from the alarm message window. mailList = KPIM::MailList::fromMimeData(data); if (!mailList.isEmpty()) sernum = mailList.at(0).serialNumber(); } alarmText.setEmail(getMailHeader("To", content), getMailHeader("From", content), getMailHeader("Cc", content), getMailHeader("Date", content), getMailHeader("Subject", content), body, sernum); } else if (KPIM::MailList::canDecode(data)) { mailList = KPIM::MailList::fromMimeData(data); // KMail message(s). Ignore all but the first. qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: KMail_list"; if (mailList.isEmpty()) return; const KPIM::MailSummary& summary = mailList.at(0); QDateTime dt; dt.setSecsSinceEpoch(summary.date()); const QString body = KAMail::getMailBody(summary.serialNumber()); alarmText.setEmail(summary.to(), summary.from(), QString(), QLocale().toString(dt), summary.subject(), body, summary.serialNumber()); } else if (ICalDrag::fromMimeData(data, calendar)) { // iCalendar - If events are included, use the first event qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: iCalendar"; const Event::List events = calendar->rawEvents(); if (!events.isEmpty()) { Event::Ptr event = events[0]; if (event->alarms().isEmpty()) { Alarm::Ptr alarm = event->newAlarm(); alarm->setEnabled(true); alarm->setTime(event->dtStart()); alarm->setDisplayAlarm(event->summary().isEmpty() ? event->description() : event->summary()); event->addAlarm(alarm); } KAEvent ev(event); KAlarm::editNewAlarm(&ev, win); return; } // If todos are included, use the first todo const Todo::List todos = calendar->rawTodos(); if (!todos.isEmpty()) { Todo::Ptr todo = todos[0]; alarmText.setTodo(todo); KADateTime start(todo->dtStart(true)); KADateTime due(todo->dtDue(true)); bool haveBothTimes = false; if (todo->hasDueDate()) { if (start.isValid()) haveBothTimes = true; else start = due; } if (todo->allDay()) start.setDateOnly(true); KAEvent::Flags flags = KAEvent::DEFAULT_FONT; if (start.isDateOnly()) flags |= KAEvent::ANY_TIME; KAEvent ev(start, alarmText.displayText(), Preferences::defaultBgColour(), Preferences::defaultFgColour(), QFont(), KAEvent::MESSAGE, 0, flags, true); ev.startChanges(); if (todo->recurs()) { ev.setRecurrence(*todo->recurrence()); ev.setNextOccurrence(KADateTime::currentUtcDateTime()); } const Alarm::List alarms = todo->alarms(); if (!alarms.isEmpty() && alarms[0]->type() == Alarm::Display) { // A display alarm represents a reminder int offset = 0; if (alarms[0]->hasStartOffset()) offset = alarms[0]->startOffset().asSeconds(); else if (alarms[0]->hasEndOffset()) { offset = alarms[0]->endOffset().asSeconds(); if (haveBothTimes) { // Get offset relative to start time instead of due time offset += start.secsTo(due); } } if (offset / 60) ev.setReminder(-offset / 60, false); } ev.endChanges(); KAlarm::editNewAlarm(&ev, win); } return; } else if (!(urls = data->urls()).isEmpty()) { const QUrl& url(urls.at(0)); const Akonadi::Item item = Akonadi::Item::fromUrl(url); if (item.isValid()) { // It's an Akonadi item qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: Akonadi item" << item.id(); if (QUrlQuery(url).queryItemValue(QStringLiteral("type")) == QLatin1String("message/rfc822")) { // It's an email held in Akonadi qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: Akonadi email"; Akonadi::ItemFetchJob* job = new Akonadi::ItemFetchJob(item); job->fetchScope().fetchFullPayload(); Akonadi::Item::List items; if (job->exec()) items = job->items(); if (items.isEmpty()) { qCWarning(KALARM_LOG) << "MainWindow::executeDropEvent: Akonadi item" << item.id() << "not found"; return; } const Akonadi::Item& it = items.at(0); if (!it.isValid() || !it.hasPayload()) { qCWarning(KALARM_LOG) << "MainWindow::executeDropEvent: invalid email"; return; } KMime::Message::Ptr message = it.payload(); QString body; if (message->textContent()) body = message->textContent()->decodedText(true, true); // strip trailing newlines & spaces alarmText.setEmail(getMailHeader("To", *message), getMailHeader("From", *message), getMailHeader("Cc", *message), getMailHeader("Date", *message), getMailHeader("Subject", *message), body, it.id()); } } else { qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: URL"; // Try to find the mime type of the file, without downloading a remote file QMimeDatabase mimeDb; const QString mimeTypeName = mimeDb.mimeTypeForUrl(url).name(); action = mimeTypeName.startsWith(QLatin1String("audio/")) ? KAEvent::AUDIO : KAEvent::FILE; alarmText.setText(url.toDisplayString()); } } if (alarmText.isEmpty()) { if (data->hasText()) { const QString text = data->text(); qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: text"; alarmText.setText(text); } else return; } if (!alarmText.isEmpty()) { if (action == KAEvent::MESSAGE && (alarmText.isEmail() || alarmText.isScript())) { // If the alarm text could be interpreted as an email or command script, // prompt for which type of alarm to create. QStringList types; types += i18nc("@item:inlistbox", "Display Alarm"); if (alarmText.isEmail()) types += i18nc("@item:inlistbox", "Email Alarm"); else if (alarmText.isScript()) types += i18nc("@item:inlistbox", "Command Alarm"); bool ok = false; QString type = QInputDialog::getItem(mainMainWindow(), i18nc("@title:window", "Alarm Type"), i18nc("@info", "Choose alarm type to create:"), types, 0, false, &ok); if (!ok) return; // user didn't press OK int i = types.indexOf(type); if (i == 1) action = alarmText.isEmail() ? KAEvent::EMAIL : KAEvent::COMMAND; } KAlarm::editNewAlarm(action, win, &alarmText); } } /****************************************************************************** * Called when the status of a calendar has changed. * Enable or disable actions appropriately. */ void MainWindow::slotCalendarStatusChanged() { // Find whether there are any writable calendars bool active = !Resources::enabledResources(CalEvent::ACTIVE, true).isEmpty(); bool templat = !Resources::enabledResources(CalEvent::TEMPLATE, true).isEmpty(); for (int i = 0, end = mWindowList.count(); i < end; ++i) { MainWindow* w = mWindowList[i]; w->mActionImportAlarms->setEnabled(active || templat); w->mActionImportBirthdays->setEnabled(active); w->mActionCreateTemplate->setEnabled(templat); // Note: w->mActionNew enabled status is set in the NewAlarmAction class. w->slotSelection(); } } /****************************************************************************** * Called when the selected items in the ListView change. * Enables the actions appropriately. */ void MainWindow::slotSelection() { // Find which events have been selected QVector events = mListView->selectedEvents(); int count = events.count(); if (!count) { selectionCleared(); // disable actions Q_EMIT selectionChanged(); return; } // Find whether there are any writable resources bool active = mActionNew->isEnabled(); bool readOnly = false; bool allArchived = true; bool enableReactivate = true; bool enableEnableDisable = true; bool enableEnable = false; bool enableDisable = false; - AlarmCalendar* resources = AlarmCalendar::resources(); + ResourcesCalendar* resources = ResourcesCalendar::instance(); const KADateTime now = KADateTime::currentUtcDateTime(); for (int i = 0; i < count; ++i) { KAEvent* ev = resources->event(EventId(events.at(i))); // get up-to-date status KAEvent* event = ev ? ev : &events[i]; bool expired = event->expired(); if (!expired) allArchived = false; if (resources->eventReadOnly(event->id())) readOnly = true; if (enableReactivate && (!expired || !event->occursAfter(now, true))) enableReactivate = false; if (enableEnableDisable) { if (expired) enableEnableDisable = enableEnable = enableDisable = false; else { if (!enableEnable && !event->enabled()) enableEnable = true; if (!enableDisable && event->enabled()) enableDisable = true; } } } qCDebug(KALARM_LOG) << "MainWindow::slotSelection: true"; mActionCreateTemplate->setEnabled((count == 1) && !Resources::enabledResources(CalEvent::TEMPLATE, true).isEmpty()); mActionExportAlarms->setEnabled(true); mActionExport->setEnabled(true); mActionCopy->setEnabled(active && count == 1); mActionModify->setEnabled(count == 1); mActionDelete->setEnabled(!readOnly && (active || allArchived)); mActionReactivate->setEnabled(active && enableReactivate); mActionEnable->setEnabled(active && !readOnly && (enableEnable || enableDisable)); if (enableEnable || enableDisable) setEnableText(enableEnable); Q_EMIT selectionChanged(); } /****************************************************************************** * Called when a context menu is requested in the ListView. * Displays a context menu to modify or delete the selected item. */ void MainWindow::slotContextMenuRequested(const QPoint& globalPos) { qCDebug(KALARM_LOG) << "MainWindow::slotContextMenuRequested"; if (mContextMenu) mContextMenu->popup(globalPos); } /****************************************************************************** * Disables actions when no item is selected. */ void MainWindow::selectionCleared() { mActionCreateTemplate->setEnabled(false); mActionExportAlarms->setEnabled(false); mActionExport->setEnabled(false); mActionCopy->setEnabled(false); mActionModify->setEnabled(false); mActionDelete->setEnabled(false); mActionReactivate->setEnabled(false); mActionEnable->setEnabled(false); } /****************************************************************************** * Set the text of the Enable/Disable menu action. */ void MainWindow::setEnableText(bool enable) { mActionEnableEnable = enable; mActionEnable->setText(enable ? i18nc("@action", "Ena&ble") : i18nc("@action", "Disa&ble")); } /****************************************************************************** * Display or hide the specified main window. * This should only be called when the application doesn't run in the system tray. */ MainWindow* MainWindow::toggleWindow(MainWindow* win) { if (win && mWindowList.indexOf(win) != -1) { // A window is specified (and it exists) if (win->isVisible()) { // The window is visible, so close it win->close(); return nullptr; } else { // The window is hidden, so display it win->hide(); // in case it's on a different desktop win->setWindowState(win->windowState() & ~Qt::WindowMinimized); win->raise(); win->activateWindow(); return win; } } // No window is specified, or the window doesn't exist. Open a new one. win = create(); win->show(); return win; } /****************************************************************************** * Called when the Edit button is clicked in an alarm message window. * This controls the alarm edit dialog created by the alarm window, and allows * it to remain unaffected by the alarm window closing. * See MessageWin::slotEdit() for more information. */ void MainWindow::editAlarm(EditAlarmDlg* dlg, const KAEvent& event) { mEditAlarmMap[dlg] = event; connect(dlg, &KEditToolBar::accepted, this, &MainWindow::editAlarmOk); connect(dlg, &KEditToolBar::destroyed, this, &MainWindow::editAlarmDeleted); dlg->setAttribute(Qt::WA_DeleteOnClose, true); // ensure no memory leaks dlg->show(); } /****************************************************************************** * Called when OK is clicked in the alarm edit dialog shown by editAlarm(). * Updates the event which has been edited. */ void MainWindow::editAlarmOk() { EditAlarmDlg* dlg = qobject_cast(sender()); if (!dlg) return; QMap::Iterator it = mEditAlarmMap.find(dlg); if (it == mEditAlarmMap.end()) return; KAEvent event = it.value(); mEditAlarmMap.erase(it); if (!event.isValid()) return; if (dlg->result() != QDialog::Accepted) return; Resource res = Resources::resourceForEvent(event.id()); KAlarm::updateEditedAlarm(dlg, event, res); } /****************************************************************************** * Called when the alarm edit dialog shown by editAlarm() is deleted. * Removes the dialog from the pending list. */ void MainWindow::editAlarmDeleted(QObject* obj) { mEditAlarmMap.remove(static_cast(obj)); } // vim: et sw=4: diff --git a/src/messagewin.cpp b/src/messagewin.cpp index 16ccfc63..0549a2fb 100644 --- a/src/messagewin.cpp +++ b/src/messagewin.cpp @@ -1,2430 +1,2431 @@ /* * messagewin.cpp - displays an alarm message * Program: kalarm * Copyright © 2001-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "messagewin.h" #include "messagewin_p.h" #include "config-kalarm.h" #include #include "alarmcalendar.h" #include "deferdlg.h" #include "editdlg.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "preferences.h" #include "resources/resources.h" #include "lib/autoqpointer.h" #include "lib/config.h" #include "lib/desktop.h" #include "lib/file.h" #include "lib/messagebox.h" #include "lib/pushbutton.h" #include "lib/shellprocess.h" #include "lib/synchtimer.h" #include "kalarm_debug.h" #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if KDEPIM_HAVE_X11 #include #include #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KCalendarCore; using namespace KAlarmCal; #if KDEPIM_HAVE_X11 enum FullScreenType { NoFullScreen = 0, FullScreen = 1, FullScreenActive = 2 }; static FullScreenType haveFullScreenWindow(int screen); static FullScreenType findFullScreenWindows(const QVector& screenRects, QVector& screenTypes); #endif #include "kmailinterface.h" static const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail"); static const QLatin1String KMAIL_DBUS_PATH("/KMail"); // The delay for enabling message window buttons if a zero delay is // configured, i.e. the windows are placed far from the cursor. static const int proximityButtonDelay = 1000; // (milliseconds) static const int proximityMultiple = 10; // multiple of button height distance from cursor for proximity // A text label widget which can be scrolled and copied with the mouse class MessageText : public KTextEdit { public: MessageText(QWidget* parent = nullptr) : KTextEdit(parent), mNewLine(false) { setReadOnly(true); setFrameStyle(NoFrame); setLineWrapMode(NoWrap); } int scrollBarHeight() const { return horizontalScrollBar()->height(); } int scrollBarWidth() const { return verticalScrollBar()->width(); } void setBackgroundColour(const QColor& c) { QPalette pal = viewport()->palette(); pal.setColor(viewport()->backgroundRole(), c); viewport()->setPalette(pal); } QSize sizeHint() const override { const QSizeF docsize = document()->size(); return QSize(static_cast(docsize.width() + 0.99) + verticalScrollBar()->width(), static_cast(docsize.height() + 0.99) + horizontalScrollBar()->height()); } bool newLine() const { return mNewLine; } void setNewLine(bool nl) { mNewLine = nl; } private: bool mNewLine; }; // Basic flags for the window static const Qt::WindowFlags WFLAGS = Qt::WindowStaysOnTopHint; static const Qt::WindowFlags WFLAGS2 = Qt::WindowContextHelpButtonHint; static const Qt::WidgetAttribute WidgetFlags = Qt::WA_DeleteOnClose; // Error message bit masks enum { ErrMsg_Speak = 0x01, ErrMsg_AudioFile = 0x02 }; QList MessageWin::mWindowList; QMap MessageWin::mErrorMessages; bool MessageWin::mRedisplayed = false; // There can only be one audio thread at a time: trying to play multiple // sound files simultaneously would result in a cacophony, and besides // that, Phonon currently crashes... QPointer MessageWin::mAudioThread; MessageWin* AudioThread::mAudioOwner = nullptr; /****************************************************************************** * Construct the message window for the specified alarm. * Other alarms in the supplied event may have been updated by the caller, so * the whole event needs to be stored for updating the calendar file when it is * displayed. */ MessageWin::MessageWin(const KAEvent* event, const KAAlarm& alarm, int flags) : MainWindowBase(nullptr, static_cast(WFLAGS | WFLAGS2 | ((flags & ALWAYS_HIDE) || getWorkAreaAndModal() ? Qt::WindowType(0) : Qt::X11BypassWindowManagerHint))) , mMessage(event->cleanText()) , mFont(event->font()) , mBgColour(event->bgColour()) , mFgColour(event->fgColour()) , mEventId(*event) , mAudioFile(event->audioFile()) , mVolume(event->soundVolume()) , mFadeVolume(event->fadeVolume()) , mFadeSeconds(qMin(event->fadeSeconds(), 86400)) , mDefaultDeferMinutes(event->deferDefaultMinutes()) , mAlarmType(alarm.type()) , mAction(event->actionSubType()) , mAkonadiItemId(event->akonadiItemId()) , mCommandError(event->commandError()) , mRestoreHeight(0) , mAudioRepeatPause(event->repeatSoundPause()) , mConfirmAck(event->confirmAck()) , mNoDefer(true) , mInvalid(false) , mEvent(*event) , mOriginalEvent(*event) , mResource(Resources::resourceForEvent(mEventId.eventId())) , mAlwaysHide(flags & ALWAYS_HIDE) , mNoPostAction(alarm.type() & KAAlarm::REMINDER_ALARM) , mBeep(event->beep()) , mSpeak(event->speak()) , mRescheduleEvent(!(flags & NO_RESCHEDULE)) { qCDebug(KALARM_LOG) << "MessageWin:" << (void*)this << "event" << mEventId; setAttribute(static_cast(WidgetFlags)); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("MessageWin")); // used by LikeBack if (alarm.type() & KAAlarm::REMINDER_ALARM) { if (event->reminderMinutes() < 0) { event->previousOccurrence(alarm.dateTime(false).effectiveKDateTime(), mDateTime, false); if (!mDateTime.isValid() && event->repeatAtLogin()) mDateTime = alarm.dateTime().addSecs(event->reminderMinutes() * 60); } else mDateTime = event->mainDateTime(true); } else mDateTime = alarm.dateTime(true); if (!(flags & (NO_INIT_VIEW | ALWAYS_HIDE))) { - const bool readonly = AlarmCalendar::resources()->eventReadOnly(mEventId.eventId()); + const bool readonly = ResourcesCalendar::instance()->eventReadOnly(mEventId.eventId()); mShowEdit = !mEventId.isEmpty() && !readonly; mNoDefer = readonly || (flags & NO_DEFER) || alarm.repeatAtLogin(); initView(); } // Set to save settings automatically, but don't save window size. // File alarm window size is saved elsewhere. setAutoSaveSettings(QStringLiteral("MessageWin"), false); mWindowList.append(this); if (event->autoClose()) mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().qDateTime().addSecs(event->lateCancel() * 60); if (mAlwaysHide) { hide(); displayComplete(); // play audio, etc. } } /****************************************************************************** * Display an error message window. * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note * that the option is specific to 'event'. */ void MessageWin::showError(const KAEvent& event, const DateTime& alarmDateTime, const QStringList& errmsgs, const QString& dontShowAgain) { if (!dontShowAgain.isEmpty() && KAlarm::dontShowErrors(EventId(event), dontShowAgain)) return; // Don't pile up duplicate error messages for the same alarm for (int i = 0, end = mWindowList.count(); i < end; ++i) { const MessageWin* w = mWindowList[i]; if (w->mErrorWindow && w->mEventId == EventId(event) && w->mErrorMsgs == errmsgs && w->mDontShowAgain == dontShowAgain) return; } (new MessageWin(&event, alarmDateTime, errmsgs, dontShowAgain))->show(); } /****************************************************************************** * Construct the message window for a specified error message. * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note * that the option is specific to 'event'. */ MessageWin::MessageWin(const KAEvent* event, const DateTime& alarmDateTime, const QStringList& errmsgs, const QString& dontShowAgain) : MainWindowBase(nullptr, WFLAGS | WFLAGS2) , mMessage(event->cleanText()) , mDateTime(alarmDateTime) , mEventId(*event) , mAlarmType(KAAlarm::MAIN_ALARM) , mAction(event->actionSubType()) , mAkonadiItemId(-1) , mCommandError(KAEvent::CMD_NO_ERROR) , mErrorMsgs(errmsgs) , mDontShowAgain(dontShowAgain) , mRestoreHeight(0) , mConfirmAck(false) , mShowEdit(false) , mNoDefer(true) , mInvalid(false) , mEvent(*event) , mOriginalEvent(*event) , mErrorWindow(true) , mNoPostAction(true) { qCDebug(KALARM_LOG) << "MessageWin: errmsg"; setAttribute(static_cast(WidgetFlags)); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("ErrorWin")); // used by LikeBack getWorkAreaAndModal(); initView(); mWindowList.append(this); } /****************************************************************************** * Construct the message window for restoration by session management. * The window is initialised by readProperties(). */ MessageWin::MessageWin() : MainWindowBase(nullptr, WFLAGS) { qCDebug(KALARM_LOG) << "MessageWin:" << (void*)this << "restore"; setAttribute(WidgetFlags); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("RestoredMsgWin")); // used by LikeBack getWorkAreaAndModal(); mWindowList.append(this); } /****************************************************************************** * Destructor. Perform any post-alarm actions before tidying up. */ MessageWin::~MessageWin() { qCDebug(KALARM_LOG) << "~MessageWin" << (void*)this << mEventId; if (AudioThread::mAudioOwner == this && !mAudioThread.isNull()) mAudioThread->quit(); mErrorMessages.remove(mEventId); mWindowList.removeAll(this); delete mTempFile; if (!mRecreating) { if (!mNoPostAction && !mEvent.postAction().isEmpty()) theApp()->alarmCompleted(mEvent); if (!instanceCount(true)) theApp()->quitIf(); // no visible windows remain - check whether to quit } } /****************************************************************************** * Construct the message window. */ void MessageWin::initView() { const bool reminder = (!mErrorWindow && (mAlarmType & KAAlarm::REMINDER_ALARM)); const int leading = fontMetrics().leading(); setCaption((mAlarmType & KAAlarm::REMINDER_ALARM) ? i18nc("@title:window", "Reminder") : i18nc("@title:window", "Message")); QWidget* topWidget = new QWidget(this); setCentralWidget(topWidget); QVBoxLayout* topLayout = new QVBoxLayout(topWidget); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->setContentsMargins(dcm, dcm, dcm, dcm); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QPalette labelPalette = palette(); labelPalette.setColor(backgroundRole(), labelPalette.color(QPalette::Window)); // Show the alarm date/time, together with a reminder text where appropriate. // Alarm date/time: display time zone if not local time zone. mTimeLabel = new QLabel(topWidget); mTimeLabel->setText(dateTimeToDisplay()); mTimeLabel->setFrameStyle(QFrame::StyledPanel); mTimeLabel->setPalette(labelPalette); mTimeLabel->setAutoFillBackground(true); topLayout->addWidget(mTimeLabel, 0, Qt::AlignHCenter); mTimeLabel->setWhatsThis(i18nc("@info:whatsthis", "The scheduled date/time for the message (as opposed to the actual time of display).")); if (mDateTime.isValid()) { // Reminder if (reminder) { // Create a label "time\nReminder" by inserting the time at the // start of the translated string, allowing for possible HTML tags // enclosing "Reminder". QString s = i18nc("@info", "Reminder"); QRegExp re(QStringLiteral("^(<[^>]+>)*")); re.indexIn(s); s.insert(re.matchedLength(), mTimeLabel->text() + QLatin1String("
")); mTimeLabel->setText(s); mTimeLabel->setAlignment(Qt::AlignHCenter); } } else mTimeLabel->hide(); if (!mErrorWindow) { // It's a normal alarm message window switch (mAction) { case KAEvent::FILE: { // Display the file name KSqueezedTextLabel* label = new KSqueezedTextLabel(mMessage, topWidget); label->setFrameStyle(QFrame::StyledPanel); label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); label->setPalette(labelPalette); label->setAutoFillBackground(true); label->setWhatsThis(i18nc("@info:whatsthis", "The file whose contents are displayed below")); topLayout->addWidget(label, 0, Qt::AlignHCenter); // Display contents of file const QUrl url = QUrl::fromUserInput(mMessage, QString(), QUrl::AssumeLocalFile); #if KIO_VERSION < QT_VERSION_CHECK(5, 69, 0) auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 0, KIO::HideProgressInfo); #else auto statJob = KIO::statDetails(url, KIO::StatJob::SourceSide, KIO::StatBasic, KIO::HideProgressInfo); #endif const bool exists = statJob->exec(); const bool isDir = statJob->statResult().isDir(); bool opened = false; if (exists && !isDir) { auto job = KIO::storedGet(url); KJobWidgets::setWindow(job, MainWindow::mainMainWindow()); if (job->exec()) { opened = true; const QByteArray data = job->data(); QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(url); if (mime.name() == QLatin1String("application/octet-stream")) mime = db.mimeTypeForData(mTempFile); const File::FileType fileType = File::fileType(mime); switch (fileType) { case File::Image: case File::TextFormatted: delete mTempFile; mTempFile = new QTemporaryFile; mTempFile->open(); mTempFile->write(data); break; default: break; } QTextBrowser* view = new QTextBrowser(topWidget); view->setFrameStyle(QFrame::NoFrame); view->setWordWrapMode(QTextOption::NoWrap); QPalette pal = view->viewport()->palette(); pal.setColor(view->viewport()->backgroundRole(), mBgColour); view->viewport()->setPalette(pal); view->setTextColor(mFgColour); view->setCurrentFont(mFont); switch (fileType) { case File::Image: view->setHtml(QLatin1String("
fileName() + QLatin1String("\">
")); mTempFile->close(); // keep the file available to be displayed break; case File::TextFormatted: view->QTextBrowser::setSource(QUrl::fromLocalFile(mTempFile->fileName())); //krazy:exclude=qclasses delete mTempFile; mTempFile = nullptr; break; default: view->setPlainText(QString::fromUtf8(data)); break; } view->setMinimumSize(view->sizeHint()); topLayout->addWidget(view); // Set the default size to 20 lines square. // Note that after the first file has been displayed, this size // is overridden by the user-set default stored in the config file. // So there is no need to calculate an accurate size. int h = 20*view->fontMetrics().lineSpacing() + 2*view->frameWidth(); view->resize(QSize(h, h).expandedTo(view->sizeHint())); view->setWhatsThis(i18nc("@info:whatsthis", "The contents of the file to be displayed")); } } if (!exists || isDir || !opened) { mErrorMsgs += isDir ? i18nc("@info", "File is a folder") : exists ? i18nc("@info", "Failed to open file") : i18nc("@info", "File not found"); } break; } case KAEvent::MESSAGE: { // Message label // Using MessageText instead of QLabel allows scrolling and mouse copying MessageText* text = new MessageText(topWidget); text->setAutoFillBackground(true); text->setBackgroundColour(mBgColour); text->setTextColor(mFgColour); text->setCurrentFont(mFont); text->insertPlainText(mMessage); const int lineSpacing = text->fontMetrics().lineSpacing(); const QSize s = text->sizeHint(); const int h = s.height(); text->setMaximumHeight(h + text->scrollBarHeight()); text->setMinimumHeight(qMin(h, lineSpacing*4)); text->setMaximumWidth(s.width() + text->scrollBarWidth()); text->setWhatsThis(i18nc("@info:whatsthis", "The alarm message")); const int vspace = lineSpacing/2; const int hspace = lineSpacing - style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->addSpacing(vspace); topLayout->addStretch(); // Don't include any horizontal margins if message is 2/3 screen width if (text->sizeHint().width() >= Desktop::workArea(mScreenNumber).width()*2/3) topLayout->addWidget(text, 1, Qt::AlignHCenter); else { QHBoxLayout* layout = new QHBoxLayout(); layout->addSpacing(hspace); layout->addWidget(text, 1, Qt::AlignHCenter); layout->addSpacing(hspace); topLayout->addLayout(layout); } if (!reminder) topLayout->addStretch(); break; } case KAEvent::COMMAND: { mCommandText = new MessageText(topWidget); mCommandText->setBackgroundColour(mBgColour); mCommandText->setTextColor(mFgColour); mCommandText->setCurrentFont(mFont); topLayout->addWidget(mCommandText); mCommandText->setWhatsThis(i18nc("@info:whatsthis", "The output of the alarm's command")); theApp()->execCommandAlarm(mEvent, mEvent.alarm(mAlarmType), this, SLOT(readProcessOutput(ShellProcess*))); break; } case KAEvent::EMAIL: default: break; } if (reminder && mEvent.reminderMinutes() > 0) { // Advance reminder: show remaining time until the actual alarm mRemainingText = new QLabel(topWidget); mRemainingText->setFrameStyle(QFrame::Box | QFrame::Raised); mRemainingText->setContentsMargins(leading, leading, leading, leading); mRemainingText->setPalette(labelPalette); mRemainingText->setAutoFillBackground(true); if (mDateTime.isDateOnly() || KADateTime::currentLocalDate().daysTo(mDateTime.date()) > 0) { setRemainingTextDay(); MidnightTimer::connect(this, SLOT(setRemainingTextDay())); // update every day } else { setRemainingTextMinute(); MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute } topLayout->addWidget(mRemainingText, 0, Qt::AlignHCenter); topLayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); topLayout->addStretch(); } } else { // It's an error message switch (mAction) { case KAEvent::EMAIL: { // Display the email addresses and subject. QFrame* frame = new QFrame(topWidget); frame->setFrameStyle(QFrame::Box | QFrame::Raised); frame->setWhatsThis(i18nc("@info:whatsthis", "The email to send")); topLayout->addWidget(frame, 0, Qt::AlignHCenter); QGridLayout* grid = new QGridLayout(frame); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@info Email addressee", "To:"), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 0, Qt::AlignLeft); label = new QLabel(mEvent.emailAddresses(QStringLiteral("\n")), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 1, Qt::AlignLeft); label = new QLabel(i18nc("@info Email subject", "Subject:"), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 1, 0, Qt::AlignLeft); label = new QLabel(mEvent.emailSubject(), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 1, 1, Qt::AlignLeft); break; } case KAEvent::COMMAND: case KAEvent::FILE: case KAEvent::MESSAGE: default: // Just display the error message strings break; } } if (!mErrorMsgs.count()) { topWidget->setAutoFillBackground(true); QPalette palette = topWidget->palette(); palette.setColor(topWidget->backgroundRole(), mBgColour); topWidget->setPalette(palette); } else { setCaption(i18nc("@title:window", "Error")); QHBoxLayout* layout = new QHBoxLayout(); int m = 2 * dcm; layout->setContentsMargins(m, m, m, m); layout->addStretch(); topLayout->addLayout(layout); QLabel* label = new QLabel(topWidget); label->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-error")).pixmap(style()->pixelMetric(QStyle::PM_MessageBoxIconSize))); label->setFixedSize(label->sizeHint()); layout->addWidget(label, 0, Qt::AlignRight); QVBoxLayout* vlayout = new QVBoxLayout(); layout->addLayout(vlayout); for (QStringList::ConstIterator it = mErrorMsgs.constBegin(); it != mErrorMsgs.constEnd(); ++it) { label = new QLabel(*it, topWidget); label->setFixedSize(label->sizeHint()); vlayout->addWidget(label, 0, Qt::AlignLeft); } layout->addStretch(); if (!mDontShowAgain.isEmpty()) { mDontShowAgainCheck = new QCheckBox(i18nc("@option:check", "Do not display this error message again for this alarm"), topWidget); mDontShowAgainCheck->setFixedSize(mDontShowAgainCheck->sizeHint()); topLayout->addWidget(mDontShowAgainCheck, 0, Qt::AlignLeft); } } QGridLayout* grid = new QGridLayout(); grid->setColumnStretch(0, 1); // keep the buttons right-adjusted in the window topLayout->addLayout(grid); int gridIndex = 1; // Close button mOkButton = new PushButton(KStandardGuiItem::close(), topWidget); // Prevent accidental acknowledgement of the message if the user is typing when the window appears mOkButton->clearFocus(); mOkButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mOkButton->setFixedSize(mOkButton->sizeHint()); connect(mOkButton, &QAbstractButton::clicked, this, &MessageWin::slotOk); grid->addWidget(mOkButton, 0, gridIndex++, Qt::AlignHCenter); mOkButton->setWhatsThis(i18nc("@info:whatsthis", "Acknowledge the alarm")); if (mShowEdit) { // Edit button mEditButton = new PushButton(i18nc("@action:button", "&Edit..."), topWidget); mEditButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mEditButton->setFixedSize(mEditButton->sizeHint()); connect(mEditButton, &QAbstractButton::clicked, this, &MessageWin::slotEdit); grid->addWidget(mEditButton, 0, gridIndex++, Qt::AlignHCenter); mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the alarm.")); } // Defer button mDeferButton = new PushButton(i18nc("@action:button", "&Defer..."), topWidget); mDeferButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mDeferButton->setFixedSize(mDeferButton->sizeHint()); connect(mDeferButton, &QAbstractButton::clicked, this, &MessageWin::slotDefer); grid->addWidget(mDeferButton, 0, gridIndex++, Qt::AlignHCenter); mDeferButton->setWhatsThis(xi18nc("@info:whatsthis", "Defer the alarm until later." "You will be prompted to specify when the alarm should be redisplayed.")); if (mNoDefer) mDeferButton->hide(); else setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more if (!mAudioFile.isEmpty() && (mVolume || mFadeVolume > 0)) { // Silence button to stop sound repetition mSilenceButton = new PushButton(topWidget); mSilenceButton->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-stop"))); grid->addWidget(mSilenceButton, 0, gridIndex++, Qt::AlignHCenter); mSilenceButton->setToolTip(i18nc("@info:tooltip", "Stop sound")); mSilenceButton->setWhatsThis(i18nc("@info:whatsthis", "Stop playing the sound")); // To avoid getting in a mess, disable the button until sound playing has been set up mSilenceButton->setEnabled(false); } if (mAkonadiItemId >= 0) { // KMail button mKMailButton = new PushButton(topWidget); mKMailButton->setIcon(QIcon::fromTheme(QStringLiteral("internet-mail"))); connect(mKMailButton, &QAbstractButton::clicked, this, &MessageWin::slotShowKMailMessage); grid->addWidget(mKMailButton, 0, gridIndex++, Qt::AlignHCenter); mKMailButton->setToolTip(xi18nc("@info:tooltip Locate this email in KMail", "Locate in KMail")); mKMailButton->setWhatsThis(xi18nc("@info:whatsthis", "Locate and highlight this email in KMail")); } // KAlarm button mKAlarmButton = new PushButton(topWidget); mKAlarmButton->setIcon(QIcon::fromTheme(KAboutData::applicationData().componentName())); connect(mKAlarmButton, &QAbstractButton::clicked, this, &MessageWin::displayMainWindow); grid->addWidget(mKAlarmButton, 0, gridIndex++, Qt::AlignHCenter); mKAlarmButton->setToolTip(xi18nc("@info:tooltip", "Activate KAlarm")); mKAlarmButton->setWhatsThis(xi18nc("@info:whatsthis", "Activate KAlarm")); int butsize = mKAlarmButton->sizeHint().height(); if (mSilenceButton) butsize = qMax(butsize, mSilenceButton->sizeHint().height()); if (mKMailButton) butsize = qMax(butsize, mKMailButton->sizeHint().height()); mKAlarmButton->setFixedSize(butsize, butsize); if (mSilenceButton) mSilenceButton->setFixedSize(butsize, butsize); if (mKMailButton) mKMailButton->setFixedSize(butsize, butsize); // Disable all buttons initially, to prevent accidental clicking on if they happen to be // under the mouse just as the window appears. mOkButton->setEnabled(false); if (mDeferButton->isVisible()) mDeferButton->setEnabled(false); if (mEditButton) mEditButton->setEnabled(false); if (mKMailButton) mKMailButton->setEnabled(false); mKAlarmButton->setEnabled(false); topLayout->activate(); setMinimumSize(QSize(grid->sizeHint().width() + 2 * style()->pixelMetric(QStyle::PM_DefaultChildMargin), sizeHint().height())); const bool modal = !(windowFlags() & Qt::X11BypassWindowManagerHint); NET::States wstate = NET::Sticky | NET::KeepAbove; if (modal) wstate |= NET::Modal; WId winid = winId(); KWindowSystem::setState(winid, wstate); KWindowSystem::setOnAllDesktops(winid, true); mInitialised = true; // the window's widgets have been created } /****************************************************************************** * Return the number of message windows, optionally excluding always-hidden ones. */ int MessageWin::instanceCount(bool excludeAlwaysHidden) { int count = mWindowList.count(); if (excludeAlwaysHidden) { for (MessageWin* win : qAsConst(mWindowList)) { if (win->mAlwaysHide) --count; } } return count; } bool MessageWin::hasDefer() const { return mDeferButton && mDeferButton->isVisible(); } /****************************************************************************** * Show the Defer button when it was previously hidden. */ void MessageWin::showDefer() { if (mDeferButton) { mNoDefer = false; mDeferButton->show(); setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more resize(sizeHint()); } } /****************************************************************************** * Convert a reminder window into a normal alarm window. */ void MessageWin::cancelReminder(const KAEvent& event, const KAAlarm& alarm) { if (!mInitialised) return; mDateTime = alarm.dateTime(true); mNoPostAction = false; mAlarmType = alarm.type(); if (event.autoClose()) mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().qDateTime().addSecs(event.lateCancel() * 60); setCaption(i18nc("@title:window", "Message")); mTimeLabel->setText(dateTimeToDisplay()); if (mRemainingText) mRemainingText->hide(); MidnightTimer::disconnect(this, SLOT(setRemainingTextDay())); MinuteTimer::disconnect(this, SLOT(setRemainingTextMinute())); setMinimumHeight(0); centralWidget()->layout()->activate(); setMinimumHeight(sizeHint().height()); resize(sizeHint()); } /****************************************************************************** * Show the alarm's trigger time. * This is assumed to have previously been hidden. */ void MessageWin::showDateTime(const KAEvent& event, const KAAlarm& alarm) { if (!mTimeLabel) return; mDateTime = (alarm.type() & KAAlarm::REMINDER_ALARM) ? event.mainDateTime(true) : alarm.dateTime(true); if (mDateTime.isValid()) { mTimeLabel->setText(dateTimeToDisplay()); mTimeLabel->show(); } } /****************************************************************************** * Get the trigger time to display. */ QString MessageWin::dateTimeToDisplay() { QString tm; if (mDateTime.isValid()) { if (mDateTime.isDateOnly()) tm = QLocale().toString(mDateTime.date(), QLocale::ShortFormat); else { bool showZone = false; if (mDateTime.timeType() == KADateTime::UTC || (mDateTime.timeType() == KADateTime::TimeZone && !mDateTime.isLocalZone())) { // Display time zone abbreviation if it's different from the local // zone. Note that the iCalendar time zone might represent the local // time zone in a slightly different way from the system time zone, // so the zone comparison above might not produce the desired result. const QString tz = mDateTime.kDateTime().toString(QStringLiteral("%Z")); KADateTime local = mDateTime.kDateTime(); local.setTimeSpec(KADateTime::Spec::LocalZone()); showZone = (local.toString(QStringLiteral("%Z")) != tz); } const QDateTime dt = mDateTime.qDateTime(); tm = QLocale().toString(dt, QLocale::ShortFormat); if (showZone) tm += QLatin1Char(' ') + mDateTime.timeZone().displayName(dt, QTimeZone::ShortName, QLocale()); } } return tm; } /****************************************************************************** * Set the remaining time text in a reminder window. * Called at the start of every day (at the user-defined start-of-day time). */ void MessageWin::setRemainingTextDay() { QString text; const int days = KADateTime::currentLocalDate().daysTo(mDateTime.date()); if (days <= 0 && !mDateTime.isDateOnly()) { // The alarm is due today, so start refreshing every minute MidnightTimer::disconnect(this, SLOT(setRemainingTextDay())); setRemainingTextMinute(); MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute } else { if (days <= 0) text = i18nc("@info", "Today"); else if (days % 7) text = i18ncp("@info", "Tomorrow", "in %1 days' time", days); else text = i18ncp("@info", "in 1 week's time", "in %1 weeks' time", days/7); } mRemainingText->setText(text); } /****************************************************************************** * Set the remaining time text in a reminder window. * Called on every minute boundary. */ void MessageWin::setRemainingTextMinute() { QString text; const int mins = (KADateTime::currentUtcDateTime().secsTo(mDateTime.effectiveKDateTime()) + 59) / 60; if (mins < 60) text = i18ncp("@info", "in 1 minute's time", "in %1 minutes' time", (mins > 0 ? mins : 0)); else if (mins % 60 == 0) text = i18ncp("@info", "in 1 hour's time", "in %1 hours' time", mins/60); else { QString hourText = i18ncp("@item:intext inserted into 'in ... %1 minute's time' below", "1 hour", "%1 hours", mins/60); text = i18ncp("@info '%2' is the previous message '1 hour'/'%1 hours'", "in %2 1 minute's time", "in %2 %1 minutes' time", mins%60, hourText); } mRemainingText->setText(text); } /****************************************************************************** * Called when output is available from the command which is providing the text * for this window. Add the output and resize the window to show it. */ void MessageWin::readProcessOutput(ShellProcess* proc) { const QByteArray data = proc->readAll(); if (!data.isEmpty()) { // Strip any trailing newline, to avoid showing trailing blank line // in message window. if (mCommandText->newLine()) mCommandText->append(QStringLiteral("\n")); const int nl = data.endsWith('\n') ? 1 : 0; mCommandText->setNewLine(nl); mCommandText->insertPlainText(QString::fromLocal8Bit(data.data(), data.length() - nl)); resize(sizeHint()); } } /****************************************************************************** * Save settings to the session managed config file, for restoration * when the program is restored. */ void MessageWin::saveProperties(KConfigGroup& config) { if (mShown && !mErrorWindow && !mAlwaysHide) { config.writeEntry("EventID", mEventId.eventId()); config.writeEntry("CollectionID", mResource.id()); config.writeEntry("AlarmType", static_cast(mAlarmType)); if (mAlarmType == KAAlarm::INVALID_ALARM) qCCritical(KALARM_LOG) << "MessageWin::saveProperties: Invalid alarm: id=" << mEventId << ", alarm count=" << mEvent.alarmCount(); config.writeEntry("Message", mMessage); config.writeEntry("Type", static_cast(mAction)); config.writeEntry("Font", mFont); config.writeEntry("BgColour", mBgColour); config.writeEntry("FgColour", mFgColour); config.writeEntry("ConfirmAck", mConfirmAck); if (mDateTime.isValid()) { config.writeEntry("Time", mDateTime.effectiveDateTime()); config.writeEntry("DateOnly", mDateTime.isDateOnly()); QByteArray zone; if (mDateTime.isUtc()) zone = "UTC"; else if (mDateTime.isOffsetFromUtc()) { int offset = mDateTime.utcOffset(); if (offset >= 0) zone = '+' + QByteArray::number(offset); else zone = QByteArray::number(offset); } else if (mDateTime.timeType() == KADateTime::TimeZone) { const QTimeZone tz = mDateTime.timeZone(); if (tz.isValid()) zone = tz.id(); } config.writeEntry("TimeZone", zone); } if (mCloseTime.isValid()) config.writeEntry("Expiry", mCloseTime); if (mAudioRepeatPause >= 0 && mSilenceButton && mSilenceButton->isEnabled()) { // Only need to restart sound file playing if it's being repeated config.writePathEntry("AudioFile", mAudioFile); config.writeEntry("Volume", static_cast(mVolume * 100)); config.writeEntry("AudioPause", mAudioRepeatPause); } config.writeEntry("Speak", mSpeak); config.writeEntry("Height", height()); config.writeEntry("DeferMins", mDefaultDeferMinutes); config.writeEntry("NoDefer", mNoDefer); config.writeEntry("NoPostAction", mNoPostAction); config.writeEntry("AkonadiItemId", mAkonadiItemId); config.writeEntry("CmdErr", static_cast(mCommandError)); config.writeEntry("DontShowAgain", mDontShowAgain); } else config.writeEntry("Invalid", true); } /****************************************************************************** * Read settings from the session managed config file. * This function is automatically called whenever the app is being restored. * Read in whatever was saved in saveProperties(). */ void MessageWin::readProperties(const KConfigGroup& config) { mInvalid = config.readEntry("Invalid", false); const QString eventId = config.readEntry("EventID"); const ResourceId collectionId = config.readEntry("CollectionID", ResourceId(-1)); mAlarmType = static_cast(config.readEntry("AlarmType", 0)); if (mAlarmType == KAAlarm::INVALID_ALARM) { mInvalid = true; qCCritical(KALARM_LOG) << "MessageWin::readProperties: Invalid alarm: id=" << eventId; } mMessage = config.readEntry("Message"); mAction = static_cast(config.readEntry("Type", 0)); mFont = config.readEntry("Font", QFont()); mBgColour = config.readEntry("BgColour", QColor(Qt::white)); mFgColour = config.readEntry("FgColour", QColor(Qt::black)); mConfirmAck = config.readEntry("ConfirmAck", false); QDateTime invalidDateTime; QDateTime dt = config.readEntry("Time", invalidDateTime); const QByteArray zoneId = config.readEntry("TimeZone").toLatin1(); KADateTime::Spec timeSpec; if (zoneId.isEmpty()) timeSpec = KADateTime::LocalZone; else if (zoneId == "UTC") timeSpec = KADateTime::UTC; else if (zoneId.startsWith('+') || zoneId.startsWith('-')) timeSpec.setType(KADateTime::OffsetFromUTC, zoneId.toInt()); else timeSpec = QTimeZone(zoneId); mDateTime = KADateTime(dt.date(), dt.time(), timeSpec); const bool dateOnly = config.readEntry("DateOnly", false); if (dateOnly) mDateTime.setDateOnly(true); mCloseTime = config.readEntry("Expiry", invalidDateTime); mCloseTime.setTimeSpec(Qt::UTC); mAudioFile = config.readPathEntry("AudioFile", QString()); mVolume = static_cast(config.readEntry("Volume", 0)) / 100; mFadeVolume = -1; mFadeSeconds = 0; if (!mAudioFile.isEmpty()) // audio file URL was only saved if it repeats mAudioRepeatPause = config.readEntry("AudioPause", 0); mBeep = false; // don't beep after restart (similar to not playing non-repeated sound file) mSpeak = config.readEntry("Speak", false); mRestoreHeight = config.readEntry("Height", 0); mDefaultDeferMinutes = config.readEntry("DeferMins", 0); mNoDefer = config.readEntry("NoDefer", false); mNoPostAction = config.readEntry("NoPostAction", true); mAkonadiItemId = config.readEntry("AkonadiItemId", QVariant(QVariant::LongLong)).toLongLong(); mCommandError = KAEvent::CmdErrType(config.readEntry("CmdErr", static_cast(KAEvent::CMD_NO_ERROR))); mDontShowAgain = config.readEntry("DontShowAgain", QString()); mShowEdit = false; // Temporarily initialise mResource and mEventId - they will be set by redisplayAlarm() mResource = Resources::resource(collectionId); mEventId = EventId(collectionId, eventId); qCDebug(KALARM_LOG) << "MessageWin::readProperties:" << eventId; if (mAlarmType != KAAlarm::INVALID_ALARM) { // Recreate the event from the calendar file (if possible) if (eventId.isEmpty()) initView(); else { // Close any other window for this alarm which has already been restored by redisplayAlarms() if (!Resources::allCreated()) { connect(Resources::instance(), &Resources::resourcesCreated, this, &MessageWin::showRestoredAlarm); return; } redisplayAlarm(); } } } /****************************************************************************** * Fetch the restored alarm from the calendar and redisplay it in this window. */ void MessageWin::showRestoredAlarm() { qCDebug(KALARM_LOG) << "MessageWin::showRestoredAlarm:" << mEventId; redisplayAlarm(); show(); } /****************************************************************************** * Fetch the restored alarm from the calendar and redisplay it in this window. */ void MessageWin::redisplayAlarm() { mResource = Resources::resourceForEvent(mEventId.eventId()); mEventId.setResourceId(mResource.id()); qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarm:" << mEventId; // Delete any already existing window for the same event MessageWin* duplicate = findEvent(mEventId, this); if (duplicate) qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarm: Deleting duplicate window:" << mEventId; delete duplicate; - KAEvent* event = AlarmCalendar::resources()->event(mEventId); + KAEvent* event = ResourcesCalendar::instance()->event(mEventId); if (event) { mEvent = *event; mShowEdit = true; } else { // It's not in the active calendar, so try the displaying or archive calendars retrieveEvent(mEvent, mResource, mShowEdit, mNoDefer); mNoDefer = !mNoDefer; } initView(); } /****************************************************************************** * Redisplay alarms which were being shown when the program last exited. * Normally, these alarms will have been displayed by session restoration, but * if the program crashed or was killed, we can redisplay them here so that * they won't be lost. */ void MessageWin::redisplayAlarms() { if (mRedisplayed) return; qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms"; mRedisplayed = true; - AlarmCalendar* cal = AlarmCalendar::displayCalendar(); + DisplayCalendar* cal = DisplayCalendar::instance(); if (cal && cal->isOpen()) { KAEvent event; Resource resource; const Event::List events = cal->kcalEvents(); for (int i = 0, end = events.count(); i < end; ++i) { bool showDefer, showEdit; reinstateFromDisplaying(events[i], event, resource, showEdit, showDefer); const EventId eventId(event); if (findEvent(eventId)) qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms: Message window already exists:" << eventId; else { // This event should be displayed, but currently isn't being const KAAlarm alarm = event.convertDisplayingAlarm(); if (alarm.type() == KAAlarm::INVALID_ALARM) { qCCritical(KALARM_LOG) << "MessageWin::redisplayAlarms: Invalid alarm: id=" << eventId; continue; } qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms:" << eventId; const bool login = alarm.repeatAtLogin(); const int flags = NO_RESCHEDULE | (login ? NO_DEFER : 0) | NO_INIT_VIEW; MessageWin* win = new MessageWin(&event, alarm, flags); win->mResource = resource; const bool rw = resource.isWritable(event.category()); win->mShowEdit = rw ? showEdit : false; win->mNoDefer = (rw && !login) ? !showDefer : true; win->initView(); win->show(); } } } } /****************************************************************************** * Retrieves the event with the current ID from the displaying calendar file, * or if not found there, from the archive calendar. */ bool MessageWin::retrieveEvent(KAEvent& event, Resource& resource, bool& showEdit, bool& showDefer) { - const Event::Ptr kcalEvent = AlarmCalendar::displayCalendar()->kcalEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING)); + const Event::Ptr kcalEvent = DisplayCalendar::instance()->kcalEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING)); if (!reinstateFromDisplaying(kcalEvent, event, resource, showEdit, showDefer)) { // The event isn't in the displaying calendar. // Try to retrieve it from the archive calendar. KAEvent* ev = nullptr; Resource archiveRes = Resources::getStandard(CalEvent::ARCHIVED); if (archiveRes.isValid()) - ev = AlarmCalendar::resources()->event(EventId(archiveRes.id(), CalEvent::uid(mEventId.eventId(), CalEvent::ARCHIVED))); + ev = ResourcesCalendar::instance()->event(EventId(archiveRes.id(), CalEvent::uid(mEventId.eventId(), CalEvent::ARCHIVED))); if (!ev) return false; event = *ev; event.setArchive(); // ensure that it gets re-archived if it's saved event.setCategory(CalEvent::ACTIVE); if (mEventId.eventId() != event.id()) qCCritical(KALARM_LOG) << "MessageWin::retrieveEvent: Wrong event ID"; event.setEventId(mEventId.eventId()); resource = Resource(); showEdit = true; showDefer = true; qCDebug(KALARM_LOG) << "MessageWin::retrieveEvent:" << event.id() << ": success"; } return true; } /****************************************************************************** * Retrieves the displayed event from the calendar file, or if not found there, * from the displaying calendar. */ bool MessageWin::reinstateFromDisplaying(const Event::Ptr& kcalEvent, KAEvent& event, Resource& resource, bool& showEdit, bool& showDefer) { if (!kcalEvent) return false; ResourceId resourceId; event.reinstateFromDisplaying(kcalEvent, resourceId, showEdit, showDefer); event.setCollectionId(resourceId); resource = Resources::resource(resourceId); qCDebug(KALARM_LOG) << "MessageWin::reinstateFromDisplaying:" << EventId(event) << ": success"; return true; } /****************************************************************************** * Called when an alarm is currently being displayed, to store a copy of the * alarm in the displaying calendar, and to reschedule it for its next repetition. * If no repetitions remain, cancel it. */ void MessageWin::alarmShowing(KAEvent& event) { qCDebug(KALARM_LOG) << "MessageWin::alarmShowing:" << event.id() << "," << KAAlarm::debugType(mAlarmType); const KAAlarm alarm = event.alarm(mAlarmType); if (!alarm.isValid()) { qCCritical(KALARM_LOG) << "MessageWin::alarmShowing: Alarm type not found:" << event.id() << ":" << mAlarmType; return; } if (!mAlwaysHide) { // Copy the alarm to the displaying calendar in case of a crash, etc. KAEvent dispEvent; const ResourceId id = Resources::resourceForEvent(event.id()).id(); dispEvent.setDisplaying(event, mAlarmType, id, mDateTime.effectiveKDateTime(), mShowEdit, !mNoDefer); - AlarmCalendar* cal = AlarmCalendar::displayCalendarOpen(); + DisplayCalendar* cal = DisplayCalendar::instanceOpen(); if (cal) { - cal->deleteDisplayEvent(dispEvent.id()); // in case it already exists + cal->deleteEvent(dispEvent.id()); // in case it already exists cal->addEvent(dispEvent); cal->save(); } } theApp()->rescheduleAlarm(event, alarm); } /****************************************************************************** * Spread alarm windows over the screen so that they are all visible, or pile * them on top of each other again. * Reply = true if windows are now scattered, false if piled up. */ bool MessageWin::spread(bool scatter) { if (instanceCount(true) <= 1) // ignore always-hidden windows return false; const QRect desk = Desktop::workArea(); // get the usable area of the desktop if (scatter == isSpread(desk.topLeft())) return scatter; if (scatter) { // Usually there won't be many windows, so a crude // scattering algorithm should suffice. int x = desk.left(); int y = desk.top(); int ynext = y; for (int errmsgs = 0; errmsgs < 2; ++errmsgs) { // Display alarm messages first, then error messages, since most // error messages tend to be the same height. for (int i = 0, end = mWindowList.count(); i < end; ++i) { MessageWin* w = mWindowList[i]; if ((!errmsgs && w->mErrorWindow) || (errmsgs && !w->mErrorWindow)) continue; const QSize sz = w->frameGeometry().size(); if (x + sz.width() > desk.right()) { x = desk.left(); y = ynext; } int ytmp = y; if (y + sz.height() > desk.bottom()) { ytmp = desk.bottom() - sz.height(); if (ytmp < desk.top()) ytmp = desk.top(); } w->move(x, ytmp); x += sz.width(); if (ytmp + sz.height() > ynext) ynext = ytmp + sz.height(); } } } else { // Move all windows to the top left corner for (int i = 0, end = mWindowList.count(); i < end; ++i) mWindowList[i]->move(desk.topLeft()); } return scatter; } /****************************************************************************** * Check whether message windows are all piled up, or are spread out. * Reply = true if windows are currently spread, false if piled up. */ bool MessageWin::isSpread(const QPoint& topLeft) { for (int i = 0, end = mWindowList.count(); i < end; ++i) { if (mWindowList[i]->pos() != topLeft) return true; } return false; } /****************************************************************************** * Returns the existing message window (if any) which is displaying the event * with the specified ID. */ MessageWin* MessageWin::findEvent(const EventId& eventId, MessageWin* exclude) { if (!eventId.isEmpty()) { for (int i = 0, end = mWindowList.count(); i < end; ++i) { MessageWin* w = mWindowList[i]; if (w != exclude && w->mEventId == eventId && !w->mErrorWindow) return w; } } return nullptr; } /****************************************************************************** * Beep and play the audio file, as appropriate. */ void MessageWin::playAudio() { if (mBeep) { // Beep using two methods, in case the sound card/speakers are switched off or not working QApplication::beep(); // beep through the internal speaker KNotification::beep(); // beep through the sound card & speakers } if (!mAudioFile.isEmpty()) { if (!mVolume && mFadeVolume <= 0) return; // ensure zero volume doesn't play anything startAudio(); // play the audio file } else if (mSpeak) { // The message is to be spoken. In case of error messges, // call it on a timer to allow the window to display first. QTimer::singleShot(0, this, &MessageWin::slotSpeak); } } /****************************************************************************** * Speak the message. * Called asynchronously to avoid delaying the display of the message. */ void MessageWin::slotSpeak() { KPIMTextEdit::TextToSpeech *tts = KPIMTextEdit::TextToSpeech::self(); if (!tts->isReady()) { KAMessageBox::detailedError(MainWindow::mainMainWindow(), i18nc("@info", "Unable to speak message"), i18nc("@info", "Text-to-speech subsystem is not available")); clearErrorMessage(ErrMsg_Speak); return; } tts->say(mMessage); } /****************************************************************************** * Called when another window's audio thread has been destructed. * Start playing this window's audio file. Because initialising the sound system * and loading the file may take some time, it is called in a separate thread to * allow the window to display first. */ void MessageWin::startAudio() { if (mAudioThread) { // An audio file is already playing for another message // window, so wait until it has finished. connect(mAudioThread.data(), &QObject::destroyed, this, &MessageWin::audioTerminating); } else { qCDebug(KALARM_LOG) << "MessageWin::startAudio:" << QThread::currentThread(); mAudioThread = new AudioThread(this, mAudioFile, mVolume, mFadeVolume, mFadeSeconds, mAudioRepeatPause); connect(mAudioThread.data(), &AudioThread::readyToPlay, this, &MessageWin::playReady); connect(mAudioThread.data(), &QThread::finished, this, &MessageWin::playFinished); if (mSilenceButton) connect(mSilenceButton, &QAbstractButton::clicked, mAudioThread.data(), &QThread::quit); // Notify after creating mAudioThread, so that isAudioPlaying() will // return the correct value. theApp()->notifyAudioPlaying(true); mAudioThread->start(); } } /****************************************************************************** * Return whether audio playback is currently active. */ bool MessageWin::isAudioPlaying() { return mAudioThread; } /****************************************************************************** * Stop audio playback. */ void MessageWin::stopAudio(bool wait) { qCDebug(KALARM_LOG) << "MessageWin::stopAudio"; if (mAudioThread) mAudioThread->stop(wait); } /****************************************************************************** * Called when another window's audio thread is being destructed. * Wait until the destructor has finished. */ void MessageWin::audioTerminating() { QTimer::singleShot(0, this, &MessageWin::startAudio); } /****************************************************************************** * Called when the audio file is ready to start playing. */ void MessageWin::playReady() { if (mSilenceButton) mSilenceButton->setEnabled(true); } /****************************************************************************** * Called when the audio file thread finishes. */ void MessageWin::playFinished() { if (mSilenceButton) mSilenceButton->setEnabled(false); if (mAudioThread) // mAudioThread can actually be null here! { const QString errmsg = mAudioThread->error(); if (!errmsg.isEmpty() && !haveErrorMessage(ErrMsg_AudioFile)) { KAMessageBox::error(this, errmsg); clearErrorMessage(ErrMsg_AudioFile); } } delete mAudioThread.data(); if (mAlwaysHide) close(); } /****************************************************************************** * Constructor for audio thread. */ AudioThread::AudioThread(MessageWin* parent, const QString& audioFile, float volume, float fadeVolume, int fadeSeconds, int repeatPause) : QThread(parent), mFile(audioFile), mVolume(volume), mFadeVolume(fadeVolume), mFadeSeconds(fadeSeconds), mRepeatPause(repeatPause), mAudioObject(nullptr) { if (mAudioOwner) qCCritical(KALARM_LOG) << "MessageWin::AudioThread: mAudioOwner already set"; mAudioOwner = parent; } /****************************************************************************** * Destructor for audio thread. Waits for thread completion and tidies up. * Note that this destructor is executed in the parent thread. */ AudioThread::~AudioThread() { qCDebug(KALARM_LOG) << "~MessageWin::AudioThread"; stop(true); // stop playing and tidy up (timeout 3 seconds) delete mAudioObject; mAudioObject = nullptr; if (mAudioOwner == parent()) mAudioOwner = nullptr; // Notify after deleting mAudioThread, so that isAudioPlaying() will // return the correct value. QTimer::singleShot(0, theApp(), &KAlarmApp::notifyAudioStopped); } /****************************************************************************** * Quits the thread and waits for thread completion and tidies up. */ void AudioThread::stop(bool waiT) { qCDebug(KALARM_LOG) << "MessageWin::AudioThread::stop"; quit(); // stop playing and tidy up wait(3000); // wait for run() to exit (timeout 3 seconds) if (!isFinished()) { // Something has gone wrong - forcibly kill the thread terminate(); if (waiT) wait(); } } /****************************************************************************** * Kick off the thread to play the audio file. */ void AudioThread::run() { mMutex.lock(); if (mAudioObject) { mMutex.unlock(); return; } qCDebug(KALARM_LOG) << "MessageWin::AudioThread::run:" << QThread::currentThread() << mFile; const QString audioFile = mFile; const QUrl url = QUrl::fromUserInput(mFile); mFile = url.isLocalFile() ? url.toLocalFile() : url.toString(); Phonon::MediaSource source(url); if (source.type() == Phonon::MediaSource::Invalid) { mError = xi18nc("@info", "Cannot open audio file: %1", audioFile); mMutex.unlock(); qCCritical(KALARM_LOG) << "MessageWin::AudioThread::run: Open failure:" << audioFile; return; } mAudioObject = new Phonon::MediaObject(); mAudioObject->setCurrentSource(source); mAudioObject->setTransitionTime(100); // workaround to prevent clipping of end of files in Xine backend Phonon::AudioOutput* output = new Phonon::AudioOutput(Phonon::NotificationCategory, mAudioObject); mPath = Phonon::createPath(mAudioObject, output); if (mVolume >= 0 || mFadeVolume >= 0) { const float vol = (mVolume >= 0) ? mVolume : output->volume(); const float maxvol = qMax(vol, mFadeVolume); output->setVolume(maxvol); if (mFadeVolume >= 0 && mFadeSeconds > 0) { Phonon::VolumeFaderEffect* fader = new Phonon::VolumeFaderEffect(mAudioObject); fader->setVolume(mFadeVolume / maxvol); fader->fadeTo(mVolume / maxvol, mFadeSeconds * 1000); mPath.insertEffect(fader); } } connect(mAudioObject, &Phonon::MediaObject::stateChanged, this, &AudioThread::playStateChanged, Qt::DirectConnection); connect(mAudioObject, &Phonon::MediaObject::finished, this, &AudioThread::checkAudioPlay, Qt::DirectConnection); mPlayedOnce = false; mPausing = false; mMutex.unlock(); Q_EMIT readyToPlay(); checkAudioPlay(); // Start an event loop. // The function will exit once exit() or quit() is called. // First, ensure that the thread object is deleted once it has completed. connect(this, &QThread::finished, this, &QObject::deleteLater); exec(); stopPlay(); } /****************************************************************************** * Called when the audio file has loaded and is ready to play, or when play * has completed. * If it is ready to play, start playing it (for the first time or repeated). * If play has not yet completed, wait a bit longer. */ void AudioThread::checkAudioPlay() { mMutex.lock(); if (!mAudioObject) { mMutex.unlock(); return; } if (mPausing) mPausing = false; else { // The file has loaded and is ready to play, or play has completed if (mPlayedOnce) { if (mRepeatPause < 0) { // Play has completed mMutex.unlock(); stopPlay(); return; } if (mRepeatPause > 0) { // Pause before playing the file again mPausing = true; QTimer::singleShot(mRepeatPause * 1000, this, &AudioThread::checkAudioPlay); mMutex.unlock(); return; } } mPlayedOnce = true; } // Start playing the file, either for the first time or again qCDebug(KALARM_LOG) << "MessageWin::AudioThread::checkAudioPlay: start"; mAudioObject->play(); mMutex.unlock(); } /****************************************************************************** * Called when the playback object changes state. * If an error has occurred, quit and return the error to the caller. */ void AudioThread::playStateChanged(Phonon::State newState) { if (newState == Phonon::ErrorState) { QMutexLocker locker(&mMutex); const QString err = mAudioObject->errorString(); if (!err.isEmpty()) { qCCritical(KALARM_LOG) << "MessageWin::AudioThread::playStateChanged: Play failure:" << mFile << ":" << err; mError = xi18nc("@info", "Error playing audio file: %1%2", mFile, err); exit(1); } } } /****************************************************************************** * Called when play completes, the Silence button is clicked, or the window is * closed, to terminate audio access. */ void AudioThread::stopPlay() { mMutex.lock(); if (mAudioObject) { mAudioObject->stop(); const QList effects = mPath.effects(); for (int i = 0; i < effects.count(); ++i) { mPath.removeEffect(effects[i]); delete effects[i]; } delete mAudioObject; mAudioObject = nullptr; } mMutex.unlock(); quit(); // exit the event loop, if it's still running } QString AudioThread::error() const { QMutexLocker locker(&mMutex); return mError; } /****************************************************************************** * Raise the alarm window, re-output any required audio notification, and * reschedule the alarm in the calendar file. */ void MessageWin::repeat(const KAAlarm& alarm) { if (!mInitialised) return; if (mDeferDlg) { // Cancel any deferral dialog so that the user notices something's going on, // and also because the deferral time limit will have changed. delete mDeferDlg; mDeferDlg = nullptr; } - KAEvent* event = mEventId.isEmpty() ? nullptr : AlarmCalendar::resources()->event(mEventId); + KAEvent* event = mEventId.isEmpty() ? nullptr : ResourcesCalendar::instance()->event(mEventId); if (event) { mAlarmType = alarm.type(); // store new alarm type for use if it is later deferred if (mAlwaysHide) playAudio(); else { if (!mDeferDlg || Preferences::modalMessages()) { raise(); playAudio(); } if (mDeferButton->isVisible()) { mDeferButton->setEnabled(true); setDeferralLimit(*event); // ensure that button is disabled when alarm can't be deferred any more } } alarmShowing(*event); } } /****************************************************************************** * Display the window. * If windows are being positioned away from the mouse cursor, it is initially * positioned at the top left to slightly reduce the number of times the * windows need to be moved in showEvent(). */ void MessageWin::show() { if (mCloseTime.isValid()) { // Set a timer to auto-close the window int delay = QDateTime::currentDateTimeUtc().secsTo(mCloseTime); if (delay < 0) delay = 0; QTimer::singleShot(delay * 1000, this, &QWidget::close); if (!delay) return; // don't show the window if auto-closing is already due } if (Preferences::messageButtonDelay() == 0) move(0, 0); MainWindowBase::show(); } /****************************************************************************** * Returns the window's recommended size exclusive of its frame. */ QSize MessageWin::sizeHint() const { QSize desired; switch (mAction) { case KAEvent::MESSAGE: desired = MainWindowBase::sizeHint(); break; case KAEvent::COMMAND: if (mShown) { // For command output, expand the window to accommodate the text const QSize texthint = mCommandText->sizeHint(); int w = texthint.width() + 2 * style()->pixelMetric(QStyle::PM_DefaultChildMargin); if (w < width()) w = width(); const int ypadding = height() - mCommandText->height(); desired = QSize(w, texthint.height() + ypadding); break; } // fall through to default Q_FALLTHROUGH(); default: return MainWindowBase::sizeHint(); } // Limit the size to fit inside the working area of the desktop const QSize desktop = Desktop::workArea(mScreenNumber).size(); const QSize frameThickness = frameGeometry().size() - geometry().size(); // title bar & window frame return desired.boundedTo(desktop - frameThickness); } /****************************************************************************** * Called when the window is shown. * The first time, output any required audio notification, and reschedule or * delete the event from the calendar file. */ void MessageWin::showEvent(QShowEvent* se) { MainWindowBase::showEvent(se); if (mShown || !mInitialised) return; if (mErrorWindow || mAlarmType == KAAlarm::INVALID_ALARM) { // Don't bother repositioning error messages, // and invalid alarms should be deleted anyway. enableButtons(); } else { /* Set the window size. * Note that the frame thickness is not yet known when this * method is called, so for large windows the size needs to be * set again later. */ bool execComplete = true; QSize s = sizeHint(); // fit the window round the message if (mAction == KAEvent::FILE && !mErrorMsgs.count()) Config::readWindowSize("FileMessage", s); resize(s); const QRect desk = Desktop::workArea(mScreenNumber); const QRect frame = frameGeometry(); mButtonDelay = Preferences::messageButtonDelay() * 1000; if (mButtonDelay) { // Position the window in the middle of the screen, and // delay enabling the buttons. mPositioning = true; move((desk.width() - frame.width())/2, (desk.height() - frame.height())/2); execComplete = false; } else { /* Try to ensure that the window can't accidentally be acknowledged * by the user clicking the mouse just as it appears. * To achieve this, move the window so that the OK button is as far away * from the cursor as possible. If the buttons are still too close to the * cursor, disable the buttons for a short time. * N.B. This can't be done in show(), since the geometry of the window * is not known until it is displayed. Unfortunately by moving the * window in showEvent(), a flicker is unavoidable. * See the Qt documentation on window geometry for more details. */ // PROBLEM: The frame size is not known yet! const QPoint cursor = QCursor::pos(); const QRect rect = geometry(); // Find the offsets from the outside of the frame to the edges of the OK button const QRect button(mOkButton->mapToParent(QPoint(0, 0)), mOkButton->mapToParent(mOkButton->rect().bottomRight())); const int buttonLeft = button.left() + rect.left() - frame.left(); const int buttonRight = width() - button.right() + frame.right() - rect.right(); const int buttonTop = button.top() + rect.top() - frame.top(); const int buttonBottom = height() - button.bottom() + frame.bottom() - rect.bottom(); const int centrex = (desk.width() + buttonLeft - buttonRight) / 2; const int centrey = (desk.height() + buttonTop - buttonBottom) / 2; const int x = (cursor.x() < centrex) ? desk.right() - frame.width() : desk.left(); const int y = (cursor.y() < centrey) ? desk.bottom() - frame.height() : desk.top(); // Find the enclosing rectangle for the new button positions // and check if the cursor is too near QRect buttons = mOkButton->geometry().united(mKAlarmButton->geometry()); buttons.translate(rect.left() + x - frame.left(), rect.top() + y - frame.top()); const int minDistance = proximityMultiple * mOkButton->height(); if ((abs(cursor.x() - buttons.left()) < minDistance || abs(cursor.x() - buttons.right()) < minDistance) && (abs(cursor.y() - buttons.top()) < minDistance || abs(cursor.y() - buttons.bottom()) < minDistance)) mButtonDelay = proximityButtonDelay; // too near - disable buttons initially if (x != frame.left() || y != frame.top()) { mPositioning = true; move(x, y); execComplete = false; } } if (execComplete) displayComplete(); // play audio, etc. } // Set the window size etc. once the frame size is known QTimer::singleShot(0, this, &MessageWin::frameDrawn); mShown = true; } /****************************************************************************** * Called when the window has been moved. */ void MessageWin::moveEvent(QMoveEvent* e) { MainWindowBase::moveEvent(e); theApp()->setSpreadWindowsState(isSpread(Desktop::workArea(mScreenNumber).topLeft())); if (mPositioning) { // The window has just been initially positioned mPositioning = false; displayComplete(); // play audio, etc. } } /****************************************************************************** * Called after (hopefully) the window frame size is known. * Reset the initial window size if it exceeds the working area of the desktop. * Set the 'spread windows' menu item status. */ void MessageWin::frameDrawn() { if (!mErrorWindow && mAction == KAEvent::MESSAGE) { const QSize s = sizeHint(); if (width() > s.width() || height() > s.height()) resize(s); } theApp()->setSpreadWindowsState(isSpread(Desktop::workArea(mScreenNumber).topLeft())); } /****************************************************************************** * Called when the window has been displayed properly (in its correct position), * to play sounds and reschedule the event. */ void MessageWin::displayComplete() { delete mTempFile; mTempFile = nullptr; playAudio(); if (mRescheduleEvent) alarmShowing(mEvent); if (!mAlwaysHide) { // Enable the window's buttons either now or after the configured delay if (mButtonDelay > 0) QTimer::singleShot(mButtonDelay, this, &MessageWin::enableButtons); else enableButtons(); } } /****************************************************************************** * Enable the window's buttons. */ void MessageWin::enableButtons() { mOkButton->setEnabled(true); mKAlarmButton->setEnabled(true); if (mDeferButton->isVisible() && !mDisableDeferral) mDeferButton->setEnabled(true); if (mEditButton) mEditButton->setEnabled(true); if (mKMailButton) mKMailButton->setEnabled(true); } /****************************************************************************** * Called when the window's size has changed (before it is painted). */ void MessageWin::resizeEvent(QResizeEvent* re) { if (mRestoreHeight) { // Restore the window height on session restoration if (mRestoreHeight != re->size().height()) { QSize size = re->size(); size.setHeight(mRestoreHeight); resize(size); } else if (isVisible()) mRestoreHeight = 0; } else { if (mShown && mAction == KAEvent::FILE && !mErrorMsgs.count()) Config::writeWindowSize("FileMessage", re->size()); MainWindowBase::resizeEvent(re); } } /****************************************************************************** * Called when a close event is received. * Only quits the application if there is no system tray icon displayed. */ void MessageWin::closeEvent(QCloseEvent* ce) { // Don't prompt or delete the alarm from the display calendar if the session is closing if (!mErrorWindow && !qApp->isSavingSession()) { if (mConfirmAck && !mNoCloseConfirm) { // Ask for confirmation of acknowledgement. Use warningYesNo() because its default is No. if (KAMessageBox::warningYesNo(this, i18nc("@info", "Do you really want to acknowledge this alarm?"), i18nc("@action:button", "Acknowledge Alarm"), KGuiItem(i18nc("@action:button", "Acknowledge")), KStandardGuiItem::cancel()) != KMessageBox::Yes) { ce->ignore(); return; } } if (!mEventId.isEmpty()) { // Delete from the display calendar KAlarm::deleteDisplayEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING)); } } MainWindowBase::closeEvent(ce); } /****************************************************************************** * Called when the OK button is clicked. */ void MessageWin::slotOk() { if (mDontShowAgainCheck && mDontShowAgainCheck->isChecked()) KAlarm::setDontShowErrors(mEventId, mDontShowAgain); close(); } /****************************************************************************** * Called when the KMail button is clicked. * Tells KMail to display the email message displayed in this message window. */ void MessageWin::slotShowKMailMessage() { qCDebug(KALARM_LOG) << "MessageWin::slotShowKMailMessage"; if (mAkonadiItemId < 0) return; const QString err = KAlarm::runKMail(); if (!err.isNull()) { KAMessageBox::sorry(this, err); return; } org::kde::kmail::kmail kmail(KMAIL_DBUS_SERVICE, KMAIL_DBUS_PATH, QDBusConnection::sessionBus()); // Display the message contents QDBusReply reply = kmail.showMail(mAkonadiItemId); bool failed1 = true; bool failed2 = true; if (!reply.isValid()) qCCritical(KALARM_LOG) << "kmail 'showMail' D-Bus call failed:" << reply.error().message(); else if (reply.value()) failed1 = false; // Select the mail folder containing the message Akonadi::ItemFetchJob* job = new Akonadi::ItemFetchJob(Akonadi::Item(mAkonadiItemId)); job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent); Akonadi::Item::List items; if (job->exec()) items = job->items(); if (items.isEmpty() || !items.at(0).isValid()) qCWarning(KALARM_LOG) << "MessageWin::slotShowKMailMessage: No parent found for item" << mAkonadiItemId; else { const Akonadi::Item& it = items.at(0); const Akonadi::Collection::Id colId = it.parentCollection().id(); reply = kmail.selectFolder(QString::number(colId)); if (!reply.isValid()) qCCritical(KALARM_LOG) << "kmail 'selectFolder' D-Bus call failed:" << reply.error().message(); else if (reply.value()) failed2 = false; } if (failed1 || failed2) KAMessageBox::sorry(this, xi18nc("@info", "Unable to locate this email in KMail")); } /****************************************************************************** * Called when the Edit... button is clicked. * Displays the alarm edit dialog. * * NOTE: The alarm edit dialog is made a child of the main window, not this * window, so that if this window closes before the dialog (e.g. on * auto-close), KAlarm doesn't crash. The dialog is set non-modal so that * the main window is unaffected, but modal mode is simulated so that * this window is inactive while the dialog is open. */ void MessageWin::slotEdit() { qCDebug(KALARM_LOG) << "MessageWin::slotEdit"; MainWindow* mainWin = MainWindow::mainMainWindow(); mEditDlg = EditAlarmDlg::create(false, &mOriginalEvent, false, mainWin, EditAlarmDlg::RES_IGNORE); mEditDlg->setAttribute(Qt::WA_NativeWindow, true); KWindowSystem::setMainWindow(mEditDlg->windowHandle(), winId()); KWindowSystem::setOnAllDesktops(mEditDlg->winId(), false); setButtonsReadOnly(true); connect(mEditDlg, &QDialog::accepted, this, &MessageWin::editCloseOk); connect(mEditDlg, &QDialog::rejected, this, &MessageWin::editCloseCancel); connect(mEditDlg, &QObject::destroyed, this, &MessageWin::editCloseCancel); connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, this, &MessageWin::activeWindowChanged); mainWin->editAlarm(mEditDlg, mOriginalEvent); } /****************************************************************************** * Called when OK is clicked in the alarm edit dialog invoked by the Edit button. * Closes the window. */ void MessageWin::editCloseOk() { mEditDlg = nullptr; mNoCloseConfirm = true; // allow window to close without confirmation prompt close(); } /****************************************************************************** * Called when Cancel is clicked in the alarm edit dialog invoked by the Edit * button, or when the dialog is deleted. */ void MessageWin::editCloseCancel() { mEditDlg = nullptr; setButtonsReadOnly(false); } /****************************************************************************** * Called when the active window has changed. If this window has become the * active window and there is an alarm edit dialog, simulate a modal dialog by * making the alarm edit dialog the active window instead. */ void MessageWin::activeWindowChanged(WId win) { if (mEditDlg && win == winId()) KWindowSystem::activateWindow(mEditDlg->winId()); } /****************************************************************************** * Set or clear the read-only state of the dialog buttons. */ void MessageWin::setButtonsReadOnly(bool ro) { mOkButton->setReadOnly(ro, true); mDeferButton->setReadOnly(ro, true); mEditButton->setReadOnly(ro, true); if (mSilenceButton) mSilenceButton->setReadOnly(ro, true); if (mKMailButton) mKMailButton->setReadOnly(ro, true); mKAlarmButton->setReadOnly(ro, true); } /****************************************************************************** * Set up to disable the defer button when the deferral limit is reached. */ void MessageWin::setDeferralLimit(const KAEvent& event) { mDeferLimit = event.deferralLimit().effectiveKDateTime().toUtc().qDateTime(); MidnightTimer::connect(this, SLOT(checkDeferralLimit())); // check every day mDisableDeferral = false; checkDeferralLimit(); } /****************************************************************************** * Check whether the deferral limit has been reached. * If so, disable the Defer button. * N.B. Ideally, just a single QTimer::singleShot() call would be made to disable * the defer button at the corret time. But for a 32-bit integer, the * milliseconds parameter overflows in about 25 days, so instead a daily * check is done until the day when the deferral limit is reached, followed * by a non-overflowing QTimer::singleShot() call. */ void MessageWin::checkDeferralLimit() { if (!mDeferButton->isEnabled() || !mDeferLimit.isValid()) return; int n = KADateTime::currentLocalDate().daysTo(KADateTime(mDeferLimit, KADateTime::LocalZone).date()); if (n > 0) return; MidnightTimer::disconnect(this, SLOT(checkDeferralLimit())); if (n == 0) { // The deferral limit will be reached today n = QDateTime::currentDateTimeUtc().secsTo(mDeferLimit); if (n > 0) { QTimer::singleShot(n * 1000, this, &MessageWin::checkDeferralLimit); return; } } mDeferButton->setEnabled(false); mDisableDeferral = true; } /****************************************************************************** * Called when the Defer... button is clicked. * Displays the defer message dialog. */ void MessageWin::slotDefer() { mDeferDlg = new DeferAlarmDlg(KADateTime::currentDateTime(Preferences::timeSpec()).addSecs(60), mDateTime.isDateOnly(), false, this); if (windowFlags() & Qt::X11BypassWindowManagerHint) mDeferDlg->setWindowFlags(mDeferDlg->windowFlags() | Qt::X11BypassWindowManagerHint); mDeferDlg->setObjectName(QStringLiteral("DeferDlg")); // used by LikeBack mDeferDlg->setDeferMinutes(mDefaultDeferMinutes > 0 ? mDefaultDeferMinutes : Preferences::defaultDeferTime()); mDeferDlg->setLimit(mEvent); if (!Preferences::modalMessages()) lower(); if (mDeferDlg->exec() == QDialog::Accepted) { const DateTime dateTime = mDeferDlg->getDateTime(); const int delayMins = mDeferDlg->deferMinutes(); // Fetch the up-to-date alarm from the calendar. Note that it could have // changed since it was displayed. - const KAEvent* event = mEventId.isEmpty() ? nullptr : AlarmCalendar::resources()->event(mEventId); + const KAEvent* event = mEventId.isEmpty() ? nullptr : ResourcesCalendar::instance()->event(mEventId); if (event) { // The event still exists in the active calendar qCDebug(KALARM_LOG) << "MessageWin::slotDefer: Deferring event" << mEventId; KAEvent newev(*event); newev.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true); newev.setDeferDefaultMinutes(delayMins); KAlarm::updateEvent(newev, mDeferDlg, true); if (newev.deferred()) mNoPostAction = true; } else { // Try to retrieve the event from the displaying or archive calendars Resource resource; KAEvent event; bool showEdit, showDefer; if (!retrieveEvent(event, resource, showEdit, showDefer)) { // The event doesn't exist any more !?!, so recurrence data, // flags, and more, have been lost. KAMessageBox::error(this, xi18nc("@info", "Cannot defer alarm:Alarm not found.")); raise(); delete mDeferDlg; mDeferDlg = nullptr; mDeferButton->setEnabled(false); mEditButton->setEnabled(false); return; } qCDebug(KALARM_LOG) << "MessageWin::slotDefer: Deferring retrieved event" << mEventId; event.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true); event.setDeferDefaultMinutes(delayMins); event.setCommandError(mCommandError); // Add the event back into the calendar file, retaining its ID // and not updating KOrganizer. KAlarm::addEvent(event, &resource, mDeferDlg, KAlarm::USE_EVENT_ID); if (event.deferred()) mNoPostAction = true; // Finally delete it from the archived calendar now that it has // been reactivated. event.setCategory(CalEvent::ARCHIVED); KAlarm::deleteEvent(event, false); } if (theApp()->wantShowInSystemTray()) { // Alarms are to be displayed only if the system tray icon is running, // so start it if necessary so that the deferred alarm will be shown. theApp()->displayTrayIcon(true); } mNoCloseConfirm = true; // allow window to close without confirmation prompt close(); } else raise(); delete mDeferDlg; mDeferDlg = nullptr; } /****************************************************************************** * Called when the KAlarm icon button in the message window is clicked. * Displays the main window, with the appropriate alarm selected. */ void MessageWin::displayMainWindow() { KAlarm::displayMainWindowSelected(mEventId.eventId()); } /****************************************************************************** * Check whether the specified error message is already displayed for this * alarm, and note that it will now be displayed. * Reply = true if message is already displayed. */ bool MessageWin::haveErrorMessage(unsigned msg) const { if (!mErrorMessages.contains(mEventId)) mErrorMessages.insert(mEventId, 0); const bool result = (mErrorMessages[mEventId] & msg); mErrorMessages[mEventId] |= msg; return result; } void MessageWin::clearErrorMessage(unsigned msg) const { if (mErrorMessages.contains(mEventId)) { if (mErrorMessages[mEventId] == msg) mErrorMessages.remove(mEventId); else mErrorMessages[mEventId] &= ~msg; } } /****************************************************************************** * Check whether the message window should be modal, i.e. with title bar etc. * Normally this follows the Preferences setting, but if there is a full screen * window displayed, on X11 the message window has to bypass the window manager * in order to display on top of it (which has the side effect that it will have * no window decoration). * * Also find the usable area of the desktop (excluding panel etc.), on the * appropriate screen if there are multiple screens. */ bool MessageWin::getWorkAreaAndModal() { mScreenNumber = -1; const bool modal = Preferences::modalMessages(); #if KDEPIM_HAVE_X11 const QList screens = QGuiApplication::screens(); const int numScreens = screens.count(); if (numScreens > 1) { // There are multiple screens. // Check for any full screen windows, even if they are not the active // window, and try not to show the alarm message their screens. mScreenNumber = QApplication::desktop()->screenNumber(MainWindow::mainMainWindow()); // default = KAlarm's screen if (QGuiApplication::primaryScreen()->virtualSiblings().size() > 1) { // The screens form a single virtual desktop. // Xinerama, for example, uses this scheme. QVector screenTypes(numScreens); QVector screenRects(numScreens); for (int s = 0; s < numScreens; ++s) screenRects[s] = screens[s]->geometry(); const FullScreenType full = findFullScreenWindows(screenRects, screenTypes); if (full == NoFullScreen || screenTypes[mScreenNumber] == NoFullScreen) return modal; for (int s = 0; s < numScreens; ++s) { if (screenTypes[s] == NoFullScreen) { // There is no full screen window on this screen mScreenNumber = s; return modal; } } // All screens contain a full screen window: use one without // an active full screen window. for (int s = 0; s < numScreens; ++s) { if (screenTypes[s] == FullScreen) { mScreenNumber = s; return modal; } } } else { // The screens are completely separate from each other. int inactiveScreen = -1; FullScreenType full = haveFullScreenWindow(mScreenNumber); //qCDebug(KALARM_LOG)<<"full="<& screenRects, QVector& screenTypes) { FullScreenType result = NoFullScreen; screenTypes.fill(NoFullScreen); xcb_connection_t* connection = QX11Info::connection(); const NETRootInfo rootInfo(connection, NET::ClientList | NET::ActiveWindow, NET::Properties2()); const xcb_window_t rootWindow = rootInfo.rootWindow(); const xcb_window_t activeWindow = rootInfo.activeWindow(); const xcb_window_t* windows = rootInfo.clientList(); const int windowCount = rootInfo.clientListCount(); //qCDebug(KALARM_LOG)<<"Virtual desktops: Window count="< * Based on KOrganizer's ResourceView class and KAddressBook's ResourceSelection class, * Copyright (C) 2003,2004 Cornelius Schumacher * Copyright (C) 2003-2004 Reinhold Kainhofer * Copyright (c) 2004 Tobias Koenig * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "resourceselector.h" #include "alarmcalendar.h" #include "kalarmapp.h" #include "preferences.h" #include "resources/akonadidatamodel.h" #include "resources/akonadiresourcecreator.h" #include "resources/akonadiresourcemigrator.h" #include "resources/datamodel.h" #include "resources/resources.h" #include "resources/resourcemodel.h" #include "lib/autoqpointer.h" #include "lib/messagebox.h" #include "lib/packedlayout.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace KCalendarCore; ResourceSelector::ResourceSelector(QWidget* parent) : QFrame(parent) { QBoxLayout* topLayout = new QVBoxLayout(this); QLabel* label = new QLabel(i18nc("@title:group", "Calendars"), this); topLayout->addWidget(label, 0, Qt::AlignHCenter); mAlarmType = new QComboBox(this); mAlarmType->addItem(i18nc("@item:inlistbox", "Active Alarms")); mAlarmType->addItem(i18nc("@item:inlistbox", "Archived Alarms")); mAlarmType->addItem(i18nc("@item:inlistbox", "Alarm Templates")); mAlarmType->setFixedHeight(mAlarmType->sizeHint().height()); mAlarmType->setWhatsThis(i18nc("@info:whatsthis", "Choose which type of data to show alarm calendars for")); topLayout->addWidget(mAlarmType); // No spacing between combo box and listview. ResourceFilterCheckListModel* model = DataModel::createResourceFilterCheckListModel(this); mListView = new ResourceView(model, this); connect(mListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ResourceSelector::selectionChanged); mListView->setContextMenuPolicy(Qt::CustomContextMenu); connect(mListView, &ResourceView::customContextMenuRequested, this, &ResourceSelector::contextMenuRequested); mListView->setWhatsThis(i18nc("@info:whatsthis", "List of available calendars of the selected type. The checked state shows whether a calendar " "is enabled (checked) or disabled (unchecked). The default calendar is shown in bold.")); topLayout->addWidget(mListView, 1); PackedLayout* blayout = new PackedLayout(Qt::AlignHCenter); blayout->setContentsMargins(0, 0, 0, 0); topLayout->addLayout(blayout); mAddButton = new QPushButton(i18nc("@action:button", "Add..."), this); mEditButton = new QPushButton(i18nc("@action:button", "Edit..."), this); mDeleteButton = new QPushButton(i18nc("@action:button", "Remove"), this); blayout->addWidget(mAddButton); blayout->addWidget(mEditButton); blayout->addWidget(mDeleteButton); mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the highlighted calendar")); mDeleteButton->setWhatsThis(xi18nc("@info:whatsthis", "Remove the highlighted calendar from the list." "The calendar itself is left intact, and may subsequently be reinstated in the list if desired.")); mEditButton->setDisabled(true); mDeleteButton->setDisabled(true); connect(mAddButton, &QPushButton::clicked, this, &ResourceSelector::addResource); connect(mEditButton, &QPushButton::clicked, this, &ResourceSelector::editResource); connect(mDeleteButton, &QPushButton::clicked, this, &ResourceSelector::removeResource); connect(mAlarmType, static_cast(&QComboBox::activated), this, &ResourceSelector::alarmTypeSelected); QTimer::singleShot(0, this, SLOT(alarmTypeSelected())); Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(archiveDaysChanged(int))); } /****************************************************************************** * Called when an alarm type has been selected. * Filter the resource list to show resources of the selected alarm type, and * add appropriate whatsThis texts to the list and to the Add button. */ void ResourceSelector::alarmTypeSelected() { QString addTip; switch (mAlarmType->currentIndex()) { case 0: mCurrentAlarmType = CalEvent::ACTIVE; addTip = i18nc("@info:tooltip", "Add a new active alarm calendar"); break; case 1: mCurrentAlarmType = CalEvent::ARCHIVED; addTip = i18nc("@info:tooltip", "Add a new archived alarm calendar"); break; case 2: mCurrentAlarmType = CalEvent::TEMPLATE; addTip = i18nc("@info:tooltip", "Add a new alarm template calendar"); break; } // WORKAROUND: Switch scroll bars off to avoid crash (see explanation // in reinstateAlarmTypeScrollBars() description). mListView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); mListView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); mListView->resourceModel()->setEventTypeFilter(mCurrentAlarmType); mAddButton->setWhatsThis(addTip); mAddButton->setToolTip(addTip); // WORKAROUND: Switch scroll bars back on after allowing geometry to update ... QTimer::singleShot(0, this, &ResourceSelector::reinstateAlarmTypeScrollBars); selectionChanged(); // enable/disable buttons } /****************************************************************************** * WORKAROUND for crash due to presumed Qt bug. * Switch scroll bars off. This is to avoid a crash which can very occasionally * happen when changing from a list of calendars which requires vertical scroll * bars, to a list whose text is very slightly wider but which doesn't require * scroll bars at all. (The suspicion is that the width is such that it would * require horizontal scroll bars if the vertical scroll bars were still * present.) Presumably due to a Qt bug, this can result in a recursive call to * ResourceView::viewportEvent() with a Resize event. * * The crash only occurs if the ResourceSelector happens to have exactly (within * one pixel) the "right" width to create the crash. */ void ResourceSelector::reinstateAlarmTypeScrollBars() { mListView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); mListView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); } /****************************************************************************** * Prompt the user for a new resource to add to the list. */ void ResourceSelector::addResource() { ResourceCreator* creator = DataModel::createResourceCreator(mCurrentAlarmType, this); connect(creator, &ResourceCreator::resourceAdded, this, &ResourceSelector::slotResourceAdded); creator->createResource(); } /****************************************************************************** * Called when a resource is added to the calendar data model, after being * created by addResource(). */ void ResourceSelector::slotResourceAdded(Resource& resource, CalEvent::Type alarmType) { const CalEvent::Types types = resource.alarmTypes(); resource.setEnabled(types); if (!(types & alarmType)) { // The user has selected alarm types for the resource which don't // include the currently displayed type. // Show a resource list which includes a selected type. int index = -1; if (types & CalEvent::ACTIVE) index = 0; else if (types & CalEvent::ARCHIVED) index = 1; else if (types & CalEvent::TEMPLATE) index = 2; if (index >= 0) { mAlarmType->setCurrentIndex(index); alarmTypeSelected(); } } } /****************************************************************************** * Edit the currently selected resource. */ void ResourceSelector::editResource() { currentResource().editResource(this); } /****************************************************************************** * Update the backend storage format for the currently selected resource in the * displayed list. */ void ResourceSelector::updateResource() { Resource resource = currentResource(); if (!resource.isValid()) return; DataModel::updateCalendarToCurrentFormat(resource, true, this); } /****************************************************************************** * Remove the currently selected resource from the displayed list. */ void ResourceSelector::removeResource() { Resource resource = currentResource(); if (!resource.isValid()) return; qCDebug(KALARM_LOG) << "ResourceSelector::removeResource:" << resource.displayName(); const QString name = resource.configName(); // Check if it's the standard or only resource for at least one type. const CalEvent::Types allTypes = resource.alarmTypes(); const CalEvent::Types standardTypes = Resources::standardTypes(resource, true); const CalEvent::Type currentType = currentResourceType(); const CalEvent::Type stdType = (standardTypes & CalEvent::ACTIVE) ? CalEvent::ACTIVE : (standardTypes & CalEvent::ARCHIVED) ? CalEvent::ARCHIVED : CalEvent::EMPTY; if (stdType == CalEvent::ACTIVE) { KAMessageBox::sorry(this, i18nc("@info", "You cannot remove your default active alarm calendar.")); return; } if (stdType == CalEvent::ARCHIVED && Preferences::archivedKeepDays()) { // Only allow the archived alarms standard resource to be removed if // we're not saving archived alarms. KAMessageBox::sorry(this, i18nc("@info", "You cannot remove your default archived alarm calendar " "while expired alarms are configured to be kept.")); return; } QString text; if (standardTypes) { // It's a standard resource for at least one alarm type if (allTypes != currentType) { // It also contains alarm types other than the currently displayed type const QString stdTypes = ResourceDataModelBase::typeListForDisplay(standardTypes); QString otherTypes; const CalEvent::Types nonStandardTypes(allTypes & ~standardTypes); if (nonStandardTypes != currentType) otherTypes = xi18nc("@info", "It also contains:%1", ResourceDataModelBase::typeListForDisplay(nonStandardTypes)); text = xi18nc("@info", "%1 is the default calendar for:%2%3" "Do you really want to remove it from all calendar lists?", name, stdTypes, otherTypes); } else text = xi18nc("@info", "Do you really want to remove your default calendar (%1) from the list?", name); } else if (allTypes != currentType) text = xi18nc("@info", "%1 contains:%2Do you really want to remove it from all calendar lists?", name, ResourceDataModelBase::typeListForDisplay(allTypes)); else text = xi18nc("@info", "Do you really want to remove the calendar %1 from the list?", name); if (KAMessageBox::warningContinueCancel(this, text, QString(), KStandardGuiItem::remove()) == KMessageBox::Cancel) return; resource.removeResource(); } /****************************************************************************** * Called when the current selection changes, to enable/disable the * Delete and Edit buttons accordingly. */ void ResourceSelector::selectionChanged() { bool state = mListView->selectionModel()->selectedRows().count(); mDeleteButton->setEnabled(state); mEditButton->setEnabled(state); } /****************************************************************************** * Initialise the button and context menu actions. */ void ResourceSelector::initActions(KActionCollection* actions) { mActionReload = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("@action Reload calendar", "Re&load"), this); actions->addAction(QStringLiteral("resReload"), mActionReload); connect(mActionReload, &QAction::triggered, this, &ResourceSelector::reloadResource); mActionShowDetails = new QAction(QIcon::fromTheme(QStringLiteral("help-about")), i18nc("@action", "Show &Details"), this); actions->addAction(QStringLiteral("resDetails"), mActionShowDetails); connect(mActionShowDetails, &QAction::triggered, this, &ResourceSelector::showInfo); mActionSetColour = new QAction(QIcon::fromTheme(QStringLiteral("color-picker")), i18nc("@action", "Set &Color..."), this); actions->addAction(QStringLiteral("resSetColour"), mActionSetColour); connect(mActionSetColour, &QAction::triggered, this, &ResourceSelector::setColour); mActionClearColour = new QAction(i18nc("@action", "Clear C&olor"), this); actions->addAction(QStringLiteral("resClearColour"), mActionClearColour); connect(mActionClearColour, &QAction::triggered, this, &ResourceSelector::clearColour); mActionEdit = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18nc("@action", "&Edit..."), this); actions->addAction(QStringLiteral("resEdit"), mActionEdit); connect(mActionEdit, &QAction::triggered, this, &ResourceSelector::editResource); mActionUpdate = new QAction(i18nc("@action", "&Update Calendar Format"), this); actions->addAction(QStringLiteral("resUpdate"), mActionUpdate); connect(mActionUpdate, &QAction::triggered, this, &ResourceSelector::updateResource); mActionRemove = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action", "&Remove"), this); actions->addAction(QStringLiteral("resRemove"), mActionRemove); connect(mActionRemove, &QAction::triggered, this, &ResourceSelector::removeResource); mActionSetDefault = new KToggleAction(this); actions->addAction(QStringLiteral("resDefault"), mActionSetDefault); connect(mActionSetDefault, &KToggleAction::triggered, this, &ResourceSelector::setStandard); QAction* action = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18nc("@action", "&Add..."), this); actions->addAction(QStringLiteral("resAdd"), action); connect(action, &QAction::triggered, this, &ResourceSelector::addResource); mActionImport = new QAction(i18nc("@action", "Im&port..."), this); actions->addAction(QStringLiteral("resImport"), mActionImport); connect(mActionImport, &QAction::triggered, this, &ResourceSelector::importCalendar); mActionExport = new QAction(i18nc("@action", "E&xport..."), this); actions->addAction(QStringLiteral("resExport"), mActionExport); connect(mActionExport, &QAction::triggered, this, &ResourceSelector::exportCalendar); } void ResourceSelector::setContextMenu(QMenu* menu) { mContextMenu = menu; } /****************************************************************************** * Display the context menu for the selected calendar. */ void ResourceSelector::contextMenuRequested(const QPoint& viewportPos) { if (!mContextMenu) return; bool active = false; bool writable = false; bool updatable = false; Resource resource; if (mListView->selectionModel()->hasSelection()) { const QModelIndex index = mListView->indexAt(viewportPos); if (index.isValid()) resource = mListView->resourceModel()->resource(index); else mListView->clearSelection(); } CalEvent::Type type = currentResourceType(); bool haveCalendar = resource.isValid(); if (haveCalendar) { active = resource.isEnabled(type); const int rw = resource.writableStatus(type); writable = (rw > 0); const KACalendar::Compat compatibility = resource.compatibility(); if (!rw && (compatibility & ~KACalendar::Converted) && !(compatibility & ~(KACalendar::Convertible | KACalendar::Converted))) updatable = true; // the calendar format is convertible to the current KAlarm format if (!(resource.alarmTypes() & type)) type = CalEvent::EMPTY; } mActionReload->setEnabled(active); mActionShowDetails->setEnabled(haveCalendar); mActionSetColour->setEnabled(haveCalendar); mActionClearColour->setEnabled(haveCalendar); mActionClearColour->setVisible(resource.backgroundColour().isValid()); mActionEdit->setEnabled(haveCalendar); mActionUpdate->setEnabled(updatable); mActionRemove->setEnabled(haveCalendar); mActionImport->setEnabled(active && writable); mActionExport->setEnabled(active); QString text; switch (type) { case CalEvent::ACTIVE: text = i18nc("@action", "Use as &Default for Active Alarms"); break; case CalEvent::ARCHIVED: text = i18nc("@action", "Use as &Default for Archived Alarms"); break; case CalEvent::TEMPLATE: text = i18nc("@action", "Use as &Default for Alarm Templates"); break; default: break; } mActionSetDefault->setText(text); bool standard = Resources::isStandard(resource, type); mActionSetDefault->setChecked(active && writable && standard); mActionSetDefault->setEnabled(active && writable); mContextMenu->popup(mListView->viewport()->mapToGlobal(viewportPos)); } /****************************************************************************** * Called from the context menu to reload the selected resource. */ void ResourceSelector::reloadResource() { Resource resource = currentResource(); if (resource.isValid()) DataModel::reload(resource); } /****************************************************************************** * Called from the context menu to save the selected resource. */ void ResourceSelector::saveResource() { // Save resource is not applicable to Akonadi } /****************************************************************************** * Called when the length of time archived alarms are to be stored changes. * If expired alarms are now to be stored, this also sets any single archived * alarm resource to be the default. */ void ResourceSelector::archiveDaysChanged(int days) { if (days) { const Resource resource = Resources::getStandard(CalEvent::ARCHIVED); if (resource.isValid()) theApp()->purgeNewArchivedDefault(resource); } } /****************************************************************************** * Called from the context menu to set the selected resource as the default * for its alarm type. The resource is automatically made active. */ void ResourceSelector::setStandard() { Resource resource = currentResource(); if (resource.isValid()) { CalEvent::Type alarmType = currentResourceType(); bool standard = mActionSetDefault->isChecked(); if (standard) resource.setEnabled(alarmType, true); Resources::setStandard(resource, alarmType, standard); if (alarmType == CalEvent::ARCHIVED) theApp()->purgeNewArchivedDefault(resource); } } /****************************************************************************** * Called from the context menu to merge alarms from an external calendar into * the selected resource (if any). */ void ResourceSelector::importCalendar() { Resource resource = currentResource(); Resources::importAlarms(resource, this); } /****************************************************************************** * Called from the context menu to copy the selected resource's alarms to an * external calendar. */ void ResourceSelector::exportCalendar() { const Resource resource = currentResource(); if (resource.isValid()) - Resources::exportAlarms(AlarmCalendar::resources()->events(resource), this); + Resources::exportAlarms(ResourcesCalendar::instance()->events(resource), this); } /****************************************************************************** * Called from the context menu to set a colour for the selected resource. */ void ResourceSelector::setColour() { Resource resource = currentResource(); if (resource.isValid()) { QColor colour = resource.backgroundColour(); if (!colour.isValid()) colour = QApplication::palette().color(QPalette::Base); colour = QColorDialog::getColor(colour, this); if (colour.isValid()) resource.setBackgroundColour(colour); } } /****************************************************************************** * Called from the context menu to clear the display colour for the selected * resource. */ void ResourceSelector::clearColour() { Resource resource = currentResource(); if (resource.isValid()) resource.setBackgroundColour(QColor()); } /****************************************************************************** * Called from the context menu to display information for the selected resource. */ void ResourceSelector::showInfo() { const Resource resource = currentResource(); if (resource.isValid()) { const QString name = resource.displayName(); const QString id = resource.configName(); // resource name const QString calType = resource.storageTypeString(true); const CalEvent::Type alarmType = currentResourceType(); const QString storage = resource.storageTypeString(false); const QString location = resource.displayLocation(); const CalEvent::Types altypes = resource.alarmTypes(); QStringList alarmTypes; if (altypes & CalEvent::ACTIVE) alarmTypes << i18nc("@info", "Active alarms"); if (altypes & CalEvent::ARCHIVED) alarmTypes << i18nc("@info", "Archived alarms"); if (altypes & CalEvent::TEMPLATE) alarmTypes << i18nc("@info", "Alarm templates"); const QString alarmTypeString = alarmTypes.join(i18nc("@info List separator", ", ")); QString perms = ResourceDataModelBase::readOnlyTooltip(resource); if (perms.isEmpty()) perms = i18nc("@info", "Read-write"); const QString enabled = resource.isEnabled(alarmType) ? i18nc("@info", "Enabled") : i18nc("@info", "Disabled"); const QString std = Resources::isStandard(resource, alarmType) ? i18nc("@info Parameter in 'Default calendar: Yes/No'", "Yes") : i18nc("@info Parameter in 'Default calendar: Yes/No'", "No"); const QString text = xi18nc("@info", "%1" "ID: %2" "Calendar type: %3" "Contents: %4" "%5: %6" "Permissions: %7" "Status: %8" "Default calendar: %9", name, id, calType, alarmTypeString, storage, location, perms, enabled, std); // Display the resource information. Because the user requested // the information, don't raise a KNotify event. KAMessageBox::information(this, text, QString(), QString(), KMessageBox::Options()); } } /****************************************************************************** * Return the currently selected resource in the list. */ Resource ResourceSelector::currentResource() const { return mListView->resource(mListView->selectionModel()->currentIndex()); } /****************************************************************************** * Return the currently selected resource type. */ CalEvent::Type ResourceSelector::currentResourceType() const { switch (mAlarmType->currentIndex()) { case 0: return CalEvent::ACTIVE; case 1: return CalEvent::ARCHIVED; case 2: return CalEvent::TEMPLATE; default: return CalEvent::EMPTY; } } void ResourceSelector::resizeEvent(QResizeEvent* re) { Q_EMIT resized(re->oldSize(), re->size()); } // vim: et sw=4: diff --git a/src/templatedlg.cpp b/src/templatedlg.cpp index 6136e18b..a7f5e615 100644 --- a/src/templatedlg.cpp +++ b/src/templatedlg.cpp @@ -1,243 +1,243 @@ /* * templatedlg.cpp - dialog to create, edit and delete alarm templates * Program: kalarm - * Copyright © 2004-2019 David Jarvie + * Copyright © 2004-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "templatedlg.h" #include "alarmcalendar.h" #include "editdlg.h" #include "functions.h" #include "newalarmaction.h" #include "templatelistview.h" #include "undo.h" #include "resources/datamodel.h" #include "resources/resources.h" #include "resources/eventmodel.h" #include "lib/config.h" #include "lib/messagebox.h" #include "lib/shellprocess.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include using namespace KCal; static const char TMPL_DIALOG_NAME[] = "TemplateDialog"; TemplateDlg* TemplateDlg::mInstance = nullptr; TemplateDlg::TemplateDlg(QWidget* parent) : QDialog(parent) { setModal(false); setWindowTitle(i18nc("@title:window", "Alarm Templates")); QBoxLayout* topLayout = new QVBoxLayout(this); - QBoxLayout* hlayout = new QHBoxLayout(this); + QBoxLayout* hlayout = new QHBoxLayout(); topLayout->addLayout(hlayout); QBoxLayout* layout = new QVBoxLayout(); layout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(layout); mListFilterModel = DataModel::createTemplateListModel(this); if (!ShellProcess::authorised()) mListFilterModel->setAlarmActionFilter(static_cast(KAEvent::ACT_ALL & ~KAEvent::ACT_COMMAND)); mListView = new TemplateListView(this); mListView->setModel(mListFilterModel); mListView->sortByColumn(TemplateListModel::TemplateNameColumn, Qt::AscendingOrder); mListView->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding)); mListView->setWhatsThis(i18nc("@info:whatsthis", "The list of alarm templates")); mListView->setItemDelegate(new TemplateListDelegate(mListView)); connect(mListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &TemplateDlg::slotSelectionChanged); layout->addWidget(mListView); layout = new QVBoxLayout(); layout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(layout); QPushButton* button = new QPushButton(i18nc("@action:button", "New")); mNewAction = new NewAlarmAction(true, i18nc("@action", "New"), this); button->setMenu(mNewAction->menu()); connect(mNewAction, &NewAlarmAction::selected, this, &TemplateDlg::slotNew); button->setWhatsThis(i18nc("@info:whatsthis", "Create a new alarm template")); layout->addWidget(button); mEditButton = new QPushButton(i18nc("@action:button", "Edit...")); connect(mEditButton, &QPushButton::clicked, this, &TemplateDlg::slotEdit); mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the currently highlighted alarm template")); layout->addWidget(mEditButton); mCopyButton = new QPushButton(i18nc("@action:button", "Copy")); connect(mCopyButton, &QPushButton::clicked, this, &TemplateDlg::slotCopy); mCopyButton->setWhatsThis(i18nc("@info:whatsthis", "Create a new alarm template based on a copy of the currently highlighted template")); layout->addWidget(mCopyButton); mDeleteButton = new QPushButton(i18nc("@action:button", "Delete")); connect(mDeleteButton, &QPushButton::clicked, this, &TemplateDlg::slotDelete); mDeleteButton->setWhatsThis(i18nc("@info:whatsthis", "Delete the currently highlighted alarm template")); layout->addWidget(mDeleteButton); layout->addStretch(); topLayout->addWidget(new KSeparator(Qt::Horizontal, this)); QDialogButtonBox* buttonBox = new QDialogButtonBox(this); buttonBox->addButton(QDialogButtonBox::Close); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::close); topLayout->addWidget(buttonBox); KActionCollection* actions = new KActionCollection(this); QAction* act = KStandardAction::selectAll(mListView, SLOT(selectAll()), actions); topLevelWidget()->addAction(act); act = KStandardAction::deselect(mListView, SLOT(clearSelection()), actions); topLevelWidget()->addAction(act); slotSelectionChanged(); // enable/disable buttons as appropriate QSize s; if (Config::readWindowSize(TMPL_DIALOG_NAME, s)) resize(s); } /****************************************************************************** * Destructor. */ TemplateDlg::~TemplateDlg() { mInstance = nullptr; } /****************************************************************************** * Create an instance, if none already exists. */ TemplateDlg* TemplateDlg::create(QWidget* parent) { if (mInstance) return nullptr; mInstance = new TemplateDlg(parent); return mInstance; } /****************************************************************************** * Called when the New Template button is clicked to create a new template. */ void TemplateDlg::slotNew(EditAlarmDlg::Type type) { KAlarm::editNewTemplate(type, mListView); } /****************************************************************************** * Called when the Copy button is clicked to edit a copy of an existing alarm, * to add to the list. */ void TemplateDlg::slotCopy() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editNewTemplate(&event, mListView); } /****************************************************************************** * Called when the Modify button is clicked to edit the currently highlighted * alarm in the list. */ void TemplateDlg::slotEdit() { KAEvent event = mListView->selectedEvent(); if (event.isValid()) KAlarm::editTemplate(&event, mListView); } /****************************************************************************** * Called when the Delete button is clicked to delete the currently highlighted * alarms in the list. */ void TemplateDlg::slotDelete() { QVector events = mListView->selectedEvents(); int n = events.count(); if (KAMessageBox::warningContinueCancel(this, i18ncp("@info", "Do you really want to delete the selected alarm template?", "Do you really want to delete the %1 selected alarm templates?", n), i18ncp("@title:window", "Delete Alarm Template", "Delete Alarm Templates", n), KGuiItem(i18nc("@action:button", "&Delete"), QStringLiteral("edit-delete"))) != KMessageBox::Continue) return; KAEvent::List delEvents; Undo::EventList undos; for (int i = 0; i < n; ++i) { KAEvent* event = &events[i]; delEvents.append(event); undos.append(*event, Resources::resourceForEvent(event->id())); } KAlarm::deleteTemplates(delEvents, this); Undo::saveDeletes(undos); } /****************************************************************************** * Called when the group of items selected changes. * Enable/disable the buttons depending on whether/how many templates are * currently highlighted. */ void TemplateDlg::slotSelectionChanged() { - AlarmCalendar* resources = AlarmCalendar::resources(); + ResourcesCalendar* resources = ResourcesCalendar::instance(); QVector events = mListView->selectedEvents(); int count = events.count(); bool readOnly = false; for (int i = 0; i < count; ++i) { const KAEvent* event = &events[i]; if (resources->eventReadOnly(event->id())) { readOnly = true; break; } } mEditButton->setEnabled(count == 1); mCopyButton->setEnabled(count == 1); mDeleteButton->setEnabled(count && !readOnly); } /****************************************************************************** * Called when the dialog's size has changed. * Records the new size in the config file. */ void TemplateDlg::resizeEvent(QResizeEvent* re) { if (isVisible()) Config::writeWindowSize(TMPL_DIALOG_NAME, re->size()); QDialog::resizeEvent(re); } // vim: et sw=4: diff --git a/src/templatemenuaction.cpp b/src/templatemenuaction.cpp index 4abae824..8742ddf4 100644 --- a/src/templatemenuaction.cpp +++ b/src/templatemenuaction.cpp @@ -1,85 +1,85 @@ /* * templatemenuaction.cpp - menu action to select a template * Program: kalarm - * Copyright © 2005,2006,2008,2011 David Jarvie + * Copyright © 2005-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "templatemenuaction.h" #include "alarmcalendar.h" #include "functions.h" #include "kalarm_debug.h" #include #include #include TemplateMenuAction::TemplateMenuAction(const QIcon& icon, const QString& label, QObject* parent) : KActionMenu(icon, label, parent) { setDelayed(false); connect(menu(), &QMenu::aboutToShow, this, &TemplateMenuAction::slotInitMenu); connect(menu(), &QMenu::triggered, this, &TemplateMenuAction::slotSelected); } /****************************************************************************** * Called when the New From Template action is clicked. * Creates a popup menu listing all alarm templates, in sorted name order. */ void TemplateMenuAction::slotInitMenu() { QMenu* m = menu(); m->clear(); mOriginalTexts.clear(); // Compile a sorted list of template names int i, end; QStringList sorted; KAEvent::List templates = KAlarm::templateList(); for (i = 0, end = templates.count(); i < end; ++i) { QString name = templates[i]->templateName(); int j = 0; for (int jend = sorted.count(); j < jend && QString::localeAwareCompare(name, sorted[j]) > 0; ++j) ; sorted.insert(j, name); } for (i = 0, end = sorted.count(); i < end; ++i) { QAction* act = m->addAction(sorted[i]); mOriginalTexts[act] = sorted[i]; // keep original text, since action text has shortcuts added } } /****************************************************************************** * Called when a template is selected from the New From Template popup menu. * Executes a New Alarm dialog, preset from the selected template. */ void TemplateMenuAction::slotSelected(QAction* action) { QMap::ConstIterator it = mOriginalTexts.constFind(action); if (it == mOriginalTexts.constEnd() || it.value().isEmpty()) return; - KAEvent* templ = AlarmCalendar::resources()->templateEvent(it.value()); + KAEvent* templ = ResourcesCalendar::instance()->templateEvent(it.value()); Q_EMIT selected(templ); } // vim: et sw=4: diff --git a/src/traywindow.cpp b/src/traywindow.cpp index ab0d58be..63438b5f 100644 --- a/src/traywindow.cpp +++ b/src/traywindow.cpp @@ -1,427 +1,427 @@ /* * traywindow.cpp - the KDE system tray applet * Program: kalarm - * Copyright © 2002-2019 David Jarvie + * Copyright © 2002-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "traywindow.h" #include "alarmcalendar.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "messagewin.h" #include "newalarmaction.h" #include "prefdlg.h" #include "preferences.h" #include "templatemenuaction.h" #include "resources/datamodel.h" #include "resources/eventmodel.h" #include "lib/synchtimer.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KAlarmCal; struct TipItem { QDateTime dateTime; QString text; }; /*============================================================================= = Class: TrayWindow = The KDE system tray window. =============================================================================*/ TrayWindow::TrayWindow(MainWindow* parent) : KStatusNotifierItem(parent) , mAssocMainWindow(parent) , mStatusUpdateTimer(new QTimer(this)) { qCDebug(KALARM_LOG) << "TrayWindow:"; setToolTipIconByName(QStringLiteral("kalarm")); setToolTipTitle(KAboutData::applicationData().displayName()); setIconByName(QStringLiteral("kalarm")); setStatus(KStatusNotifierItem::Active); // Set up the context menu mActionEnabled = KAlarm::createAlarmEnableAction(this); addAction(QStringLiteral("tAlarmsEnable"), mActionEnabled); contextMenu()->addAction(mActionEnabled); connect(theApp(), &KAlarmApp::alarmEnabledToggled, this, &TrayWindow::setEnabledStatus); contextMenu()->addSeparator(); mActionNew = new NewAlarmAction(false, i18nc("@action", "&New Alarm"), this); addAction(QStringLiteral("tNew"), mActionNew); contextMenu()->addAction(mActionNew); connect(mActionNew, &NewAlarmAction::selected, this, &TrayWindow::slotNewAlarm); connect(mActionNew->fromTemplateAlarmAction(QString()), &TemplateMenuAction::selected, this, &TrayWindow::slotNewFromTemplate); contextMenu()->addSeparator(); QAction* a = KAlarm::createStopPlayAction(this); addAction(QStringLiteral("tStopPlay"), a); contextMenu()->addAction(a); QObject::connect(theApp(), &KAlarmApp::audioPlaying, a, &QAction::setVisible); QObject::connect(theApp(), &KAlarmApp::audioPlaying, this, &TrayWindow::updateStatus); a = KAlarm::createSpreadWindowsAction(this); addAction(QStringLiteral("tSpread"), a); contextMenu()->addAction(a); contextMenu()->addSeparator(); contextMenu()->addAction(KStandardAction::preferences(this, SLOT(slotPreferences()), this)); // Disable standard quit behaviour. We have to intercept the quit even, // if the main window is hidden. QAction* act = action(QStringLiteral("quit")); if (act) { act->disconnect(SIGNAL(triggered(bool)), this, SLOT(maybeQuit())); connect(act, &QAction::triggered, this, &TrayWindow::slotQuit); } // Set icon to correspond with the alarms enabled menu status setEnabledStatus(theApp()->alarmsEnabled()); - connect(AlarmCalendar::resources(), &AlarmCalendar::haveDisabledAlarmsChanged, this, &TrayWindow::slotHaveDisabledAlarms); + connect(ResourcesCalendar::instance(), &ResourcesCalendar::haveDisabledAlarmsChanged, this, &TrayWindow::slotHaveDisabledAlarms); connect(this, &TrayWindow::activateRequested, this, &TrayWindow::slotActivateRequested); connect(this, &TrayWindow::secondaryActivateRequested, this, &TrayWindow::slotSecondaryActivateRequested); - slotHaveDisabledAlarms(AlarmCalendar::resources()->haveDisabledAlarms()); + slotHaveDisabledAlarms(ResourcesCalendar::instance()->haveDisabledAlarms()); // Hack: KSNI does not let us know when it is about to show the tooltip, // so we need to update it whenever something change in it. // This timer ensures that updateToolTip() is not called several times in a row mToolTipUpdateTimer = new QTimer(this); mToolTipUpdateTimer->setInterval(0); mToolTipUpdateTimer->setSingleShot(true); connect(mToolTipUpdateTimer, &QTimer::timeout, this, &TrayWindow::updateToolTip); // Update every minute to show accurate deadlines MinuteTimer::connect(mToolTipUpdateTimer, SLOT(start())); // Update when alarms are modified AlarmListModel* all = DataModel::allAlarmListModel(); connect(all, SIGNAL(dataChanged(QModelIndex,QModelIndex)), mToolTipUpdateTimer, SLOT(start())); connect(all, SIGNAL(rowsInserted(QModelIndex,int,int)), mToolTipUpdateTimer, SLOT(start())); connect(all, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), mToolTipUpdateTimer, SLOT(start())); connect(all, SIGNAL(rowsRemoved(QModelIndex,int,int)), mToolTipUpdateTimer, SLOT(start())); connect(all, SIGNAL(modelReset()), mToolTipUpdateTimer, SLOT(start())); // Set auto-hide status when next alarm or preferences change mStatusUpdateTimer->setSingleShot(true); connect(mStatusUpdateTimer, &QTimer::timeout, this, &TrayWindow::updateStatus); - connect(AlarmCalendar::resources(), &AlarmCalendar::earliestAlarmChanged, this, &TrayWindow::updateStatus); + connect(ResourcesCalendar::instance(), &ResourcesCalendar::earliestAlarmChanged, this, &TrayWindow::updateStatus); Preferences::connect(SIGNAL(autoHideSystemTrayChanged(int)), this, SLOT(updateStatus())); updateStatus(); // Update when tooltip preferences are modified Preferences::connect(SIGNAL(tooltipPreferencesChanged()), mToolTipUpdateTimer, SLOT(start())); } TrayWindow::~TrayWindow() { qCDebug(KALARM_LOG) << "~TrayWindow"; theApp()->removeWindow(this); Q_EMIT deleted(); } /****************************************************************************** * Called when the "New Alarm" menu item is selected to edit a new alarm. */ void TrayWindow::slotNewAlarm(EditAlarmDlg::Type type) { KAlarm::editNewAlarm(type); } /****************************************************************************** * Called when the "New Alarm" menu item is selected to edit a new alarm from a * template. */ void TrayWindow::slotNewFromTemplate(const KAEvent* event) { KAlarm::editNewAlarm(event); } /****************************************************************************** * Called when the "Configure KAlarm" menu item is selected. */ void TrayWindow::slotPreferences() { KAlarmPrefDlg::display(); } /****************************************************************************** * Called when the Quit context menu item is selected. * Note that KAlarmApp::doQuit() must be called by the event loop, not directly * from the menu item, since otherwise the tray icon will be deleted while still * processing the menu, resulting in a crash. * Ideally, the connect() call setting up this slot in the constructor would use * Qt::QueuedConnection, but the slot is never called in that case. */ void TrayWindow::slotQuit() { // Note: QTimer::singleShot(0, ...) never calls the slot. QTimer::singleShot(1, this, &TrayWindow::slotQuitAfter); } void TrayWindow::slotQuitAfter() { theApp()->doQuit(static_cast(parent())); } /****************************************************************************** * Called when the Alarms Enabled action status has changed. * Updates the alarms enabled menu item check state, and the icon pixmap. */ void TrayWindow::setEnabledStatus(bool status) { qCDebug(KALARM_LOG) << "TrayWindow::setEnabledStatus:" << status; updateIcon(); updateStatus(); updateToolTip(); } /****************************************************************************** * Called when individual alarms are enabled or disabled. * Set the enabled icon to show or hide a disabled indication. */ void TrayWindow::slotHaveDisabledAlarms(bool haveDisabled) { qCDebug(KALARM_LOG) << "TrayWindow::slotHaveDisabledAlarms:" << haveDisabled; mHaveDisabledAlarms = haveDisabled; updateIcon(); updateToolTip(); } /****************************************************************************** * Show the associated main window. */ void TrayWindow::showAssocMainWindow() { if (mAssocMainWindow) { mAssocMainWindow->show(); mAssocMainWindow->raise(); mAssocMainWindow->activateWindow(); } } /****************************************************************************** * A left click displays the KAlarm main window. */ void TrayWindow::slotActivateRequested() { // Left click: display/hide the first main window if (mAssocMainWindow && mAssocMainWindow->isVisible()) { mAssocMainWindow->raise(); mAssocMainWindow->activateWindow(); } } /****************************************************************************** * A middle button click displays the New Alarm window. */ void TrayWindow::slotSecondaryActivateRequested() { if (mActionNew->isEnabled()) mActionNew->trigger(); // display a New Alarm dialog } /****************************************************************************** * Adjust icon auto-hide status according to when the next alarm is due. * The icon is always shown if audio is playing, to give access to the 'stop' * menu option. */ void TrayWindow::updateStatus() { mStatusUpdateTimer->stop(); int period = Preferences::autoHideSystemTray(); // If the icon is always to be shown (AutoHideSystemTray = 0), // or audio is playing, show the icon. bool active = !period || MessageWin::isAudioPlaying(); if (!active) { // Show the icon only if the next active alarm complies active = theApp()->alarmsEnabled(); if (active) { - KAEvent* event = AlarmCalendar::resources()->earliestAlarm(); + KAEvent* event = ResourcesCalendar::instance()->earliestAlarm(); active = static_cast(event); if (event && period > 0) { const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); qint64 delay = KADateTime::currentLocalDateTime().secsTo(dt); delay -= static_cast(period) * 60; // delay until icon to be shown active = (delay <= 0); if (!active) { // First alarm trigger is too far in future, so tray icon is to // be auto-hidden. Set timer for when it should be shown again. delay *= 1000; // convert to msec int delay_int = static_cast(delay); if (delay_int != delay) delay_int = INT_MAX; mStatusUpdateTimer->setInterval(delay_int); mStatusUpdateTimer->start(); } } } } setStatus(active ? Active : Passive); } /****************************************************************************** * Adjust tooltip according to the app state. * The tooltip text shows alarms due in the next 24 hours. The limit of 24 * hours is because only times, not dates, are displayed. */ void TrayWindow::updateToolTip() { bool enabled = theApp()->alarmsEnabled(); QString subTitle; if (enabled && Preferences::tooltipAlarmCount()) subTitle = tooltipAlarmText(); if (!enabled) subTitle = i18n("Disabled"); else if (mHaveDisabledAlarms) { if (!subTitle.isEmpty()) subTitle += QLatin1String("
"); subTitle += i18nc("@info:tooltip Brief: some alarms are disabled", "(Some alarms disabled)"); } setToolTipSubTitle(subTitle); } /****************************************************************************** * Adjust icon according to the app state. */ void TrayWindow::updateIcon() { setIconByName(!theApp()->alarmsEnabled() ? QStringLiteral("kalarm-disabled") : mHaveDisabledAlarms ? QStringLiteral("kalarm-partdisabled") : QStringLiteral("kalarm")); } /****************************************************************************** * Return the tooltip text showing alarms due in the next 24 hours. * The limit of 24 hours is because only times, not dates, are displayed. */ QString TrayWindow::tooltipAlarmText() const { KAEvent event; const QString& prefix = Preferences::tooltipTimeToPrefix(); int maxCount = Preferences::tooltipAlarmCount(); const KADateTime now = KADateTime::currentLocalDateTime(); const KADateTime tomorrow = now.addDays(1); // Get today's and tomorrow's alarms, sorted in time order int i, iend; QList items; QVector events = KAlarm::getSortedActiveEvents(const_cast(this), &mAlarmsModel); for (i = 0, iend = events.count(); i < iend; ++i) { KAEvent* event = &events[i]; if (event->actionSubType() == KAEvent::MESSAGE) { TipItem item; QDateTime dateTime = event->nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone().qDateTime(); if (dateTime > tomorrow.qDateTime()) break; // ignore alarms after tomorrow at the current clock time item.dateTime = dateTime; // The alarm is due today, or early tomorrow if (Preferences::showTooltipAlarmTime()) { item.text += QLocale().toString(item.dateTime.time(), QLocale::ShortFormat); item.text += QLatin1Char(' '); } if (Preferences::showTooltipTimeToAlarm()) { int mins = (now.qDateTime().secsTo(item.dateTime) + 59) / 60; if (mins < 0) mins = 0; char minutes[3] = "00"; minutes[0] = static_cast((mins%60) / 10 + '0'); minutes[1] = static_cast((mins%60) % 10 + '0'); if (Preferences::showTooltipAlarmTime()) item.text += i18nc("@info prefix + hours:minutes", "(%1%2:%3)", prefix, mins/60, QLatin1String(minutes)); else item.text += i18nc("@info prefix + hours:minutes", "%1%2:%3", prefix, mins/60, QLatin1String(minutes)); item.text += QLatin1Char(' '); } item.text += AlarmText::summary(*event); // Insert the item into the list in time-sorted order int it = 0; for (int itend = items.count(); it < itend; ++it) { if (item.dateTime <= items[it].dateTime) break; } items.insert(it, item); } } qCDebug(KALARM_LOG) << "TrayWindow::tooltipAlarmText"; QString text; int count = 0; for (i = 0, iend = items.count(); i < iend; ++i) { qCDebug(KALARM_LOG) << "TrayWindow::tooltipAlarmText: --" << (count+1) << ")" << items[i].text; if (i > 0) text += QLatin1String("
"); text += items[i].text; if (++count == maxCount) break; } return text; } /****************************************************************************** * Called when the associated main window is closed. */ void TrayWindow::removeWindow(MainWindow* win) { if (win == mAssocMainWindow) mAssocMainWindow = nullptr; } // vim: et sw=4: diff --git a/src/undo.cpp b/src/undo.cpp index 698e8338..9cfd3d0f 100644 --- a/src/undo.cpp +++ b/src/undo.cpp @@ -1,1441 +1,1441 @@ /* * undo.cpp - undo/redo facility * Program: kalarm - * Copyright © 2005-2019 David Jarvie + * Copyright © 2005-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "undo.h" #include "alarmcalendar.h" #include "functions.h" #include "resources/resources.h" #include "lib/messagebox.h" #include "kalarm_debug.h" #include #include #include #include static int maxCount = 12; #ifdef DELETE #undef DELETE // conflicting Windows macro #endif class UndoItem { public: enum Operation { ADD, EDIT, DELETE, REACTIVATE, DEACTIVATE, MULTI }; UndoItem(); // needed by QList virtual ~UndoItem(); virtual Operation operation() const = 0; virtual QString actionText() const { return !mName.isEmpty() ? mName : defaultActionText(); } virtual QString defaultActionText() const = 0; virtual QString description() const { return QString(); } virtual QString eventID() const { return QString(); } virtual QString oldEventID() const { return QString(); } virtual QString newEventID() const { return QString(); } virtual Resource resource() const { return Resource(); } int id() const { return mId; } Undo::Type type() const { return mType; } void setType(Undo::Type t) { mType = t; } CalEvent::Type calendar() const { return mCalendar; } virtual void setCalendar(CalEvent::Type s) { mCalendar = s; } virtual UndoItem* restore() = 0; virtual bool deleteID(const QString& /*id*/) { return false; } virtual void dumpDebug() const; enum Error { ERR_NONE, ERR_PROG, ERR_NOT_FOUND, ERR_CREATE, ERR_TEMPLATE, ERR_ARCHIVED }; enum Warning { WARN_NONE, WARN_KORG_ADD, WARN_KORG_MODIFY, WARN_KORG_DELETE }; static int mLastId; static Error mRestoreError; // error code valid only if restore() returns 0 static Warning mRestoreWarning; // warning code set by restore() static KAlarm::UpdateResult mRestoreWarningKorg; // KOrganizer error status set by restore() static int mRestoreWarningCount; // item count for mRestoreWarning (to allow i18n messages to work correctly) protected: UndoItem(Undo::Type, const QString& name = QString()); static QString addDeleteActionText(CalEvent::Type, bool add); QString description(const KAEvent&) const; void replaceWith(UndoItem* item) { Undo::replace(this, item); } virtual void dumpDebugTitle(const char* typeName) const; QString mName; // specified action name (overrides default) int mId {0}; // unique identifier (only for mType = UNDO, REDO) Undo::Type mType; // which list (if any) the object is in CalEvent::Type mCalendar; }; class UndoMultiBase : public UndoItem { public: UndoMultiBase(Undo::Type t, const QString& name) : UndoItem(t, name), mUndos(new Undo::List) {} UndoMultiBase(Undo::Type t, Undo::List* undos, const QString& name) : UndoItem(t, name), mUndos(undos) {} ~UndoMultiBase() { delete mUndos; } const Undo::List* undos() const { return mUndos; } protected: void dumpDebugTitle(const char* typeName) const override; Undo::List* mUndos; // this list must always have >= 2 entries }; template class UndoMulti : public UndoMultiBase { public: UndoMulti(Undo::Type, const Undo::EventList&, const QString& name); UndoMulti(Undo::Type t, Undo::List* undos, const QString& name) : UndoMultiBase(t, undos, name) {} Operation operation() const override { return MULTI; } UndoItem* restore() override; bool deleteID(const QString& id) override; virtual UndoItem* createRedo(Undo::List*) = 0; }; class UndoAdd : public UndoItem { public: UndoAdd(Undo::Type, const Undo::Event&, const QString& name = QString()); UndoAdd(Undo::Type, const KAEvent&, const Resource&, const QString& name = QString()); UndoAdd(Undo::Type, const KAEvent&, const Resource&, const QString& name, CalEvent::Type); Operation operation() const override { return ADD; } QString defaultActionText() const override; QString description() const override { return mDescription; } Resource resource() const override { return mResource; } QString eventID() const override { return mEventId; } QString newEventID() const override { return mEventId; } UndoItem* restore() override { return doRestore(); } void dumpDebug() const override; protected: UndoItem* doRestore(bool setArchive = false); virtual UndoItem* createRedo(const KAEvent&, const Resource&); void dumpDebugTitle(const char* typeName) const override; private: Resource mResource; // resource containing the event QString mEventId; QString mDescription; }; class UndoEdit : public UndoItem { public: UndoEdit(Undo::Type, const KAEvent& oldEvent, const QString& newEventID, const Resource&, const QStringList& dontShowErrors, const QString& description); ~UndoEdit(); Operation operation() const override { return EDIT; } QString defaultActionText() const override; QString description() const override { return mDescription; } Resource resource() const override { return mResource; } QString eventID() const override { return mNewEventId; } QString oldEventID() const override { return mOldEvent->id(); } QString newEventID() const override { return mNewEventId; } UndoItem* restore() override; void dumpDebug() const override; protected: void dumpDebugTitle(const char* typeName) const override; private: Resource mResource; // resource containing the event KAEvent* mOldEvent; QString mNewEventId; QString mDescription; QStringList mDontShowErrors; }; class UndoDelete : public UndoItem { public: UndoDelete(Undo::Type, const Undo::Event&, const QString& name = QString()); UndoDelete(Undo::Type, const KAEvent&, const Resource&, const QStringList& dontShowErrors, const QString& name = QString()); ~UndoDelete(); Operation operation() const override { return DELETE; } QString defaultActionText() const override; QString description() const override { return UndoItem::description(*mEvent); } Resource resource() const override { return mResource; } QString eventID() const override { return mEvent->id(); } QString oldEventID() const override { return mEvent->id(); } UndoItem* restore() override; KAEvent* event() const { return mEvent; } void dumpDebug() const override; protected: virtual UndoItem* createRedo(const KAEvent&, const Resource&); void dumpDebugTitle(const char* typeName) const override; private: Resource mResource; // resource containing the event KAEvent* mEvent; QStringList mDontShowErrors; }; class UndoReactivate : public UndoAdd { public: UndoReactivate(Undo::Type t, const Undo::Event& e, const QString& name = QString()) : UndoAdd(t, e.event, e.resource, name, CalEvent::ACTIVE) {} UndoReactivate(Undo::Type t, const KAEvent& e, const Resource& r, const QString& name = QString()) : UndoAdd(t, e, r, name, CalEvent::ACTIVE) {} Operation operation() const override { return REACTIVATE; } QString defaultActionText() const override; UndoItem* restore() override; void dumpDebug() const override; protected: UndoItem* createRedo(const KAEvent&, const Resource&) override; }; class UndoDeactivate : public UndoDelete { public: UndoDeactivate(Undo::Type t, const KAEvent& e, const Resource& r, const QString& name = QString()) : UndoDelete(t, e, r, QStringList(), name) {} Operation operation() const override { return DEACTIVATE; } QString defaultActionText() const override; UndoItem* restore() override; void dumpDebug() const override; protected: UndoItem* createRedo(const KAEvent&, const Resource&) override; }; class UndoAdds : public UndoMulti { public: UndoAdds(Undo::Type t, const Undo::EventList& events, const QString& name = QString()) : UndoMulti(t, events, name) {} // UNDO only UndoAdds(Undo::Type t, Undo::List* undos, const QString& name) : UndoMulti(t, undos, name) {} QString defaultActionText() const override; UndoItem* createRedo(Undo::List*) override; void dumpDebug() const override; }; class UndoDeletes : public UndoMulti { public: UndoDeletes(Undo::Type t, const Undo::EventList& events, const QString& name = QString()) : UndoMulti(t, events, name) {} // UNDO only UndoDeletes(Undo::Type t, Undo::List* undos, const QString& name) : UndoMulti(t, undos, name) {} QString defaultActionText() const override; UndoItem* createRedo(Undo::List*) override; void dumpDebug() const override; }; class UndoReactivates : public UndoMulti { public: UndoReactivates(Undo::Type t, const Undo::EventList& events, const QString& name = QString()) : UndoMulti(t, events, name) {} // UNDO only UndoReactivates(Undo::Type t, Undo::List* undos, const QString& name) : UndoMulti(t, undos, name) {} QString defaultActionText() const override; UndoItem* createRedo(Undo::List*) override; void dumpDebug() const override; }; Undo* Undo::mInstance = nullptr; Undo::List Undo::mUndoList; Undo::List Undo::mRedoList; /****************************************************************************** * Create the one and only instance of the Undo class. */ Undo* Undo::instance() { if (!mInstance) mInstance = new Undo(qApp); return mInstance; } /****************************************************************************** * Clear the lists of undo and redo items. */ void Undo::clear() { if (!mUndoList.isEmpty() || !mRedoList.isEmpty()) { mInstance->blockSignals(true); while (!mUndoList.isEmpty()) delete mUndoList.first(); // N.B. 'delete' removes the object from the list while (!mRedoList.isEmpty()) delete mRedoList.first(); // N.B. 'delete' removes the object from the list mInstance->blockSignals(false); emitChanged(); } } /****************************************************************************** * Create an undo item and add it to the list of undos. * N.B. The base class constructor adds the object to the undo list. */ void Undo::saveAdd(const KAEvent& event, const Resource& resource, const QString& name) { new UndoAdd(UNDO, event, resource, name); emitChanged(); } void Undo::saveAdds(const Undo::EventList& events, const QString& name) { int count = events.count(); if (count == 1) saveAdd(events.first().event, events.first().resource, name); else if (count > 1) { new UndoAdds(UNDO, events, name); emitChanged(); } } void Undo::saveEdit(const Undo::Event& oldEvent, const KAEvent& newEvent) { new UndoEdit(UNDO, oldEvent.event, newEvent.id(), oldEvent.resource, oldEvent.dontShowErrors, AlarmText::summary(newEvent)); removeRedos(oldEvent.event.id()); // remove any redos which are made invalid by this edit emitChanged(); } void Undo::saveDelete(const Undo::Event& event, const QString& name) { new UndoDelete(UNDO, event.event, event.resource, event.dontShowErrors, name); removeRedos(event.event.id()); // remove any redos which are made invalid by this deletion emitChanged(); } void Undo::saveDeletes(const Undo::EventList& events, const QString& name) { int count = events.count(); if (count == 1) saveDelete(events[0], name); else if (count > 1) { new UndoDeletes(UNDO, events, name); for (const Undo::Event& event : events) removeRedos(event.event.id()); // remove any redos which are made invalid by these deletions emitChanged(); } } void Undo::saveReactivate(const KAEvent& event, const Resource& resource, const QString& name) { new UndoReactivate(UNDO, event, resource, name); emitChanged(); } void Undo::saveReactivates(const EventList& events, const QString& name) { int count = events.count(); if (count == 1) saveReactivate(events[0].event, events[0].resource, name); else if (count > 1) { new UndoReactivates(UNDO, events, name); emitChanged(); } } /****************************************************************************** * Remove any redos which are made invalid by a new undo. */ void Undo::removeRedos(const QString& eventID) { QString id = eventID; for (int i = 0; i < mRedoList.count(); ) { UndoItem* item = mRedoList[i]; if (item->operation() == UndoItem::MULTI) { if (item->deleteID(id)) { // The old multi-redo was replaced with a new single redo delete item; // N.B. 'delete' removes the object from the list } } else if (item->eventID() == id) { if (item->operation() == UndoItem::EDIT) id = item->oldEventID(); // continue looking for its post-edit ID delete item; // N.B. 'delete' removes the object from the list } else ++i; } } /****************************************************************************** * Undo or redo a specified item. * Reply = true if success, or if the item no longer exists. */ bool Undo::undo(int i, Undo::Type type, QWidget* parent, const QString& action) { UndoItem::mRestoreError = UndoItem::ERR_NONE; UndoItem::mRestoreWarning = UndoItem::WARN_NONE; UndoItem::mRestoreWarningKorg = KAlarm::UPDATE_OK; UndoItem::mRestoreWarningCount = 0; List& list = (type == UNDO) ? mUndoList : mRedoList; if (i < list.count() && list[i]->type() == type) { list[i]->restore(); delete list[i]; // N.B. 'delete' removes the object from its list emitChanged(); } QString err; switch (UndoItem::mRestoreError) { case UndoItem::ERR_NONE: { KAlarm::UpdateError errcode; switch (UndoItem::mRestoreWarning) { case UndoItem::WARN_KORG_ADD: errcode = KAlarm::ERR_ADD; break; case UndoItem::WARN_KORG_MODIFY: errcode = KAlarm::ERR_MODIFY; break; case UndoItem::WARN_KORG_DELETE: errcode = KAlarm::ERR_DELETE; break; case UndoItem::WARN_NONE: default: return true; } KAlarm::displayKOrgUpdateError(parent, errcode, UndoItem::mRestoreWarningKorg, UndoItem::mRestoreWarningCount); return true; } case UndoItem::ERR_NOT_FOUND: err = i18nc("@info", "Alarm not found"); break; case UndoItem::ERR_CREATE: err = i18nc("@info", "Error recreating alarm"); break; case UndoItem::ERR_TEMPLATE: err = i18nc("@info", "Error recreating alarm template"); break; case UndoItem::ERR_ARCHIVED: err = i18nc("@info", "Cannot reactivate archived alarm"); break; case UndoItem::ERR_PROG: err = i18nc("@info", "Program error"); break; default: err = i18nc("@info", "Unknown error"); break; } KAMessageBox::sorry(parent, i18nc("@info Undo-action: message", "%1: %2", action, err)); return false; } /****************************************************************************** * Add an undo item to the start of one of the lists. */ void Undo::add(UndoItem* item, bool undo) { if (item) { // Limit the number of items stored const int undoCount = mUndoList.count(); const int redoCount = mRedoList.count(); if (undoCount + redoCount >= maxCount - 1) { if (undoCount) mUndoList.pop_back(); else mRedoList.pop_back(); } // Append the new item List* const list = undo ? &mUndoList : &mRedoList; list->prepend(item); } } /****************************************************************************** * Remove an undo item from one of the lists. */ void Undo::remove(UndoItem* item, bool undo) { List* const list = undo ? &mUndoList : &mRedoList; if (!list->isEmpty()) list->removeAt(list->indexOf(item)); } /****************************************************************************** * Replace an undo item in one of the lists. */ void Undo::replace(UndoItem* old, UndoItem* New) { const Type type = old->type(); List* const list = (type == UNDO) ? &mUndoList : (type == REDO) ? &mRedoList : nullptr; if (!list) return; const int i = list->indexOf(old); if (i >= 0) { New->setType(type); // ensure the item points to the correct list (*list)[i] = New; old->setType(NONE); // mark the old item as no longer being in a list } } /****************************************************************************** * Return the action description of the latest undo/redo item. */ QString Undo::actionText(Undo::Type type) { const List* const list = (type == UNDO) ? &mUndoList : (type == REDO) ? &mRedoList : nullptr; return (list && !list->isEmpty()) ? (*list)[0]->actionText() : QString(); } /****************************************************************************** * Return the action description of the undo/redo item with the specified ID. */ QString Undo::actionText(Undo::Type type, int id) { const UndoItem* undo = getItem(id, type); return undo ? undo->actionText() : QString(); } /****************************************************************************** * Return the alarm description of the undo/redo item with the specified ID. */ QString Undo::description(Undo::Type type, int id) { const UndoItem* undo = getItem(id, type); return undo ? undo->description() : QString(); } /****************************************************************************** * Return the descriptions of all undo or redo items, in order latest first. * For alarms which have undergone more than one change, only the first one is * listed, to force dependent undos to be executed in their correct order. * If 'ids' is non-null, also returns a list of their corresponding IDs. */ QList Undo::ids(Undo::Type type) { QList ids; QStringList ignoreIDs; //int n=0; const List* const list = (type == UNDO) ? &mUndoList : (type == REDO) ? &mRedoList : nullptr; if (!list) return ids; for (const UndoItem* item : *list) { // Check whether this item should be ignored because it is a // dependent undo. If not, add this item's ID to the ignore list. bool omit = false; if (item->operation() == UndoItem::MULTI) { // If any item in a multi-undo is disqualified, omit the whole multi-undo QStringList newIDs; const Undo::List* undos = ((UndoMultiBase*)item)->undos(); for (const UndoItem* undo : *undos) { const QString evid = undo->eventID(); if (ignoreIDs.contains(evid)) omit = true; else if (omit) ignoreIDs.append(evid); else newIDs.append(evid); } if (omit) { for (const QString& newID : qAsConst(newIDs)) ignoreIDs.append(newID); } } else { omit = ignoreIDs.contains(item->eventID()); if (!omit) ignoreIDs.append(item->eventID()); if (item->operation() == UndoItem::EDIT) ignoreIDs.append(item->oldEventID()); // continue looking for its post-edit ID } if (!omit) ids.append(item->id()); //else qCDebug(KALARM_LOG)<<"Undo::ids(): omit"<actionText()<<":"<description(); } //qCDebug(KALARM_LOG)<<"Undo::ids():"<"<emitChanged(actionText(UNDO), actionText(REDO)); } /****************************************************************************** * Return the item with the specified ID. */ UndoItem* Undo::getItem(int id, Undo::Type type) { List* const list = (type == UNDO) ? &mUndoList : (type == REDO) ? &mRedoList : nullptr; if (list) { for (int i = 0, end = list->count(); i < end; ++i) { UndoItem* item = (*list)[i]; if (item->id() == id) return item; } } return nullptr; } /****************************************************************************** * Find an item with the specified ID. */ int Undo::findItem(int id, Undo::Type type) { const List& list = (type == UNDO) ? mUndoList : mRedoList; int i = 0; for (const UndoItem* item : list) { if (item->id() == id) break; } return i; } /****************************************************************************** * Dump the last 'count' undos or redos to debug, starting with the most recent. */ void Undo::dumpDebug(Undo::Type type, int count) { #ifndef KDE_NO_DEBUG_OUTPUT const List& list = (type == UNDO) ? mUndoList : mRedoList; if (count > list.count()) count = list.count(); qCDebug(KALARM_LOG) << "Undo::dumpDebug():" << count; for (int i = 0; i < count; ++i) list[i]->dumpDebug(); #endif } /*============================================================================= = Class: UndoItem = A single undo action. =============================================================================*/ int UndoItem::mLastId = 0; UndoItem::Error UndoItem::mRestoreError; UndoItem::Warning UndoItem::mRestoreWarning; KAlarm::UpdateResult UndoItem::mRestoreWarningKorg; int UndoItem::mRestoreWarningCount; /****************************************************************************** * Constructor. * Optionally appends the undo to the list of undos. */ UndoItem::UndoItem(Undo::Type type, const QString& name) : mName(name) , mType(type) , mCalendar(CalEvent::EMPTY) { if (type != Undo::NONE) { mId = ++mLastId; if (mId < 0) mId = mLastId = 1; // wrap round if we reach a negative number Undo::add(this, (mType == Undo::UNDO)); } } /****************************************************************************** * Destructor. * Removes the undo from the list (if it's in the list). */ UndoItem::~UndoItem() { if (mType != Undo::NONE) Undo::remove(this, (mType == Undo::UNDO)); } /****************************************************************************** * Return the description of an event. */ QString UndoItem::description(const KAEvent& event) const { return (mCalendar == CalEvent::TEMPLATE) ? event.templateName() : AlarmText::summary(event); } /****************************************************************************** * Return the action description of an add or delete Undo/Redo item for displaying. */ QString UndoItem::addDeleteActionText(CalEvent::Type calendar, bool add) { switch (calendar) { case CalEvent::ACTIVE: if (add) return i18nc("@info Action to create a new alarm", "New alarm"); else return i18nc("@info Action to delete an alarm", "Delete alarm"); case CalEvent::TEMPLATE: if (add) return i18nc("@info Action to create a new alarm template", "New template"); else return i18nc("@info Action to delete an alarm template", "Delete template"); case CalEvent::ARCHIVED: return i18nc("@info", "Delete archived alarm"); default: break; } return QString(); } /****************************************************************************** * Dump the instance's contents to debug. */ void UndoItem::dumpDebug() const { #ifndef KDE_NO_DEBUG_OUTPUT dumpDebugTitle("UndoItem"); #endif } void UndoItem::dumpDebugTitle(const char* typeName) const { #ifndef KDE_NO_DEBUG_OUTPUT qCDebug(KALARM_LOG) << typeName << "begin:"; qCDebug(KALARM_LOG) << "-- mName: " << mName; qCDebug(KALARM_LOG) << "-- mId: " << mId; qCDebug(KALARM_LOG) << "-- mType: " << (mType == Undo::UNDO ? "Undo" : mType == Undo::REDO ? "Redo" : "None"); qCDebug(KALARM_LOG) << "-- mCalendar: " << mCalendar; #endif } /*============================================================================= = Class: UndoMultiBase = Undo item for multiple alarms. =============================================================================*/ /****************************************************************************** * Dump the instance's contents to debug. */ void UndoMultiBase::dumpDebugTitle(const char* typeName) const { #ifndef KDE_NO_DEBUG_OUTPUT UndoItem::dumpDebugTitle(typeName); qCDebug(KALARM_LOG) << "-- mUndos count:" << mUndos->count(); #endif } /*============================================================================= = Class: UndoMulti = Undo item for multiple alarms. =============================================================================*/ template UndoMulti::UndoMulti(Undo::Type type, const Undo::EventList& events, const QString& name) : UndoMultiBase(type, name) // UNDO only { for (const Undo::Event& event : events) mUndos->append(new T(Undo::NONE, event)); } /****************************************************************************** * Undo the item, i.e. restore multiple alarms which were deleted (or delete * alarms which were restored). * Create a redo item to delete (or restore) the alarms again. * Reply = redo item. */ template UndoItem* UndoMulti::restore() { Undo::List* newUndos = new Undo::List; for (int i = 0, end = mUndos->count(); i < end; ++i) { UndoItem* undo = (*mUndos)[i]->restore(); if (undo) newUndos->append(undo); } if (newUndos->isEmpty()) { delete newUndos; return nullptr; } // Create a redo item to delete the alarm again return createRedo(newUndos); } /****************************************************************************** * If one of the multiple items has the specified ID, delete it. * If an item is deleted and there is only one item left, the UndoMulti * instance is removed from its list and replaced by the remaining UndoItem instead. * Reply = true if this instance was replaced. The caller must delete it. * = false otherwise. */ template bool UndoMulti::deleteID(const QString& id) { for (int i = 0, end = mUndos->count(); i < end; ++i) { UndoItem* item = (*mUndos)[i]; if (item->eventID() == id) { // Found a matching entry - remove it mUndos->removeAt(i); if (mUndos->count() == 1) { // There is only one entry left after removal. // Replace 'this' multi instance with the remaining single entry. replaceWith(item); return true; } else { delete item; return false; } } } return false; } /*============================================================================= = Class: UndoAdd = Undo item for alarm creation. =============================================================================*/ UndoAdd::UndoAdd(Undo::Type type, const Undo::Event& undo, const QString& name) : UndoItem(type, name) , mResource(undo.resource) , mEventId(undo.event.id()) { setCalendar(undo.event.category()); mDescription = UndoItem::description(undo.event); // calendar must be set before calling this } UndoAdd::UndoAdd(Undo::Type type, const KAEvent& event, const Resource& resource, const QString& name) : UndoItem(type, name) , mResource(resource) , mEventId(event.id()) { setCalendar(event.category()); mDescription = UndoItem::description(event); // calendar must be set before calling this } UndoAdd::UndoAdd(Undo::Type type, const KAEvent& event, const Resource& resource, const QString& name, CalEvent::Type cal) : UndoItem(type, name) , mResource(resource) , mEventId(CalEvent::uid(event.id(), cal)) // convert if old-style event ID { setCalendar(cal); mDescription = UndoItem::description(event); // calendar must be set before calling this } /****************************************************************************** * Undo the item, i.e. delete the alarm which was added. * Create a redo item to add the alarm back again. * Reply = redo item. */ UndoItem* UndoAdd::doRestore(bool setArchive) { // Retrieve the current state of the alarm qCDebug(KALARM_LOG) << "UndoAdd::doRestore:" << mEventId; - const KAEvent* ev = AlarmCalendar::getEvent(EventId(mResource.id(), mEventId)); + const KAEvent* ev = ResourcesCalendar::getEvent(EventId(mResource.id(), mEventId)); if (!ev) { mRestoreError = ERR_NOT_FOUND; // alarm is no longer in calendar return nullptr; } KAEvent event(*ev); // Create a redo item to recreate the alarm. // Do it now, since 'event' gets modified by KAlarm::deleteEvent() UndoItem* undo = createRedo(event, mResource); switch (calendar()) { case CalEvent::ACTIVE: { if (setArchive) event.setArchive(); // Archive it if it has already triggered KAlarm::UpdateResult status = KAlarm::deleteEvent(event, true); switch (status.status) { case KAlarm::UPDATE_ERROR: case KAlarm::UPDATE_FAILED: case KAlarm::SAVE_FAILED: mRestoreError = ERR_CREATE; break; case KAlarm::UPDATE_KORG_FUNCERR: case KAlarm::UPDATE_KORG_ERRINIT: case KAlarm::UPDATE_KORG_ERRSTART: case KAlarm::UPDATE_KORG_ERR: mRestoreWarning = WARN_KORG_DELETE; ++mRestoreWarningCount; if (status.status > mRestoreWarningKorg.status) mRestoreWarningKorg = status; break; default: break; } break; } case CalEvent::TEMPLATE: if (KAlarm::deleteTemplate(event) != KAlarm::UPDATE_OK) mRestoreError = ERR_TEMPLATE; break; case CalEvent::ARCHIVED: // redoing the deletion of an archived alarm KAlarm::deleteEvent(event); break; default: delete undo; mRestoreError = ERR_PROG; return nullptr; } return undo; } /****************************************************************************** * Create a redo item to add the alarm back again. */ UndoItem* UndoAdd::createRedo(const KAEvent& event, const Resource& resource) { const Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; return new UndoDelete(t, event, resource, QStringList(), mName); } /****************************************************************************** * Return the action description of the Undo item for displaying. */ QString UndoAdd::defaultActionText() const { return addDeleteActionText(calendar(), (type() == Undo::UNDO)); } /****************************************************************************** * Dump the instance's contents to debug. */ void UndoAdd::dumpDebug() const { #ifndef KDE_NO_DEBUG_OUTPUT dumpDebugTitle("UndoAdd"); #endif } void UndoAdd::dumpDebugTitle(const char* typeName) const { #ifndef KDE_NO_DEBUG_OUTPUT UndoItem::dumpDebugTitle(typeName); qCDebug(KALARM_LOG) << "-- mResource: " << mResource.id(); qCDebug(KALARM_LOG) << "-- mEventId: " << mEventId; qCDebug(KALARM_LOG) << "-- mDescript: " << mDescription; #endif } /*============================================================================= = Class: UndoAdds = Undo item for multiple alarm creation. =============================================================================*/ /****************************************************************************** * Create a redo item to add the alarms again. */ UndoItem* UndoAdds::createRedo(Undo::List* undos) { const Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; return new UndoAdds(t, undos, mName); } /****************************************************************************** * Return the action description of the Undo item for displaying. */ QString UndoAdds::defaultActionText() const { return i18nc("@info", "Create multiple alarms"); } /****************************************************************************** * Dump the instance's contents to debug. */ void UndoAdds::dumpDebug() const { #ifndef KDE_NO_DEBUG_OUTPUT dumpDebugTitle("UndoAdds"); #endif } /*============================================================================= = Class: UndoEdit = Undo item for alarm edit. =============================================================================*/ UndoEdit::UndoEdit(Undo::Type type, const KAEvent& oldEvent, const QString& newEventID, const Resource& resource, const QStringList& dontShowErrors, const QString& description) : UndoItem(type) , mResource(resource) , mOldEvent(new KAEvent(oldEvent)) , mNewEventId(newEventID) , mDescription(description) , mDontShowErrors(dontShowErrors) { setCalendar(oldEvent.category()); } UndoEdit::~UndoEdit() { delete mOldEvent; } /****************************************************************************** * Undo the item, i.e. undo an edit to a previously existing alarm. * Create a redo item to reapply the edit. * Reply = redo item. */ UndoItem* UndoEdit::restore() { qCDebug(KALARM_LOG) << "UndoEdit::restore:" << mNewEventId; // Retrieve the current state of the alarm - const KAEvent* event = AlarmCalendar::getEvent(EventId(mResource.id(), mNewEventId)); + const KAEvent* event = ResourcesCalendar::getEvent(EventId(mResource.id(), mNewEventId)); if (!event) { mRestoreError = ERR_NOT_FOUND; // alarm is no longer in calendar return nullptr; } KAEvent newEvent(*event); // Create a redo item to restore the edit const Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; UndoItem* undo = new UndoEdit(t, newEvent, mOldEvent->id(), mResource, KAlarm::dontShowErrors(EventId(newEvent)), mDescription); switch (calendar()) { case CalEvent::ACTIVE: { KAlarm::UpdateResult status = KAlarm::modifyEvent(newEvent, *mOldEvent); switch (status.status) { case KAlarm::UPDATE_ERROR: case KAlarm::UPDATE_FAILED: case KAlarm::SAVE_FAILED: mRestoreError = ERR_CREATE; break; case KAlarm::UPDATE_KORG_FUNCERR: case KAlarm::UPDATE_KORG_ERRINIT: case KAlarm::UPDATE_KORG_ERRSTART: case KAlarm::UPDATE_KORG_ERR: mRestoreWarning = WARN_KORG_MODIFY; ++mRestoreWarningCount; if (status.status > mRestoreWarningKorg.status) mRestoreWarningKorg = status; // fall through to default Q_FALLTHROUGH(); default: KAlarm::setDontShowErrors(EventId(*mOldEvent), mDontShowErrors); break; } break; } case CalEvent::TEMPLATE: if (KAlarm::updateTemplate(*mOldEvent) != KAlarm::UPDATE_OK) mRestoreError = ERR_TEMPLATE; break; case CalEvent::ARCHIVED: // editing of archived events is not allowed default: delete undo; mRestoreError = ERR_PROG; return nullptr; } return undo; } /****************************************************************************** * Return the action description of the Undo item for displaying. */ QString UndoEdit::defaultActionText() const { switch (calendar()) { case CalEvent::ACTIVE: return i18nc("@info Action to edit an alarm", "Edit alarm"); case CalEvent::TEMPLATE: return i18nc("@info Action to edit an alarm template", "Edit template"); default: break; } return QString(); } /****************************************************************************** * Dump the instance's contents to debug. */ void UndoEdit::dumpDebug() const { #ifndef KDE_NO_DEBUG_OUTPUT dumpDebugTitle("UndoEdit"); #endif } void UndoEdit::dumpDebugTitle(const char* typeName) const { #ifndef KDE_NO_DEBUG_OUTPUT UndoItem::dumpDebugTitle(typeName); qCDebug(KALARM_LOG) << "-- mResource: " << mResource.id(); qCDebug(KALARM_LOG) << "-- mOldEvent: " << mOldEvent->id(); qCDebug(KALARM_LOG) << "-- mNewEventId: " << mNewEventId; qCDebug(KALARM_LOG) << "-- mDescription:" << mDescription; qCDebug(KALARM_LOG) << "-- mDontShowErr:" << mDontShowErrors; #endif } /*============================================================================= = Class: UndoDelete = Undo item for alarm deletion. =============================================================================*/ UndoDelete::UndoDelete(Undo::Type type, const Undo::Event& undo, const QString& name) : UndoItem(type, name) , mResource(undo.resource) , mEvent(new KAEvent(undo.event)) , mDontShowErrors(undo.dontShowErrors) { setCalendar(mEvent->category()); } UndoDelete::UndoDelete(Undo::Type type, const KAEvent& event, const Resource& resource, const QStringList& dontShowErrors, const QString& name) : UndoItem(type, name) , mResource(resource) , mEvent(new KAEvent(event)) , mDontShowErrors(dontShowErrors) { setCalendar(mEvent->category()); } UndoDelete::~UndoDelete() { delete mEvent; } /****************************************************************************** * Undo the item, i.e. restore an alarm which was deleted. * Create a redo item to delete the alarm again. * Reply = redo item. */ UndoItem* UndoDelete::restore() { qCDebug(KALARM_LOG) << "UndoDelete::restore:" << mEvent->id(); // Restore the original event CalEvent::Type saveType = calendar(); switch (calendar()) { case CalEvent::ACTIVE: if (mEvent->toBeArchived()) { // It was archived when it was deleted mEvent->setCategory(CalEvent::ARCHIVED); mEvent->setResourceId(Resources::resourceForEvent(mEvent->id()).id()); const KAlarm::UpdateResult status = KAlarm::reactivateEvent(*mEvent, &mResource); switch (status.status) { case KAlarm::UPDATE_KORG_FUNCERR: case KAlarm::UPDATE_KORG_ERRINIT: case KAlarm::UPDATE_KORG_ERRSTART: case KAlarm::UPDATE_KORG_ERR: mRestoreWarning = WARN_KORG_ADD; ++mRestoreWarningCount; if (status.status > mRestoreWarningKorg.status) mRestoreWarningKorg = status; break; case KAlarm::UPDATE_ERROR: case KAlarm::UPDATE_FAILED: case KAlarm::SAVE_FAILED: mRestoreError = ERR_ARCHIVED; return nullptr; case KAlarm::UPDATE_OK: break; } } else { const KAlarm::UpdateResult status = KAlarm::addEvent(*mEvent, &mResource, nullptr, true); switch (status.status) { case KAlarm::UPDATE_KORG_FUNCERR: case KAlarm::UPDATE_KORG_ERRINIT: case KAlarm::UPDATE_KORG_ERRSTART: case KAlarm::UPDATE_KORG_ERR: mRestoreWarning = WARN_KORG_ADD; ++mRestoreWarningCount; if (status.status > mRestoreWarningKorg.status) mRestoreWarningKorg = status; break; case KAlarm::UPDATE_ERROR: case KAlarm::UPDATE_FAILED: case KAlarm::SAVE_FAILED: mRestoreError = ERR_CREATE; return nullptr; case KAlarm::UPDATE_OK: break; } } KAlarm::setDontShowErrors(EventId(*mEvent), mDontShowErrors); break; case CalEvent::TEMPLATE: if (KAlarm::addTemplate(*mEvent, &mResource) != KAlarm::UPDATE_OK) { mRestoreError = ERR_CREATE; return nullptr; } break; case CalEvent::ARCHIVED: if (!KAlarm::addArchivedEvent(*mEvent, &mResource)) { mRestoreError = ERR_CREATE; return nullptr; } break; default: mRestoreError = ERR_PROG; return nullptr; } // Create a redo item to delete the alarm again mEvent->setCategory(saveType); return createRedo(*mEvent, mResource); } /****************************************************************************** * Create a redo item to archive the alarm again. */ UndoItem* UndoDelete::createRedo(const KAEvent& event, const Resource& resource) { const Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; return new UndoAdd(t, event, resource, mName); } /****************************************************************************** * Return the action description of the Undo item for displaying. */ QString UndoDelete::defaultActionText() const { return addDeleteActionText(calendar(), (type() == Undo::REDO)); } /****************************************************************************** * Dump the instance's contents to debug. */ void UndoDelete::dumpDebug() const { #ifndef KDE_NO_DEBUG_OUTPUT dumpDebugTitle("UndoDelete"); #endif } void UndoDelete::dumpDebugTitle(const char* typeName) const { #ifndef KDE_NO_DEBUG_OUTPUT UndoItem::dumpDebugTitle(typeName); qCDebug(KALARM_LOG) << "-- mResource: " << mResource.id(); qCDebug(KALARM_LOG) << "-- mEvent: " << mEvent->id(); qCDebug(KALARM_LOG) << "-- mDontShowErr:" << mDontShowErrors; #endif } /*============================================================================= = Class: UndoDeletes = Undo item for multiple alarm deletion. =============================================================================*/ /****************************************************************************** * Create a redo item to delete the alarms again. */ UndoItem* UndoDeletes::createRedo(Undo::List* undos) { const Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; return new UndoDeletes(t, undos, mName); } /****************************************************************************** * Return the action description of the Undo item for displaying. */ QString UndoDeletes::defaultActionText() const { if (mUndos->isEmpty()) return QString(); for (const UndoItem* item : *mUndos) { switch (item->calendar()) { case CalEvent::ACTIVE: return i18nc("@info", "Delete multiple alarms"); case CalEvent::TEMPLATE: return i18nc("@info", "Delete multiple templates"); case CalEvent::ARCHIVED: break; // check if they are ALL archived default: return QString(); } } return i18nc("@info", "Delete multiple archived alarms"); } /****************************************************************************** * Dump the instance's contents to debug. */ void UndoDeletes::dumpDebug() const { #ifndef KDE_NO_DEBUG_OUTPUT dumpDebugTitle("UndoDeletes"); #endif } /*============================================================================= = Class: UndoReactivate = Undo item for alarm reactivation. =============================================================================*/ /****************************************************************************** * Undo the item, i.e. re-archive the alarm which was reactivated. * Create a redo item to reactivate the alarm back again. * Reply = redo item. */ UndoItem* UndoReactivate::restore() { qCDebug(KALARM_LOG) << "UndoReactivate::restore"; // Validate the alarm's calendar switch (calendar()) { case CalEvent::ACTIVE: break; default: mRestoreError = ERR_PROG; return nullptr; } return UndoAdd::doRestore(true); // restore alarm, ensuring that it is re-archived } /****************************************************************************** * Create a redo item to add the alarm back again. */ UndoItem* UndoReactivate::createRedo(const KAEvent& event, const Resource& resource) { const Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; return new UndoDeactivate(t, event, resource, mName); } /****************************************************************************** * Return the action description of the Undo item for displaying. */ QString UndoReactivate::defaultActionText() const { return i18nc("@info", "Reactivate alarm"); } /****************************************************************************** * Dump the instance's contents to debug. */ void UndoReactivate::dumpDebug() const { #ifndef KDE_NO_DEBUG_OUTPUT dumpDebugTitle("UndoReactivate"); #endif } /*============================================================================= = Class: UndoDeactivate = Redo item for alarm reactivation. =============================================================================*/ /****************************************************************************** * Undo the item, i.e. reactivate an alarm which was archived. * Create a redo item to archive the alarm again. * Reply = redo item. */ UndoItem* UndoDeactivate::restore() { qCDebug(KALARM_LOG) << "UndoDeactivate::restore"; // Validate the alarm's calendar switch (calendar()) { case CalEvent::ACTIVE: break; default: mRestoreError = ERR_PROG; return nullptr; } return UndoDelete::restore(); } /****************************************************************************** * Create a redo item to archive the alarm again. */ UndoItem* UndoDeactivate::createRedo(const KAEvent& event, const Resource& resource) { const Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; return new UndoReactivate(t, event, resource, mName); } /****************************************************************************** * Return the action description of the Undo item for displaying. */ QString UndoDeactivate::defaultActionText() const { return i18nc("@info", "Reactivate alarm"); } /****************************************************************************** * Dump the instance's contents to debug. */ void UndoDeactivate::dumpDebug() const { #ifndef KDE_NO_DEBUG_OUTPUT dumpDebugTitle("UndoDeactivate"); #endif } /*============================================================================= = Class: UndoReactivates = Undo item for multiple alarm reactivation. =============================================================================*/ /****************************************************************************** * Create a redo item to reactivate the alarms again. */ UndoItem* UndoReactivates::createRedo(Undo::List* undos) { const Undo::Type t = (type() == Undo::UNDO) ? Undo::REDO : (type() == Undo::REDO) ? Undo::UNDO : Undo::NONE; return new UndoReactivates(t, undos, mName); } /****************************************************************************** * Return the action description of the Undo item for displaying. */ QString UndoReactivates::defaultActionText() const { return i18nc("@info", "Reactivate multiple alarms"); } /****************************************************************************** * Dump the instance's contents to debug. */ void UndoReactivates::dumpDebug() const { #ifndef KDE_NO_DEBUG_OUTPUT dumpDebugTitle("UndoReactivates"); #endif } /*============================================================================= = Class: Event = Event details for external calls. =============================================================================*/ Undo::Event::Event(const KAEvent& e, const Resource& r) : event(e) , resource(r) { if (e.category() == CalEvent::ACTIVE) dontShowErrors = KAlarm::dontShowErrors(EventId(e)); } // vim: et sw=4: diff --git a/src/wakedlg.cpp b/src/wakedlg.cpp index e436e9d0..f0654fea 100644 --- a/src/wakedlg.cpp +++ b/src/wakedlg.cpp @@ -1,204 +1,204 @@ /* * wakedlg.cpp - dialog to configure wake-from-suspend alarms * Program: kalarm - * Copyright © 2011-2019 David Jarvie + * Copyright © 2011-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "wakedlg.h" #include "ui_wakedlg.h" #include "alarmcalendar.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "preferences.h" #include "lib/messagebox.h" #include "kalarm_debug.h" #include #include #include #include #include #include using namespace KAlarmCal; WakeFromSuspendDlg* WakeFromSuspendDlg::mInstance = nullptr; WakeFromSuspendDlg* WakeFromSuspendDlg::create(QWidget* parent) { if (!mInstance) mInstance = new WakeFromSuspendDlg(parent); return mInstance; } WakeFromSuspendDlg::WakeFromSuspendDlg(QWidget* parent) : QDialog(parent) { setAttribute(Qt::WA_DeleteOnClose); setWindowTitle(i18nc("@title:window", "Wake From Suspend")); mUi = new Ui_WakeFromSuspendDlgWidget; mUi->setupUi(this); mUi->advanceWakeTime->setValue(Preferences::wakeFromSuspendAdvance()); mMainWindow = qobject_cast(parent); if (!mMainWindow) mMainWindow = MainWindow::mainMainWindow(); // Check if there is any alarm selected in the main window, and enable/disable // the Show and Cancel buttons as necessary. enableDisableUseButton(); // Update the Show and Cancel button status every 5 seconds mTimer = new QTimer(this); connect(mTimer, &QTimer::timeout, this, &WakeFromSuspendDlg::checkPendingAlarm); mTimer->start(5000); connect(mMainWindow, &MainWindow::selectionChanged, this, &WakeFromSuspendDlg::enableDisableUseButton); connect(mUi->showWakeButton, &QPushButton::clicked, this, &WakeFromSuspendDlg::showWakeClicked); connect(mUi->useWakeButton, &QPushButton::clicked, this, &WakeFromSuspendDlg::useWakeClicked); connect(mUi->cancelWakeButton, &QPushButton::clicked, this, &WakeFromSuspendDlg::cancelWakeClicked); connect(mUi->buttonBox, &QDialogButtonBox::rejected, this, &WakeFromSuspendDlg::close); connect(theApp(), &KAlarmApp::alarmEnabledToggled, this, &WakeFromSuspendDlg::enableDisableUseButton); } WakeFromSuspendDlg::~WakeFromSuspendDlg() { if (mInstance == this) mInstance = nullptr; delete mUi; } /****************************************************************************** * Called when the alarm selection in the main window changes. * Enable or disable the Use Highlighted Alarm button. */ void WakeFromSuspendDlg::enableDisableUseButton() { bool enable = theApp()->alarmsEnabled(); if (enable) { const QString wakeFromSuspendId = KAlarm::checkRtcWakeConfig().value(0); const KAEvent event = mMainWindow->selectedEvent(); enable = event.isValid() && event.category() == CalEvent::ACTIVE && event.enabled() && !event.mainDateTime().isDateOnly() && event.id() != wakeFromSuspendId; } mUi->useWakeButton->setEnabled(enable); checkPendingAlarm(); } /****************************************************************************** * Update the Show and Cancel buttons if the pending alarm status has changed. * Reply = true if an alarm is still pending. */ bool WakeFromSuspendDlg::checkPendingAlarm() { if (KAlarm::checkRtcWakeConfig(true).isEmpty()) { mUi->showWakeButton->setEnabled(false); mUi->cancelWakeButton->setEnabled(false); return false; } return true; } /****************************************************************************** * Called when the user clicks the Show Current Alarm button. * Highlight the currently scheduled wake-from-suspend alarm in the main window. */ void WakeFromSuspendDlg::showWakeClicked() { if (checkPendingAlarm()) { const QStringList params = KAlarm::checkRtcWakeConfig(); if (!params.isEmpty()) { - const KAEvent* event = AlarmCalendar::resources()->event(EventId(params[0].toLongLong(), params[1])); + const KAEvent* event = ResourcesCalendar::instance()->event(EventId(params[0].toLongLong(), params[1])); if (event) { mMainWindow->selectEvent(event->id()); return; } } } mMainWindow->clearSelection(); } /****************************************************************************** * Called when the user clicks the Use Highlighted Alarm button. * Schedules system wakeup for that alarm. */ void WakeFromSuspendDlg::useWakeClicked() { const KAEvent event = mMainWindow->selectedEvent(); if (!event.isValid()) return; const KADateTime dt = event.mainDateTime().kDateTime(); if (dt.isDateOnly()) { KAMessageBox::sorry(this, i18nc("@info", "Cannot schedule wakeup time for a date-only alarm")); return; } if (KAMessageBox::warningContinueCancel(this, xi18nc("@info", "This wakeup will cancel any existing wakeup which has been set by KAlarm " "or any other application, because your computer can only schedule a single wakeup time." "Note: Wake From Suspend is not supported at all on some computers, especially older ones, " "and some computers only support setting a wakeup time up to 24 hours ahead. " "You may wish to set up a test alarm to check your system's capability."), QString(), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("wakeupWarning")) != KMessageBox::Continue) return; const int advance = mUi->advanceWakeTime->value(); #if KALARMCAL_VERSION >= QT_VERSION_CHECK(5,12,1) const qint64 triggerTime = dt.addSecs(-advance * 60).toSecsSinceEpoch(); #else unsigned triggerTime = dt.addSecs(-advance * 60).toTime_t(); #endif if (KAlarm::setRtcWakeTime(triggerTime, this)) { const QStringList param{QString::number(event.collectionId()), event.id(), QString::number(triggerTime)}; KConfigGroup config(KSharedConfig::openConfig(), "General"); config.writeEntry("RtcWake", param); config.sync(); Preferences::setWakeFromSuspendAdvance(advance); close(); } } /****************************************************************************** * Called when the user clicks the Cancel Wake From Suspend button. * Cancels any currently scheduled system wakeup. */ void WakeFromSuspendDlg::cancelWakeClicked() { KAlarm::setRtcWakeTime(0, this); KAlarm::deleteRtcWakeConfig(); mUi->showWakeButton->setEnabled(false); mUi->cancelWakeButton->setEnabled(false); enableDisableUseButton(); } // vim: et sw=4: