diff --git a/CMakeLists.txt b/CMakeLists.txt index 2feefd4e..dd28109f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,127 +1,127 @@ cmake_minimum_required(VERSION 3.5) set(KDEPIM_VERSION_NUMBER "5.11.40") set(PIM_VERSION ${KDEPIM_VERSION_NUMBER}) -set(KALARM_VERSION "2.12.4") +set(KALARM_VERSION "2.12.5") project(kalarm VERSION ${KALARM_VERSION}) set(KF5_MIN_VERSION "5.59.0") find_package(ECM ${KF5_MIN_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${kalarm_SOURCE_DIR}/cmake/modules ${ECM_MODULE_PATH}) include(ECMInstallIcons) include(ECMSetupVersion) include(ECMAddTests) include(GenerateExportHeader) include(ECMGenerateHeaders) include(FeatureSummary) include(CheckFunctionExists) include(ECMGeneratePriFile) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMAddAppIcon) include(ECMQtDeclareLoggingCategory) # Do NOT add quote set(KDEPIM_DEV_VERSION alpha) # add an extra space if(DEFINED KDEPIM_DEV_VERSION) set(KDEPIM_DEV_VERSION " ${KDEPIM_DEV_VERSION}") endif() set(KDEPIM_VERSION "${KDEPIM_VERSION_NUMBER}${KDEPIM_DEV_VERSION}") set(KIMAP_LIB_VERSION "5.11.40") set(AKONADI_MIMELIB_VERSION "5.11.40") set(AKONADI_CONTACT_VERSION "5.11.40") set(KMAILTRANSPORT_LIB_VERSION "5.11.40") set(KPIMTEXTEDIT_LIB_VERSION "5.11.40") set(IDENTITYMANAGEMENT_LIB_VERSION "5.11.40") set(AKONADI_VERSION "5.11.40") set(KMIME_LIB_VERSION "5.11.40") set(AKONADIKALARM_LIB_VERSION "5.11.40") set(PIMCOMMON_LIB_VERSION_LIB "5.11.40") set(KDEPIM_LIB_VERSION "${KDEPIM_VERSION_NUMBER}") set(KDEPIM_LIB_SOVERSION "5") set(QT_REQUIRED_VERSION "5.10.0") find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED DBus Gui Network Widgets) find_package(Qt5X11Extras NO_MODULE) set(MAILCOMMON_LIB_VERSION_LIB "5.11.40") set(LIBKDEPIM_LIB_VERSION_LIB "5.11.40") set(KCALENDARCORE_LIB_VERSION "5.11.42") set(CALENDARUTILS_LIB_VERSION "5.11.40") set(KDEPIM_APPS_LIB_VERSION_LIB "5.11.40") # Find KF5 package find_package(KF5Auth ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Codecs ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Completion ${KF5_MIN_VERSION} REQUIRED) find_package(KF5Config ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5ConfigWidgets ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5DBusAddons ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5DocTools ${KF5_MIN_VERSION} REQUIRED) find_package(KF5GlobalAccel ${KF5_MIN_VERSION} REQUIRED) find_package(KF5GuiAddons ${KF5_MIN_VERSION} REQUIRED) find_package(KF5I18n ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5IconThemes ${KF5_MIN_VERSION} REQUIRED) find_package(KF5JobWidgets ${KF5_MIN_VERSION} REQUIRED) find_package(KF5KCMUtils ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5KDELibs4Support ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5KIO ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Notifications ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Service ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5WidgetsAddons ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5WindowSystem ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5XmlGui ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Holidays ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(Phonon4Qt5 CONFIG REQUIRED) # Find KdepimLibs Package find_package(KF5IMAP ${KIMAP_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Akonadi ${AKONADI_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiContact ${AKONADI_CONTACT_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiMime ${AKONADI_MIMELIB_VERSION} CONFIG REQUIRED) find_package(KF5AlarmCalendar ${AKONADIKALARM_LIB_VERSION} CONFIG REQUIRED) find_package(KF5CalendarCore ${KCALENDARCORE_LIB_VERSION} CONFIG REQUIRED) find_package(KF5CalendarUtils ${CALENDARUTILS_LIB_VERSION} CONFIG REQUIRED) find_package(KF5IdentityManagement ${IDENTITYMANAGEMENT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5KdepimDBusInterfaces ${KDEPIM_APPS_LIB_VERSION_LIB} CONFIG REQUIRED) find_package(KF5Libkdepim ${LIBKDEPIM_LIB_VERSION_LIB} CONFIG REQUIRED) find_package(KF5MailCommon ${MAILCOMMON_LIB_VERSION_LIB} CONFIG REQUIRED) find_package(KF5MailTransportAkonadi ${KMAILTRANSPORT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Mime ${KMIME_LIB_VERSION} CONFIG REQUIRED) find_package(KF5PimCommon ${PIMCOMMON_LIB_VERSION_LIB} CONFIG REQUIRED) find_package(KF5PimTextEdit ${KPIMTEXTEDIT_LIB_VERSION} CONFIG REQUIRED) if (NOT APPLE) find_package(X11) endif() set(CMAKE_MODULE_PATH ${kalarm_SOURCE_DIR}/cmake/modules ${ECM_MODULE_PATH}) find_package(Xsltproc) set_package_properties(Xsltproc PROPERTIES DESCRIPTION "XSLT processor from libxslt" TYPE REQUIRED PURPOSE "Required to generate D-Bus interfaces for all Akonadi resources.") set(KDEPIM_HAVE_X11 ${X11_FOUND}) configure_file(src/config-kalarm.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kalarm.h ) include_directories(${kalarm_SOURCE_DIR} ${kalarm_BINARY_DIR}) add_definitions(-DQT_MESSAGELOGCONTEXT) #add_definitions(-DQT_NO_FOREACH) add_subdirectory(src) install(FILES kalarm.renamecategories kalarm.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}) add_subdirectory(doc) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/Changelog b/Changelog index 0f1ff43e..2f11af4a 100644 --- a/Changelog +++ b/Changelog @@ -1,1128 +1,1131 @@ KAlarm Change Log +=== Version 2.12.5 --- 25 June 2019 === ++ Enable alarm list columns to be hidden using context menu on list header [KDE Bug 397093] + === Version 2.12.4 (KDE Applications 19.04.3) --- 18 June 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] === 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/akonadimodel.cpp b/src/akonadimodel.cpp index b6532b91..6305a278 100644 --- a/src/akonadimodel.cpp +++ b/src/akonadimodel.cpp @@ -1,1983 +1,1983 @@ /* * akonadimodel.cpp - KAlarm calendar file access using Akonadi * Program: kalarm * Copyright © 2007-2019 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 "akonadimodel.h" #include "alarmtime.h" #include "autoqpointer.h" #include "calendarmigrator.h" #include "mainwindow.h" #include "messagebox.h" #include "preferences.h" #include "synchtimer.h" #include "kalarmsettings.h" #include "kalarmdirsettings.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 "kalarm_debug.h" using namespace Akonadi; using namespace KAlarmCal; static const Collection::Rights writableRights = Collection::CanChangeItem | Collection::CanCreateItem | Collection::CanDeleteItem; //static bool checkItem_true(const Item&) { return true; } /*============================================================================= = Class: AkonadiModel =============================================================================*/ AkonadiModel* AkonadiModel::mInstance = nullptr; QPixmap* AkonadiModel::mTextIcon = nullptr; QPixmap* AkonadiModel::mFileIcon = nullptr; QPixmap* AkonadiModel::mCommandIcon = nullptr; QPixmap* AkonadiModel::mEmailIcon = nullptr; QPixmap* AkonadiModel::mAudioIcon = nullptr; QSize AkonadiModel::mIconSize; int AkonadiModel::mTimeHourPos = -2; /****************************************************************************** * Construct and return the singleton. */ AkonadiModel* AkonadiModel::instance() { if (!mInstance) mInstance = new AkonadiModel(new ChangeRecorder(qApp), qApp); return mInstance; } /****************************************************************************** * Constructor. */ AkonadiModel::AkonadiModel(ChangeRecorder* monitor, QObject* parent) : EntityTreeModel(monitor, parent), mMonitor(monitor), mResourcesChecked(false), mMigrating(false) { // Set lazy population to enable the contents of unselected collections to be ignored setItemPopulationStrategy(LazyPopulation); // Restrict monitoring to collections containing the KAlarm mime types monitor->setCollectionMonitored(Collection::root()); monitor->setResourceMonitored("akonadi_kalarm_resource"); monitor->setResourceMonitored("akonadi_kalarm_dir_resource"); monitor->setMimeTypeMonitored(KAlarmCal::MIME_ACTIVE); monitor->setMimeTypeMonitored(KAlarmCal::MIME_ARCHIVED); monitor->setMimeTypeMonitored(KAlarmCal::MIME_TEMPLATE); monitor->itemFetchScope().fetchFullPayload(); monitor->itemFetchScope().fetchAttribute(); AttributeFactory::registerAttribute(); AttributeFactory::registerAttribute(); AttributeFactory::registerAttribute(); if (!mTextIcon) { mTextIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("dialog-information")).pixmap(16, 16)); mFileIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("document-open")).pixmap(16, 16)); mCommandIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("system-run")).pixmap(16, 16)); mEmailIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("mail-unread")).pixmap(16, 16)); mAudioIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("audio-x-generic")).pixmap(16, 16)); mIconSize = mTextIcon->size().expandedTo(mFileIcon->size()).expandedTo(mCommandIcon->size()).expandedTo(mEmailIcon->size()).expandedTo(mAudioIcon->size()); } #ifdef __GNUC__ #warning Only want to monitor collection properties, not content, when this becomes possible #endif connect(monitor, SIGNAL(collectionChanged(Akonadi::Collection,QSet)), SLOT(slotCollectionChanged(Akonadi::Collection,QSet))); connect(monitor, &Monitor::collectionRemoved, this, &AkonadiModel::slotCollectionRemoved); initCalendarMigrator(); MinuteTimer::connect(this, SLOT(slotUpdateTimeTo())); Preferences::connect(SIGNAL(archivedColourChanged(QColor)), this, SLOT(slotUpdateArchivedColour(QColor))); Preferences::connect(SIGNAL(disabledColourChanged(QColor)), this, SLOT(slotUpdateDisabledColour(QColor))); Preferences::connect(SIGNAL(holidaysChanged(KHolidays::HolidayRegion)), this, SLOT(slotUpdateHolidays())); Preferences::connect(SIGNAL(workTimeChanged(QTime,QTime,QBitArray)), this, SLOT(slotUpdateWorkingHours())); connect(this, &AkonadiModel::rowsInserted, this, &AkonadiModel::slotRowsInserted); connect(this, &AkonadiModel::rowsAboutToBeRemoved, this, &AkonadiModel::slotRowsAboutToBeRemoved); connect(monitor, &Monitor::itemChanged, this, &AkonadiModel::slotMonitoredItemChanged); connect(ServerManager::self(), &ServerManager::stateChanged, this, &AkonadiModel::checkResources); checkResources(ServerManager::state()); } AkonadiModel::~AkonadiModel() { if (mInstance == this) mInstance = nullptr; } /****************************************************************************** * Called when the server manager changes state. * If it is now running, i.e. the agent manager knows about * all existing resources. * Once it is running, i.e. the agent manager knows about * all existing resources, if necessary migrate any KResources alarm calendars from * pre-Akonadi versions of KAlarm, or create default Akonadi calendar resources * if any are missing. */ void AkonadiModel::checkResources(ServerManager::State state) { switch (state) { case ServerManager::Running: if (!mResourcesChecked) { qCDebug(KALARM_LOG) << "AkonadiModel::checkResources: Server running"; mResourcesChecked = true; mMigrating = true; CalendarMigrator::execute(); } break; case ServerManager::NotRunning: qCDebug(KALARM_LOG) << "AkonadiModel::checkResources: Server stopped"; mResourcesChecked = false; mMigrating = false; mCollectionAlarmTypes.clear(); mCollectionRights.clear(); mCollectionEnabled.clear(); initCalendarMigrator(); Q_EMIT serverStopped(); break; default: break; } } /****************************************************************************** * Initialise the calendar migrator so that it can be run (either for the first * time, or again). */ void AkonadiModel::initCalendarMigrator() { CalendarMigrator::reset(); connect(CalendarMigrator::instance(), &CalendarMigrator::creating, this, &AkonadiModel::slotCollectionBeingCreated); connect(CalendarMigrator::instance(), &QObject::destroyed, this, &AkonadiModel::slotMigrationCompleted); } /****************************************************************************** * Return whether calendar migration has completed. */ bool AkonadiModel::isMigrationCompleted() const { return mResourcesChecked && !mMigrating; } /****************************************************************************** * Return the data for a given role, for a specified item. */ QVariant AkonadiModel::data(const QModelIndex& index, int role) const { // First check that it's a role we're interested in - if not, use the base method switch (role) { case Qt::BackgroundRole: case Qt::ForegroundRole: case Qt::DisplayRole: case Qt::TextAlignmentRole: case Qt::DecorationRole: case Qt::SizeHintRole: case Qt::AccessibleTextRole: case Qt::ToolTipRole: case Qt::CheckStateRole: case SortRole: case TimeDisplayRole: case ValueRole: case StatusRole: case AlarmActionsRole: case AlarmSubActionRole: case EnabledRole: case EnabledTypesRole: case CommandErrorRole: case BaseColourRole: case AlarmTypeRole: case IsStandardRole: break; default: return EntityTreeModel::data(index, role); } const Collection collection = index.data(CollectionRole).value(); if (collection.isValid()) { // This is a Collection row switch (role) { case Qt::DisplayRole: return collection.displayName(); case EnabledTypesRole: if (mCollectionAttributes.contains(collection.id())) return static_cast(mCollectionAttributes.value(collection.id()).enabled()); if (collection.hasAttribute()) return static_cast(collection.attribute()->enabled()); return 0; case BaseColourRole: role = Qt::BackgroundRole; break; case Qt::BackgroundRole: { const QColor colour = backgroundColor_p(collection); if (colour.isValid()) return colour; break; } case Qt::ForegroundRole: return foregroundColor(collection, collection.contentMimeTypes()); case Qt::ToolTipRole: return tooltip(collection, CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE); case AlarmTypeRole: return static_cast(types(collection)); case IsStandardRole: if (isCompatible(collection)) { if (mCollectionAttributes.contains(collection.id())) return static_cast(mCollectionAttributes.value(collection.id()).standard()); if (collection.hasAttribute()) return static_cast(collection.attribute()->standard()); } return 0; case KeepFormatRole: if (mCollectionAttributes.contains(collection.id())) return mCollectionAttributes.value(collection.id()).keepFormat(); if (collection.hasAttribute()) return collection.attribute()->keepFormat(); return false; default: break; } } else { const Item item = index.data(ItemRole).value(); if (item.isValid()) { // This is an Item row const QString mime = item.mimeType(); if ((mime != KAlarmCal::MIME_ACTIVE && mime != KAlarmCal::MIME_ARCHIVED && mime != KAlarmCal::MIME_TEMPLATE) || !item.hasPayload()) return QVariant(); switch (role) { case StatusRole: // Mime type has a one-to-one relationship to event's category() if (mime == KAlarmCal::MIME_ACTIVE) return CalEvent::ACTIVE; if (mime == KAlarmCal::MIME_ARCHIVED) return CalEvent::ARCHIVED; if (mime == KAlarmCal::MIME_TEMPLATE) return CalEvent::TEMPLATE; return QVariant(); case CommandErrorRole: if (!item.hasAttribute()) return KAEvent::CMD_NO_ERROR; return item.attribute()->commandError(); default: break; } const int column = index.column(); if (role == Qt::WhatsThisRole) return whatsThisText(column); const KAEvent event(this->event(item)); if (!event.isValid()) return QVariant(); if (role == AlarmActionsRole) return event.actionTypes(); if (role == AlarmSubActionRole) return event.actionSubType(); bool calendarColour = false; switch (column) { case TimeColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: if (event.expired()) return AlarmTime::alarmTimeText(event.startDateTime(), '0'); return AlarmTime::alarmTimeText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER), '0'); case TimeDisplayRole: if (event.expired()) return AlarmTime::alarmTimeText(event.startDateTime(), '~'); return AlarmTime::alarmTimeText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER), '~'); case Qt::TextAlignmentRole: return Qt::AlignRight; case SortRole: { DateTime due; if (event.expired()) due = event.startDateTime(); else due = event.nextTrigger(KAEvent::DISPLAY_TRIGGER); return due.isValid() ? due.effectiveKDateTime().toUtc().qDateTime() : QDateTime(QDate(9999,12,31), QTime(0,0,0)); } default: break; } break; case TimeToColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: if (event.expired()) return QString(); return AlarmTime::timeToAlarmText(event.nextTrigger(KAEvent::DISPLAY_TRIGGER)); case Qt::TextAlignmentRole: return Qt::AlignRight; case SortRole: { if (event.expired()) return -1; const DateTime due = event.nextTrigger(KAEvent::DISPLAY_TRIGGER); const KADateTime now = KADateTime::currentUtcDateTime(); if (due.isDateOnly()) return now.date().daysTo(due.date()) * 1440; return (now.secsTo(due.effectiveKDateTime()) + 59) / 60; } } break; case RepeatColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: return repeatText(event); case Qt::TextAlignmentRole: return Qt::AlignHCenter; case SortRole: return repeatOrder(event); } break; case ColourColumn: switch (role) { case Qt::BackgroundRole: { const KAEvent::Actions type = event.actionTypes(); if (type & KAEvent::ACT_DISPLAY) return event.bgColour(); if (type == KAEvent::ACT_COMMAND) { if (event.commandError() != KAEvent::CMD_NO_ERROR) return QColor(Qt::red); } break; } case Qt::ForegroundRole: if (event.commandError() != KAEvent::CMD_NO_ERROR) { if (event.actionTypes() == KAEvent::ACT_COMMAND) return QColor(Qt::white); QColor colour = Qt::red; int r, g, b; event.bgColour().getRgb(&r, &g, &b); if (r > 128 && g <= 128 && b <= 128) colour = QColor(Qt::white); return colour; } break; case Qt::DisplayRole: if (event.commandError() != KAEvent::CMD_NO_ERROR) return QLatin1String("!"); break; case SortRole: { const unsigned i = (event.actionTypes() == KAEvent::ACT_DISPLAY) ? event.bgColour().rgb() : 0; return QStringLiteral("%1").arg(i, 6, 10, QLatin1Char('0')); } default: break; } break; case TypeColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DecorationRole: { QVariant v; v.setValue(*eventIcon(event)); return v; } case Qt::TextAlignmentRole: return Qt::AlignHCenter; case Qt::SizeHintRole: return mIconSize; case Qt::AccessibleTextRole: #ifdef __GNUC__ #warning Implement accessibility #endif return QString(); case ValueRole: return static_cast(event.actionSubType()); case SortRole: return QStringLiteral("%1").arg(event.actionSubType(), 2, 10, QLatin1Char('0')); } break; case TextColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: case SortRole: return AlarmText::summary(event, 1); case Qt::ToolTipRole: return AlarmText::summary(event, 10); default: break; } break; case TemplateNameColumn: switch (role) { case Qt::BackgroundRole: calendarColour = true; break; case Qt::DisplayRole: return event.templateName(); case SortRole: return event.templateName().toUpper(); } break; default: break; } switch (role) { case Qt::ForegroundRole: if (!event.enabled()) return Preferences::disabledColour(); if (event.expired()) return Preferences::archivedColour(); break; // use the default for normal active alarms case Qt::ToolTipRole: // Show the last command execution error message switch (event.commandError()) { case KAEvent::CMD_ERROR: return i18nc("@info:tooltip", "Command execution failed"); case KAEvent::CMD_ERROR_PRE: return i18nc("@info:tooltip", "Pre-alarm action execution failed"); case KAEvent::CMD_ERROR_POST: return i18nc("@info:tooltip", "Post-alarm action execution failed"); case KAEvent::CMD_ERROR_PRE_POST: return i18nc("@info:tooltip", "Pre- and post-alarm action execution failed"); default: case KAEvent::CMD_NO_ERROR: break; } break; case EnabledRole: return event.enabled(); default: break; } if (calendarColour) { Collection parent = item.parentCollection(); const QColor colour = backgroundColor(parent); if (colour.isValid()) return colour; } } } return EntityTreeModel::data(index, role); } /****************************************************************************** * Set the font to use for all items, or the checked state of one item. * The font must always be set at initialisation. */ bool AkonadiModel::setData(const QModelIndex& index, const QVariant& value, int role) { if (!index.isValid()) return false; // NOTE: need to Q_EMIT dataChanged() whenever something is updated (except via a job). Collection collection = index.data(CollectionRole).value(); if (collection.isValid()) { // This is a Collection row bool updateCollectionAttr = false; CollectionAttribute attr; switch (role) { case Qt::BackgroundRole: { const QColor colour = value.value(); attr = mCollectionAttributes.value(collection.id()); if (attr.backgroundColor() == colour) return true; // no change attr.setBackgroundColor(colour); updateCollectionAttr = true; break; } case EnabledTypesRole: { const CalEvent::Types types = static_cast(value.toInt()); bool newAttr = !collection.hasAttribute(); attr = mCollectionAttributes.value(collection.id()); if (attr.enabled() == types) return true; // no change qCDebug(KALARM_LOG) << "AkonadiModel:" << collection.id() << "Set enabled:" << types << " was=" << attr.enabled(); attr.setEnabled(types); updateCollectionAttr = true; if (newAttr) { // Akonadi often doesn't notify changes to the enabled status // (surely a bug?), so ensure that the change is noticed. mNewCollectionEnabled[collection.id()] = types; } break; } case IsStandardRole: if (collection.hasAttribute() && isCompatible(collection)) { const CalEvent::Types types = static_cast(value.toInt()); attr = mCollectionAttributes.value(collection.id()); qCDebug(KALARM_LOG) << "AkonadiModel:" << collection.id() << "Set standard:" << types << " was=" << attr.standard(); attr.setStandard(types); updateCollectionAttr = true; } break; case KeepFormatRole: { const bool keepFormat = value.toBool(); attr = mCollectionAttributes.value(collection.id()); if (attr.keepFormat() == keepFormat) return true; // no change attr.setKeepFormat(keepFormat); updateCollectionAttr = true; break; } default: break; } if (updateCollectionAttr) { // Update the CollectionAttribute value. // Note that we can't supply 'collection' to CollectionModifyJob since // that also contains the CompatibilityAttribute value, which is read-only // for applications. So create a new Collection instance and only set a // value for CollectionAttribute. mCollectionAttributes[collection.id()] = attr; Collection c(collection.id()); CollectionAttribute* att = c.attribute(Collection::AddIfMissing); *att = attr; CollectionModifyJob* job = new CollectionModifyJob(c, this); connect(job, &CollectionModifyJob::result, this, &AkonadiModel::modifyCollectionAttrJobDone); return true; } } else { Item item = index.data(ItemRole).value(); if (item.isValid()) { bool updateItem = false; switch (role) { case CommandErrorRole: { const KAEvent::CmdErrType err = static_cast(value.toInt()); switch (err) { case KAEvent::CMD_NO_ERROR: case KAEvent::CMD_ERROR: case KAEvent::CMD_ERROR_PRE: case KAEvent::CMD_ERROR_POST: case KAEvent::CMD_ERROR_PRE_POST: { if (err == KAEvent::CMD_NO_ERROR && !item.hasAttribute()) return true; // no change EventAttribute* attr = item.attribute(Item::AddIfMissing); if (attr->commandError() == err) return true; // no change attr->setCommandError(err); updateItem = true; qCDebug(KALARM_LOG)<<"Item:"<"<= ColumnCount) return QVariant(); - if (role == Qt::DisplayRole) + if (role == Qt::DisplayRole || role == ColumnTitleRole) { switch (section) { case TimeColumn: return i18nc("@title:column", "Time"); case TimeToColumn: return i18nc("@title:column", "Time To"); case RepeatColumn: return i18nc("@title:column", "Repeat"); case ColourColumn: - return QString(); + return (role == Qt::DisplayRole) ? QString() : i18nc("@title:column", "Color"); case TypeColumn: - return QString(); + return (role == Qt::DisplayRole) ? QString() : i18nc("@title:column", "Type"); case TextColumn: return i18nc("@title:column", "Message, File or Command"); case TemplateNameColumn: return i18nc("@title:column Template name", "Name"); } } else if (role == Qt::WhatsThisRole) return whatsThisText(section); break; default: break; } } return EntityTreeModel::entityHeaderData(section, orientation, role, group); } /****************************************************************************** * Recursive function to Q_EMIT the dataChanged() signal for all items in a * specified column range. */ void AkonadiModel::signalDataChanged(bool (*checkFunc)(const Item&), int startColumn, int endColumn, const QModelIndex& parent) { int start = -1; int end = -1; for (int row = 0, count = rowCount(parent); row < count; ++row) { const QModelIndex ix = index(row, 0, parent); const Item item = data(ix, ItemRole).value(); const bool isItem = item.isValid(); if (isItem) { if ((*checkFunc)(item)) { // For efficiency, Q_EMIT a single signal for each group of // consecutive items, rather than a separate signal for each item. if (start < 0) start = row; end = row; continue; } } if (start >= 0) Q_EMIT dataChanged(index(start, startColumn, parent), index(end, endColumn, parent)); start = -1; if (!isItem) signalDataChanged(checkFunc, startColumn, endColumn, ix); } if (start >= 0) Q_EMIT dataChanged(index(start, startColumn, parent), index(end, endColumn, parent)); } /****************************************************************************** * Signal every minute that the time-to-alarm values have changed. */ static bool checkItem_isActive(const Item& item) { return item.mimeType() == KAlarmCal::MIME_ACTIVE; } void AkonadiModel::slotUpdateTimeTo() { signalDataChanged(&checkItem_isActive, TimeToColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Called when the colour used to display archived alarms has changed. */ static bool checkItem_isArchived(const Item& item) { return item.mimeType() == KAlarmCal::MIME_ARCHIVED; } void AkonadiModel::slotUpdateArchivedColour(const QColor&) { qCDebug(KALARM_LOG) << "AkonadiModel::slotUpdateArchivedColour"; signalDataChanged(&checkItem_isArchived, 0, ColumnCount - 1, QModelIndex()); } /****************************************************************************** * Called when the colour used to display disabled alarms has changed. */ static bool checkItem_isDisabled(const Item& item) { if (item.hasPayload()) { const KAEvent event = item.payload(); if (event.isValid()) return !event.enabled(); } return false; } void AkonadiModel::slotUpdateDisabledColour(const QColor&) { qCDebug(KALARM_LOG) << "AkonadiModel::slotUpdateDisabledColour"; signalDataChanged(&checkItem_isDisabled, 0, ColumnCount - 1, QModelIndex()); } /****************************************************************************** * Called when the definition of holidays has changed. */ static bool checkItem_excludesHolidays(const Item& item) { if (item.hasPayload()) { const KAEvent event = item.payload(); if (event.isValid() && event.holidaysExcluded()) return true; } return false; } void AkonadiModel::slotUpdateHolidays() { qCDebug(KALARM_LOG) << "AkonadiModel::slotUpdateHolidays"; Q_ASSERT(TimeToColumn == TimeColumn + 1); // signal should be emitted only for TimeTo and Time columns signalDataChanged(&checkItem_excludesHolidays, TimeColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Called when the definition of working hours has changed. */ static bool checkItem_workTimeOnly(const Item& item) { if (item.hasPayload()) { const KAEvent event = item.payload(); if (event.isValid() && event.workTimeOnly()) return true; } return false; } void AkonadiModel::slotUpdateWorkingHours() { qCDebug(KALARM_LOG) << "AkonadiModel::slotUpdateWorkingHours"; Q_ASSERT(TimeToColumn == TimeColumn + 1); // signal should be emitted only for TimeTo and Time columns signalDataChanged(&checkItem_workTimeOnly, TimeColumn, TimeToColumn, QModelIndex()); } /****************************************************************************** * Called when the command error status of an alarm has changed, to save the new * status and update the visual command error indication. */ void AkonadiModel::updateCommandError(const KAEvent& event) { const QModelIndex ix = itemIndex(event.itemId()); if (ix.isValid()) setData(ix, QVariant(static_cast(event.commandError())), CommandErrorRole); } /****************************************************************************** * Return the foreground color for displaying a collection, based on the * supplied mime types which it contains, and on whether it is fully writable. */ QColor AkonadiModel::foregroundColor(const Akonadi::Collection& collection, const QStringList& mimeTypes) { QColor colour; if (mimeTypes.contains(KAlarmCal::MIME_ACTIVE)) colour = KColorScheme(QPalette::Active).foreground(KColorScheme::NormalText).color(); else if (mimeTypes.contains(KAlarmCal::MIME_ARCHIVED)) colour = Preferences::archivedColour(); else if (mimeTypes.contains(KAlarmCal::MIME_TEMPLATE)) colour = KColorScheme(QPalette::Active).foreground(KColorScheme::LinkText).color(); if (colour.isValid() && isWritable(collection) <= 0) return KColorUtils::lighten(colour, 0.2); return colour; } /****************************************************************************** * Set the background color for displaying the collection and its alarms. */ void AkonadiModel::setBackgroundColor(Collection& collection, const QColor& colour) { const QModelIndex ix = modelIndexForCollection(this, collection); if (ix.isValid()) setData(ix, QVariant(colour), Qt::BackgroundRole); } /****************************************************************************** * Return the background color for displaying the collection and its alarms, * after updating the collection from the Akonadi database. */ QColor AkonadiModel::backgroundColor(Akonadi::Collection& collection) const { if (!collection.isValid()) return QColor(); refresh(collection); return backgroundColor_p(collection); } /****************************************************************************** * Return the background color for displaying the collection and its alarms. */ QColor AkonadiModel::backgroundColor_p(const Akonadi::Collection& collection) const { if (collection.isValid()) { if (mCollectionAttributes.contains(collection.id())) return mCollectionAttributes.value(collection.id()).backgroundColor(); if (collection.hasAttribute()) return collection.attribute()->backgroundColor(); } return QColor(); } /****************************************************************************** * Return the display name for the collection, after updating the collection * from the Akonadi database. */ QString AkonadiModel::displayName(Akonadi::Collection& collection) const { if (!collection.isValid()) return QString(); refresh(collection); return collection.displayName(); } /****************************************************************************** * Return the storage type (file, directory, etc.) for the collection. */ QString AkonadiModel::storageType(const Akonadi::Collection& collection) const { const QUrl url = QUrl::fromUserInput(collection.remoteId(), QString(), QUrl::AssumeLocalFile); return !url.isLocalFile() ? i18nc("@info", "URL") : QFileInfo(url.toLocalFile()).isDir() ? i18nc("@info Directory in filesystem", "Directory") : i18nc("@info", "File"); } /****************************************************************************** * Return a collection's tooltip text. The collection's enabled status is * evaluated for specified alarm types. */ QString AkonadiModel::tooltip(const Collection& collection, CalEvent::Types types) const { const QString name = QLatin1Char('@') + collection.displayName(); // insert markers for stripping out name const QUrl url = QUrl::fromUserInput(collection.remoteId(), QString(), QUrl::AssumeLocalFile); const QString type = QLatin1Char('@') + storageType(collection); // file/directory/URL etc. const QString locn = url.toDisplayString(QUrl::PreferLocalFile); const bool inactive = !collection.hasAttribute() || !(collection.attribute()->enabled() & types); const QString disabled = i18nc("@info", "Disabled"); const QString readonly = readOnlyTooltip(collection); const bool writable = readonly.isEmpty(); if (inactive && !writable) return xi18nc("@info:tooltip", "%1" "%2: %3" "%4, %5", name, type, locn, disabled, readonly); if (inactive || !writable) return xi18nc("@info:tooltip", "%1" "%2: %3" "%4", name, type, locn, (inactive ? disabled : readonly)); return xi18nc("@info:tooltip", "%1" "%2: %3", name, type, locn); } /****************************************************************************** * Return the read-only status tooltip for a collection. * A null string is returned if the collection is fully writable. */ QString AkonadiModel::readOnlyTooltip(const Collection& collection) { KACalendar::Compat compat; switch (AkonadiModel::isWritable(collection, compat)) { case 1: return QString(); case 0: return i18nc("@info", "Read-only (old format)"); default: if (compat == KACalendar::Current) return i18nc("@info", "Read-only"); return i18nc("@info", "Read-only (other format)"); } } /****************************************************************************** * Return the repetition text. */ QString AkonadiModel::repeatText(const KAEvent& event) const { QString repeatText = event.recurrenceText(true); if (repeatText.isEmpty()) repeatText = event.repetitionText(true); return repeatText; } /****************************************************************************** * Return a string for sorting the repetition column. */ QString AkonadiModel::repeatOrder(const KAEvent& event) const { int repeatOrder = 0; int repeatInterval = 0; if (event.repeatAtLogin()) repeatOrder = 1; else { repeatInterval = event.recurInterval(); switch (event.recurType()) { case KARecurrence::MINUTELY: repeatOrder = 2; break; case KARecurrence::DAILY: repeatOrder = 3; break; case KARecurrence::WEEKLY: repeatOrder = 4; break; case KARecurrence::MONTHLY_DAY: case KARecurrence::MONTHLY_POS: repeatOrder = 5; break; case KARecurrence::ANNUAL_DATE: case KARecurrence::ANNUAL_POS: repeatOrder = 6; break; case KARecurrence::NO_RECUR: default: break; } } return QStringLiteral("%1%2").arg(static_cast('0' + repeatOrder)).arg(repeatInterval, 8, 10, QLatin1Char('0')); } /****************************************************************************** * Return the icon associated with the event's action. */ QPixmap* AkonadiModel::eventIcon(const KAEvent& event) const { switch (event.actionTypes()) { case KAEvent::ACT_EMAIL: return mEmailIcon; case KAEvent::ACT_AUDIO: return mAudioIcon; case KAEvent::ACT_COMMAND: return mCommandIcon; case KAEvent::ACT_DISPLAY: if (event.actionSubType() == KAEvent::FILE) return mFileIcon; // fall through to ACT_DISPLAY_COMMAND Q_FALLTHROUGH(); case KAEvent::ACT_DISPLAY_COMMAND: default: return mTextIcon; } } /****************************************************************************** * Returns the QWhatsThis text for a specified column. */ QString AkonadiModel::whatsThisText(int column) const { switch (column) { case TimeColumn: return i18nc("@info:whatsthis", "Next scheduled date and time of the alarm"); case TimeToColumn: return i18nc("@info:whatsthis", "How long until the next scheduled trigger of the alarm"); case RepeatColumn: return i18nc("@info:whatsthis", "How often the alarm recurs"); case ColourColumn: return i18nc("@info:whatsthis", "Background color of alarm message"); case TypeColumn: return i18nc("@info:whatsthis", "Alarm type (message, file, command or email)"); case TextColumn: return i18nc("@info:whatsthis", "Alarm message text, URL of text file to display, command to execute, or email subject line"); case TemplateNameColumn: return i18nc("@info:whatsthis", "Name of the alarm template"); default: return QString(); } } /****************************************************************************** * Remove a collection from Akonadi. The calendar file is not removed. */ bool AkonadiModel::removeCollection(const Akonadi::Collection& collection) { if (!collection.isValid()) return false; qCDebug(KALARM_LOG) << "AkonadiModel::removeCollection:" << collection.id(); Collection col = collection; mCollectionsDeleting << collection.id(); // Note: CollectionDeleteJob deletes the backend storage also. AgentManager* agentManager = AgentManager::self(); const AgentInstance instance = agentManager->instance(collection.resource()); if (instance.isValid()) agentManager->removeInstance(instance); #if 0 CollectionDeleteJob* job = new CollectionDeleteJob(col); connect(job, &CollectionDeleteJob::result, this, &AkonadiModel::deleteCollectionJobDone); mPendingCollectionJobs[job] = CollJobData(col.id(), displayName(col)); job->start(); #endif return true; } /****************************************************************************** * Return whether a collection is currently being deleted. */ bool AkonadiModel::isCollectionBeingDeleted(Collection::Id id) const { return mCollectionsDeleting.contains(id); } #if 0 /****************************************************************************** * Called when a collection deletion job has completed. * Checks for any error. */ void AkonadiModel::deleteCollectionJobDone(KJob* j) { QHash::iterator it = mPendingCollectionJobs.find(j); CollJobData jobData; if (it != mPendingCollectionJobs.end()) { jobData = it.value(); mPendingCollectionJobs.erase(it); } if (j->error()) { Q_EMIT collectionDeleted(jobData.id, false); const QString errMsg = xi18nc("@info", "Failed to remove calendar %1.", jobData.displayName); qCCritical(KALARM_LOG) << "AkonadiModel::deleteCollectionJobDone:" << errMsg << ":" << j->errorString(); KAMessageBox::error(MainWindow::mainMainWindow(), xi18nc("@info", "%1(%2)", errMsg, j->errorString())); } else Q_EMIT collectionDeleted(jobData.id, true); } #endif /****************************************************************************** * Reload a collection from Akonadi storage. The backend data is not reloaded. */ bool AkonadiModel::reloadCollection(const Akonadi::Collection& collection) { if (!collection.isValid()) return false; qCDebug(KALARM_LOG) << "AkonadiModel::reloadCollection:" << collection.id(); mMonitor->setCollectionMonitored(collection, false); mMonitor->setCollectionMonitored(collection, true); return true; } /****************************************************************************** * Reload a collection from Akonadi storage. The backend data is not reloaded. */ void AkonadiModel::reload() { qCDebug(KALARM_LOG) << "AkonadiModel::reload"; const Collection::List collections = mMonitor->collectionsMonitored(); for (const Collection& collection : collections) { mMonitor->setCollectionMonitored(collection, false); mMonitor->setCollectionMonitored(collection, true); } } /****************************************************************************** * Called when a CollectionAttribute modification job has completed. * Checks for any error. */ void AkonadiModel::modifyCollectionAttrJobDone(KJob* j) { Collection collection = static_cast(j)->collection(); const Collection::Id id = collection.id(); bool newEnable = mNewCollectionEnabled.contains(id); bool newEnabled = mNewCollectionEnabled.value(id, false); mNewCollectionEnabled.remove(id); if (j->error()) { if (mCollectionsDeleted.contains(id)) mCollectionsDeleted.removeAll(id); else { const QString errMsg = i18nc("@info", "Failed to update calendar \"%1\".", displayName(collection)); qCCritical(KALARM_LOG) << "AkonadiModel::modifyCollectionAttrJobDone:" << collection.id() << errMsg << ":" << j->errorString(); KAMessageBox::error(MainWindow::mainMainWindow(), i18nc("@info", "%1\n(%2)", errMsg, j->errorString())); } } else { if (newEnable) Q_EMIT collectionStatusChanged(collection, Enabled, newEnabled, false); } } /****************************************************************************** * Returns the index to a specified event. */ QModelIndex AkonadiModel::eventIndex(const KAEvent& event) { return itemIndex(event.itemId()); } /****************************************************************************** * Search for an event's item ID. This method ignores any itemId() value * contained in the KAEvent. The collectionId() is used if available. */ Item::Id AkonadiModel::findItemId(const KAEvent& event) { Collection::Id colId = event.collectionId(); QModelIndex start = (colId < 0) ? index(0, 0) : collectionIndex(Collection(colId)); Qt::MatchFlags flags = (colId < 0) ? Qt::MatchExactly | Qt::MatchRecursive | Qt::MatchCaseSensitive | Qt::MatchWrap : Qt::MatchExactly | Qt::MatchRecursive | Qt::MatchCaseSensitive; const QModelIndexList indexes = match(start, RemoteIdRole, event.id(), -1, flags); for (const QModelIndex& ix : indexes) { if (ix.isValid()) { Item::Id id = ix.data(ItemIdRole).toLongLong(); if (id >= 0) { if (colId < 0 || ix.data(ParentCollectionRole).value().id() == colId) return id; } } } return -1; } #if 0 /****************************************************************************** * Return all events of a given type belonging to a collection. */ KAEvent::List AkonadiModel::events(Akonadi::Collection& collection, CalEvent::Type type) const { KAEvent::List list; const QModelIndex ix = modelIndexForCollection(this, collection); if (ix.isValid()) getChildEvents(ix, type, list); return list; } /****************************************************************************** * Recursive function to append all child Events with a given mime type. */ void AkonadiModel::getChildEvents(const QModelIndex& parent, CalEvent::Type type, KAEvent::List& events) const { for (int row = 0, count = rowCount(parent); row < count; ++row) { const QModelIndex ix = index(row, 0, parent); const Item item = data(ix, ItemRole).value(); if (item.isValid()) { if (item.hasPayload()) { KAEvent event = item.payload(); if (event.isValid() && event.category() == type) events += event; } } else { const Collection c = ix.data(CollectionRole).value(); if (c.isValid()) getChildEvents(ix, type, events); } } } #endif KAEvent AkonadiModel::event(Akonadi::Item::Id itemId) const { const QModelIndex ix = itemIndex(itemId); if (!ix.isValid()) return KAEvent(); return event(ix.data(ItemRole).value(), ix, nullptr); } KAEvent AkonadiModel::event(const QModelIndex& index) const { return event(index.data(ItemRole).value(), index, nullptr); } KAEvent AkonadiModel::event(const Akonadi::Item& item, const QModelIndex& index, Akonadi::Collection* collection) const { if (!item.isValid() || !item.hasPayload()) return KAEvent(); const QModelIndex ix = index.isValid() ? index : itemIndex(item.id()); if (!ix.isValid()) return KAEvent(); KAEvent e = item.payload(); if (e.isValid()) { Collection c = data(ix, ParentCollectionRole).value(); // Set collection ID using a const method, to avoid unnecessary copying of KAEvent e.setCollectionId_const(c.id()); if (collection) *collection = c; } return e; } #if 0 /****************************************************************************** * Add an event to the default or a user-selected Collection. */ AkonadiModel::Result AkonadiModel::addEvent(KAEvent* event, CalEvent::Type type, QWidget* promptParent, bool noPrompt) { qCDebug(KALARM_LOG) << "AkonadiModel::addEvent:" << event->id(); // Determine parent collection - prompt or use default bool cancelled; const Collection collection = destination(type, Collection::CanCreateItem, promptParent, noPrompt, &cancelled); if (!collection.isValid()) { delete event; if (cancelled) return Cancelled; qCDebug(KALARM_LOG) << "No collection"; return Failed; } if (!addEvent(event, collection)) { qCDebug(KALARM_LOG) << "Failed"; return Failed; // event was deleted by addEvent() } return Success; } #endif /****************************************************************************** * Add events to a specified Collection. * Events which are scheduled to be added to the collection are updated with * their Akonadi item ID. * The caller must connect to the itemDone() signal to check whether events * have been added successfully. Note that the first signal may be emitted * before this function returns. * Reply = true if item creation has been scheduled for all events, * = false if at least one item creation failed to be scheduled. */ bool AkonadiModel::addEvents(const KAEvent::List& events, Collection& collection) { bool ok = true; for (int i = 0, count = events.count(); i < count; ++i) ok = ok && addEvent(*events[i], collection); return ok; } /****************************************************************************** * Add an event to a specified Collection. * If the event is scheduled to be added to the collection, it is updated with * its Akonadi item ID. * The event's 'updated' flag is cleared. * The caller must connect to the itemDone() signal to check whether events * have been added successfully. * Reply = true if item creation has been scheduled. */ bool AkonadiModel::addEvent(KAEvent& event, Collection& collection) { qCDebug(KALARM_LOG) << "AkonadiModel::addEvent: ID:" << event.id(); Item item; if (!event.setItemPayload(item, collection.contentMimeTypes())) { qCWarning(KALARM_LOG) << "Invalid mime type for collection"; return false; } event.setItemId(item.id()); qCDebug(KALARM_LOG)<<"-> item id="<start(); qCDebug(KALARM_LOG)<<"...exiting"; return true; } /****************************************************************************** * Update an event in its collection. * The event retains its existing Akonadi item ID. * The event's 'updated' flag is cleared. * The caller must connect to the itemDone() signal to check whether the event * has been updated successfully. * Reply = true if item update has been scheduled. */ bool AkonadiModel::updateEvent(KAEvent& event) { qCDebug(KALARM_LOG) << "AkonadiModel::updateEvent: ID:" << event.id(); return updateEvent(event.itemId(), event); } bool AkonadiModel::updateEvent(Akonadi::Item::Id itemId, KAEvent& newEvent) { qCDebug(KALARM_LOG)<<"item id="<(); Item item = ix.data(ItemRole).value(); qCDebug(KALARM_LOG)<<"item id="<().id())) { qCDebug(KALARM_LOG) << "Collection being deleted"; return true; // the event's collection is being deleted } const Item item = ix.data(ItemRole).value(); ItemDeleteJob* job = new ItemDeleteJob(item); connect(job, &ItemDeleteJob::result, this, &AkonadiModel::itemJobDone); mPendingItemJobs[job] = itemId; job->start(); return true; } /****************************************************************************** * Queue an ItemModifyJob for execution. Ensure that only one job is * simultaneously active for any one Item. * * This is necessary because we can't call two ItemModifyJobs for the same Item * at the same time; otherwise Akonadi will detect a conflict and require manual * intervention to resolve it. */ void AkonadiModel::queueItemModifyJob(const Item& item) { qCDebug(KALARM_LOG) << "AkonadiModel::queueItemModifyJob:" << item.id(); QHash::Iterator it = mItemModifyJobQueue.find(item.id()); if (it != mItemModifyJobQueue.end()) { // A job is already queued for this item. Replace the queued item value with the new one. qCDebug(KALARM_LOG) << "Replacing previously queued job"; it.value() = item; } else { // There is no job already queued for this item if (mItemsBeingCreated.contains(item.id())) { qCDebug(KALARM_LOG) << "Waiting for item initialisation"; mItemModifyJobQueue[item.id()] = item; // wait for item initialisation to complete } else { Item newItem = item; const Item current = itemById(item.id()); // fetch the up-to-date item if (current.isValid()) newItem.setRevision(current.revision()); mItemModifyJobQueue[item.id()] = Item(); // mark the queued item as now executing ItemModifyJob* job = new ItemModifyJob(newItem); job->disableRevisionCheck(); connect(job, &ItemModifyJob::result, this, &AkonadiModel::itemJobDone); mPendingItemJobs[job] = item.id(); qCDebug(KALARM_LOG) << "Executing Modify job for item" << item.id() << ", revision=" << newItem.revision(); } } } /****************************************************************************** * Called when an item job has completed. * Checks for any error. * Note that for an ItemModifyJob, the item revision number may not be updated * to the post-modification value. The next queued ItemModifyJob is therefore * not kicked off from here, but instead from the slot attached to the * itemChanged() signal, which has the revision updated. */ void AkonadiModel::itemJobDone(KJob* j) { const QHash::iterator it = mPendingItemJobs.find(j); Item::Id itemId = -1; if (it != mPendingItemJobs.end()) { itemId = it.value(); mPendingItemJobs.erase(it); } const QByteArray jobClass = j->metaObject()->className(); qCDebug(KALARM_LOG) << "AkonadiModel::itemJobDone:" << jobClass; if (j->error()) { QString errMsg; if (jobClass == "Akonadi::ItemCreateJob") errMsg = i18nc("@info", "Failed to create alarm."); else if (jobClass == "Akonadi::ItemModifyJob") errMsg = i18nc("@info", "Failed to update alarm."); else if (jobClass == "Akonadi::ItemDeleteJob") errMsg = i18nc("@info", "Failed to delete alarm."); else Q_ASSERT(0); qCCritical(KALARM_LOG) << "AkonadiModel::itemJobDone:" << errMsg << itemId << ":" << j->errorString(); Q_EMIT itemDone(itemId, false); if (itemId >= 0 && jobClass == "Akonadi::ItemModifyJob") { // Execute the next queued job for this item const Item current = itemById(itemId); // fetch the up-to-date item checkQueuedItemModifyJob(current); } // Don't show error details by default, since it's from Akonadi and likely // to be too technical for general users. KAMessageBox::detailedError(MainWindow::mainMainWindow(), errMsg, j->errorString()); } else { if (jobClass == "Akonadi::ItemCreateJob") { // Prevent modification of the item until it is fully initialised. // Either slotMonitoredItemChanged() or slotRowsInserted(), or both, // will be called when the item is done. qCDebug(KALARM_LOG) << "item id=" << static_cast(j)->item().id(); mItemsBeingCreated << static_cast(j)->item().id(); } Q_EMIT itemDone(itemId); } /* if (itemId >= 0 && jobClass == "Akonadi::ItemModifyJob") { const QHash::iterator it = mItemModifyJobQueue.find(itemId); if (it != mItemModifyJobQueue.end()) { if (!it.value().isValid()) mItemModifyJobQueue.erase(it); // there are no more jobs queued for the item } }*/ } /****************************************************************************** * Check whether there are any ItemModifyJobs waiting for a specified item, and * if so execute the first one provided its creation has completed. This * prevents clashes in Akonadi conflicts between simultaneous ItemModifyJobs for * the same item. * * Note that when an item is newly created (e.g. via addEvent()), the KAlarm * resource itemAdded() function creates an ItemModifyJob to give it a remote * ID. Until that job is complete, any other ItemModifyJob for the item will * cause a conflict. */ void AkonadiModel::checkQueuedItemModifyJob(const Item& item) { if (mItemsBeingCreated.contains(item.id())) {qCDebug(KALARM_LOG)<<"Still being created"; return; // the item hasn't been fully initialised yet } const QHash::iterator it = mItemModifyJobQueue.find(item.id()); if (it == mItemModifyJobQueue.end()) {qCDebug(KALARM_LOG)<<"No jobs queued"; return; // there are no jobs queued for the item } Item qitem = it.value(); if (!qitem.isValid()) { // There is no further job queued for the item, so remove the item from the list qCDebug(KALARM_LOG)<<"No more jobs queued"; mItemModifyJobQueue.erase(it); } else { // Queue the next job for the Item, after updating the Item's // revision number to match that set by the job just completed. qitem.setRevision(item.revision()); mItemModifyJobQueue[item.id()] = Item(); // mark the queued item as now executing ItemModifyJob* job = new ItemModifyJob(qitem); job->disableRevisionCheck(); connect(job, &ItemModifyJob::result, this, &AkonadiModel::itemJobDone); mPendingItemJobs[job] = qitem.id(); qCDebug(KALARM_LOG) << "Executing queued Modify job for item" << qitem.id() << ", revision=" << qitem.revision(); } } /****************************************************************************** * Called when rows have been inserted into the model. */ void AkonadiModel::slotRowsInserted(const QModelIndex& parent, int start, int end) { qCDebug(KALARM_LOG) << "AkonadiModel::slotRowsInserted:" << start << "-" << end << "(parent =" << parent << ")"; for (int row = start; row <= end; ++row) { const QModelIndex ix = index(row, 0, parent); const Collection collection = ix.data(CollectionRole).value(); if (collection.isValid()) { // A collection has been inserted. // Ignore it if it isn't owned by a valid resource. qCDebug(KALARM_LOG) << "Collection" << collection.id() << collection.name(); if (AgentManager::self()->instance(collection.resource()).isValid()) { QSet attrs; attrs += CollectionAttribute::name(); if (collection.hasAttribute()) mCollectionAttributes[collection.id()] = *collection.attribute(); setCollectionChanged(collection, attrs, true); Q_EMIT collectionAdded(collection); if (!mCollectionsBeingCreated.contains(collection.remoteId()) && (collection.rights() & writableRights) == writableRights) { // Update to current KAlarm format if necessary, and if the user agrees CalendarMigrator::updateToCurrentFormat(collection, false, MainWindow::mainMainWindow()); } } } else { // An item has been inserted const Item item = ix.data(ItemRole).value(); if (item.isValid()) { qCDebug(KALARM_LOG) << "item id=" << item.id() << ", revision=" << item.revision(); if (mItemsBeingCreated.removeAll(item.id())) // the new item has now been initialised checkQueuedItemModifyJob(item); // execute the next job queued for the item } } } const EventList events = eventList(parent, start, end); if (!events.isEmpty()) Q_EMIT eventsAdded(events); } /****************************************************************************** * Called when rows are about to be removed from the model. */ void AkonadiModel::slotRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { qCDebug(KALARM_LOG) << "AkonadiModel::slotRowsAboutToBeRemoved:" << start << "-" << end << "(parent =" << parent << ")"; const EventList events = eventList(parent, start, end); if (!events.isEmpty()) { for (const Event& event : events) qCDebug(KALARM_LOG) << "Collection:" << event.collection.id() << ", Event ID:" << event.event.id(); Q_EMIT eventsToBeRemoved(events); } } /****************************************************************************** * Return a list of KAEvent/Collection pairs for a given range of rows. */ AkonadiModel::EventList AkonadiModel::eventList(const QModelIndex& parent, int start, int end) { EventList events; for (int row = start; row <= end; ++row) { Collection c; const QModelIndex ix = index(row, 0, parent); const KAEvent evnt = event(ix.data(ItemRole).value(), ix, &c); if (evnt.isValid()) events += Event(evnt, c); } return events; } /****************************************************************************** * Called when a monitored collection has changed. */ void AkonadiModel::slotCollectionChanged(const Akonadi::Collection& c, const QSet& attrNames) { qCDebug(KALARM_LOG) << "AkonadiModel::slotCollectionChanged:" << c.id() << attrNames; setCollectionChanged(c, attrNames, false); } /****************************************************************************** * Called when a monitored collection's properties or content have changed. * Optionally emits a signal if properties of interest have changed. */ void AkonadiModel::setCollectionChanged(const Collection& collection, const QSet& attributeNames, bool rowInserted) { // Check for a read/write permission change const Collection::Rights oldRights = mCollectionRights.value(collection.id(), Collection::AllRights); const Collection::Rights newRights = collection.rights() & writableRights; if (newRights != oldRights) { qCDebug(KALARM_LOG) << "AkonadiModel::setCollectionChanged:" << collection.id() << ": rights ->" << newRights; mCollectionRights[collection.id()] = newRights; Q_EMIT collectionStatusChanged(collection, ReadOnly, (newRights != writableRights), rowInserted); } // Check for a change in content mime types // (e.g. when a collection is first created at startup). const CalEvent::Types oldAlarmTypes = mCollectionAlarmTypes.value(collection.id(), CalEvent::EMPTY); const CalEvent::Types newAlarmTypes = CalEvent::types(collection.contentMimeTypes()); if (newAlarmTypes != oldAlarmTypes) { qCDebug(KALARM_LOG) << "AkonadiModel::setCollectionChanged:" << collection.id() << ": alarm types ->" << newAlarmTypes; mCollectionAlarmTypes[collection.id()] = newAlarmTypes; Q_EMIT collectionStatusChanged(collection, AlarmTypes, static_cast(newAlarmTypes), rowInserted); } // Check for the collection being enabled/disabled if (attributeNames.contains(CollectionAttribute::name()) && collection.hasAttribute()) { // Enabled/disabled can only be set by KAlarm (not the resource), so if the // attibute doesn't exist, it is ignored. static bool firstEnabled = true; const CalEvent::Types oldEnabled = mCollectionEnabled.value(collection.id(), CalEvent::EMPTY); const CalEvent::Types newEnabled = collection.attribute()->enabled(); if (firstEnabled || newEnabled != oldEnabled) { qCDebug(KALARM_LOG) << "AkonadiModel::setCollectionChanged:" << collection.id() << ": enabled ->" << newEnabled; firstEnabled = false; mCollectionEnabled[collection.id()] = newEnabled; Q_EMIT collectionStatusChanged(collection, Enabled, static_cast(newEnabled), rowInserted); } } // Check for the backend calendar format changing if (attributeNames.contains(CompatibilityAttribute::name())) { // Update to current KAlarm format if necessary, and if the user agrees qCDebug(KALARM_LOG) << "AkonadiModel::setCollectionChanged: CompatibilityAttribute"; Collection col(collection); refresh(col); CalendarMigrator::updateToCurrentFormat(col, false, MainWindow::mainMainWindow()); } if (mMigrating) { mCollectionIdsBeingCreated.removeAll(collection.id()); if (mCollectionsBeingCreated.isEmpty() && mCollectionIdsBeingCreated.isEmpty() && CalendarMigrator::completed()) { qCDebug(KALARM_LOG) << "AkonadiModel::setCollectionChanged: Migration completed"; mMigrating = false; Q_EMIT migrationCompleted(); } } } /****************************************************************************** * Called when a monitored collection is removed. */ void AkonadiModel::slotCollectionRemoved(const Collection& collection) { const Collection::Id id = collection.id(); qCDebug(KALARM_LOG) << "AkonadiModel::slotCollectionRemoved:" << id; mCollectionRights.remove(id); mCollectionsDeleting.removeAll(id); while (mCollectionsDeleted.count() > 20) // don't let list grow indefinitely mCollectionsDeleted.removeFirst(); mCollectionsDeleted << id; Q_EMIT collectionDeleted(id); } /****************************************************************************** * Called when a collection creation is about to start, or has completed. */ void AkonadiModel::slotCollectionBeingCreated(const QString& path, Akonadi::Collection::Id id, bool finished) { if (finished) { mCollectionsBeingCreated.removeAll(path); mCollectionIdsBeingCreated << id; } else mCollectionsBeingCreated << path; } /****************************************************************************** * Called when calendar migration has completed. */ void AkonadiModel::slotMigrationCompleted() { if (mCollectionsBeingCreated.isEmpty() && mCollectionIdsBeingCreated.isEmpty()) { qCDebug(KALARM_LOG) << "AkonadiModel: Migration completed"; mMigrating = false; Q_EMIT migrationCompleted(); } } /****************************************************************************** * Called when an item in the monitored collections has changed. */ void AkonadiModel::slotMonitoredItemChanged(const Akonadi::Item& item, const QSet&) { qCDebug(KALARM_LOG) << "AkonadiModel::slotMonitoredItemChanged: item id=" << item.id() << ", revision=" << item.revision(); mItemsBeingCreated.removeAll(item.id()); // the new item has now been initialised checkQueuedItemModifyJob(item); // execute the next job queued for the item KAEvent evnt = event(item); if (!evnt.isValid()) return; const QModelIndexList indexes = modelIndexesForItem(this, item); for (const QModelIndex& index : indexes) { if (index.isValid()) { // Wait to ensure that the base EntityTreeModel has processed the // itemChanged() signal first, before we Q_EMIT eventChanged(). Collection c = data(index, ParentCollectionRole).value(); evnt.setCollectionId(c.id()); mPendingEventChanges.enqueue(Event(evnt, c)); QTimer::singleShot(0, this, &AkonadiModel::slotEmitEventChanged); break; } } } /****************************************************************************** * Called to Q_EMIT a signal when an event in the monitored collections has * changed. */ void AkonadiModel::slotEmitEventChanged() { while (!mPendingEventChanges.isEmpty()) { Q_EMIT eventChanged(mPendingEventChanges.dequeue()); } } /****************************************************************************** * Refresh the specified Collection with up to date data. * Return: true if successful, false if collection not found. */ bool AkonadiModel::refresh(Akonadi::Collection& collection) const { const QModelIndex ix = modelIndexForCollection(this, collection); if (!ix.isValid()) return false; collection = ix.data(CollectionRole).value(); return true; } /****************************************************************************** * Refresh the specified Item with up to date data. * Return: true if successful, false if item not found. */ bool AkonadiModel::refresh(Akonadi::Item& item) const { const QModelIndexList ixs = modelIndexesForItem(this, item); if (ixs.isEmpty() || !ixs[0].isValid()) return false; item = ixs[0].data(ItemRole).value(); return true; } /****************************************************************************** * Find the QModelIndex of a collection. */ QModelIndex AkonadiModel::collectionIndex(const Akonadi::Collection& collection) const { const QModelIndex ix = modelIndexForCollection(this, collection); if (!ix.isValid()) return QModelIndex(); return ix; } /****************************************************************************** * Return the up to date collection with the specified Akonadi ID. */ Collection AkonadiModel::collectionById(Collection::Id id) const { const QModelIndex ix = modelIndexForCollection(this, Collection(id)); if (!ix.isValid()) return Collection(); return ix.data(CollectionRole).value(); } /****************************************************************************** * Find the QModelIndex of an item. */ QModelIndex AkonadiModel::itemIndex(const Akonadi::Item& item) const { const QModelIndexList ixs = modelIndexesForItem(this, item); if (ixs.isEmpty() || !ixs[0].isValid()) return QModelIndex(); return ixs[0]; } /****************************************************************************** * Return the up to date item with the specified Akonadi ID. */ Item AkonadiModel::itemById(Item::Id id) const { const QModelIndexList ixs = modelIndexesForItem(this, Item(id)); if (ixs.isEmpty() || !ixs[0].isValid()) return Item(); return ixs[0].data(ItemRole).value(); } /****************************************************************************** * Find the collection containing the specified Akonadi item ID. */ Collection AkonadiModel::collectionForItem(Item::Id id) const { const QModelIndex ix = itemIndex(id); if (!ix.isValid()) return Collection(); return ix.data(ParentCollectionRole).value(); } bool AkonadiModel::isCompatible(const Collection& collection) { return collection.hasAttribute() && collection.attribute()->compatibility() == KACalendar::Current; } /****************************************************************************** * Return whether a collection is fully writable. */ int AkonadiModel::isWritable(const Akonadi::Collection& collection) { KACalendar::Compat format; return isWritable(collection, format); } int AkonadiModel::isWritable(const Akonadi::Collection& collection, KACalendar::Compat& format) { format = KACalendar::Incompatible; if (!collection.isValid()) return -1; Collection col = collection; instance()->refresh(col); // update with latest data if ((col.rights() & writableRights) != writableRights) { format = KACalendar::Current; return -1; } if (!col.hasAttribute()) return -1; format = col.attribute()->compatibility(); switch (format) { case KACalendar::Current: return 1; case KACalendar::Converted: case KACalendar::Convertible: return 0; default: return -1; } } CalEvent::Types AkonadiModel::types(const Collection& collection) { return CalEvent::types(collection.contentMimeTypes()); } // vim: et sw=4: diff --git a/src/akonadimodel.h b/src/akonadimodel.h index 968af051..4982674a 100644 --- a/src/akonadimodel.h +++ b/src/akonadimodel.h @@ -1,344 +1,345 @@ /* * akonadimodel.h - KAlarm calendar file access using Akonadi * Program: kalarm * Copyright © 2010-2019 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 AKONADIMODEL_H #define AKONADIMODEL_H #include "eventid.h" #include #include #include #include #include #include #include #include #include namespace Akonadi { class ChangeRecorder; } class QPixmap; class KJob; using namespace KAlarmCal; class AkonadiModel : public Akonadi::EntityTreeModel { Q_OBJECT public: enum Change { Added, Deleted, Invalidated, Enabled, ReadOnly, AlarmTypes, WrongType, Location, Colour }; enum { // data columns // Item columns TimeColumn = 0, TimeToColumn, RepeatColumn, ColourColumn, TypeColumn, TextColumn, TemplateNameColumn, ColumnCount }; enum { // additional data roles // Collection roles EnabledTypesRole = UserRole, // alarm types which are enabled for the collection BaseColourRole, // background colour ignoring collection colour AlarmTypeRole, // OR of event types which collection contains IsStandardRole, // OR of event types which collection is standard for KeepFormatRole, // user has chosen not to update collection's calendar storage format // Item roles EnabledRole, // true for enabled alarm, false for disabled StatusRole, // KAEvent::ACTIVE/ARCHIVED/TEMPLATE AlarmActionsRole, // KAEvent::Actions AlarmSubActionRole, // KAEvent::Action ValueRole, // numeric value SortRole, // the value to use for sorting TimeDisplayRole, // time column value with '~' representing omitted leading zeroes + ColumnTitleRole, // column titles (whether displayed or not) CommandErrorRole // last command execution error for alarm (per user) }; /** Struct containing a KAEvent and its parent Collection. */ struct Event { Event(const KAEvent& e, const Akonadi::Collection& c) : event(e), collection(c) {} EventId eventId() const { return EventId(collection.id(), event.id()); } bool isConsistent() const { return event.collectionId() == collection.id(); } KAEvent event; Akonadi::Collection collection; }; typedef QList EventList; static AkonadiModel* instance(); ~AkonadiModel() override; /** Return the display name for a collection. */ QString displayName(Akonadi::Collection&) const; /** Return the storage type (file/directory/URL etc.) for a collection. */ QString storageType(const Akonadi::Collection&) const; /** Get the foreground color for a collection, based on specified mime types. */ static QColor foregroundColor(const Akonadi::Collection&, const QStringList& mimeTypes); /** Set the background color for a collection and its alarms. */ void setBackgroundColor(Akonadi::Collection&, const QColor&); /** Get the background color for a collection and its alarms. */ QColor backgroundColor(Akonadi::Collection&) const; /** Get the tooltip for a collection. The collection's enabled status is * evaluated for specified alarm types. */ QString tooltip(const Akonadi::Collection&, CalEvent::Types) const; /** Return the read-only status tooltip for a collection. * A null string is returned if the collection is fully writable. */ static QString readOnlyTooltip(const Akonadi::Collection&); /** To be called when the command error status of an alarm has changed, * to set in the Akonadi database and update the visual command error indications. */ void updateCommandError(const KAEvent&); QVariant data(const QModelIndex&, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex&, const QVariant& value, int role) override; /** Refresh the specified collection instance with up to date data. */ bool refresh(Akonadi::Collection&) const; /** Refresh the specified item instance with up to date data. */ bool refresh(Akonadi::Item&) const; QModelIndex collectionIndex(Akonadi::Collection::Id id) const { return collectionIndex(Akonadi::Collection(id)); } QModelIndex collectionIndex(const Akonadi::Collection&) const; Akonadi::Collection collectionById(Akonadi::Collection::Id) const; Akonadi::Collection collectionForItem(Akonadi::Item::Id) const; Akonadi::Collection collection(const KAEvent& e) const { return collectionForItem(e.itemId()); } /** Remove a collection from Akonadi. The calendar file is not removed. * @return true if a removal job has been scheduled. */ bool removeCollection(const Akonadi::Collection&); /** Reload a collection's data from Akonadi storage (not from the backend). */ bool reloadCollection(const Akonadi::Collection&); /** Reload all collections' data from Akonadi storage (not from the backend). */ void reload(); /** Return whether calendar migration/creation at initialisation has completed. */ bool isMigrationCompleted() const; bool isCollectionBeingDeleted(Akonadi::Collection::Id) const; QModelIndex itemIndex(Akonadi::Item::Id id) const { return itemIndex(Akonadi::Item(id)); } QModelIndex itemIndex(const Akonadi::Item&) const; Akonadi::Item itemById(Akonadi::Item::Id) const; /** Return the alarm with the specified unique identifier. * @return the event, or invalid event if no such event exists. */ KAEvent event(const Akonadi::Item& item) const { return event(item, QModelIndex(), nullptr); } KAEvent event(Akonadi::Item::Id) const; KAEvent event(const QModelIndex&) const; using QObject::event; // prevent warning about hidden virtual method /** Return an event's model index, based on its itemId() value. */ QModelIndex eventIndex(const KAEvent&); /** Search for an event's item ID. This method ignores any itemId() value * contained in the KAEvent. The collectionId() is used if available. */ Akonadi::Item::Id findItemId(const KAEvent&); #if 0 /** Return all events in a collection, optionally of a specified type. */ KAEvent::List events(Akonadi::Collection&, CalEvent::Type = CalEvent::EMPTY) const; #endif bool addEvent(KAEvent&, Akonadi::Collection&); bool addEvents(const KAEvent::List&, Akonadi::Collection&); bool updateEvent(KAEvent& event); bool updateEvent(Akonadi::Item::Id oldId, KAEvent& newEvent); bool deleteEvent(const KAEvent& event); bool deleteEvent(Akonadi::Item::Id itemId); /** Check whether a collection is stored in the current KAlarm calendar format. */ static bool isCompatible(const Akonadi::Collection&); /** Return whether a collection is fully writable, i.e. with * create/delete/change rights and compatible with the current KAlarm * calendar format. * * @return 1 = fully writable, * 0 = writable except that backend calendar is in an old KAlarm format, * -1 = read-only or incompatible format. */ static int isWritable(const Akonadi::Collection&); /** Return whether a collection is fully writable, i.e. with * create/delete/change rights and compatible with the current KAlarm * calendar format. * * @param collection The collection to be inspected * @param format Updated to contain the backend calendar storage format. * If read-only, = KACalendar::Current; * if unknown format, = KACalendar::Incompatible; * otherwise = the backend calendar storage format. * @return 1 = fully writable, * 0 = writable except that backend calendar is in an old KAlarm format, * -1 = read-only (if @p compat == KACalendar::Current), or * incompatible format otherwise. */ static int isWritable(const Akonadi::Collection& collection, KACalendar::Compat& format); static CalEvent::Types types(const Akonadi::Collection&); static QSize iconSize() { return mIconSize; } Q_SIGNALS: /** Signal emitted when a collection has been added to the model. */ void collectionAdded(const Akonadi::Collection&); /** Signal emitted when a collection's enabled, read-only or alarm types * status has changed. * @param change The type of status which has changed * @param newValue The new value of the status that has changed * @param inserted true if the reason for the change is that the collection * has been inserted into the model */ void collectionStatusChanged(const Akonadi::Collection&, AkonadiModel::Change change, const QVariant& newValue, bool inserted); /** Signal emitted when events have been added to the model. */ void eventsAdded(const AkonadiModel::EventList&); /** Signal emitted when events are about to be removed from the model. */ void eventsToBeRemoved(const AkonadiModel::EventList&); /** Signal emitted when an event in the model has changed. */ void eventChanged(const AkonadiModel::Event&); /** Signal emitted when Akonadi has completed a collection deletion. * @param id Akonadi ID for the collection */ void collectionDeleted(Akonadi::Collection::Id id); /** Signal emitted when Akonadi has completed an item creation, update * or deletion. * @param id Akonadi ID for the item * @param status true if successful, false if error */ void itemDone(Akonadi::Item::Id id, bool status = true); /** Signal emitted when calendar migration/creation has completed. */ void migrationCompleted(); /** Signal emitted when the Akonadi server has stopped. */ void serverStopped(); protected: QVariant entityHeaderData(int section, Qt::Orientation, int role, HeaderGroup) const override; int entityColumnCount(HeaderGroup) const override; private Q_SLOTS: void checkResources(Akonadi::ServerManager::State); void slotMigrationCompleted(); void slotCollectionChanged(const Akonadi::Collection& c, const QSet& attrNames); void slotCollectionRemoved(const Akonadi::Collection&); void slotCollectionBeingCreated(const QString& path, Akonadi::Collection::Id, bool finished); void slotUpdateTimeTo(); void slotUpdateArchivedColour(const QColor&); void slotUpdateDisabledColour(const QColor&); void slotUpdateHolidays(); void slotUpdateWorkingHours(); void slotRowsInserted(const QModelIndex& parent, int start, int end); void slotRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end); void slotMonitoredItemChanged(const Akonadi::Item&, const QSet&); void slotEmitEventChanged(); void modifyCollectionAttrJobDone(KJob*); void itemJobDone(KJob*); private: struct CalData // data per collection { CalData() : enabled(false) { } CalData(bool e, const QColor& c) : colour(c), enabled(e) { } QColor colour; // user selected color for the calendar bool enabled; // whether the collection is enabled }; struct CollJobData // collection data for jobs in progress { CollJobData() : id(-1) {} CollJobData(Akonadi::Collection::Id i, const QString& d) : id(i), displayName(d) {} Akonadi::Collection::Id id; QString displayName; }; struct CollTypeData // data for configuration dialog for collection creation job { CollTypeData() : parent(nullptr), alarmType(CalEvent::EMPTY) {} CollTypeData(CalEvent::Type t, QWidget* p) : parent(p), alarmType(t) {} QWidget* parent; CalEvent::Type alarmType; }; AkonadiModel(Akonadi::ChangeRecorder*, QObject* parent); void initCalendarMigrator(); KAEvent event(const Akonadi::Item&, const QModelIndex&, Akonadi::Collection*) const; void signalDataChanged(bool (*checkFunc)(const Akonadi::Item&), int startColumn, int endColumn, const QModelIndex& parent); void setCollectionChanged(const Akonadi::Collection&, const QSet&, bool rowInserted); void queueItemModifyJob(const Akonadi::Item&); void checkQueuedItemModifyJob(const Akonadi::Item&); #if 0 void getChildEvents(const QModelIndex& parent, CalEvent::Type, KAEvent::List&) const; #endif QColor backgroundColor_p(const Akonadi::Collection&) const; QString repeatText(const KAEvent&) const; QString repeatOrder(const KAEvent&) const; QPixmap* eventIcon(const KAEvent&) const; QString whatsThisText(int column) const; EventList eventList(const QModelIndex& parent, int start, int end); static AkonadiModel* mInstance; static QPixmap* mTextIcon; static QPixmap* mFileIcon; static QPixmap* mCommandIcon; static QPixmap* mEmailIcon; static QPixmap* mAudioIcon; static QSize mIconSize; static int mTimeHourPos; // position of hour within time string, or -1 if leading zeroes included Akonadi::ChangeRecorder* mMonitor; QHash mCollectionAlarmTypes; // last content mime types of each collection QHash mCollectionRights; // last writable status of each collection QHash mCollectionEnabled; // last enabled mime types of each collection QHash mCollectionAttributes; // current set value of CollectionAttribute of each collection QHash mNewCollectionEnabled; // enabled statuses of new collections QHash mPendingCollectionJobs; // pending collection creation/deletion jobs, with collection ID & name QHash mPendingColCreateJobs; // default alarm type for pending collection creation jobs QHash mPendingItemJobs; // pending item creation/deletion jobs, with event ID QHash mItemModifyJobQueue; // pending item modification jobs, invalid item = queue empty but job active QList mCollectionsBeingCreated; // path names of new collections being created by migrator QList mCollectionIdsBeingCreated; // ids of new collections being created by migrator QList mItemsBeingCreated; // new items not fully initialised yet QList mCollectionsDeleting; // collections currently being removed QList mCollectionsDeleted; // collections recently removed QQueue mPendingEventChanges; // changed events with changedEvent() signal pending bool mResourcesChecked; // whether resource existence has been checked yet bool mMigrating; // currently migrating calendars }; #endif // AKONADIMODEL_H // vim: et sw=4: diff --git a/src/alarmlistview.cpp b/src/alarmlistview.cpp index bb42aea3..466ecfa1 100644 --- a/src/alarmlistview.cpp +++ b/src/alarmlistview.cpp @@ -1,123 +1,177 @@ /* * alarmlistview.cpp - widget showing list of alarms * Program: kalarm - * Copyright © 2007,2008,2010 by David Jarvie + * Copyright © 2007,2008,2010,2019 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 "kalarm.h" #include "alarmlistview.h" #include #include #include +#include +#include #include AlarmListView::AlarmListView(const QByteArray& configGroup, QWidget* parent) : EventListView(parent), mConfigGroup(configGroup) { setEditOnSingleClick(true); connect(header(), &QHeaderView::sectionMoved, this, &AlarmListView::sectionMoved); } void AlarmListView::setModel(QAbstractItemModel* model) { EventListView::setModel(model); KConfigGroup config(KSharedConfig::openConfig(), mConfigGroup.constData()); QByteArray settings = config.readEntry("ListHead", QByteArray()); if (!settings.isEmpty()) header()->restoreState(settings); header()->setSectionsMovable(true); header()->setStretchLastSection(false); header()->setSectionResizeMode(AlarmListModel::TimeColumn, QHeaderView::ResizeToContents); header()->setSectionResizeMode(AlarmListModel::TimeToColumn, QHeaderView::ResizeToContents); header()->setSectionResizeMode(AlarmListModel::RepeatColumn, QHeaderView::ResizeToContents); header()->setSectionResizeMode(AlarmListModel::ColourColumn, QHeaderView::Fixed); header()->setSectionResizeMode(AlarmListModel::TypeColumn, QHeaderView::Fixed); header()->setSectionResizeMode(AlarmListModel::TextColumn, QHeaderView::Stretch); header()->setStretchLastSection(true); // necessary to ensure ResizeToContents columns do resize to contents! const int margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin); header()->resizeSection(AlarmListModel::ColourColumn, viewOptions().fontMetrics.lineSpacing() * 3 / 4); header()->resizeSection(AlarmListModel::TypeColumn, AlarmListModel::iconWidth() + 2*margin + 2); + header()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(header(), &QWidget::customContextMenuRequested, this, &AlarmListView::headerContextMenuRequested); +} + +QList AlarmListView::columnsVisible() const +{ + if (!model()) + return {}; + return { !header()->isSectionHidden(AlarmListModel::TimeColumn), + !header()->isSectionHidden(AlarmListModel::TimeToColumn), + !header()->isSectionHidden(AlarmListModel::RepeatColumn), + !header()->isSectionHidden(AlarmListModel::ColourColumn), + !header()->isSectionHidden(AlarmListModel::TypeColumn) }; +} + +void AlarmListView::setColumnsVisible(const QList& show) +{ + if (!model() || show.size() < 5) + return; + header()->setSectionHidden(AlarmListModel::TimeColumn, !show[0]); + header()->setSectionHidden(AlarmListModel::TimeToColumn, !show[1]); + header()->setSectionHidden(AlarmListModel::RepeatColumn, !show[2]); + header()->setSectionHidden(AlarmListModel::ColourColumn, !show[3]); + header()->setSectionHidden(AlarmListModel::TypeColumn, !show[4]); + sortByColumn(show[0] ? AlarmListModel::TimeColumn : AlarmListModel::TimeToColumn, Qt::AscendingOrder); } /****************************************************************************** * Called when the column order is changed. * Save the new order for restoration on program restart. */ void AlarmListView::sectionMoved() { KConfigGroup config(KSharedConfig::openConfig(), mConfigGroup.constData()); config.writeEntry("ListHead", header()->saveState()); config.sync(); } /****************************************************************************** -* Set which time columns are to be displayed. +* Called when a context menu is requested for the header. +* Allow the user to choose which columns to display. */ -void AlarmListView::selectTimeColumns(bool time, bool timeTo) +void AlarmListView::headerContextMenuRequested(const QPoint& pt) { - if (!time && !timeTo) - return; // always show at least one time column -// bool changed = false; - bool hidden = header()->isSectionHidden(AlarmListModel::TimeColumn); - if (time && hidden) - { - // Unhide the time column - header()->setSectionHidden(AlarmListModel::TimeColumn, false); -// changed = true; - } - else if (!time && !hidden) - { - // Hide the time column - header()->setSectionHidden(AlarmListModel::TimeColumn, true); -// changed = true; - } - hidden = header()->isSectionHidden(AlarmListModel::TimeToColumn); - if (timeTo && hidden) - { - // Unhide the time-to-alarm column - header()->setSectionHidden(AlarmListModel::TimeToColumn, false); -// changed = true; - } - else if (!timeTo && !hidden) + QAbstractItemModel* almodel = model(); + int count = header()->count(); + QMenu menu; + for (int col = 0; col < count; ++col) { - // Hide the time-to-alarm column - header()->setSectionHidden(AlarmListModel::TimeToColumn, true); -// changed = true; + const QString title = almodel->headerData(col, Qt::Horizontal, AkonadiModel::ColumnTitleRole).toString(); + if (!title.isEmpty()) + { + QAction* act = menu.addAction(title); + act->setData(col); + act->setCheckable(true); + act->setChecked(!header()->isSectionHidden(col)); + if (col == AlarmListModel::TextColumn) + act->setEnabled(false); // don't allow text column to be hidden + else + QObject::connect(act, &QAction::triggered, + this, [this, &menu, act] { showHideColumn(menu, act); }); + } } -// if (changed) -// { -// resizeLastColumn(); -// triggerUpdate(); // ensure scroll bar appears if needed -// } + enableTimeColumns(&menu); + menu.exec(header()->mapToGlobal(pt)); } -/* -void AlarmListView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) +/****************************************************************************** +* Show or hide a column according to the header context menu. +*/ +void AlarmListView::showHideColumn(QMenu& menu, QAction* act) { - for (int col = topLeft.column(); col < bottomRight.column(); ++col) + int col = act->data().toInt(); + if (col < 0 || col >= header()->count()) + return; + bool show = act->isChecked(); + header()->setSectionHidden(col, !show); + if (col == AlarmListModel::TimeColumn || col == AlarmListModel::TimeToColumn) + enableTimeColumns(&menu); + Q_EMIT columnsVisibleChanged(); +} + +/****************************************************************************** +* Disable Time or Time To in the context menu if the other one is not +* selected to be displayed, to ensure that at least one is always shown. +*/ +void AlarmListView::enableTimeColumns(QMenu* menu) +{ + bool timeShown = !header()->isSectionHidden(AlarmListModel::TimeColumn); + bool timeToShown = !header()->isSectionHidden(AlarmListModel::TimeToColumn); + QList actions = menu->actions(); + if (!timeToShown) + { + header()->setSectionHidden(AlarmListModel::TimeColumn, false); + for (QAction* act : qAsConst(actions)) + { + if (act->data().toInt() == AlarmListModel::TimeColumn) + { + act->setEnabled(false); + break; + } + } + } + else if (!timeShown) { - if (col != header()->resizeMode(col) == QHeaderView::ResizeToContents) - resizeColumnToContents(col); + header()->setSectionHidden(AlarmListModel::TimeToColumn, false); + for (QAction* act : qAsConst(actions)) + { + if (act->data().toInt() == AlarmListModel::TimeToColumn) + { + act->setEnabled(false); + break; + } + } } } -*/ // vim: et sw=4: diff --git a/src/alarmlistview.h b/src/alarmlistview.h index dbcc830e..6de5542e 100644 --- a/src/alarmlistview.h +++ b/src/alarmlistview.h @@ -1,48 +1,56 @@ /* * alarmlistview.h - widget showing list of alarms * Program: kalarm - * Copyright © 2007 by David Jarvie + * Copyright © 2007,2019 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 ALARMLISTVIEW_H #define ALARMLISTVIEW_H #include "kalarm.h" #include "eventlistview.h" #include class AlarmListView : public EventListView { Q_OBJECT public: explicit AlarmListView(const QByteArray& configGroup, QWidget* parent = nullptr); void setModel(QAbstractItemModel*) override; - void selectTimeColumns(bool time, bool timeTo); + QList columnsVisible() const; + void setColumnsVisible(const QList& show); + + Q_SIGNALS: + void columnsVisibleChanged(); private Q_SLOTS: void sectionMoved(); + void headerContextMenuRequested(const QPoint&); private: + void showHideColumn(QMenu&, QAction*); + void enableTimeColumns(QMenu*); + QByteArray mConfigGroup; }; #endif // ALARMLISTVIEW_H // vim: et sw=4: diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index dfa5569c..93d95143 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,1590 +1,1543 @@ /* * mainwindow.cpp - main application window * Program: kalarm - * Copyright © 2001-2018 by David Jarvie + * Copyright © 2001-2019 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 "kalarm.h" #include "mainwindow.h" #include "alarmcalendar.h" #include "alarmlistdelegate.h" #include "autoqpointer.h" #include "alarmlistview.h" #include "birthdaydlg.h" #include "functions.h" #include "kalarmapp.h" #include "kamail.h" #include "messagebox.h" #include "newalarmaction.h" #include "prefdlg.h" #include "preferences.h" #include "resourceselector.h" #include "synchtimer.h" #include "templatedlg.h" #include "templatemenuaction.h" #include "templatepickdlg.h" #include "traywindow.h" #include "wakedlg.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include using namespace KCalCore; 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 using namespace KAlarmCal; namespace { const QString UI_FILE(QStringLiteral("kalarmui.rc")); const char* WINDOW_NAME = "MainWindow"; const char* VIEW_GROUP = "View"; -const char* SHOW_TIME_KEY = "ShowAlarmTime"; -const char* SHOW_TIME_TO_KEY = "ShowTimeToAlarm"; +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; -// Collect these widget labels together to ensure consistent wording and -// translations across different modules. -QString MainWindow::i18n_a_ShowAlarmTimes() { return i18nc("@action", "Show &Alarm Times"); } -QString MainWindow::i18n_chk_ShowAlarmTime() { return i18nc("@option:check", "Show alarm time"); } -QString MainWindow::i18n_o_ShowTimeToAlarms() { return i18nc("@action", "Show Time t&o Alarms"); } -QString MainWindow::i18n_chk_ShowTimeToAlarm() { return i18nc("@option:check", "Show time until alarm"); } - /****************************************************************************** * 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), mResourcesWidth(-1), mHiddenTrayParent(false), mShown(false), mResizing(false) { 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); - mShowTime = config.readEntry(SHOW_TIME_KEY, true); - mShowTimeTo = config.readEntry(SHOW_TIME_TO_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(qApp->desktop()->width()), (int)0); } setAcceptDrops(true); // allow drag-and-drop onto this window - if (!mShowTimeTo) - mShowTime = true; // ensure at least one time column is visible 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 = new AlarmListModel(this); mListFilterModel->setEventTypeFilter(mShowArchived ? CalEvent::ACTIVE | CalEvent::ARCHIVED : CalEvent::ACTIVE); mListView = new AlarmListView(WINDOW_NAME, mSplitter); mListView->setModel(mListFilterModel); - mListView->selectTimeColumns(mShowTime, mShowTimeTo); - mListView->sortByColumn(mShowTime ? AlarmListModel::TimeColumn : AlarmListModel::TimeToColumn, Qt::AscendingOrder); + 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(AkonadiModel::instance(), &AkonadiModel::collectionStatusChanged, 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("ShowTime", mShowTime); - config.writeEntry("ShowTimeTo", mShowTimeTo); + 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); - mShowTime = config.readEntry("ShowTime", true); - mShowTimeTo = config.readEntry("ShowTimeTo", 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()->trayWindow() ? theApp()->trayWindow()->assocMainWindow() : nullptr; if (tray && tray->isVisible()) return tray; for (int i = 0, end = mWindowList.count(); i < end; ++i) if (mWindowList[i]->isVisible()) return mWindowList[i]; if (tray) return tray; if (mWindowList.isEmpty()) return nullptr; return mWindowList[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); } } 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(qApp->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 - mActionShowTime = new KToggleAction(i18n_a_ShowAlarmTimes(), this); - actions->addAction(QStringLiteral("showAlarmTimes"), mActionShowTime); - connect(mActionShowTime, &KToggleAction::triggered, this, &MainWindow::slotShowTime); - - mActionShowTimeTo = new KToggleAction(i18n_o_ShowTimeToAlarms(), this); - actions->addAction(QStringLiteral("showTimeToAlarms"), mActionShowTimeTo); - actions->setDefaultShortcut(mActionShowTimeTo, QKeySequence(Qt::CTRL + Qt::Key_I)); - connect(mActionShowTimeTo, &KToggleAction::triggered, this, &MainWindow::slotShowTimeTo); - 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); 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); - mActionShowTime->setChecked(mShowTime); - mActionShowTimeTo->setChecked(mShowTimeTo); 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); 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"; AkonadiModel::instance()->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(Akonadi::Item::Id eventId) { mListView->clearSelection(); 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; AlarmCalendar* resources = AlarmCalendar::resources(); for (int i = 0; i < events.count(); ) { Akonadi::Collection c = resources->collectionForEvent(events[i].itemId()); if (!c.isValid()) events.remove(i); else undos.append(events[i++], c); } 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 AlarmCalendar* resources = AlarmCalendar::resources(); for (int i = 0, end = events.count(); i < end; ++i) { if (!ineligibleIDs.contains(EventId(events[i]))) { undos.append(events[i], resources->collectionForEvent(events[i].itemId())); } } 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 Show Alarm Times menu item is selected or deselected. +* Called when the columns visible in the alarm list view have changed. */ -void MainWindow::slotShowTime() +void MainWindow::slotAlarmListColumnsChanged() { - mShowTime = !mShowTime; - mActionShowTime->setChecked(mShowTime); - if (!mShowTime && !mShowTimeTo) - slotShowTimeTo(); // at least one time column must be displayed - else - { - mListView->selectTimeColumns(mShowTime, mShowTimeTo); - KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); - config.writeEntry(SHOW_TIME_KEY, mShowTime); - config.writeEntry(SHOW_TIME_TO_KEY, mShowTimeTo); - } -} - -/****************************************************************************** -* Called when the Show Time To Alarms menu item is selected or deselected. -*/ -void MainWindow::slotShowTimeTo() -{ - mShowTimeTo = !mShowTimeTo; - mActionShowTimeTo->setChecked(mShowTimeTo); - if (!mShowTimeTo && !mShowTime) - slotShowTime(); // at least one time column must be displayed - else - { - mListView->selectTimeColumns(mShowTime, mShowTimeTo); - KConfigGroup config(KSharedConfig::openConfig(), VIEW_GROUP); - config.writeEntry(SHOW_TIME_KEY, mShowTime); - config.writeEntry(SHOW_TIME_TO_KEY, mShowTimeTo); - } + 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() { AlarmCalendar::importAlarms(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()) { KAEvent::List evts = KAEvent::ptrList(events); AlarmCalendar::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; AlarmCalendar* resources = AlarmCalendar::resources(); for (int i = 0, end = events.count(); i < end; ++i) { Akonadi::Collection c = resources->collectionForEvent(events[i].itemId()); undos.append(events[i], c); } 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 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 files; MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone())); #ifndef NDEBUG QString fmts = data->formats().join(QStringLiteral(", ")); 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[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; KPIM::MailSummary& summary = mailList[0]; QDateTime dt; dt.setTime_t(summary.date()); 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"; Event::List events = calendar->rawEvents(); if (!events.isEmpty()) { KAEvent ev(events[0]); KAlarm::editNewAlarm(&ev, win); return; } // If todos are included, use the first todo Todo::List todos = calendar->rawTodos(); if (todos.isEmpty()) return; Todo::Ptr todo = todos[0]; alarmText.setTodo(todo); KADateTime start(todo->dtStart(true)); if (!start.isValid() && todo->hasDueDate()) start = KADateTime(todo->dtDue(true)); 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); if (todo->recurs()) { ev.setRecurrence(*todo->recurrence()); ev.setNextOccurrence(KADateTime::currentUtcDateTime()); } ev.endChanges(); KAlarm::editNewAlarm(&ev, win); return; } else if (!(files = data->urls()).isEmpty()) { 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(files[0]).name(); action = mimeTypeName.startsWith(QStringLiteral("audio/")) ? KAEvent::AUDIO : KAEvent::FILE; alarmText.setText(files[0].toDisplayString()); } else if (data->hasText()) { 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 = !CollectionControlModel::enabledCollections(CalEvent::ACTIVE, true).isEmpty(); bool templat = !CollectionControlModel::enabledCollections(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(); 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->itemId())) 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) && !CollectionControlModel::enabledCollections(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; Akonadi::Collection c = AkonadiModel::instance()->collection(event); KAlarm::updateEditedAlarm(dlg, event, c); } /****************************************************************************** * 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/mainwindow.h b/src/mainwindow.h index 02ea55b6..1ac3d390 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -1,215 +1,205 @@ /* * mainwindow.h - main application window * Program: kalarm - * Copyright © 2001-2011 by David Jarvie + * Copyright © 2001-2019 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 MAINWINDOW_H #define MAINWINDOW_H /** @file mainwindow.h - main application window */ #include "editdlg.h" #include "mainwindowbase.h" #include "undo.h" #include #include #include #include #include class QDragEnterEvent; class QHideEvent; class QShowEvent; class QResizeEvent; class QDropEvent; class QCloseEvent; class QSplitter; class QMenu; class QAction; class KToggleAction; class KToolBarPopupAction; class AlarmListModel; class AlarmListView; class NewAlarmAction; class TemplateDlg; class ResourceSelector; class MainWindow : public MainWindowBase, public KCalCore::Calendar::CalendarObserver { Q_OBJECT public: static MainWindow* create(bool restored = false); ~MainWindow() override; bool isTrayParent() const; bool isHiddenTrayParent() const { return mHiddenTrayParent; } bool showingArchived() const { return mShowArchived; } void selectEvent(Akonadi::Item::Id); KAEvent selectedEvent() const; void editAlarm(EditAlarmDlg*, const KAEvent&); void clearSelection(); bool eventFilter(QObject*, QEvent*) override; static void refresh(); static void executeDragEnterEvent(QDragEnterEvent*); static void executeDropEvent(MainWindow*, QDropEvent*); static void closeAll(); static MainWindow* toggleWindow(MainWindow*); static MainWindow* mainMainWindow(); static MainWindow* firstWindow() { return mWindowList.isEmpty() ? nullptr : mWindowList[0]; } static int count() { return mWindowList.count(); } - static QString i18n_a_ShowAlarmTimes(); // text of 'Show Alarm Times' action, with 'A' shortcut - static QString i18n_chk_ShowAlarmTime(); // text of 'Show alarm time' checkbox - static QString i18n_o_ShowTimeToAlarms(); // text of 'Show Time to Alarms' action, with 'O' shortcut - static QString i18n_chk_ShowTimeToAlarm(); // text of 'Show time until alarm' checkbox - public Q_SLOTS: virtual void show(); Q_SIGNALS: void selectionChanged(); protected: void resizeEvent(QResizeEvent*) override; void showEvent(QShowEvent*) override; void hideEvent(QHideEvent*) override; void closeEvent(QCloseEvent*) override; void dragEnterEvent(QDragEnterEvent* e) override { executeDragEnterEvent(e); } void dropEvent(QDropEvent*) override; void saveProperties(KConfigGroup&) override; void readProperties(const KConfigGroup&) override; private Q_SLOTS: void slotNew(EditAlarmDlg::Type); void slotNewDisplay() { slotNew(EditAlarmDlg::DISPLAY); } void slotNewCommand() { slotNew(EditAlarmDlg::COMMAND); } void slotNewEmail() { slotNew(EditAlarmDlg::EMAIL); } void slotNewAudio() { slotNew(EditAlarmDlg::AUDIO); } void slotNewFromTemplate(const KAEvent*); void slotNewTemplate(); void slotCopy(); void slotModify(); void slotDeleteIf() { slotDelete(false); } void slotDeleteForce() { slotDelete(true); } void slotReactivate(); void slotEnable(); void slotToggleTrayIcon(); void slotRefreshAlarms(); void slotImportAlarms(); void slotExportAlarms(); void slotBirthdays(); void slotTemplates(); void slotTemplatesEnd(); void slotPreferences(); void slotConfigureKeys(); void slotConfigureToolbar(); void slotNewToolbarConfig(); void slotQuit(); void slotSelection(); void slotContextMenuRequested(const QPoint& globalPos); - void slotShowTime(); - void slotShowTimeTo(); void slotShowArchived(); void slotSpreadWindowsShortcut(); void slotWakeFromSuspend(); void updateKeepArchived(int days); void slotUndo(); void slotUndoItem(QAction* id); void slotRedo(); void slotRedoItem(QAction* id); void slotInitUndoMenu(); void slotInitRedoMenu(); void slotUndoStatus(const QString&, const QString&); void slotFindActive(bool); void updateTrayIconAction(); void slotToggleResourceSelector(); void slotCalendarStatusChanged(); + void slotAlarmListColumnsChanged(); void resourcesResized(); void showErrorMessage(const QString&); void editAlarmOk(); void editAlarmDeleted(QObject*); private: typedef QList WindowList; explicit MainWindow(bool restored); void createListView(bool recreate); void initActions(); void initCalendarResources(); void selectionCleared(); void setEnableText(bool enable); void initUndoMenu(QMenu*, Undo::Type); void slotDelete(bool force); static KAEvent::SubAction getDropAction(QDropEvent*, QString& text); static void setUpdateTimer(); static void enableTemplateMenuItem(bool); static WindowList mWindowList; // active main windows static TemplateDlg* mTemplateDlg; // the one and only template dialog AlarmListModel* mListFilterModel; AlarmListView* mListView; ResourceSelector* mResourceSelector; // resource selector widget QSplitter* mSplitter; // splits window into list and resource selector QMap mEditAlarmMap; // edit alarm dialogs to be handled by this window KToggleAction* mActionToggleResourceSel; QAction* mActionImportAlarms; QAction* mActionExportAlarms; QAction* mActionExport; QAction* mActionImportBirthdays; QAction* mActionTemplates; NewAlarmAction* mActionNew; QAction* mActionCreateTemplate; QAction* mActionCopy; QAction* mActionModify; QAction* mActionDelete; QAction* mActionDeleteForce; QAction* mActionReactivate; QAction* mActionEnable; QAction* mActionFindNext; QAction* mActionFindPrev; KToolBarPopupAction* mActionUndo; KToolBarPopupAction* mActionRedo; KToggleAction* mActionToggleTrayIcon; - KToggleAction* mActionShowTime; - KToggleAction* mActionShowTimeTo; KToggleAction* mActionShowArchived; KToggleAction* mActionSpreadWindows; QMenu* mActionsMenu; QMenu* mContextMenu; QMap mUndoMenuIds; // items in the undo/redo menu, in order of appearance int mResourcesWidth; // width of resource selector widget bool mHiddenTrayParent; // on session restoration, hide this window bool mShowResources; // show resource selector bool mShowArchived; // include archived alarms in the displayed list - bool mShowTime; // show alarm times - bool mShowTimeTo; // show time-to-alarms bool mShown; // true once the window has been displayed bool mActionEnableEnable; // Enable/Disable action is set to "Enable" bool mMenuError; // error occurred creating menus: need to show error message bool mResizing; // window resize is in progress }; #endif // MAINWINDOW_H // vim: et sw=4: diff --git a/src/prefdlg.cpp b/src/prefdlg.cpp index dea0f47f..b5fbed75 100644 --- a/src/prefdlg.cpp +++ b/src/prefdlg.cpp @@ -1,2007 +1,2007 @@ /* * prefdlg.cpp - program preferences dialog * Program: kalarm - * Copyright © 2001-2018 by David Jarvie + * Copyright © 2001-2019 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 "kalarm.h" #include "alarmcalendar.h" #include "collectionmodel.h" #include "alarmtimewidget.h" #include "buttongroup.h" #include "colourbutton.h" #include "editdlg.h" #include "editdlgtypes.h" #include "fontcolour.h" #include "functions.h" #include "kalarmapp.h" #include "kalocale.h" #include "kamail.h" #include "label.h" #include "latecancel.h" #include "mainwindow.h" #include "messagebox.h" #include "preferences.h" #include "radiobutton.h" #include "recurrenceedit.h" #include "sounddlg.h" #include "soundpicker.h" #include "specialactions.h" #include "stackedwidgets.h" #include "timeedit.h" #include "timespinbox.h" #include "timezonecombo.h" #include "traywindow.h" #include "prefdlg_p.h" #include "prefdlg.h" #include "config-kalarm.h" #include #include using namespace KHolidays; #include #include #include #include #include #include #include #if KDEPIM_HAVE_X11 #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kalarm_debug.h" using namespace KCalCore; using namespace KAlarmCal; static const char PREF_DIALOG_NAME[] = "PrefDialog"; // Command strings for executing commands in different types of terminal windows. // %t = window title parameter // %c = command to execute in terminal // %w = command to execute in terminal, with 'sleep 86400' appended // %C = temporary command file to execute in terminal // %W = temporary command file to execute in terminal, with 'sleep 86400' appended static QString xtermCommands[] = { QStringLiteral("xterm -sb -hold -title %t -e %c"), QStringLiteral("konsole --noclose -p tabtitle=%t -e ${SHELL:-sh} -c %c"), QStringLiteral("gnome-terminal -t %t -e %W"), QStringLiteral("eterm --pause -T %t -e %C"), // some systems use eterm... QStringLiteral("Eterm --pause -T %t -e %C"), // while some use Eterm QStringLiteral("rxvt -title %t -e ${SHELL:-sh} -c %w"), QString() // end of list indicator - don't change! }; /*============================================================================= = Class KAlarmPrefDlg =============================================================================*/ KAlarmPrefDlg* KAlarmPrefDlg::mInstance = nullptr; void KAlarmPrefDlg::display() { if (!mInstance) { mInstance = new KAlarmPrefDlg; QSize s; if (KAlarm::readConfigWindowSize(PREF_DIALOG_NAME, s)) mInstance->resize(s); mInstance->show(); } else { #if KDEPIM_HAVE_X11 KWindowInfo info = KWindowInfo(mInstance->winId(), NET::WMGeometry | NET::WMDesktop); KWindowSystem::setCurrentDesktop(info.desktop()); #endif mInstance->setWindowState(mInstance->windowState() & ~Qt::WindowMinimized); // un-minimize it if necessary mInstance->raise(); mInstance->activateWindow(); } } KAlarmPrefDlg::KAlarmPrefDlg() : KPageDialog(), mShown(false) { setAttribute(Qt::WA_DeleteOnClose); setObjectName(QStringLiteral("PrefDlg")); // used by LikeBack setWindowTitle(i18nc("@title:window", "Configure")); setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Help | QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Apply); button(QDialogButtonBox::Ok)->setDefault(true); setFaceType(List); mTabScrollGroup = new StackedScrollGroup(this, this); mMiscPage = new MiscPrefTab(mTabScrollGroup); mMiscPageItem = new KPageWidgetItem(mMiscPage, i18nc("@title:tab General preferences", "General")); mMiscPageItem->setHeader(i18nc("@title General preferences", "General")); mMiscPageItem->setIcon(QIcon::fromTheme(QStringLiteral("preferences-other"))); addPage(mMiscPageItem); mTimePage = new TimePrefTab(mTabScrollGroup); mTimePageItem = new KPageWidgetItem(mTimePage, i18nc("@title:tab", "Time & Date")); mTimePageItem->setHeader(i18nc("@title", "Time and Date")); mTimePageItem->setIcon(QIcon::fromTheme(QStringLiteral("preferences-system-time"))); addPage(mTimePageItem); mStorePage = new StorePrefTab(mTabScrollGroup); mStorePageItem = new KPageWidgetItem(mStorePage, i18nc("@title:tab", "Storage")); mStorePageItem->setHeader(i18nc("@title", "Alarm Storage")); mStorePageItem->setIcon(QIcon::fromTheme(QStringLiteral("system-file-manager"))); addPage(mStorePageItem); mEmailPage = new EmailPrefTab(mTabScrollGroup); mEmailPageItem = new KPageWidgetItem(mEmailPage, i18nc("@title:tab Email preferences", "Email")); mEmailPageItem->setHeader(i18nc("@title", "Email Alarm Settings")); mEmailPageItem->setIcon(QIcon::fromTheme(QStringLiteral("internet-mail"))); addPage(mEmailPageItem); mViewPage = new ViewPrefTab(mTabScrollGroup); mViewPageItem = new KPageWidgetItem(mViewPage, i18nc("@title:tab", "View")); mViewPageItem->setHeader(i18nc("@title", "View Settings")); mViewPageItem->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-theme"))); addPage(mViewPageItem); mEditPage = new EditPrefTab(mTabScrollGroup); mEditPageItem = new KPageWidgetItem(mEditPage, i18nc("@title:tab", "Edit")); mEditPageItem->setHeader(i18nc("@title", "Default Alarm Edit Settings")); mEditPageItem->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); addPage(mEditPageItem); connect(button(QDialogButtonBox::Ok), &QAbstractButton::clicked, this, &KAlarmPrefDlg::slotOk); connect(button(QDialogButtonBox::Cancel), &QAbstractButton::clicked, this, &KAlarmPrefDlg::slotCancel); connect(button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &KAlarmPrefDlg::slotApply); connect(button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, this, &KAlarmPrefDlg::slotDefault); connect(button(QDialogButtonBox::Help), &QAbstractButton::clicked, this, &KAlarmPrefDlg::slotHelp); restore(false); adjustSize(); } KAlarmPrefDlg::~KAlarmPrefDlg() { mInstance = nullptr; } void KAlarmPrefDlg::slotHelp() { KHelpClient::invokeHelp(QStringLiteral("preferences")); } // Apply the preferences that are currently selected void KAlarmPrefDlg::slotApply() { qCDebug(KALARM_LOG) << "KAlarmPrefDlg::slotApply"; QString errmsg = mEmailPage->validate(); if (!errmsg.isEmpty()) { setCurrentPage(mEmailPageItem); if (KAMessageBox::warningYesNo(this, errmsg) != KMessageBox::Yes) { mValid = false; return; } } errmsg = mEditPage->validate(); if (!errmsg.isEmpty()) { setCurrentPage(mEditPageItem); KAMessageBox::sorry(this, errmsg); mValid = false; return; } mValid = true; mEmailPage->apply(false); mViewPage->apply(false); mEditPage->apply(false); mStorePage->apply(false); mTimePage->apply(false); mMiscPage->apply(false); Preferences::self()->save(); } // Apply the preferences that are currently selected void KAlarmPrefDlg::slotOk() { qCDebug(KALARM_LOG) << "KAlarmPrefDlg::slotOk"; mValid = true; slotApply(); if (mValid) QDialog::accept(); } // Discard the current preferences and close the dialog void KAlarmPrefDlg::slotCancel() { qCDebug(KALARM_LOG) << "KAlarmPrefDlg::slotCancel"; restore(false); KPageDialog::reject(); } // Reset all controls to the application defaults void KAlarmPrefDlg::slotDefault() { switch (KAMessageBox::questionYesNoCancel(this, i18nc("@info", "Reset all tabs to their default values, or only reset the current tab?"), QString(), KGuiItem(i18nc("@action:button Reset ALL tabs", "&All")), KGuiItem(i18nc("@action:button Reset the CURRENT tab", "C&urrent")))) { case KMessageBox::Yes: restore(true); // restore all tabs break; case KMessageBox::No: Preferences::self()->useDefaults(true); static_cast(currentPage()->widget())->restore(true, false); Preferences::self()->useDefaults(false); break; default: break; } } // Discard the current preferences and use the present ones void KAlarmPrefDlg::restore(bool defaults) { qCDebug(KALARM_LOG) << "KAlarmPrefDlg::restore:" << (defaults ? "defaults" : ""); if (defaults) Preferences::self()->useDefaults(true); mEmailPage->restore(defaults, true); mViewPage->restore(defaults, true); mEditPage->restore(defaults, true); mStorePage->restore(defaults, true); mTimePage->restore(defaults, true); mMiscPage->restore(defaults, true); if (defaults) Preferences::self()->useDefaults(false); } /****************************************************************************** * Return the minimum size for the dialog. * If the minimum size would be too high to fit the desktop, the tab contents * are made scrollable. */ QSize KAlarmPrefDlg::minimumSizeHint() const { if (!mTabScrollGroup->sized()) { QSize s = mTabScrollGroup->adjustSize(); if (s.isValid()) { if (mTabScrollGroup->heightReduction()) { s = QSize(s.width(), s.height() - mTabScrollGroup->heightReduction()); const_cast(this)->resize(s); } return s; } } return KPageDialog::minimumSizeHint(); } void KAlarmPrefDlg::showEvent(QShowEvent* e) { KPageDialog::showEvent(e); if (!mShown) { mTabScrollGroup->adjustSize(true); mShown = true; } } /****************************************************************************** * Called when the dialog's size has changed. * Records the new size in the config file. */ void KAlarmPrefDlg::resizeEvent(QResizeEvent* re) { if (isVisible()) KAlarm::writeConfigWindowSize(PREF_DIALOG_NAME, re->size()); KPageDialog::resizeEvent(re); } /*============================================================================= = Class PrefsTabBase =============================================================================*/ int PrefsTabBase::mIndentWidth = 0; PrefsTabBase::PrefsTabBase(StackedScrollGroup* scrollGroup) : StackedScrollWidget(scrollGroup), mLabelsAligned(false) { QFrame* topWidget = new QFrame(this); setWidget(topWidget); mTopLayout = new QVBoxLayout(topWidget); mTopLayout->setContentsMargins(0, 0, 0, 0); mTopLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); if (!mIndentWidth) { QRadioButton radio(this); QStyleOptionButton opt; opt.initFrom(&radio); mIndentWidth = style()->subElementRect(QStyle::SE_RadioButtonIndicator, &opt).width(); } } void PrefsTabBase::apply(bool syncToDisc) { if (syncToDisc) Preferences::self()->save(); } void PrefsTabBase::addAlignedLabel(QLabel* label) { mLabels += label; } void PrefsTabBase::showEvent(QShowEvent*) { if (!mLabelsAligned) { int wid = 0; int i; int end = mLabels.count(); QList xpos; for (i = 0; i < end; ++i) { int x = mLabels[i]->mapTo(this, QPoint(0, 0)).x(); xpos += x; int w = x + mLabels[i]->sizeHint().width(); if (w > wid) wid = w; } for (i = 0; i < end; ++i) { mLabels[i]->setFixedWidth(wid - xpos[i]); mLabels[i]->setAlignment(Qt::AlignRight | Qt::AlignVCenter); } mLabelsAligned = true; } } /*============================================================================= = Class MiscPrefTab =============================================================================*/ MiscPrefTab::MiscPrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup) { QGroupBox* group = new QGroupBox(i18nc("@title:group", "Run Mode")); topLayout()->addWidget(group); QVBoxLayout* vlayout = new QVBoxLayout(group); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); vlayout->setContentsMargins(dcm, dcm, dcm, dcm); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // Start at login mAutoStart = new QCheckBox(i18nc("@option:check", "Start at login"), group); connect(mAutoStart, &QAbstractButton::clicked, this, &MiscPrefTab::slotAutostartClicked); mAutoStart->setWhatsThis(xi18nc("@info:whatsthis", "Automatically start KAlarm whenever you start KDE." "This option should always be checked unless you intend to discontinue use of KAlarm.")); vlayout->addWidget(mAutoStart, 0, Qt::AlignLeft); mQuitWarn = new QCheckBox(i18nc("@option:check", "Warn before quitting"), group); mQuitWarn->setWhatsThis(xi18nc("@info:whatsthis", "Check to display a warning prompt before quitting KAlarm.")); vlayout->addWidget(mQuitWarn, 0, Qt::AlignLeft); // Confirm alarm deletion? QWidget* widget = new QWidget; // this is for consistent left alignment topLayout()->addWidget(widget); QHBoxLayout* hbox = new QHBoxLayout(widget); hbox->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mConfirmAlarmDeletion = new QCheckBox(i18nc("@option:check", "Confirm alarm deletions")); mConfirmAlarmDeletion->setMinimumSize(mConfirmAlarmDeletion->sizeHint()); mConfirmAlarmDeletion->setWhatsThis(i18nc("@info:whatsthis", "Check to be prompted for confirmation each time you delete an alarm.")); hbox->addWidget(mConfirmAlarmDeletion); hbox->addStretch(); // left adjust the controls // Default alarm deferral time widget = new QWidget; // this is to control the QWhatsThis text display area topLayout()->addWidget(widget); hbox = new QHBoxLayout(widget); hbox->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label:spinbox", "Default defer time interval:")); hbox->addWidget(label); mDefaultDeferTime = new TimeSpinBox(1, 5999); mDefaultDeferTime->setMinimumSize(mDefaultDeferTime->sizeHint()); hbox->addWidget(mDefaultDeferTime); widget->setWhatsThis(i18nc("@info:whatsthis", "Enter the default time interval (hours & minutes) to defer alarms, used by the Defer Alarm dialog.")); label->setBuddy(mDefaultDeferTime); hbox->addStretch(); // left adjust the controls // Terminal window to use for command alarms group = new QGroupBox(i18nc("@title:group", "Terminal for Command Alarms")); group->setWhatsThis(i18nc("@info:whatsthis", "Choose which application to use when a command alarm is executed in a terminal window")); topLayout()->addWidget(group); QGridLayout* grid = new QGridLayout(group); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); int row = 0; mXtermType = new ButtonGroup(group); int index = 0; mXtermFirst = -1; for (mXtermCount = 0; !xtermCommands[mXtermCount].isNull(); ++mXtermCount) { QString cmd = xtermCommands[mXtermCount]; QStringList args = KShell::splitArgs(cmd); if (args.isEmpty() || QStandardPaths::findExecutable(args[0]).isEmpty()) continue; QRadioButton* radio = new QRadioButton(args[0], group); radio->setMinimumSize(radio->sizeHint()); mXtermType->addButton(radio, mXtermCount); if (mXtermFirst < 0) mXtermFirst = mXtermCount; // note the id of the first button cmd.replace(QStringLiteral("%t"), KAboutData::applicationData().displayName()); cmd.replace(QStringLiteral("%c"), QStringLiteral("")); cmd.replace(QStringLiteral("%w"), QStringLiteral("")); cmd.replace(QStringLiteral("%C"), QStringLiteral("[command]")); cmd.replace(QStringLiteral("%W"), QStringLiteral("[command; sleep]")); radio->setWhatsThis( xi18nc("@info:whatsthis", "Check to execute command alarms in a terminal window by %1", cmd)); grid->addWidget(radio, (row = index/3), index % 3, Qt::AlignLeft); ++index; } // QHBox used here doesn't allow the QLineEdit to expand!? QHBoxLayout* hlayout = new QHBoxLayout(); hlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->addLayout(hlayout, row + 1, 0, 1, 3, Qt::AlignLeft); QRadioButton* radio = new QRadioButton(i18nc("@option:radio Other terminal window command", "Other:"), group); hlayout->addWidget(radio); connect(radio, &QAbstractButton::toggled, this, &MiscPrefTab::slotOtherTerminalToggled); mXtermType->addButton(radio, mXtermCount); if (mXtermFirst < 0) mXtermFirst = mXtermCount; // note the id of the first button mXtermCommand = new QLineEdit(group); mXtermCommand->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); hlayout->addWidget(mXtermCommand); QString wt = xi18nc("@info:whatsthis", "Enter the full command line needed to execute a command in your chosen terminal window. " "By default the alarm's command string will be appended to what you enter here. " "See the KAlarm Handbook for details of special codes to tailor the command line."); radio->setWhatsThis(wt); mXtermCommand->setWhatsThis(wt); topLayout()->addStretch(); // top adjust the widgets } void MiscPrefTab::restore(bool defaults, bool) { mAutoStart->setChecked(defaults ? true : Preferences::autoStart()); mQuitWarn->setChecked(Preferences::quitWarn()); mConfirmAlarmDeletion->setChecked(Preferences::confirmAlarmDeletion()); mDefaultDeferTime->setValue(Preferences::defaultDeferTime()); QString xtermCmd = Preferences::cmdXTermCommand(); int id = mXtermFirst; if (!xtermCmd.isEmpty()) { for ( ; id < mXtermCount; ++id) { if (mXtermType->find(id) && xtermCmd == xtermCommands[id]) break; } } mXtermType->setButton(id); mXtermCommand->setEnabled(id == mXtermCount); mXtermCommand->setText(id == mXtermCount ? xtermCmd : QString()); } void MiscPrefTab::apply(bool syncToDisc) { // First validate anything entered in Other X-terminal command int xtermID = mXtermType->selectedId(); if (xtermID >= mXtermCount) { QString cmd = mXtermCommand->text(); if (cmd.isEmpty()) xtermID = -1; // 'Other' is only acceptable if it's non-blank else { QStringList args = KShell::splitArgs(cmd); cmd = args.isEmpty() ? QString() : args[0]; if (QStandardPaths::findExecutable(cmd).isEmpty()) { mXtermCommand->setFocus(); if (KAMessageBox::warningContinueCancel(topLayout()->parentWidget(), xi18nc("@info", "Command to invoke terminal window not found: %1", cmd)) != KMessageBox::Continue) return; } } } if (xtermID < 0) { xtermID = mXtermFirst; mXtermType->setButton(mXtermFirst); } if (mQuitWarn->isEnabled()) { bool b = mQuitWarn->isChecked(); if (b != Preferences::quitWarn()) Preferences::setQuitWarn(b); } bool b = mAutoStart->isChecked(); if (b != Preferences::autoStart()) { Preferences::setAutoStart(b); Preferences::setAskAutoStart(true); // cancel any start-at-login prompt suppression if (b) Preferences::setNoAutoStart(false); Preferences::setAutoStartChangedByUser(true); // prevent prompting the user on quit, about start-at-login } b = mConfirmAlarmDeletion->isChecked(); if (b != Preferences::confirmAlarmDeletion()) Preferences::setConfirmAlarmDeletion(b); int i = mDefaultDeferTime->value(); if (i != Preferences::defaultDeferTime()) Preferences::setDefaultDeferTime(i); QString text = (xtermID < mXtermCount) ? xtermCommands[xtermID] : mXtermCommand->text(); if (text != Preferences::cmdXTermCommand()) Preferences::setCmdXTermCommand(text); PrefsTabBase::apply(syncToDisc); } void MiscPrefTab::slotAutostartClicked() { if (!mAutoStart->isChecked() && KAMessageBox::warningYesNo(topLayout()->parentWidget(), xi18nc("@info", "You should not uncheck this option unless you intend to discontinue use of KAlarm"), QString(), KStandardGuiItem::cont(), KStandardGuiItem::cancel() ) != KMessageBox::Yes) mAutoStart->setChecked(true); } void MiscPrefTab::slotOtherTerminalToggled(bool on) { mXtermCommand->setEnabled(on); } /*============================================================================= = Class TimePrefTab =============================================================================*/ TimePrefTab::TimePrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup) { // Default time zone QHBoxLayout* itemBox = new QHBoxLayout(); itemBox->setContentsMargins(0, 0, 0, 0); qobject_cast(topLayout())->addLayout(itemBox); QWidget* widget = new QWidget; // this is to control the QWhatsThis text display area itemBox->addWidget(widget); QHBoxLayout* box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label:listbox", "Time zone:")); box->addWidget(label); addAlignedLabel(label); mTimeZone = new TimeZoneCombo(widget); mTimeZone->setMaxVisibleItems(15); box->addWidget(mTimeZone); widget->setWhatsThis(xi18nc("@info:whatsthis", "Select the time zone which KAlarm should use " "as its default for displaying and entering dates and times.")); label->setBuddy(mTimeZone); itemBox->addStretch(); // Holiday region itemBox = new QHBoxLayout(); itemBox->setContentsMargins(0, 0, 0, 0); qobject_cast(topLayout())->addLayout(itemBox); widget = new QWidget; // this is to control the QWhatsThis text display area itemBox->addWidget(widget); box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:listbox", "Holiday region:")); addAlignedLabel(label); box->addWidget(label); mHolidays = new QComboBox(); mHolidays->setSizeAdjustPolicy(QComboBox::AdjustToContentsOnFirstShow); box->addWidget(mHolidays); itemBox->addStretch(); label->setBuddy(mHolidays); widget->setWhatsThis(i18nc("@info:whatsthis", "Select which holiday region to use")); const QStringList regions = HolidayRegion::regionCodes(); QMap regionsMap; for (const QString& regionCode : regions) { const QString name = HolidayRegion::name(regionCode); const QString languageName = QLocale::languageToString(QLocale(HolidayRegion::languageCode(regionCode)).language()); const QString label = languageName.isEmpty() ? name : i18nc("Holiday region, region language", "%1 (%2)", name, languageName); regionsMap.insert(label, regionCode); } mHolidays->addItem(i18nc("No holiday region", "None"), QString()); for (QMapIterator it(regionsMap); it.hasNext(); ) { it.next(); mHolidays->addItem(it.key(), it.value()); } // Start-of-day time itemBox = new QHBoxLayout(); itemBox->setContentsMargins(0, 0, 0, 0); qobject_cast(topLayout())->addLayout(itemBox); widget = new QWidget; // this is to control the QWhatsThis text display area itemBox->addWidget(widget); box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:spinbox", "Start of day for date-only alarms:")); addAlignedLabel(label); box->addWidget(label); mStartOfDay = new TimeEdit(); box->addWidget(mStartOfDay); label->setBuddy(mStartOfDay); widget->setWhatsThis(xi18nc("@info:whatsthis", "The earliest time of day at which a date-only alarm will be triggered." "%1", TimeSpinBox::shiftWhatsThis())); itemBox->addStretch(); // Working hours QGroupBox* group = new QGroupBox(i18nc("@title:group", "Working Hours")); topLayout()->addWidget(group); QBoxLayout* layout = new QVBoxLayout(group); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); layout->setContentsMargins(dcm, dcm, dcm, dcm); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QWidget* daybox = new QWidget(group); // this is to control the QWhatsThis text display area layout->addWidget(daybox); QGridLayout* wgrid = new QGridLayout(daybox); wgrid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); const QLocale locale; for (int i = 0; i < 7; ++i) { int day = KAlarm::localeDayInWeek_to_weekDay(i); mWorkDays[i] = new QCheckBox(locale.dayName(day), daybox); wgrid->addWidget(mWorkDays[i], i/3, i%3, Qt::AlignLeft); } daybox->setWhatsThis(i18nc("@info:whatsthis", "Check the days in the week which are work days")); itemBox = new QHBoxLayout(); itemBox->setContentsMargins(0, 0, 0, 0); layout->addLayout(itemBox); widget = new QWidget; // this is to control the QWhatsThis text display area itemBox->addWidget(widget); box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:spinbox", "Daily start time:")); addAlignedLabel(label); box->addWidget(label); mWorkStart = new TimeEdit(); box->addWidget(mWorkStart); label->setBuddy(mWorkStart); widget->setWhatsThis(xi18nc("@info:whatsthis", "Enter the start time of the working day." "%1", TimeSpinBox::shiftWhatsThis())); itemBox->addStretch(); itemBox = new QHBoxLayout(); itemBox->setContentsMargins(0, 0, 0, 0); layout->addLayout(itemBox); widget = new QWidget; // this is to control the QWhatsThis text display area itemBox->addWidget(widget); box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:spinbox", "Daily end time:")); addAlignedLabel(label); box->addWidget(label); mWorkEnd = new TimeEdit(); box->addWidget(mWorkEnd); label->setBuddy(mWorkEnd); widget->setWhatsThis(xi18nc("@info:whatsthis", "Enter the end time of the working day." "%1", TimeSpinBox::shiftWhatsThis())); itemBox->addStretch(); // KOrganizer event duration group = new QGroupBox(i18nc("@title:group", "KOrganizer")); topLayout()->addWidget(group); layout = new QVBoxLayout(group); layout->setContentsMargins(dcm, dcm, dcm, dcm); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); widget = new QWidget; // this is to control the QWhatsThis text display area layout->addWidget(widget); box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:spinbox", "KOrganizer event duration:")); addAlignedLabel(label); box->addWidget(label); mKOrgEventDuration = new TimeSpinBox(0, 5999); mKOrgEventDuration->setMinimumSize(mKOrgEventDuration->sizeHint()); box->addWidget(mKOrgEventDuration); widget->setWhatsThis(xi18nc("@info:whatsthis", "Enter the event duration in hours and minutes, for alarms which are copied to KOrganizer." "%1", TimeSpinBox::shiftWhatsThis())); label->setBuddy(mKOrgEventDuration); box->addStretch(); // left adjust the controls topLayout()->addStretch(); // top adjust the widgets } void TimePrefTab::restore(bool, bool) { KADateTime::Spec timeSpec = Preferences::timeSpec(); mTimeZone->setTimeZone(timeSpec.type() == KADateTime::TimeZone ? timeSpec.timeZone() : QTimeZone()); int i = Preferences::holidays().isValid() ? mHolidays->findData(Preferences::holidays().regionCode()) : 0; mHolidays->setCurrentIndex(i); mStartOfDay->setValue(Preferences::startOfDay()); mWorkStart->setValue(Preferences::workDayStart()); mWorkEnd->setValue(Preferences::workDayEnd()); QBitArray days = Preferences::workDays(); for (int i = 0; i < 7; ++i) { bool x = days.testBit(KAlarm::localeDayInWeek_to_weekDay(i) - 1); mWorkDays[i]->setChecked(x); } mKOrgEventDuration->setValue(Preferences::kOrgEventDuration()); } void TimePrefTab::apply(bool syncToDisc) { Preferences::setTimeSpec(mTimeZone->timeZone()); QString hol = mHolidays->itemData(mHolidays->currentIndex()).toString(); if (hol != Preferences::holidays().regionCode()) Preferences::setHolidayRegion(hol); int t = mStartOfDay->value(); QTime sodt(t/60, t%60, 0); if (sodt != Preferences::startOfDay()) Preferences::setStartOfDay(sodt); t = mWorkStart->value(); Preferences::setWorkDayStart(QTime(t/60, t%60, 0)); t = mWorkEnd->value(); Preferences::setWorkDayEnd(QTime(t/60, t%60, 0)); QBitArray workDays(7); for (int i = 0; i < 7; ++i) if (mWorkDays[i]->isChecked()) workDays.setBit(KAlarm::localeDayInWeek_to_weekDay(i) - 1, 1); Preferences::setWorkDays(workDays); Preferences::setKOrgEventDuration(mKOrgEventDuration->value()); t = mKOrgEventDuration->value(); if (t != Preferences::kOrgEventDuration()) Preferences::setKOrgEventDuration(t); PrefsTabBase::apply(syncToDisc); } /*============================================================================= = Class StorePrefTab =============================================================================*/ StorePrefTab::StorePrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup), mCheckKeepChanges(false) { // Which resource to save to QGroupBox* group = new QGroupBox(i18nc("@title:group", "New Alarms && Templates")); topLayout()->addWidget(group); QButtonGroup* bgroup = new QButtonGroup(group); QBoxLayout* layout = new QVBoxLayout(group); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); layout->setContentsMargins(dcm, dcm, dcm, dcm); layout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mDefaultResource = new QRadioButton(i18nc("@option:radio", "Store in default calendar"), group); bgroup->addButton(mDefaultResource); mDefaultResource->setWhatsThis(i18nc("@info:whatsthis", "Add all new alarms and alarm templates to the default calendars, without prompting.")); layout->addWidget(mDefaultResource, 0, Qt::AlignLeft); mAskResource = new QRadioButton(i18nc("@option:radio", "Prompt for which calendar to store in"), group); bgroup->addButton(mAskResource); mAskResource->setWhatsThis(xi18nc("@info:whatsthis", "When saving a new alarm or alarm template, prompt for which calendar to store it in, if there is more than one active calendar." "Note that archived alarms are always stored in the default archived alarm calendar.")); layout->addWidget(mAskResource, 0, Qt::AlignLeft); // Archived alarms group = new QGroupBox(i18nc("@title:group", "Archived Alarms")); topLayout()->addWidget(group); QGridLayout* grid = new QGridLayout(group); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(1, 1); grid->setColumnMinimumWidth(0, indentWidth()); mKeepArchived = new QCheckBox(i18nc("@option:check", "Keep alarms after expiry"), group); connect(mKeepArchived, &QAbstractButton::toggled, this, &StorePrefTab::slotArchivedToggled); mKeepArchived->setWhatsThis( i18nc("@info:whatsthis", "Check to archive alarms after expiry or deletion (except deleted alarms which were never triggered).")); grid->addWidget(mKeepArchived, 0, 0, 1, 2, Qt::AlignLeft); QWidget* widget = new QWidget; QHBoxLayout* box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mPurgeArchived = new QCheckBox(i18nc("@option:check", "Discard archived alarms after:")); mPurgeArchived->setMinimumSize(mPurgeArchived->sizeHint()); box->addWidget(mPurgeArchived); connect(mPurgeArchived, &QAbstractButton::toggled, this, &StorePrefTab::slotArchivedToggled); mPurgeAfter = new SpinBox(); mPurgeAfter->setMinimum(1); mPurgeAfter->setSingleShiftStep(10); mPurgeAfter->setMinimumSize(mPurgeAfter->sizeHint()); box->addWidget(mPurgeAfter); mPurgeAfterLabel = new QLabel(i18nc("@label Time unit for user-entered number", "days")); mPurgeAfterLabel->setMinimumSize(mPurgeAfterLabel->sizeHint()); mPurgeAfterLabel->setBuddy(mPurgeAfter); box->addWidget(mPurgeAfterLabel); widget->setWhatsThis(i18nc("@info:whatsthis", "Uncheck to store archived alarms indefinitely. Check to enter how long archived alarms should be stored.")); grid->addWidget(widget, 1, 1, Qt::AlignLeft); mClearArchived = new QPushButton(i18nc("@action:button", "Clear Archived Alarms"), group); connect(mClearArchived, &QAbstractButton::clicked, this, &StorePrefTab::slotClearArchived); mClearArchived->setWhatsThis((CollectionControlModel::enabledCollections(CalEvent::ARCHIVED, false).count() <= 1) ? i18nc("@info:whatsthis", "Delete all existing archived alarms.") : i18nc("@info:whatsthis", "Delete all existing archived alarms (from the default archived alarm calendar only).")); grid->addWidget(mClearArchived, 2, 1, Qt::AlignLeft); topLayout()->addStretch(); // top adjust the widgets } void StorePrefTab::restore(bool defaults, bool) { mCheckKeepChanges = defaults; if (Preferences::askResource()) mAskResource->setChecked(true); else mDefaultResource->setChecked(true); int keepDays = Preferences::archivedKeepDays(); if (!defaults) mOldKeepArchived = keepDays; setArchivedControls(keepDays); mCheckKeepChanges = true; } void StorePrefTab::apply(bool syncToDisc) { bool b = mAskResource->isChecked(); if (b != Preferences::askResource()) Preferences::setAskResource(mAskResource->isChecked()); int days = !mKeepArchived->isChecked() ? 0 : mPurgeArchived->isChecked() ? mPurgeAfter->value() : -1; if (days != Preferences::archivedKeepDays()) Preferences::setArchivedKeepDays(days); PrefsTabBase::apply(syncToDisc); } void StorePrefTab::setArchivedControls(int purgeDays) { mKeepArchived->setChecked(purgeDays); mPurgeArchived->setChecked(purgeDays > 0); mPurgeAfter->setValue(purgeDays > 0 ? purgeDays : 0); slotArchivedToggled(true); } void StorePrefTab::slotArchivedToggled(bool) { bool keep = mKeepArchived->isChecked(); if (keep && !mOldKeepArchived && mCheckKeepChanges && !CollectionControlModel::getStandard(CalEvent::ARCHIVED).isValid()) { KAMessageBox::sorry(topLayout()->parentWidget(), xi18nc("@info", "A default calendar is required in order to archive alarms, but none is currently enabled." "If you wish to keep expired alarms, please first use the calendars view to select a default " "archived alarms calendar.")); mKeepArchived->setChecked(false); return; } mOldKeepArchived = keep; mPurgeArchived->setEnabled(keep); mPurgeAfter->setEnabled(keep && mPurgeArchived->isChecked()); mPurgeAfterLabel->setEnabled(keep); mClearArchived->setEnabled(keep); } void StorePrefTab::slotClearArchived() { bool single = CollectionControlModel::enabledCollections(CalEvent::ARCHIVED, false).count() <= 1; if (KAMessageBox::warningContinueCancel(topLayout()->parentWidget(), single ? i18nc("@info", "Do you really want to delete all archived alarms?") : i18nc("@info", "Do you really want to delete all alarms in the default archived alarm calendar?")) != KMessageBox::Continue) return; theApp()->purgeAll(); } /*============================================================================= = Class EmailPrefTab =============================================================================*/ EmailPrefTab::EmailPrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup), mAddressChanged(false), mBccAddressChanged(false) { QWidget* widget = new QWidget; topLayout()->addWidget(widget); QHBoxLayout* box = new QHBoxLayout(widget); box->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@label", "Email client:")); box->addWidget(label); mEmailClient = new ButtonGroup(widget); QString kmailOption = i18nc("@option:radio", "KMail"); QString sendmailOption = i18nc("@option:radio", "Sendmail"); mKMailButton = new RadioButton(kmailOption); mKMailButton->setMinimumSize(mKMailButton->sizeHint()); box->addWidget(mKMailButton); mEmailClient->addButton(mKMailButton, Preferences::kmail); mSendmailButton = new RadioButton(sendmailOption); mSendmailButton->setMinimumSize(mSendmailButton->sizeHint()); box->addWidget(mSendmailButton); mEmailClient->addButton(mSendmailButton, Preferences::sendmail); connect(mEmailClient, &ButtonGroup::buttonSet, this, &EmailPrefTab::slotEmailClientChanged); widget->setWhatsThis(xi18nc("@info:whatsthis", "Choose how to send email when an email alarm is triggered." "%1: The email is sent automatically via KMail. KMail is started first if necessary." "%2: The email is sent automatically. This option will only work if " "your system is configured to use sendmail or a sendmail compatible mail transport agent.", kmailOption, sendmailOption)); widget = new QWidget; // this is to allow left adjustment topLayout()->addWidget(widget); box = new QHBoxLayout(widget); mEmailCopyToKMail = new QCheckBox(xi18nc("@option:check", "Copy sent emails into KMail's %1 folder", KAMail::i18n_sent_mail())); mEmailCopyToKMail->setWhatsThis(xi18nc("@info:whatsthis", "After sending an email, store a copy in KMail's %1 folder", KAMail::i18n_sent_mail())); box->addWidget(mEmailCopyToKMail); box->setStretchFactor(new QWidget(widget), 1); // left adjust the controls widget = new QWidget; // this is to allow left adjustment topLayout()->addWidget(widget); box = new QHBoxLayout(widget); mEmailQueuedNotify = new QCheckBox(i18nc("@option:check", "Notify when remote emails are queued")); mEmailQueuedNotify->setWhatsThis( i18nc("@info:whatsthis", "Display a notification message whenever an email alarm has queued an email for sending to a remote system. " "This could be useful if, for example, you have a dial-up connection, so that you can then ensure that the email is actually transmitted.")); box->addWidget(mEmailQueuedNotify); box->setStretchFactor(new QWidget(widget), 1); // left adjust the controls // Your Email Address group box QGroupBox* group = new QGroupBox(i18nc("@title:group", "Your Email Address")); topLayout()->addWidget(group); QGridLayout* grid = new QGridLayout(group); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(2, 1); // 'From' email address controls ... label = new Label(i18nc("@label 'From' email address", "From:"), group); grid->addWidget(label, 1, 0); mFromAddressGroup = new ButtonGroup(group); connect(mFromAddressGroup, &ButtonGroup::buttonSet, this, &EmailPrefTab::slotFromAddrChanged); // Line edit to enter a 'From' email address mFromAddrButton = new RadioButton(group); mFromAddressGroup->addButton(mFromAddrButton, Preferences::MAIL_FROM_ADDR); label->setBuddy(mFromAddrButton); grid->addWidget(mFromAddrButton, 1, 1); mEmailAddress = new QLineEdit(group); connect(mEmailAddress, &QLineEdit::textChanged, this, &EmailPrefTab::slotAddressChanged); QString whatsThis = i18nc("@info:whatsthis", "Your email address, used to identify you as the sender when sending email alarms."); mFromAddrButton->setWhatsThis(whatsThis); mEmailAddress->setWhatsThis(whatsThis); mFromAddrButton->setFocusWidget(mEmailAddress); grid->addWidget(mEmailAddress, 1, 2); // 'From' email address to be taken from System Settings mFromCCentreButton = new RadioButton(xi18nc("@option:radio", "Use default address from KMail or System Settings"), group); mFromAddressGroup->addButton(mFromCCentreButton, Preferences::MAIL_FROM_SYS_SETTINGS); mFromCCentreButton->setWhatsThis( xi18nc("@info:whatsthis", "Check to use the default email address set in KMail or KDE System Settings, to identify you as the sender when sending email alarms.")); grid->addWidget(mFromCCentreButton, 2, 1, 1, 2, Qt::AlignLeft); // 'From' email address to be picked from KMail's identities when the email alarm is configured mFromKMailButton = new RadioButton(xi18nc("@option:radio", "Use KMail identities"), group); mFromAddressGroup->addButton(mFromKMailButton, Preferences::MAIL_FROM_KMAIL); mFromKMailButton->setWhatsThis( xi18nc("@info:whatsthis", "Check to use KMail's email identities to identify you as the sender when sending email alarms. " "For existing email alarms, KMail's default identity will be used. " "For new email alarms, you will be able to pick which of KMail's identities to use.")); grid->addWidget(mFromKMailButton, 3, 1, 1, 2, Qt::AlignLeft); // 'Bcc' email address controls ... grid->setRowMinimumHeight(4, style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new Label(i18nc("@label 'Bcc' email address", "Bcc:"), group); grid->addWidget(label, 5, 0); mBccAddressGroup = new ButtonGroup(group); connect(mBccAddressGroup, &ButtonGroup::buttonSet, this, &EmailPrefTab::slotBccAddrChanged); // Line edit to enter a 'Bcc' email address mBccAddrButton = new RadioButton(group); mBccAddressGroup->addButton(mBccAddrButton, Preferences::MAIL_FROM_ADDR); label->setBuddy(mBccAddrButton); grid->addWidget(mBccAddrButton, 5, 1); mEmailBccAddress = new QLineEdit(group); whatsThis = xi18nc("@info:whatsthis", "Your email address, used for blind copying email alarms to yourself. " "If you want blind copies to be sent to your account on the computer which KAlarm runs on, you can simply enter your user login name."); mBccAddrButton->setWhatsThis(whatsThis); mEmailBccAddress->setWhatsThis(whatsThis); mBccAddrButton->setFocusWidget(mEmailBccAddress); grid->addWidget(mEmailBccAddress, 5, 2); // 'Bcc' email address to be taken from System Settings mBccCCentreButton = new RadioButton(xi18nc("@option:radio", "Use default address from KMail or System Settings"), group); mBccAddressGroup->addButton(mBccCCentreButton, Preferences::MAIL_FROM_SYS_SETTINGS); mBccCCentreButton->setWhatsThis( xi18nc("@info:whatsthis", "Check to use the default email address set in KMail or KDE System Settings, for blind copying email alarms to yourself.")); grid->addWidget(mBccCCentreButton, 6, 1, 1, 2, Qt::AlignLeft); topLayout()->addStretch(); // top adjust the widgets } void EmailPrefTab::restore(bool defaults, bool) { mEmailClient->setButton(Preferences::emailClient()); mEmailCopyToKMail->setChecked(Preferences::emailCopyToKMail()); setEmailAddress(Preferences::emailFrom(), Preferences::emailAddress()); setEmailBccAddress((Preferences::emailBccFrom() == Preferences::MAIL_FROM_SYS_SETTINGS), Preferences::emailBccAddress()); mEmailQueuedNotify->setChecked(Preferences::emailQueuedNotify()); if (!defaults) mAddressChanged = mBccAddressChanged = false; } void EmailPrefTab::apply(bool syncToDisc) { int client = mEmailClient->selectedId(); if (client >= 0 && static_cast(client) != Preferences::emailClient()) Preferences::setEmailClient(static_cast(client)); bool b = mEmailCopyToKMail->isChecked(); if (b != Preferences::emailCopyToKMail()) Preferences::setEmailCopyToKMail(b); int from = mFromAddressGroup->selectedId(); QString text = mEmailAddress->text().trimmed(); if ((from >= 0 && static_cast(from) != Preferences::emailFrom()) || text != Preferences::emailAddress()) Preferences::setEmailAddress(static_cast(from), text); b = (mBccAddressGroup->checkedButton() == mBccCCentreButton); Preferences::MailFrom bfrom = b ? Preferences::MAIL_FROM_SYS_SETTINGS : Preferences::MAIL_FROM_ADDR;; text = mEmailBccAddress->text().trimmed(); if (bfrom != Preferences::emailBccFrom() || text != Preferences::emailBccAddress()) Preferences::setEmailBccAddress(b, text); b = mEmailQueuedNotify->isChecked(); if (b != Preferences::emailQueuedNotify()) Preferences::setEmailQueuedNotify(mEmailQueuedNotify->isChecked()); PrefsTabBase::apply(syncToDisc); } void EmailPrefTab::setEmailAddress(Preferences::MailFrom from, const QString& address) { mFromAddressGroup->setButton(from); mEmailAddress->setText(from == Preferences::MAIL_FROM_ADDR ? address.trimmed() : QString()); } void EmailPrefTab::setEmailBccAddress(bool useSystemSettings, const QString& address) { mBccAddressGroup->setButton(useSystemSettings ? Preferences::MAIL_FROM_SYS_SETTINGS : Preferences::MAIL_FROM_ADDR); mEmailBccAddress->setText(useSystemSettings ? QString() : address.trimmed()); } void EmailPrefTab::slotEmailClientChanged(QAbstractButton* button) { mEmailCopyToKMail->setEnabled(button == mSendmailButton); } void EmailPrefTab::slotFromAddrChanged(QAbstractButton* button) { mEmailAddress->setEnabled(button == mFromAddrButton); mAddressChanged = true; } void EmailPrefTab::slotBccAddrChanged(QAbstractButton* button) { mEmailBccAddress->setEnabled(button == mBccAddrButton); mBccAddressChanged = true; } QString EmailPrefTab::validate() { if (mAddressChanged) { mAddressChanged = false; QString errmsg = validateAddr(mFromAddressGroup, mEmailAddress, KAMail::i18n_NeedFromEmailAddress()); if (!errmsg.isEmpty()) return errmsg; } if (mBccAddressChanged) { mBccAddressChanged = false; return validateAddr(mBccAddressGroup, mEmailBccAddress, i18nc("@info", "No valid 'Bcc' email address is specified.")); } return QString(); } QString EmailPrefTab::validateAddr(ButtonGroup* group, QLineEdit* addr, const QString& msg) { QString errmsg = xi18nc("@info", "%1Are you sure you want to save your changes?", msg); switch (group->selectedId()) { case Preferences::MAIL_FROM_SYS_SETTINGS: if (!KAMail::controlCentreAddress().isEmpty()) return QString(); errmsg = xi18nc("@info", "No default email address is currently set in KMail or KDE System Settings. %1", errmsg); break; case Preferences::MAIL_FROM_KMAIL: if (Identities::identitiesExist()) return QString(); errmsg = xi18nc("@info", "No KMail identities currently exist. %1", errmsg); break; case Preferences::MAIL_FROM_ADDR: if (!addr->text().trimmed().isEmpty()) return QString(); break; } return errmsg; } /*============================================================================= = Class EditPrefTab =============================================================================*/ EditPrefTab::EditPrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup) { KLocalizedString defsetting = kxi18nc("@info:whatsthis", "The default setting for %1 in the alarm edit dialog."); mTabs = new QTabWidget(); topLayout()->addWidget(mTabs); StackedGroupT* tabgroup = new StackedGroupT(mTabs); StackedWidgetT* topGeneral = new StackedWidgetT(tabgroup); QVBoxLayout* tgLayout = new QVBoxLayout(topGeneral); mTabGeneral = mTabs->addTab(topGeneral, i18nc("@title:tab", "General")); StackedWidgetT* topTypes = new StackedWidgetT(tabgroup); QVBoxLayout* ttLayout = new QVBoxLayout(topTypes); mTabTypes = mTabs->addTab(topTypes, i18nc("@title:tab", "Alarm Types")); StackedWidgetT* topFontColour = new StackedWidgetT(tabgroup); QVBoxLayout* tfLayout = new QVBoxLayout(topFontColour); mTabFontColour = mTabs->addTab(topFontColour, i18nc("@title:tab", "Font && Color")); // MISCELLANEOUS // Show in KOrganizer mCopyToKOrganizer = new QCheckBox(EditAlarmDlg::i18n_chk_ShowInKOrganizer()); mCopyToKOrganizer->setMinimumSize(mCopyToKOrganizer->sizeHint()); mCopyToKOrganizer->setWhatsThis(defsetting.subs(EditAlarmDlg::i18n_chk_ShowInKOrganizer()).toString()); tgLayout->addWidget(mCopyToKOrganizer); // Late cancellation QWidget* widget = new QWidget; tgLayout->addWidget(widget); QHBoxLayout* box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(0); mLateCancel = new QCheckBox(LateCancelSelector::i18n_chk_CancelIfLate()); mLateCancel->setMinimumSize(mLateCancel->sizeHint()); mLateCancel->setWhatsThis(defsetting.subs(LateCancelSelector::i18n_chk_CancelIfLate()).toString()); box->addWidget(mLateCancel); // Recurrence widget = new QWidget; // this is to control the QWhatsThis text display area tgLayout->addWidget(widget); box = new QHBoxLayout(widget); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); box->setContentsMargins(0, 0, 0, 0); QLabel* label = new QLabel(i18nc("@label:listbox", "Recurrence:")); box->addWidget(label); mRecurPeriod = new ComboBox(); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_NoRecur()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_AtLogin()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_HourlyMinutely()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_Daily()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_Weekly()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_Monthly()); mRecurPeriod->addItem(RecurrenceEdit::i18n_combo_Yearly()); box->addWidget(mRecurPeriod); box->addStretch(); label->setBuddy(mRecurPeriod); widget->setWhatsThis(i18nc("@info:whatsthis", "The default setting for the recurrence rule in the alarm edit dialog.")); // How to handle February 29th in yearly recurrences QWidget* febBox = new QWidget; // this is to control the QWhatsThis text display area tgLayout->addWidget(febBox); QVBoxLayout* vbox = new QVBoxLayout(febBox); vbox->setContentsMargins(0, 0, 0, 0); vbox->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label", "In non-leap years, repeat yearly February 29th alarms on:")); label->setAlignment(Qt::AlignLeft); label->setWordWrap(true); vbox->addWidget(label); box = new QHBoxLayout(); vbox->addLayout(box); vbox->setContentsMargins(0, 0, 0, 0); box->setSpacing(2 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mFeb29 = new ButtonGroup(febBox); widget = new QWidget(); widget->setFixedWidth(3 * style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); box->addWidget(widget); QRadioButton* radio = new QRadioButton(i18nc("@option:radio", "February 2&8th")); radio->setMinimumSize(radio->sizeHint()); box->addWidget(radio); mFeb29->addButton(radio, Preferences::Feb29_Feb28); radio = new QRadioButton(i18nc("@option:radio", "March &1st")); radio->setMinimumSize(radio->sizeHint()); box->addWidget(radio); mFeb29->addButton(radio, Preferences::Feb29_Mar1); radio = new QRadioButton(i18nc("@option:radio", "Do not repeat")); radio->setMinimumSize(radio->sizeHint()); box->addWidget(radio); mFeb29->addButton(radio, Preferences::Feb29_None); febBox->setWhatsThis(xi18nc("@info:whatsthis", "For yearly recurrences, choose what date, if any, alarms due on February 29th should occur in non-leap years." "The next scheduled occurrence of existing alarms is not re-evaluated when you change this setting.")); tgLayout->addStretch(); // top adjust the widgets // DISPLAY ALARMS QGroupBox* group = new QGroupBox(i18nc("@title:group", "Display Alarms")); ttLayout->addWidget(group); QVBoxLayout* vlayout = new QVBoxLayout(group); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); vlayout->setContentsMargins(dcm, dcm, dcm, dcm); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mConfirmAck = new QCheckBox(EditDisplayAlarmDlg::i18n_chk_ConfirmAck()); mConfirmAck->setMinimumSize(mConfirmAck->sizeHint()); mConfirmAck->setWhatsThis(defsetting.subs(EditDisplayAlarmDlg::i18n_chk_ConfirmAck()).toString()); vlayout->addWidget(mConfirmAck, 0, Qt::AlignLeft); mAutoClose = new QCheckBox(LateCancelSelector::i18n_chk_AutoCloseWinLC()); mAutoClose->setMinimumSize(mAutoClose->sizeHint()); mAutoClose->setWhatsThis(defsetting.subs(LateCancelSelector::i18n_chk_AutoCloseWin()).toString()); vlayout->addWidget(mAutoClose, 0, Qt::AlignLeft); widget = new QWidget; vlayout->addWidget(widget); box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); label = new QLabel(i18nc("@label:listbox", "Reminder units:")); box->addWidget(label); mReminderUnits = new QComboBox(); mReminderUnits->addItem(i18nc("@item:inlistbox", "Minutes"), TimePeriod::Minutes); mReminderUnits->addItem(i18nc("@item:inlistbox", "Hours/Minutes"), TimePeriod::HoursMinutes); box->addWidget(mReminderUnits); label->setBuddy(mReminderUnits); widget->setWhatsThis(i18nc("@info:whatsthis", "The default units for the reminder in the alarm edit dialog, for alarms due soon.")); box->addStretch(); // left adjust the control mSpecialActionsButton = new SpecialActionsButton(true); box->addWidget(mSpecialActionsButton); // SOUND QGroupBox* bbox = new QGroupBox(i18nc("@title:group Audio options group", "Sound")); ttLayout->addWidget(bbox); vlayout = new QVBoxLayout(bbox); vlayout->setContentsMargins(dcm, dcm, dcm, dcm); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QHBoxLayout* hlayout = new QHBoxLayout; hlayout->setContentsMargins(0, 0, 0, 0); vlayout->addLayout(hlayout); mSound = new QComboBox(); mSound->addItem(SoundPicker::i18n_combo_None()); // index 0 mSound->addItem(SoundPicker::i18n_combo_Beep()); // index 1 mSound->addItem(SoundPicker::i18n_combo_File()); // index 2 if (KPIMTextEdit::TextToSpeech::self()->isReady()) mSound->addItem(SoundPicker::i18n_combo_Speak()); // index 3 mSound->setMinimumSize(mSound->sizeHint()); mSound->setWhatsThis(defsetting.subs(SoundPicker::i18n_label_Sound()).toString()); hlayout->addWidget(mSound); hlayout->addStretch(); mSoundRepeat = new QCheckBox(i18nc("@option:check", "Repeat sound file")); mSoundRepeat->setMinimumSize(mSoundRepeat->sizeHint()); mSoundRepeat->setWhatsThis( xi18nc("@info:whatsthis sound file 'Repeat' checkbox", "The default setting for sound file %1 in the alarm edit dialog.", SoundWidget::i18n_chk_Repeat())); hlayout->addWidget(mSoundRepeat); widget = new QWidget; // this is to control the QWhatsThis text display area box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mSoundFileLabel = new QLabel(i18nc("@label:textbox", "Sound file:")); box->addWidget(mSoundFileLabel); mSoundFile = new QLineEdit(); box->addWidget(mSoundFile); mSoundFileLabel->setBuddy(mSoundFile); mSoundFileBrowse = new QPushButton(); mSoundFileBrowse->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); connect(mSoundFileBrowse, &QAbstractButton::clicked, this, &EditPrefTab::slotBrowseSoundFile); mSoundFileBrowse->setToolTip(i18nc("@info:tooltip", "Choose a sound file")); box->addWidget(mSoundFileBrowse); widget->setWhatsThis(i18nc("@info:whatsthis", "Enter the default sound file to use in the alarm edit dialog.")); vlayout->addWidget(widget); // COMMAND ALARMS group = new QGroupBox(i18nc("@title:group", "Command Alarms")); ttLayout->addWidget(group); vlayout = new QVBoxLayout(group); vlayout->setContentsMargins(dcm, dcm, dcm, dcm); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0, 0, 0, 0); vlayout->addLayout(hlayout); mCmdScript = new QCheckBox(EditCommandAlarmDlg::i18n_chk_EnterScript(), group); mCmdScript->setMinimumSize(mCmdScript->sizeHint()); mCmdScript->setWhatsThis(defsetting.subs(EditCommandAlarmDlg::i18n_chk_EnterScript()).toString()); hlayout->addWidget(mCmdScript); hlayout->addStretch(); mCmdXterm = new QCheckBox(EditCommandAlarmDlg::i18n_chk_ExecInTermWindow(), group); mCmdXterm->setMinimumSize(mCmdXterm->sizeHint()); mCmdXterm->setWhatsThis(defsetting.subs(EditCommandAlarmDlg::i18n_radio_ExecInTermWindow()).toString()); hlayout->addWidget(mCmdXterm); // EMAIL ALARMS group = new QGroupBox(i18nc("@title:group", "Email Alarms")); ttLayout->addWidget(group); vlayout = new QVBoxLayout(group); vlayout->setContentsMargins(dcm, dcm, dcm, dcm); vlayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); // BCC email to sender mEmailBcc = new QCheckBox(EditEmailAlarmDlg::i18n_chk_CopyEmailToSelf(), group); mEmailBcc->setMinimumSize(mEmailBcc->sizeHint()); mEmailBcc->setWhatsThis(defsetting.subs(EditEmailAlarmDlg::i18n_chk_CopyEmailToSelf()).toString()); vlayout->addWidget(mEmailBcc, 0, Qt::AlignLeft); ttLayout->addStretch(); // FONT / COLOUR TAB mFontChooser = new FontColourChooser(topFontColour, QStringList(), i18nc("@title:group", "Message Font && Color"), true); tfLayout->addWidget(mFontChooser); } void EditPrefTab::restore(bool, bool allTabs) { int index; if (allTabs || mTabs->currentIndex() == mTabGeneral) { mCopyToKOrganizer->setChecked(Preferences::defaultCopyToKOrganizer()); mLateCancel->setChecked(Preferences::defaultLateCancel()); switch (Preferences::defaultRecurPeriod()) { case Preferences::Recur_Yearly: index = 6; break; case Preferences::Recur_Monthly: index = 5; break; case Preferences::Recur_Weekly: index = 4; break; case Preferences::Recur_Daily: index = 3; break; case Preferences::Recur_SubDaily: index = 2; break; case Preferences::Recur_Login: index = 1; break; case Preferences::Recur_None: default: index = 0; break; } mRecurPeriod->setCurrentIndex(index); mFeb29->setButton(Preferences::defaultFeb29Type()); } if (allTabs || mTabs->currentIndex() == mTabTypes) { mConfirmAck->setChecked(Preferences::defaultConfirmAck()); mAutoClose->setChecked(Preferences::defaultAutoClose()); switch (Preferences::defaultReminderUnits()) { case TimePeriod::Weeks: index = 3; break; case TimePeriod::Days: index = 2; break; default: case TimePeriod::HoursMinutes: index = 1; break; case TimePeriod::Minutes: index = 0; break; } mReminderUnits->setCurrentIndex(index); KAEvent::ExtraActionOptions opts{}; if (Preferences::defaultExecPreActionOnDeferral()) opts |= KAEvent::ExecPreActOnDeferral; if (Preferences::defaultCancelOnPreActionError()) opts |= KAEvent::CancelOnPreActError; if (Preferences::defaultDontShowPreActionError()) opts |= KAEvent::DontShowPreActError; mSpecialActionsButton->setActions(Preferences::defaultPreAction(), Preferences::defaultPostAction(), opts); mSound->setCurrentIndex(soundIndex(Preferences::defaultSoundType())); mSoundFile->setText(Preferences::defaultSoundFile()); mSoundRepeat->setChecked(Preferences::defaultSoundRepeat()); mCmdScript->setChecked(Preferences::defaultCmdScript()); mCmdXterm->setChecked(Preferences::defaultCmdLogType() == Preferences::Log_Terminal); mEmailBcc->setChecked(Preferences::defaultEmailBcc()); } if (allTabs || mTabs->currentIndex() == mTabFontColour) { mFontChooser->setFgColour(Preferences::defaultFgColour()); mFontChooser->setBgColour(Preferences::defaultBgColour()); mFontChooser->setFont(Preferences::messageFont()); } } void EditPrefTab::apply(bool syncToDisc) { bool b = mAutoClose->isChecked(); if (b != Preferences::defaultAutoClose()) Preferences::setDefaultAutoClose(b); b = mConfirmAck->isChecked(); if (b != Preferences::defaultConfirmAck()) Preferences::setDefaultConfirmAck(b); TimePeriod::Units units; switch (mReminderUnits->currentIndex()) { case 3: units = TimePeriod::Weeks; break; case 2: units = TimePeriod::Days; break; default: case 1: units = TimePeriod::HoursMinutes; break; case 0: units = TimePeriod::Minutes; break; } if (units != Preferences::defaultReminderUnits()) Preferences::setDefaultReminderUnits(units); QString text = mSpecialActionsButton->preAction(); if (text != Preferences::defaultPreAction()) Preferences::setDefaultPreAction(text); text = mSpecialActionsButton->postAction(); if (text != Preferences::defaultPostAction()) Preferences::setDefaultPostAction(text); KAEvent::ExtraActionOptions opts = mSpecialActionsButton->options(); b = opts & KAEvent::ExecPreActOnDeferral; if (b != Preferences::defaultExecPreActionOnDeferral()) Preferences::setDefaultExecPreActionOnDeferral(b); b = opts & KAEvent::CancelOnPreActError; if (b != Preferences::defaultCancelOnPreActionError()) Preferences::setDefaultCancelOnPreActionError(b); b = opts & KAEvent::DontShowPreActError; if (b != Preferences::defaultDontShowPreActionError()) Preferences::setDefaultDontShowPreActionError(b); Preferences::SoundType snd; switch (mSound->currentIndex()) { case 3: snd = Preferences::Sound_Speak; break; case 2: snd = Preferences::Sound_File; break; case 1: snd = Preferences::Sound_Beep; break; case 0: default: snd = Preferences::Sound_None; break; } if (snd != Preferences::defaultSoundType()) Preferences::setDefaultSoundType(snd); text = mSoundFile->text(); if (text != Preferences::defaultSoundFile()) Preferences::setDefaultSoundFile(text); b = mSoundRepeat->isChecked(); if (b != Preferences::defaultSoundRepeat()) Preferences::setDefaultSoundRepeat(b); b = mCmdScript->isChecked(); if (b != Preferences::defaultCmdScript()) Preferences::setDefaultCmdScript(b); Preferences::CmdLogType log = mCmdXterm->isChecked() ? Preferences::Log_Terminal : Preferences::Log_Discard; if (log != Preferences::defaultCmdLogType()) Preferences::setDefaultCmdLogType(log); b = mEmailBcc->isChecked(); if (b != Preferences::defaultEmailBcc()) Preferences::setDefaultEmailBcc(b); b = mCopyToKOrganizer->isChecked(); if (b != Preferences::defaultCopyToKOrganizer()) Preferences::setDefaultCopyToKOrganizer(b); int i = mLateCancel->isChecked() ? 1 : 0; if (i != Preferences::defaultLateCancel()) Preferences::setDefaultLateCancel(i); Preferences::RecurType period; switch (mRecurPeriod->currentIndex()) { case 6: period = Preferences::Recur_Yearly; break; case 5: period = Preferences::Recur_Monthly; break; case 4: period = Preferences::Recur_Weekly; break; case 3: period = Preferences::Recur_Daily; break; case 2: period = Preferences::Recur_SubDaily; break; case 1: period = Preferences::Recur_Login; break; case 0: default: period = Preferences::Recur_None; break; } if (period != Preferences::defaultRecurPeriod()) Preferences::setDefaultRecurPeriod(period); int feb29 = mFeb29->selectedId(); if (feb29 >= 0 && static_cast(feb29) != Preferences::defaultFeb29Type()) Preferences::setDefaultFeb29Type(static_cast(feb29)); QColor colour = mFontChooser->fgColour(); if (colour != Preferences::defaultFgColour()) Preferences::setDefaultFgColour(colour); colour = mFontChooser->bgColour(); if (colour != Preferences::defaultBgColour()) Preferences::setDefaultBgColour(colour); QFont font = mFontChooser->font(); if (font != Preferences::messageFont()) Preferences::setMessageFont(font); PrefsTabBase::apply(syncToDisc); } void EditPrefTab::slotBrowseSoundFile() { QString defaultDir; QString url = SoundPicker::browseFile(defaultDir, mSoundFile->text()); if (!url.isEmpty()) mSoundFile->setText(url); } int EditPrefTab::soundIndex(Preferences::SoundType type) { switch (type) { case Preferences::Sound_Speak: return 3; case Preferences::Sound_File: return 2; case Preferences::Sound_Beep: return 1; case Preferences::Sound_None: default: return 0; } } QString EditPrefTab::validate() { if (mSound->currentIndex() == soundIndex(Preferences::Sound_File) && mSoundFile->text().isEmpty()) { mSoundFile->setFocus(); return xi18nc("@info", "You must enter a sound file when %1 is selected as the default sound type", SoundPicker::i18n_combo_File());; } return QString(); } /*============================================================================= = Class ViewPrefTab =============================================================================*/ ViewPrefTab::ViewPrefTab(StackedScrollGroup* scrollGroup) : PrefsTabBase(scrollGroup), mShowInSystemTrayCheck(nullptr), mShowInSystemTrayGroup(nullptr), mAutoHideSystemTray(nullptr), mAutoHideSystemTrayPeriod(nullptr) { mTabs = new QTabWidget(); topLayout()->addWidget(mTabs); QWidget* widget = new QWidget; QVBoxLayout* topGeneral = new QVBoxLayout(widget); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); int m = dcm / 2; topGeneral->setContentsMargins(m, m, m, m); topGeneral->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTabGeneral = mTabs->addTab(widget, i18nc("@title:tab", "General")); widget = new QWidget; QVBoxLayout* topWindows = new QVBoxLayout(widget); topWindows->setContentsMargins(m, m, m, m); topWindows->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTabWindows = mTabs->addTab(widget, i18nc("@title:tab", "Alarm Windows")); // Run-in-system-tray check box or group. static const QString showInSysTrayText = i18nc("@option:check", "Show in system tray"); static const QString showInSysTrayWhatsThis = xi18nc("@info:whatsthis", "Check to show KAlarm's icon in the system tray." " Showing it in the system tray provides easy access and a status indication."); if(Preferences::noAutoHideSystemTrayDesktops().contains(KAlarm::currentDesktopIdentityName())) { // Run-in-system-tray check box. // This desktop type doesn't provide GUI controls to view hidden system tray // icons, so don't show options to hide the system tray icon. widget = new QWidget; // this is to allow left adjustment topGeneral->addWidget(widget); QHBoxLayout* box = new QHBoxLayout(widget); mShowInSystemTrayCheck = new QCheckBox(showInSysTrayText); mShowInSystemTrayCheck->setWhatsThis(showInSysTrayWhatsThis); box->addWidget(mShowInSystemTrayCheck); box->setStretchFactor(new QWidget(widget), 1); // left adjust the controls } else { // Run-in-system-tray group box mShowInSystemTrayGroup = new QGroupBox(showInSysTrayText); mShowInSystemTrayGroup->setCheckable(true); mShowInSystemTrayGroup->setWhatsThis(showInSysTrayWhatsThis); topGeneral->addWidget(mShowInSystemTrayGroup); QGridLayout* grid = new QGridLayout(mShowInSystemTrayGroup); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(1, 1); grid->setColumnMinimumWidth(0, indentWidth()); mAutoHideSystemTray = new ButtonGroup(mShowInSystemTrayGroup); connect(mAutoHideSystemTray, &ButtonGroup::buttonSet, this, &ViewPrefTab::slotAutoHideSysTrayChanged); QRadioButton* radio = new QRadioButton(i18nc("@option:radio Always show KAlarm icon", "Always show"), mShowInSystemTrayGroup); mAutoHideSystemTray->addButton(radio, 0); radio->setWhatsThis( xi18nc("@info:whatsthis", "Check to show KAlarm's icon in the system tray " "regardless of whether alarms are due.")); grid->addWidget(radio, 0, 0, 1, 2, Qt::AlignLeft); radio = new QRadioButton(i18nc("@option:radio", "Automatically hide if no active alarms"), mShowInSystemTrayGroup); mAutoHideSystemTray->addButton(radio, 1); radio->setWhatsThis( xi18nc("@info:whatsthis", "Check to automatically hide KAlarm's icon in " "the system tray if there are no active alarms. When hidden, the icon can " "always be made visible by use of the system tray option to show hidden icons.")); grid->addWidget(radio, 1, 0, 1, 2, Qt::AlignLeft); QString text = xi18nc("@info:whatsthis", "Check to automatically hide KAlarm's icon in the " "system tray if no alarms are due within the specified time period. When hidden, " "the icon can always be made visible by use of the system tray option to show hidden icons."); radio = new QRadioButton(i18nc("@option:radio", "Automatically hide if no alarm due within time period:"), mShowInSystemTrayGroup); radio->setWhatsThis(text); mAutoHideSystemTray->addButton(radio, 2); grid->addWidget(radio, 2, 0, 1, 2, Qt::AlignLeft); mAutoHideSystemTrayPeriod = new TimePeriod(true, mShowInSystemTrayGroup); mAutoHideSystemTrayPeriod->setWhatsThis(text); mAutoHideSystemTrayPeriod->setMaximumWidth(mAutoHideSystemTrayPeriod->sizeHint().width()); grid->addWidget(mAutoHideSystemTrayPeriod, 3, 1, 1, 1, Qt::AlignLeft); mShowInSystemTrayGroup->setMaximumHeight(mShowInSystemTrayGroup->sizeHint().height()); } // System tray tooltip group box QGroupBox* group = new QGroupBox(i18nc("@title:group", "System Tray Tooltip")); topGeneral->addWidget(group); QGridLayout* grid = new QGridLayout(group); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(2, 1); grid->setColumnMinimumWidth(0, indentWidth()); grid->setColumnMinimumWidth(1, indentWidth()); mTooltipShowAlarms = new QCheckBox(i18nc("@option:check", "Show next &24 hours' alarms"), group); mTooltipShowAlarms->setMinimumSize(mTooltipShowAlarms->sizeHint()); connect(mTooltipShowAlarms, &QAbstractButton::toggled, this, &ViewPrefTab::slotTooltipAlarmsToggled); mTooltipShowAlarms->setWhatsThis( i18nc("@info:whatsthis", "Specify whether to include in the system tray tooltip, a summary of alarms due in the next 24 hours.")); grid->addWidget(mTooltipShowAlarms, 0, 0, 1, 3, Qt::AlignLeft); widget = new QWidget; QHBoxLayout* box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTooltipMaxAlarms = new QCheckBox(i18nc("@option:check", "Maximum number of alarms to show:")); mTooltipMaxAlarms->setMinimumSize(mTooltipMaxAlarms->sizeHint()); box->addWidget(mTooltipMaxAlarms); connect(mTooltipMaxAlarms, &QAbstractButton::toggled, this, &ViewPrefTab::slotTooltipMaxToggled); mTooltipMaxAlarmCount = new SpinBox(1, 99); mTooltipMaxAlarmCount->setSingleShiftStep(5); mTooltipMaxAlarmCount->setMinimumSize(mTooltipMaxAlarmCount->sizeHint()); box->addWidget(mTooltipMaxAlarmCount); widget->setWhatsThis( i18nc("@info:whatsthis", "Uncheck to display all of the next 24 hours' alarms in the system tray tooltip. " "Check to enter an upper limit on the number to be displayed.")); grid->addWidget(widget, 1, 1, 1, 2, Qt::AlignLeft); - mTooltipShowTime = new QCheckBox(MainWindow::i18n_chk_ShowAlarmTime(), group); + mTooltipShowTime = new QCheckBox(i18nc("@option:check", "Show alarm time"), group); mTooltipShowTime->setMinimumSize(mTooltipShowTime->sizeHint()); connect(mTooltipShowTime, &QAbstractButton::toggled, this, &ViewPrefTab::slotTooltipTimeToggled); mTooltipShowTime->setWhatsThis(i18nc("@info:whatsthis", "Specify whether to show in the system tray tooltip, the time at which each alarm is due.")); grid->addWidget(mTooltipShowTime, 2, 1, 1, 2, Qt::AlignLeft); - mTooltipShowTimeTo = new QCheckBox(MainWindow::i18n_chk_ShowTimeToAlarm(), group); + mTooltipShowTimeTo = new QCheckBox(i18nc("@option:check", "Show time until alarm"), group); mTooltipShowTimeTo->setMinimumSize(mTooltipShowTimeTo->sizeHint()); connect(mTooltipShowTimeTo, &QAbstractButton::toggled, this, &ViewPrefTab::slotTooltipTimeToToggled); mTooltipShowTimeTo->setWhatsThis(i18nc("@info:whatsthis", "Specify whether to show in the system tray tooltip, how long until each alarm is due.")); grid->addWidget(mTooltipShowTimeTo, 3, 1, 1, 2, Qt::AlignLeft); widget = new QWidget; // this is to control the QWhatsThis text display area box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mTooltipTimeToPrefixLabel = new QLabel(i18nc("@label:textbox", "Prefix:")); box->addWidget(mTooltipTimeToPrefixLabel); mTooltipTimeToPrefix = new QLineEdit(); box->addWidget(mTooltipTimeToPrefix); mTooltipTimeToPrefixLabel->setBuddy(mTooltipTimeToPrefix); widget->setWhatsThis(i18nc("@info:whatsthis", "Enter the text to be displayed in front of the time until the alarm, in the system tray tooltip.")); grid->addWidget(widget, 4, 2, Qt::AlignLeft); group->setMaximumHeight(group->sizeHint().height()); group = new QGroupBox(i18nc("@title:group", "Alarm List")); topGeneral->addWidget(group); QHBoxLayout* hlayout = new QHBoxLayout(group); hlayout->setContentsMargins(dcm, dcm, dcm, dcm); QVBoxLayout* colourLayout = new QVBoxLayout(); colourLayout->setContentsMargins(0, 0, 0, 0); hlayout->addLayout(colourLayout); widget = new QWidget; // to group widgets for QWhatsThis text box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing) / 2); colourLayout->addWidget(widget); QLabel* label1 = new QLabel(i18nc("@label:listbox", "Disabled alarm color:")); box->addWidget(label1); box->setStretchFactor(new QWidget(widget), 0); mDisabledColour = new ColourButton(); box->addWidget(mDisabledColour); label1->setBuddy(mDisabledColour); widget->setWhatsThis(i18nc("@info:whatsthis", "Choose the text color in the alarm list for disabled alarms.")); widget = new QWidget; // to group widgets for QWhatsThis text box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing) / 2); colourLayout->addWidget(widget); QLabel* label2 = new QLabel(i18nc("@label:listbox", "Archived alarm color:")); box->addWidget(label2); box->setStretchFactor(new QWidget(widget), 0); mArchivedColour = new ColourButton(); box->addWidget(mArchivedColour); label2->setBuddy(mArchivedColour); widget->setWhatsThis(i18nc("@info:whatsthis", "Choose the text color in the alarm list for archived alarms.")); hlayout->addStretch(); if (topGeneral) topGeneral->addStretch(); // top adjust the widgets group = new QGroupBox(i18nc("@title:group", "Alarm Message Windows")); topWindows->addWidget(group); grid = new QGridLayout(group); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); grid->setColumnStretch(1, 1); grid->setColumnMinimumWidth(0, indentWidth()); mWindowPosition = new ButtonGroup(group); connect(mWindowPosition, &ButtonGroup::buttonSet, this, &ViewPrefTab::slotWindowPosChanged); QString whatsthis = xi18nc("@info:whatsthis", "Choose how to reduce the chance of alarm messages being accidentally acknowledged:" "Position alarm message windows as far as possible from the current mouse cursor location, or" "Position alarm message windows in the center of the screen, but disable buttons for a short time after the window is displayed."); QRadioButton* radio = new QRadioButton(i18nc("@option:radio", "Position windows far from mouse cursor"), group); mWindowPosition->addButton(radio, 0); radio->setWhatsThis(whatsthis); grid->addWidget(radio, 0, 0, 1, 2, Qt::AlignLeft); radio = new QRadioButton(i18nc("@option:radio", "Center windows, delay activating window buttons"), group); mWindowPosition->addButton(radio, 1); radio->setWhatsThis(whatsthis); grid->addWidget(radio, 1, 0, 1, 2, Qt::AlignLeft); widget = new QWidget; // this is to control the QWhatsThis text display area box = new QHBoxLayout(widget); box->setContentsMargins(0, 0, 0, 0); box->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mWindowButtonDelayLabel = new QLabel(i18nc("@label:spinbox", "Button activation delay (seconds):")); box->addWidget(mWindowButtonDelayLabel); mWindowButtonDelay = new QSpinBox(); mWindowButtonDelay->setRange(1, 10); mWindowButtonDelayLabel->setBuddy(mWindowButtonDelay); box->addWidget(mWindowButtonDelay); widget->setWhatsThis(i18nc("@info:whatsthis", "Enter how long its buttons should remain disabled after the alarm message window is shown.")); box->setStretchFactor(new QWidget(widget), 1); // left adjust the controls grid->addWidget(widget, 2, 1, Qt::AlignLeft); grid->setRowMinimumHeight(3, style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); mModalMessages = new QCheckBox(i18nc("@option:check", "Message windows have a title bar and take keyboard focus"), group); mModalMessages->setMinimumSize(mModalMessages->sizeHint()); mModalMessages->setWhatsThis(xi18nc("@info:whatsthis", "Specify the characteristics of alarm message windows:" "If checked, the window is a normal window with a title bar, which grabs keyboard input when it is displayed." "If unchecked, the window does not interfere with your typing when " "it is displayed, but it has no title bar and cannot be moved or resized.")); grid->addWidget(mModalMessages, 4, 0, 1, 2, Qt::AlignLeft); if (topWindows) topWindows->addStretch(); // top adjust the widgets } void ViewPrefTab::restore(bool, bool allTabs) { if (allTabs || mTabs->currentIndex() == mTabGeneral) { if (mShowInSystemTrayGroup) mShowInSystemTrayGroup->setChecked(Preferences::showInSystemTray()); else mShowInSystemTrayCheck->setChecked(Preferences::showInSystemTray()); int id; int mins = Preferences::autoHideSystemTray(); switch (mins) { case -1: id = 1; break; // hide if no active alarms case 0: id = 0; break; // never hide default: { id = 2; int days = 0; int secs = 0; if (mins % 1440) secs = mins * 60; else days = mins / 1440; TimePeriod::Units units = secs ? TimePeriod::HoursMinutes : (days % 7) ? TimePeriod::Days : TimePeriod::Weeks; Duration duration((secs ? secs : days), (secs ? Duration::Seconds : Duration::Days)); mAutoHideSystemTrayPeriod->setPeriod(duration, false, units); break; } } if (mAutoHideSystemTray) mAutoHideSystemTray->setButton(id); setTooltip(Preferences::tooltipAlarmCount(), Preferences::showTooltipAlarmTime(), Preferences::showTooltipTimeToAlarm(), Preferences::tooltipTimeToPrefix()); mDisabledColour->setColor(Preferences::disabledColour()); mArchivedColour->setColor(Preferences::archivedColour()); } if (allTabs || mTabs->currentIndex() == mTabWindows) { mWindowPosition->setButton(Preferences::messageButtonDelay() ? 1 : 0); mWindowButtonDelay->setValue(Preferences::messageButtonDelay()); mModalMessages->setChecked(Preferences::modalMessages()); } } void ViewPrefTab::apply(bool syncToDisc) { QColor colour = mDisabledColour->color(); if (colour != Preferences::disabledColour()) Preferences::setDisabledColour(colour); colour = mArchivedColour->color(); if (colour != Preferences::archivedColour()) Preferences::setArchivedColour(colour); int n = mTooltipShowAlarms->isChecked() ? -1 : 0; if (n && mTooltipMaxAlarms->isChecked()) n = mTooltipMaxAlarmCount->value(); if (n != Preferences::tooltipAlarmCount()) Preferences::setTooltipAlarmCount(n); bool b = mTooltipShowTime->isChecked(); if (b != Preferences::showTooltipAlarmTime()) Preferences::setShowTooltipAlarmTime(b); b = mTooltipShowTimeTo->isChecked(); if (b != Preferences::showTooltipTimeToAlarm()) Preferences::setShowTooltipTimeToAlarm(b); QString text = mTooltipTimeToPrefix->text(); if (text != Preferences::tooltipTimeToPrefix()) Preferences::setTooltipTimeToPrefix(text); b = mShowInSystemTrayGroup ? mShowInSystemTrayGroup->isChecked() : mShowInSystemTrayCheck->isChecked(); if (b != Preferences::showInSystemTray()) Preferences::setShowInSystemTray(b); if (b && mAutoHideSystemTray) { switch (mAutoHideSystemTray->selectedId()) { case 0: n = 0; break; // never hide case 1: n = -1; break; // hide if no active alarms case 2: // hide if no alarms due within period n = mAutoHideSystemTrayPeriod->period().asSeconds() / 60; break; } if (n != Preferences::autoHideSystemTray()) Preferences::setAutoHideSystemTray(n); } n = mWindowPosition->selectedId(); if (n) n = mWindowButtonDelay->value(); if (n != Preferences::messageButtonDelay()) Preferences::setMessageButtonDelay(n); b = mModalMessages->isChecked(); if (b != Preferences::modalMessages()) Preferences::setModalMessages(b); PrefsTabBase::apply(syncToDisc); } void ViewPrefTab::setTooltip(int maxAlarms, bool time, bool timeTo, const QString& prefix) { if (!timeTo) time = true; // ensure that at least one time option is ticked // Set the states of the controls without calling signal // handlers, since these could change the checkboxes' states. mTooltipShowAlarms->blockSignals(true); mTooltipShowTime->blockSignals(true); mTooltipShowTimeTo->blockSignals(true); mTooltipShowAlarms->setChecked(maxAlarms); mTooltipMaxAlarms->setChecked(maxAlarms > 0); mTooltipMaxAlarmCount->setValue(maxAlarms > 0 ? maxAlarms : 1); mTooltipShowTime->setChecked(time); mTooltipShowTimeTo->setChecked(timeTo); mTooltipTimeToPrefix->setText(prefix); mTooltipShowAlarms->blockSignals(false); mTooltipShowTime->blockSignals(false); mTooltipShowTimeTo->blockSignals(false); // Enable/disable controls according to their states slotTooltipTimeToToggled(timeTo); slotTooltipAlarmsToggled(maxAlarms); } void ViewPrefTab::slotTooltipAlarmsToggled(bool on) { mTooltipMaxAlarms->setEnabled(on); mTooltipMaxAlarmCount->setEnabled(on && mTooltipMaxAlarms->isChecked()); mTooltipShowTime->setEnabled(on); mTooltipShowTimeTo->setEnabled(on); on = on && mTooltipShowTimeTo->isChecked(); mTooltipTimeToPrefix->setEnabled(on); mTooltipTimeToPrefixLabel->setEnabled(on); } void ViewPrefTab::slotTooltipMaxToggled(bool on) { mTooltipMaxAlarmCount->setEnabled(on && mTooltipMaxAlarms->isEnabled()); } void ViewPrefTab::slotTooltipTimeToggled(bool on) { if (!on && !mTooltipShowTimeTo->isChecked()) mTooltipShowTimeTo->setChecked(true); } void ViewPrefTab::slotTooltipTimeToToggled(bool on) { if (!on && !mTooltipShowTime->isChecked()) mTooltipShowTime->setChecked(true); on = on && mTooltipShowTimeTo->isEnabled(); mTooltipTimeToPrefix->setEnabled(on); mTooltipTimeToPrefixLabel->setEnabled(on); } void ViewPrefTab::slotAutoHideSysTrayChanged(QAbstractButton* button) { if (mAutoHideSystemTray) mAutoHideSystemTrayPeriod->setEnabled(mAutoHideSystemTray->id(button) == 2); } void ViewPrefTab::slotWindowPosChanged(QAbstractButton* button) { bool enable = mWindowPosition->id(button); mWindowButtonDelay->setEnabled(enable); mWindowButtonDelayLabel->setEnabled(enable); } #include "moc_prefdlg_p.cpp" #include "moc_prefdlg.cpp" // vim: et sw=4: