diff --git a/CMakeLists.txt b/CMakeLists.txt index f8487b45..f4a441bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,134 +1,133 @@ cmake_minimum_required(VERSION 3.5) set(KDEPIM_VERSION_NUMBER "5.13.40") set(PIM_VERSION ${KDEPIM_VERSION_NUMBER}) set(RELEASE_SERVICE_VERSION "20.03.70") set(KALARM_VERSION "2.13.0") project(kalarm VERSION ${KALARM_VERSION}) set(KALARM_FULL_VERSION "${KALARM_VERSION} (KDE Apps ${RELEASE_SERVICE_VERSION})") set(KF5_MIN_VERSION "5.64.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} (${RELEASE_SERVICE_VERSION})") set(KIMAP_LIB_VERSION "5.13.40") set(AKONADI_MIMELIB_VERSION "5.13.40") set(AKONADI_CONTACT_VERSION "5.13.40") set(KMAILTRANSPORT_LIB_VERSION "5.13.40") set(KPIMTEXTEDIT_LIB_VERSION "5.13.40") set(IDENTITYMANAGEMENT_LIB_VERSION "5.13.40") set(AKONADI_VERSION "5.13.40") set(KMIME_LIB_VERSION "5.13.40") set(AKONADIKALARM_LIB_VERSION "5.13.40") set(PIMCOMMON_LIB_VERSION_LIB "5.13.40") set(KDEPIM_LIB_VERSION "${KDEPIM_VERSION_NUMBER}") set(KDEPIM_LIB_SOVERSION "5") set(QT_REQUIRED_VERSION "5.12.0") find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED DBus Gui Network Widgets) find_package(Qt5X11Extras NO_MODULE) set(MAILCOMMON_LIB_VERSION_LIB "5.13.40") set(LIBKDEPIM_LIB_VERSION_LIB "5.13.40") set(KCALENDARCORE_LIB_VERSION "5.13.40") set(CALENDARUTILS_LIB_VERSION "5.13.40") set(KDEPIM_APPS_LIB_VERSION_LIB "5.13.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(KF5Crash ${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(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) configure_file(kalarm-version-string.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/kalarm-version-string.h @ONLY) 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) if (EXISTS "${CMAKE_SOURCE_DIR}/.git") #add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x060000) # for the moment we can't port QWheelEvent add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x060000) endif() 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/src/CMakeLists.txt b/src/CMakeLists.txt index a293aabc..1864a920 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,195 +1,194 @@ include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/ ) add_subdirectory(appicons) add_subdirectory(pixmaps) add_subdirectory(autostart) add_subdirectory(kconf_update) set(libkalarm_common_SRCS) ecm_qt_declare_logging_category(libkalarm_common_SRCS HEADER kalarm_debug.h IDENTIFIER KALARM_LOG CATEGORY_NAME org.kde.pim.kalarm DEFAULT_SEVERITY Warning) ########### next target ############### set(libkalarm_SRCS lib/buttongroup.cpp lib/checkbox.cpp lib/colourbutton.cpp lib/combobox.cpp lib/config.cpp lib/desktop.cpp lib/file.cpp lib/filedialog.cpp lib/groupbox.cpp lib/label.cpp lib/locale.cpp lib/messagebox.cpp lib/packedlayout.cpp lib/pushbutton.cpp lib/radiobutton.cpp lib/timeedit.cpp lib/timespinbox.cpp lib/timeperiod.cpp lib/timezonecombo.cpp lib/shellprocess.cpp lib/slider.cpp lib/spinbox.cpp lib/spinbox2.cpp lib/stackedwidgets.cpp lib/lineedit.cpp lib/synchtimer.cpp ) set(kalarm_bin_SRCS ${libkalarm_SRCS} ${libkalarm_common_SRCS} birthdaydlg.cpp birthdaymodel.cpp main.cpp editdlg.cpp editdlgtypes.cpp soundpicker.cpp sounddlg.cpp alarmcalendar.cpp undo.cpp kalarmapp.cpp mainwindowbase.cpp mainwindow.cpp messagewin.cpp preferences.cpp prefdlg.cpp traywindow.cpp dbushandler.cpp recurrenceedit.cpp deferdlg.cpp functions.cpp fontcolour.cpp fontcolourbutton.cpp alarmtime.cpp alarmtimewidget.cpp specialactions.cpp reminder.cpp startdaytimer.cpp eventlistview.cpp alarmlistdelegate.cpp alarmlistview.cpp templatelistview.cpp kamail.cpp timeselector.cpp latecancel.cpp repetitionbutton.cpp emailidcombo.cpp find.cpp pickfileradio.cpp newalarmaction.cpp commandoptions.cpp resourceselector.cpp templatepickdlg.cpp templatedlg.cpp templatemenuaction.cpp wakedlg.cpp ) set(kalarm_bin_SRCS ${kalarm_bin_SRCS} resources/resourcetype.cpp resources/resource.cpp resources/resources.cpp resources/resourcedatamodelbase.cpp resources/resourcemodel.cpp resources/resourceselectdialog.cpp resources/eventmodel.cpp resources/datamodel.cpp resources/akonadidatamodel.cpp resources/akonadiresource.cpp resources/akonadiresourcecreator.cpp resources/akonadiresourcemigrator.cpp kalarmmigrateapplication.cpp akonadicollectionsearch.cpp eventid.cpp ) ki18n_wrap_ui(kalarm_bin_SRCS wakedlg.ui ) qt5_add_dbus_adaptor(kalarm_bin_SRCS org.kde.kalarm.kalarm.xml dbushandler.h DBusHandler) qt5_add_dbus_interfaces(kalarm_bin_SRCS kmail/org.kde.kmail.kmail.xml) kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/kalarmresource.kcfg org.kde.Akonadi.KAlarm.Settings) qt5_add_dbus_interface(kalarm_bin_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.KAlarm.Settings.xml kalarmsettings KAlarmSettings) kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/kalarmdirresource.kcfg org.kde.Akonadi.KAlarmDir.Settings) qt5_add_dbus_interface(kalarm_bin_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.KAlarmDir.Settings.xml kalarmdirsettings KAlarmDirSettings) kconfig_add_kcfg_files(kalarm_bin_SRCS GENERATE_MOC kalarmconfig.kcfgc) #if (UNIX) file(GLOB ICONS_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/appicons/*-apps-kalarm.png") ecm_add_app_icon(kalarm_bin_SRCS ICONS ${ICONS_SRCS}) add_executable(kalarm_bin ${kalarm_bin_SRCS}) set_target_properties(kalarm_bin PROPERTIES OUTPUT_NAME kalarm) target_compile_definitions(kalarm_bin PRIVATE -DVERSION="${KALARM_VERSION}") target_link_libraries(kalarm_bin KF5::Codecs KF5::ConfigCore KF5::Completion KF5::DBusAddons KF5::GlobalAccel KF5::GuiAddons KF5::Holidays - KF5::IconThemes KF5::KIOWidgets KF5::Notifications KF5::TextWidgets KF5::WindowSystem KF5::XmlGui KF5::KIOFileWidgets KF5::Crash Phonon::phonon4qt5 KF5::AkonadiCore KF5::AkonadiMime KF5::AkonadiContact KF5::AkonadiWidgets KF5::AlarmCalendar KF5::CalendarCore KF5::CalendarUtils KF5::Contacts KF5::IdentityManagement KF5::Libkdepim KF5::MailTransportAkonadi KF5::Mime KF5::PimCommon ) if (Qt5X11Extras_FOUND) target_link_libraries(kalarm_bin Qt5::X11Extras ${X11_X11_LIB}) endif() install(TARGETS kalarm_bin ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) #endif (UNIX) ########### install files ############### install(FILES org.kde.kalarm.desktop DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES kalarm.autostart.desktop DESTINATION ${KDE_INSTALL_AUTOSTARTDIR}) install(FILES org.kde.kalarm.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) install(FILES kalarmconfig.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) install(FILES kalarmui.rc DESTINATION ${KDE_INSTALL_KXMLGUI5DIR}/kalarm) install(FILES org.kde.kalarm.kalarm.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR}) ########### KAuth helper ############### add_executable(kalarm_helper rtcwakeaction.cpp ${libkalarm_common_SRCS}) target_link_libraries(kalarm_helper KF5::AuthCore KF5::I18n) install(TARGETS kalarm_helper DESTINATION ${KAUTH_HELPER_INSTALL_DIR}) kauth_install_helper_files(kalarm_helper org.kde.kalarm.rtcwake root) kauth_install_actions(org.kde.kalarm.rtcwake org.kde.kalarm.rtcwake.actions) diff --git a/src/messagewin.cpp b/src/messagewin.cpp index 07486ab7..7cef28e2 100644 --- a/src/messagewin.cpp +++ b/src/messagewin.cpp @@ -1,2418 +1,2413 @@ /* * messagewin.cpp - displays an alarm message * Program: kalarm * 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 "messagewin.h" #include "messagewin_p.h" #include "config-kalarm.h" #include "alarmcalendar.h" #include "deferdlg.h" #include "editdlg.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "preferences.h" #include "resources/resources.h" #include "lib/autoqpointer.h" #include "lib/config.h" #include "lib/desktop.h" #include "lib/file.h" #include "lib/messagebox.h" #include "lib/pushbutton.h" #include "lib/shellprocess.h" #include "lib/synchtimer.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include -#include #include #include #include #include #include #include #include #include #include #include #if KDEPIM_HAVE_X11 #include #include #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KCalendarCore; using namespace KAlarmCal; #if KDEPIM_HAVE_X11 enum FullScreenType { NoFullScreen = 0, FullScreen = 1, FullScreenActive = 2 }; static FullScreenType haveFullScreenWindow(int screen); static FullScreenType findFullScreenWindows(const QVector& screenRects, QVector& screenTypes); #endif #include "kmailinterface.h" static const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail"); static const QLatin1String KMAIL_DBUS_PATH("/KMail"); // The delay for enabling message window buttons if a zero delay is // configured, i.e. the windows are placed far from the cursor. static const int proximityButtonDelay = 1000; // (milliseconds) static const int proximityMultiple = 10; // multiple of button height distance from cursor for proximity // A text label widget which can be scrolled and copied with the mouse class MessageText : public KTextEdit { public: MessageText(QWidget* parent = nullptr) : KTextEdit(parent), mNewLine(false) { setReadOnly(true); setFrameStyle(NoFrame); setLineWrapMode(NoWrap); } int scrollBarHeight() const { return horizontalScrollBar()->height(); } int scrollBarWidth() const { return verticalScrollBar()->width(); } void setBackgroundColour(const QColor& c) { QPalette pal = viewport()->palette(); pal.setColor(viewport()->backgroundRole(), c); viewport()->setPalette(pal); } QSize sizeHint() const override { const QSizeF docsize = document()->size(); return QSize(static_cast(docsize.width() + 0.99) + verticalScrollBar()->width(), static_cast(docsize.height() + 0.99) + horizontalScrollBar()->height()); } bool newLine() const { return mNewLine; } void setNewLine(bool nl) { mNewLine = nl; } private: bool mNewLine; }; // Basic flags for the window static const Qt::WindowFlags WFLAGS = Qt::WindowStaysOnTopHint; static const Qt::WindowFlags WFLAGS2 = Qt::WindowContextHelpButtonHint; static const Qt::WidgetAttribute WidgetFlags = Qt::WA_DeleteOnClose; // Error message bit masks enum { ErrMsg_Speak = 0x01, ErrMsg_AudioFile = 0x02 }; QList MessageWin::mWindowList; QMap MessageWin::mErrorMessages; bool MessageWin::mRedisplayed = false; // There can only be one audio thread at a time: trying to play multiple // sound files simultaneously would result in a cacophony, and besides // that, Phonon currently crashes... QPointer MessageWin::mAudioThread; MessageWin* AudioThread::mAudioOwner = nullptr; /****************************************************************************** * Construct the message window for the specified alarm. * Other alarms in the supplied event may have been updated by the caller, so * the whole event needs to be stored for updating the calendar file when it is * displayed. */ MessageWin::MessageWin(const KAEvent* event, const KAAlarm& alarm, int flags) : MainWindowBase(nullptr, static_cast(WFLAGS | WFLAGS2 | ((flags & ALWAYS_HIDE) || getWorkAreaAndModal() ? Qt::WindowType(0) : Qt::X11BypassWindowManagerHint))) , mMessage(event->cleanText()) , mFont(event->font()) , mBgColour(event->bgColour()) , mFgColour(event->fgColour()) , mEventId(*event) , mAudioFile(event->audioFile()) , mVolume(event->soundVolume()) , mFadeVolume(event->fadeVolume()) , mFadeSeconds(qMin(event->fadeSeconds(), 86400)) , mDefaultDeferMinutes(event->deferDefaultMinutes()) , mAlarmType(alarm.type()) , mAction(event->actionSubType()) , mAkonadiItemId(event->akonadiItemId()) , mCommandError(event->commandError()) , mRestoreHeight(0) , mAudioRepeatPause(event->repeatSoundPause()) , mConfirmAck(event->confirmAck()) , mNoDefer(true) , mInvalid(false) , mEvent(*event) , mOriginalEvent(*event) , mResource(Resources::resourceForEvent(mEventId.eventId())) , mAlwaysHide(flags & ALWAYS_HIDE) , mNoPostAction(alarm.type() & KAAlarm::REMINDER_ALARM) , mBeep(event->beep()) , mSpeak(event->speak()) , mRescheduleEvent(!(flags & NO_RESCHEDULE)) { qCDebug(KALARM_LOG) << "MessageWin:" << (void*)this << "event" << mEventId; setAttribute(static_cast(WidgetFlags)); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("MessageWin")); // used by LikeBack if (alarm.type() & KAAlarm::REMINDER_ALARM) { if (event->reminderMinutes() < 0) { event->previousOccurrence(alarm.dateTime(false).effectiveKDateTime(), mDateTime, false); if (!mDateTime.isValid() && event->repeatAtLogin()) mDateTime = alarm.dateTime().addSecs(event->reminderMinutes() * 60); } else mDateTime = event->mainDateTime(true); } else mDateTime = alarm.dateTime(true); if (!(flags & (NO_INIT_VIEW | ALWAYS_HIDE))) { const bool readonly = AlarmCalendar::resources()->eventReadOnly(mEventId.eventId()); mShowEdit = !mEventId.isEmpty() && !readonly; mNoDefer = readonly || (flags & NO_DEFER) || alarm.repeatAtLogin(); initView(); } // Set to save settings automatically, but don't save window size. // File alarm window size is saved elsewhere. setAutoSaveSettings(QStringLiteral("MessageWin"), false); mWindowList.append(this); if (event->autoClose()) mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().qDateTime().addSecs(event->lateCancel() * 60); if (mAlwaysHide) { hide(); displayComplete(); // play audio, etc. } } /****************************************************************************** * Display an error message window. * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note * that the option is specific to 'event'. */ void MessageWin::showError(const KAEvent& event, const DateTime& alarmDateTime, const QStringList& errmsgs, const QString& dontShowAgain) { if (!dontShowAgain.isEmpty() && KAlarm::dontShowErrors(EventId(event), dontShowAgain)) return; // Don't pile up duplicate error messages for the same alarm for (int i = 0, end = mWindowList.count(); i < end; ++i) { const MessageWin* w = mWindowList[i]; if (w->mErrorWindow && w->mEventId == EventId(event) && w->mErrorMsgs == errmsgs && w->mDontShowAgain == dontShowAgain) return; } (new MessageWin(&event, alarmDateTime, errmsgs, dontShowAgain))->show(); } /****************************************************************************** * Construct the message window for a specified error message. * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note * that the option is specific to 'event'. */ MessageWin::MessageWin(const KAEvent* event, const DateTime& alarmDateTime, const QStringList& errmsgs, const QString& dontShowAgain) : MainWindowBase(nullptr, WFLAGS | WFLAGS2) , mMessage(event->cleanText()) , mDateTime(alarmDateTime) , mEventId(*event) , mAlarmType(KAAlarm::MAIN_ALARM) , mAction(event->actionSubType()) , mAkonadiItemId(-1) , mCommandError(KAEvent::CMD_NO_ERROR) , mErrorMsgs(errmsgs) , mDontShowAgain(dontShowAgain) , mRestoreHeight(0) , mConfirmAck(false) , mShowEdit(false) , mNoDefer(true) , mInvalid(false) , mEvent(*event) , mOriginalEvent(*event) , mErrorWindow(true) , mNoPostAction(true) { qCDebug(KALARM_LOG) << "MessageWin: errmsg"; setAttribute(static_cast(WidgetFlags)); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("ErrorWin")); // used by LikeBack getWorkAreaAndModal(); initView(); mWindowList.append(this); } /****************************************************************************** * Construct the message window for restoration by session management. * The window is initialised by readProperties(). */ MessageWin::MessageWin() : MainWindowBase(nullptr, WFLAGS) { qCDebug(KALARM_LOG) << "MessageWin:" << (void*)this << "restore"; setAttribute(WidgetFlags); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("RestoredMsgWin")); // used by LikeBack getWorkAreaAndModal(); mWindowList.append(this); } /****************************************************************************** * Destructor. Perform any post-alarm actions before tidying up. */ MessageWin::~MessageWin() { qCDebug(KALARM_LOG) << "~MessageWin" << (void*)this << mEventId; if (AudioThread::mAudioOwner == this && !mAudioThread.isNull()) mAudioThread->quit(); mErrorMessages.remove(mEventId); mWindowList.removeAll(this); delete mTempFile; if (!mRecreating) { if (!mNoPostAction && !mEvent.postAction().isEmpty()) theApp()->alarmCompleted(mEvent); if (!instanceCount(true)) theApp()->quitIf(); // no visible windows remain - check whether to quit } } /****************************************************************************** * Construct the message window. */ void MessageWin::initView() { const bool reminder = (!mErrorWindow && (mAlarmType & KAAlarm::REMINDER_ALARM)); const int leading = fontMetrics().leading(); setCaption((mAlarmType & KAAlarm::REMINDER_ALARM) ? i18nc("@title:window", "Reminder") : i18nc("@title:window", "Message")); QWidget* topWidget = new QWidget(this); setCentralWidget(topWidget); QVBoxLayout* topLayout = new QVBoxLayout(topWidget); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->setContentsMargins(dcm, dcm, dcm, dcm); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QPalette labelPalette = palette(); labelPalette.setColor(backgroundRole(), labelPalette.color(QPalette::Window)); // Show the alarm date/time, together with a reminder text where appropriate. // Alarm date/time: display time zone if not local time zone. mTimeLabel = new QLabel(topWidget); mTimeLabel->setText(dateTimeToDisplay()); mTimeLabel->setFrameStyle(QFrame::StyledPanel); mTimeLabel->setPalette(labelPalette); mTimeLabel->setAutoFillBackground(true); topLayout->addWidget(mTimeLabel, 0, Qt::AlignHCenter); mTimeLabel->setWhatsThis(i18nc("@info:whatsthis", "The scheduled date/time for the message (as opposed to the actual time of display).")); if (mDateTime.isValid()) { // Reminder if (reminder) { // Create a label "time\nReminder" by inserting the time at the // start of the translated string, allowing for possible HTML tags // enclosing "Reminder". QString s = i18nc("@info", "Reminder"); QRegExp re(QStringLiteral("^(<[^>]+>)*")); re.indexIn(s); s.insert(re.matchedLength(), mTimeLabel->text() + QLatin1String("
")); mTimeLabel->setText(s); mTimeLabel->setAlignment(Qt::AlignHCenter); } } else mTimeLabel->hide(); if (!mErrorWindow) { // It's a normal alarm message window switch (mAction) { case KAEvent::FILE: { // Display the file name KSqueezedTextLabel* label = new KSqueezedTextLabel(mMessage, topWidget); label->setFrameStyle(QFrame::StyledPanel); label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); label->setPalette(labelPalette); label->setAutoFillBackground(true); label->setWhatsThis(i18nc("@info:whatsthis", "The file whose contents are displayed below")); topLayout->addWidget(label, 0, Qt::AlignHCenter); // Display contents of file const QUrl url = QUrl::fromUserInput(mMessage, QString(), QUrl::AssumeLocalFile); auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 0, KIO::HideProgressInfo); const bool exists = statJob->exec(); const bool isDir = statJob->statResult().isDir(); bool opened = false; if (exists && !isDir) { auto job = KIO::storedGet(url); KJobWidgets::setWindow(job, MainWindow::mainMainWindow()); if (job->exec()) { opened = true; const QByteArray data = job->data(); QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(url); if (mime.name() == QLatin1String("application/octet-stream")) mime = db.mimeTypeForData(mTempFile); const File::FileType fileType = File::fileType(mime); switch (fileType) { case File::Image: case File::TextFormatted: delete mTempFile; mTempFile = new QTemporaryFile; mTempFile->open(); mTempFile->write(data); break; default: break; } QTextBrowser* view = new QTextBrowser(topWidget); view->setFrameStyle(QFrame::NoFrame); view->setWordWrapMode(QTextOption::NoWrap); QPalette pal = view->viewport()->palette(); pal.setColor(view->viewport()->backgroundRole(), mBgColour); view->viewport()->setPalette(pal); view->setTextColor(mFgColour); view->setCurrentFont(mFont); switch (fileType) { case File::Image: view->setHtml(QLatin1String("
fileName() + QLatin1String("\">
")); mTempFile->close(); // keep the file available to be displayed break; case File::TextFormatted: view->QTextBrowser::setSource(QUrl::fromLocalFile(mTempFile->fileName())); //krazy:exclude=qclasses delete mTempFile; mTempFile = nullptr; break; default: view->setPlainText(QString::fromUtf8(data)); break; } view->setMinimumSize(view->sizeHint()); topLayout->addWidget(view); // Set the default size to 20 lines square. // Note that after the first file has been displayed, this size // is overridden by the user-set default stored in the config file. // So there is no need to calculate an accurate size. int h = 20*view->fontMetrics().lineSpacing() + 2*view->frameWidth(); view->resize(QSize(h, h).expandedTo(view->sizeHint())); view->setWhatsThis(i18nc("@info:whatsthis", "The contents of the file to be displayed")); } } if (!exists || isDir || !opened) { mErrorMsgs += isDir ? i18nc("@info", "File is a folder") : exists ? i18nc("@info", "Failed to open file") : i18nc("@info", "File not found"); } break; } case KAEvent::MESSAGE: { // Message label // Using MessageText instead of QLabel allows scrolling and mouse copying MessageText* text = new MessageText(topWidget); text->setAutoFillBackground(true); text->setBackgroundColour(mBgColour); text->setTextColor(mFgColour); text->setCurrentFont(mFont); text->insertPlainText(mMessage); const int lineSpacing = text->fontMetrics().lineSpacing(); const QSize s = text->sizeHint(); const int h = s.height(); text->setMaximumHeight(h + text->scrollBarHeight()); text->setMinimumHeight(qMin(h, lineSpacing*4)); text->setMaximumWidth(s.width() + text->scrollBarWidth()); text->setWhatsThis(i18nc("@info:whatsthis", "The alarm message")); const int vspace = lineSpacing/2; const int hspace = lineSpacing - style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->addSpacing(vspace); topLayout->addStretch(); // Don't include any horizontal margins if message is 2/3 screen width if (text->sizeHint().width() >= Desktop::workArea(mScreenNumber).width()*2/3) topLayout->addWidget(text, 1, Qt::AlignHCenter); else { QHBoxLayout* layout = new QHBoxLayout(); layout->addSpacing(hspace); layout->addWidget(text, 1, Qt::AlignHCenter); layout->addSpacing(hspace); topLayout->addLayout(layout); } if (!reminder) topLayout->addStretch(); break; } case KAEvent::COMMAND: { mCommandText = new MessageText(topWidget); mCommandText->setBackgroundColour(mBgColour); mCommandText->setTextColor(mFgColour); mCommandText->setCurrentFont(mFont); topLayout->addWidget(mCommandText); mCommandText->setWhatsThis(i18nc("@info:whatsthis", "The output of the alarm's command")); theApp()->execCommandAlarm(mEvent, mEvent.alarm(mAlarmType), this, SLOT(readProcessOutput(ShellProcess*))); break; } case KAEvent::EMAIL: default: break; } if (reminder && mEvent.reminderMinutes() > 0) { // Advance reminder: show remaining time until the actual alarm mRemainingText = new QLabel(topWidget); mRemainingText->setFrameStyle(QFrame::Box | QFrame::Raised); mRemainingText->setContentsMargins(leading, leading, leading, leading); mRemainingText->setPalette(labelPalette); mRemainingText->setAutoFillBackground(true); if (mDateTime.isDateOnly() || KADateTime::currentLocalDate().daysTo(mDateTime.date()) > 0) { setRemainingTextDay(); MidnightTimer::connect(this, SLOT(setRemainingTextDay())); // update every day } else { setRemainingTextMinute(); MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute } topLayout->addWidget(mRemainingText, 0, Qt::AlignHCenter); topLayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); topLayout->addStretch(); } } else { // It's an error message switch (mAction) { case KAEvent::EMAIL: { // Display the email addresses and subject. QFrame* frame = new QFrame(topWidget); frame->setFrameStyle(QFrame::Box | QFrame::Raised); frame->setWhatsThis(i18nc("@info:whatsthis", "The email to send")); topLayout->addWidget(frame, 0, Qt::AlignHCenter); QGridLayout* grid = new QGridLayout(frame); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@info Email addressee", "To:"), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 0, Qt::AlignLeft); label = new QLabel(mEvent.emailAddresses(QStringLiteral("\n")), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 1, Qt::AlignLeft); label = new QLabel(i18nc("@info Email subject", "Subject:"), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 1, 0, Qt::AlignLeft); label = new QLabel(mEvent.emailSubject(), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 1, 1, Qt::AlignLeft); break; } case KAEvent::COMMAND: case KAEvent::FILE: case KAEvent::MESSAGE: default: // Just display the error message strings break; } } if (!mErrorMsgs.count()) { topWidget->setAutoFillBackground(true); QPalette palette = topWidget->palette(); palette.setColor(topWidget->backgroundRole(), mBgColour); topWidget->setPalette(palette); } else { setCaption(i18nc("@title:window", "Error")); QHBoxLayout* layout = new QHBoxLayout(); int m = 2 * dcm; layout->setContentsMargins(m, m, m, m); layout->addStretch(); topLayout->addLayout(layout); QLabel* label = new QLabel(topWidget); - label->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-error")).pixmap(IconSize(KIconLoader::Desktop), IconSize(KIconLoader::Desktop))); + label->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-error")).pixmap(style()->pixelMetric(QStyle::PM_MessageBoxIconSize))); label->setFixedSize(label->sizeHint()); layout->addWidget(label, 0, Qt::AlignRight); QVBoxLayout* vlayout = new QVBoxLayout(); layout->addLayout(vlayout); for (QStringList::ConstIterator it = mErrorMsgs.constBegin(); it != mErrorMsgs.constEnd(); ++it) { label = new QLabel(*it, topWidget); label->setFixedSize(label->sizeHint()); vlayout->addWidget(label, 0, Qt::AlignLeft); } layout->addStretch(); if (!mDontShowAgain.isEmpty()) { mDontShowAgainCheck = new QCheckBox(i18nc("@option:check", "Do not display this error message again for this alarm"), topWidget); mDontShowAgainCheck->setFixedSize(mDontShowAgainCheck->sizeHint()); topLayout->addWidget(mDontShowAgainCheck, 0, Qt::AlignLeft); } } QGridLayout* grid = new QGridLayout(); grid->setColumnStretch(0, 1); // keep the buttons right-adjusted in the window topLayout->addLayout(grid); int gridIndex = 1; // Close button mOkButton = new PushButton(KStandardGuiItem::close(), topWidget); // Prevent accidental acknowledgement of the message if the user is typing when the window appears mOkButton->clearFocus(); mOkButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mOkButton->setFixedSize(mOkButton->sizeHint()); connect(mOkButton, &QAbstractButton::clicked, this, &MessageWin::slotOk); grid->addWidget(mOkButton, 0, gridIndex++, Qt::AlignHCenter); mOkButton->setWhatsThis(i18nc("@info:whatsthis", "Acknowledge the alarm")); if (mShowEdit) { // Edit button mEditButton = new PushButton(i18nc("@action:button", "&Edit..."), topWidget); mEditButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mEditButton->setFixedSize(mEditButton->sizeHint()); connect(mEditButton, &QAbstractButton::clicked, this, &MessageWin::slotEdit); grid->addWidget(mEditButton, 0, gridIndex++, Qt::AlignHCenter); mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the alarm.")); } // Defer button mDeferButton = new PushButton(i18nc("@action:button", "&Defer..."), topWidget); mDeferButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mDeferButton->setFixedSize(mDeferButton->sizeHint()); connect(mDeferButton, &QAbstractButton::clicked, this, &MessageWin::slotDefer); grid->addWidget(mDeferButton, 0, gridIndex++, Qt::AlignHCenter); mDeferButton->setWhatsThis(xi18nc("@info:whatsthis", "Defer the alarm until later." "You will be prompted to specify when the alarm should be redisplayed.")); if (mNoDefer) mDeferButton->hide(); else setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more - KIconLoader iconLoader; if (!mAudioFile.isEmpty() && (mVolume || mFadeVolume > 0)) { // Silence button to stop sound repetition - const QPixmap pixmap = iconLoader.loadIcon(QStringLiteral("media-playback-stop"), KIconLoader::MainToolbar); mSilenceButton = new PushButton(topWidget); - mSilenceButton->setIcon(pixmap); + mSilenceButton->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-stop"))); grid->addWidget(mSilenceButton, 0, gridIndex++, Qt::AlignHCenter); mSilenceButton->setToolTip(i18nc("@info:tooltip", "Stop sound")); mSilenceButton->setWhatsThis(i18nc("@info:whatsthis", "Stop playing the sound")); // To avoid getting in a mess, disable the button until sound playing has been set up mSilenceButton->setEnabled(false); } if (mAkonadiItemId >= 0) { // KMail button - const QPixmap pixmap = iconLoader.loadIcon(QStringLiteral("internet-mail"), KIconLoader::MainToolbar); mKMailButton = new PushButton(topWidget); - mKMailButton->setIcon(pixmap); + mKMailButton->setIcon(QIcon::fromTheme(QStringLiteral("internet-mail"))); connect(mKMailButton, &QAbstractButton::clicked, this, &MessageWin::slotShowKMailMessage); grid->addWidget(mKMailButton, 0, gridIndex++, Qt::AlignHCenter); mKMailButton->setToolTip(xi18nc("@info:tooltip Locate this email in KMail", "Locate in KMail")); mKMailButton->setWhatsThis(xi18nc("@info:whatsthis", "Locate and highlight this email in KMail")); } // KAlarm button - const QPixmap pixmap = iconLoader.loadIcon(KAboutData::applicationData().componentName(), KIconLoader::MainToolbar); mKAlarmButton = new PushButton(topWidget); - mKAlarmButton->setIcon(pixmap); + mKAlarmButton->setIcon(QIcon::fromTheme(KAboutData::applicationData().componentName())); connect(mKAlarmButton, &QAbstractButton::clicked, this, &MessageWin::displayMainWindow); grid->addWidget(mKAlarmButton, 0, gridIndex++, Qt::AlignHCenter); mKAlarmButton->setToolTip(xi18nc("@info:tooltip", "Activate KAlarm")); mKAlarmButton->setWhatsThis(xi18nc("@info:whatsthis", "Activate KAlarm")); int butsize = mKAlarmButton->sizeHint().height(); if (mSilenceButton) butsize = qMax(butsize, mSilenceButton->sizeHint().height()); if (mKMailButton) butsize = qMax(butsize, mKMailButton->sizeHint().height()); mKAlarmButton->setFixedSize(butsize, butsize); if (mSilenceButton) mSilenceButton->setFixedSize(butsize, butsize); if (mKMailButton) mKMailButton->setFixedSize(butsize, butsize); // Disable all buttons initially, to prevent accidental clicking on if they happen to be // under the mouse just as the window appears. mOkButton->setEnabled(false); if (mDeferButton->isVisible()) mDeferButton->setEnabled(false); if (mEditButton) mEditButton->setEnabled(false); if (mKMailButton) mKMailButton->setEnabled(false); mKAlarmButton->setEnabled(false); topLayout->activate(); setMinimumSize(QSize(grid->sizeHint().width() + 2 * style()->pixelMetric(QStyle::PM_DefaultChildMargin), sizeHint().height())); const bool modal = !(windowFlags() & Qt::X11BypassWindowManagerHint); NET::States wstate = NET::Sticky | NET::KeepAbove; if (modal) wstate |= NET::Modal; WId winid = winId(); KWindowSystem::setState(winid, wstate); KWindowSystem::setOnAllDesktops(winid, true); mInitialised = true; // the window's widgets have been created } /****************************************************************************** * Return the number of message windows, optionally excluding always-hidden ones. */ int MessageWin::instanceCount(bool excludeAlwaysHidden) { int count = mWindowList.count(); if (excludeAlwaysHidden) { for (MessageWin* win : qAsConst(mWindowList)) { if (win->mAlwaysHide) --count; } } return count; } bool MessageWin::hasDefer() const { return mDeferButton && mDeferButton->isVisible(); } /****************************************************************************** * Show the Defer button when it was previously hidden. */ void MessageWin::showDefer() { if (mDeferButton) { mNoDefer = false; mDeferButton->show(); setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more resize(sizeHint()); } } /****************************************************************************** * Convert a reminder window into a normal alarm window. */ void MessageWin::cancelReminder(const KAEvent& event, const KAAlarm& alarm) { if (!mInitialised) return; mDateTime = alarm.dateTime(true); mNoPostAction = false; mAlarmType = alarm.type(); if (event.autoClose()) mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().qDateTime().addSecs(event.lateCancel() * 60); setCaption(i18nc("@title:window", "Message")); mTimeLabel->setText(dateTimeToDisplay()); if (mRemainingText) mRemainingText->hide(); MidnightTimer::disconnect(this, SLOT(setRemainingTextDay())); MinuteTimer::disconnect(this, SLOT(setRemainingTextMinute())); setMinimumHeight(0); centralWidget()->layout()->activate(); setMinimumHeight(sizeHint().height()); resize(sizeHint()); } /****************************************************************************** * Show the alarm's trigger time. * This is assumed to have previously been hidden. */ void MessageWin::showDateTime(const KAEvent& event, const KAAlarm& alarm) { if (!mTimeLabel) return; mDateTime = (alarm.type() & KAAlarm::REMINDER_ALARM) ? event.mainDateTime(true) : alarm.dateTime(true); if (mDateTime.isValid()) { mTimeLabel->setText(dateTimeToDisplay()); mTimeLabel->show(); } } /****************************************************************************** * Get the trigger time to display. */ QString MessageWin::dateTimeToDisplay() { QString tm; if (mDateTime.isValid()) { if (mDateTime.isDateOnly()) tm = QLocale().toString(mDateTime.date(), QLocale::ShortFormat); else { bool showZone = false; if (mDateTime.timeType() == KADateTime::UTC || (mDateTime.timeType() == KADateTime::TimeZone && !mDateTime.isLocalZone())) { // Display time zone abbreviation if it's different from the local // zone. Note that the iCalendar time zone might represent the local // time zone in a slightly different way from the system time zone, // so the zone comparison above might not produce the desired result. const QString tz = mDateTime.kDateTime().toString(QStringLiteral("%Z")); KADateTime local = mDateTime.kDateTime(); local.setTimeSpec(KADateTime::Spec::LocalZone()); showZone = (local.toString(QStringLiteral("%Z")) != tz); } const QDateTime dt = mDateTime.qDateTime(); tm = QLocale().toString(dt, QLocale::ShortFormat); if (showZone) tm += QLatin1Char(' ') + mDateTime.timeZone().displayName(dt, QTimeZone::ShortName, QLocale()); } } return tm; } /****************************************************************************** * Set the remaining time text in a reminder window. * Called at the start of every day (at the user-defined start-of-day time). */ void MessageWin::setRemainingTextDay() { QString text; const int days = KADateTime::currentLocalDate().daysTo(mDateTime.date()); if (days <= 0 && !mDateTime.isDateOnly()) { // The alarm is due today, so start refreshing every minute MidnightTimer::disconnect(this, SLOT(setRemainingTextDay())); setRemainingTextMinute(); MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute } else { if (days <= 0) text = i18nc("@info", "Today"); else if (days % 7) text = i18ncp("@info", "Tomorrow", "in %1 days' time", days); else text = i18ncp("@info", "in 1 week's time", "in %1 weeks' time", days/7); } mRemainingText->setText(text); } /****************************************************************************** * Set the remaining time text in a reminder window. * Called on every minute boundary. */ void MessageWin::setRemainingTextMinute() { QString text; const int mins = (KADateTime::currentUtcDateTime().secsTo(mDateTime.effectiveKDateTime()) + 59) / 60; if (mins < 60) text = i18ncp("@info", "in 1 minute's time", "in %1 minutes' time", (mins > 0 ? mins : 0)); else if (mins % 60 == 0) text = i18ncp("@info", "in 1 hour's time", "in %1 hours' time", mins/60); else { QString hourText = i18ncp("@item:intext inserted into 'in ... %1 minute's time' below", "1 hour", "%1 hours", mins/60); text = i18ncp("@info '%2' is the previous message '1 hour'/'%1 hours'", "in %2 1 minute's time", "in %2 %1 minutes' time", mins%60, hourText); } mRemainingText->setText(text); } /****************************************************************************** * Called when output is available from the command which is providing the text * for this window. Add the output and resize the window to show it. */ void MessageWin::readProcessOutput(ShellProcess* proc) { const QByteArray data = proc->readAll(); if (!data.isEmpty()) { // Strip any trailing newline, to avoid showing trailing blank line // in message window. if (mCommandText->newLine()) mCommandText->append(QStringLiteral("\n")); const int nl = data.endsWith('\n') ? 1 : 0; mCommandText->setNewLine(nl); mCommandText->insertPlainText(QString::fromLocal8Bit(data.data(), data.length() - nl)); resize(sizeHint()); } } /****************************************************************************** * Save settings to the session managed config file, for restoration * when the program is restored. */ void MessageWin::saveProperties(KConfigGroup& config) { if (mShown && !mErrorWindow && !mAlwaysHide) { config.writeEntry("EventID", mEventId.eventId()); config.writeEntry("CollectionID", mResource.id()); config.writeEntry("AlarmType", static_cast(mAlarmType)); if (mAlarmType == KAAlarm::INVALID_ALARM) qCCritical(KALARM_LOG) << "MessageWin::saveProperties: Invalid alarm: id=" << mEventId << ", alarm count=" << mEvent.alarmCount(); config.writeEntry("Message", mMessage); config.writeEntry("Type", static_cast(mAction)); config.writeEntry("Font", mFont); config.writeEntry("BgColour", mBgColour); config.writeEntry("FgColour", mFgColour); config.writeEntry("ConfirmAck", mConfirmAck); if (mDateTime.isValid()) { //TODO: Write KADateTime when it becomes possible config.writeEntry("Time", mDateTime.effectiveDateTime()); config.writeEntry("DateOnly", mDateTime.isDateOnly()); QByteArray zone; if (mDateTime.isUtc()) zone = "UTC"; else if (mDateTime.timeType() == KADateTime::TimeZone) { const QTimeZone tz = mDateTime.timeZone(); if (tz.isValid()) zone = tz.id(); } config.writeEntry("TimeZone", zone); } if (mCloseTime.isValid()) config.writeEntry("Expiry", mCloseTime); if (mAudioRepeatPause >= 0 && mSilenceButton && mSilenceButton->isEnabled()) { // Only need to restart sound file playing if it's being repeated config.writePathEntry("AudioFile", mAudioFile); config.writeEntry("Volume", static_cast(mVolume * 100)); config.writeEntry("AudioPause", mAudioRepeatPause); } config.writeEntry("Speak", mSpeak); config.writeEntry("Height", height()); config.writeEntry("DeferMins", mDefaultDeferMinutes); config.writeEntry("NoDefer", mNoDefer); config.writeEntry("NoPostAction", mNoPostAction); config.writeEntry("AkonadiItemId", mAkonadiItemId); config.writeEntry("CmdErr", static_cast(mCommandError)); config.writeEntry("DontShowAgain", mDontShowAgain); } else config.writeEntry("Invalid", true); } /****************************************************************************** * Read settings from the session managed config file. * This function is automatically called whenever the app is being restored. * Read in whatever was saved in saveProperties(). */ void MessageWin::readProperties(const KConfigGroup& config) { mInvalid = config.readEntry("Invalid", false); const QString eventId = config.readEntry("EventID"); const ResourceId collectionId = config.readEntry("CollectionID", ResourceId(-1)); mAlarmType = static_cast(config.readEntry("AlarmType", 0)); if (mAlarmType == KAAlarm::INVALID_ALARM) { mInvalid = true; qCCritical(KALARM_LOG) << "MessageWin::readProperties: Invalid alarm: id=" << eventId; } mMessage = config.readEntry("Message"); mAction = static_cast(config.readEntry("Type", 0)); mFont = config.readEntry("Font", QFont()); mBgColour = config.readEntry("BgColour", QColor(Qt::white)); mFgColour = config.readEntry("FgColour", QColor(Qt::black)); mConfirmAck = config.readEntry("ConfirmAck", false); QDateTime invalidDateTime; QDateTime dt = config.readEntry("Time", invalidDateTime); const QByteArray zoneId = config.readEntry("TimeZone").toLatin1(); KADateTime::Spec timeSpec; if (zoneId.isEmpty()) timeSpec = KADateTime::LocalZone; else if (zoneId == "UTC") timeSpec = KADateTime::UTC; else timeSpec = QTimeZone(zoneId); mDateTime = KADateTime(dt.date(), dt.time(), timeSpec); const bool dateOnly = config.readEntry("DateOnly", false); if (dateOnly) mDateTime.setDateOnly(true); mCloseTime = config.readEntry("Expiry", invalidDateTime); mCloseTime.setTimeSpec(Qt::UTC); mAudioFile = config.readPathEntry("AudioFile", QString()); mVolume = static_cast(config.readEntry("Volume", 0)) / 100; mFadeVolume = -1; mFadeSeconds = 0; if (!mAudioFile.isEmpty()) // audio file URL was only saved if it repeats mAudioRepeatPause = config.readEntry("AudioPause", 0); mBeep = false; // don't beep after restart (similar to not playing non-repeated sound file) mSpeak = config.readEntry("Speak", false); mRestoreHeight = config.readEntry("Height", 0); mDefaultDeferMinutes = config.readEntry("DeferMins", 0); mNoDefer = config.readEntry("NoDefer", false); mNoPostAction = config.readEntry("NoPostAction", true); mAkonadiItemId = config.readEntry("AkonadiItemId", QVariant(QVariant::LongLong)).toLongLong(); mCommandError = KAEvent::CmdErrType(config.readEntry("CmdErr", static_cast(KAEvent::CMD_NO_ERROR))); mDontShowAgain = config.readEntry("DontShowAgain", QString()); mShowEdit = false; // Temporarily initialise mResource and mEventId - they will be set by redisplayAlarm() mResource = Resources::resource(collectionId); mEventId = EventId(collectionId, eventId); qCDebug(KALARM_LOG) << "MessageWin::readProperties:" << eventId; if (mAlarmType != KAAlarm::INVALID_ALARM) { // Recreate the event from the calendar file (if possible) if (eventId.isEmpty()) initView(); else { // Close any other window for this alarm which has already been restored by redisplayAlarms() if (!Resources::allCreated()) { connect(Resources::instance(), &Resources::resourcesCreated, this, &MessageWin::showRestoredAlarm); return; } redisplayAlarm(); } } } /****************************************************************************** * Fetch the restored alarm from the calendar and redisplay it in this window. */ void MessageWin::showRestoredAlarm() { qCDebug(KALARM_LOG) << "MessageWin::showRestoredAlarm:" << mEventId; redisplayAlarm(); show(); } /****************************************************************************** * Fetch the restored alarm from the calendar and redisplay it in this window. */ void MessageWin::redisplayAlarm() { mResource = Resources::resourceForEvent(mEventId.eventId()); mEventId.setCollectionId(mResource.id()); qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarm:" << mEventId; // Delete any already existing window for the same event MessageWin* duplicate = findEvent(mEventId, this); if (duplicate) qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarm: Deleting duplicate window:" << mEventId; delete duplicate; KAEvent* event = AlarmCalendar::resources()->event(mEventId); if (event) { mEvent = *event; mShowEdit = true; } else { // It's not in the active calendar, so try the displaying or archive calendars retrieveEvent(mEvent, mResource, mShowEdit, mNoDefer); mNoDefer = !mNoDefer; } initView(); } /****************************************************************************** * Redisplay alarms which were being shown when the program last exited. * Normally, these alarms will have been displayed by session restoration, but * if the program crashed or was killed, we can redisplay them here so that * they won't be lost. */ void MessageWin::redisplayAlarms() { if (mRedisplayed) return; qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms"; mRedisplayed = true; AlarmCalendar* cal = AlarmCalendar::displayCalendar(); if (cal && cal->isOpen()) { KAEvent event; Resource resource; const Event::List events = cal->kcalEvents(); for (int i = 0, end = events.count(); i < end; ++i) { bool showDefer, showEdit; reinstateFromDisplaying(events[i], event, resource, showEdit, showDefer); const EventId eventId(event); if (findEvent(eventId)) qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms: Message window already exists:" << eventId; else { // This event should be displayed, but currently isn't being const KAAlarm alarm = event.convertDisplayingAlarm(); if (alarm.type() == KAAlarm::INVALID_ALARM) { qCCritical(KALARM_LOG) << "MessageWin::redisplayAlarms: Invalid alarm: id=" << eventId; continue; } qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms:" << eventId; const bool login = alarm.repeatAtLogin(); const int flags = NO_RESCHEDULE | (login ? NO_DEFER : 0) | NO_INIT_VIEW; MessageWin* win = new MessageWin(&event, alarm, flags); win->mResource = resource; const bool rw = resource.isWritable(event.category()); win->mShowEdit = rw ? showEdit : false; win->mNoDefer = (rw && !login) ? !showDefer : true; win->initView(); win->show(); } } } } /****************************************************************************** * Retrieves the event with the current ID from the displaying calendar file, * or if not found there, from the archive calendar. */ bool MessageWin::retrieveEvent(KAEvent& event, Resource& resource, bool& showEdit, bool& showDefer) { const Event::Ptr kcalEvent = AlarmCalendar::displayCalendar()->kcalEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING)); if (!reinstateFromDisplaying(kcalEvent, event, resource, showEdit, showDefer)) { // The event isn't in the displaying calendar. // Try to retrieve it from the archive calendar. KAEvent* ev = nullptr; Resource archiveRes = Resources::getStandard(CalEvent::ARCHIVED); if (archiveRes.isValid()) ev = AlarmCalendar::resources()->event(EventId(archiveRes.id(), CalEvent::uid(mEventId.eventId(), CalEvent::ARCHIVED))); if (!ev) return false; event = *ev; event.setArchive(); // ensure that it gets re-archived if it's saved event.setCategory(CalEvent::ACTIVE); if (mEventId.eventId() != event.id()) qCCritical(KALARM_LOG) << "MessageWin::retrieveEvent: Wrong event ID"; event.setEventId(mEventId.eventId()); resource = Resource(); showEdit = true; showDefer = true; qCDebug(KALARM_LOG) << "MessageWin::retrieveEvent:" << event.id() << ": success"; } return true; } /****************************************************************************** * Retrieves the displayed event from the calendar file, or if not found there, * from the displaying calendar. */ bool MessageWin::reinstateFromDisplaying(const Event::Ptr& kcalEvent, KAEvent& event, Resource& resource, bool& showEdit, bool& showDefer) { if (!kcalEvent) return false; ResourceId resourceId; event.reinstateFromDisplaying(kcalEvent, resourceId, showEdit, showDefer); event.setCollectionId(resourceId); resource = Resources::resource(resourceId); qCDebug(KALARM_LOG) << "MessageWin::reinstateFromDisplaying:" << EventId(event) << ": success"; return true; } /****************************************************************************** * Called when an alarm is currently being displayed, to store a copy of the * alarm in the displaying calendar, and to reschedule it for its next repetition. * If no repetitions remain, cancel it. */ void MessageWin::alarmShowing(KAEvent& event) { qCDebug(KALARM_LOG) << "MessageWin::alarmShowing:" << event.id() << "," << KAAlarm::debugType(mAlarmType); const KAAlarm alarm = event.alarm(mAlarmType); if (!alarm.isValid()) { qCCritical(KALARM_LOG) << "MessageWin::alarmShowing: Alarm type not found:" << event.id() << ":" << mAlarmType; return; } if (!mAlwaysHide) { // Copy the alarm to the displaying calendar in case of a crash, etc. KAEvent dispEvent; const ResourceId id = Resources::resourceForEvent(event.id()).id(); dispEvent.setDisplaying(event, mAlarmType, id, mDateTime.effectiveKDateTime(), mShowEdit, !mNoDefer); AlarmCalendar* cal = AlarmCalendar::displayCalendarOpen(); if (cal) { cal->deleteDisplayEvent(dispEvent.id()); // in case it already exists cal->addEvent(dispEvent); cal->save(); } } theApp()->rescheduleAlarm(event, alarm); } /****************************************************************************** * Spread alarm windows over the screen so that they are all visible, or pile * them on top of each other again. * Reply = true if windows are now scattered, false if piled up. */ bool MessageWin::spread(bool scatter) { if (instanceCount(true) <= 1) // ignore always-hidden windows return false; const QRect desk = Desktop::workArea(); // get the usable area of the desktop if (scatter == isSpread(desk.topLeft())) return scatter; if (scatter) { // Usually there won't be many windows, so a crude // scattering algorithm should suffice. int x = desk.left(); int y = desk.top(); int ynext = y; for (int errmsgs = 0; errmsgs < 2; ++errmsgs) { // Display alarm messages first, then error messages, since most // error messages tend to be the same height. for (int i = 0, end = mWindowList.count(); i < end; ++i) { MessageWin* w = mWindowList[i]; if ((!errmsgs && w->mErrorWindow) || (errmsgs && !w->mErrorWindow)) continue; const QSize sz = w->frameGeometry().size(); if (x + sz.width() > desk.right()) { x = desk.left(); y = ynext; } int ytmp = y; if (y + sz.height() > desk.bottom()) { ytmp = desk.bottom() - sz.height(); if (ytmp < desk.top()) ytmp = desk.top(); } w->move(x, ytmp); x += sz.width(); if (ytmp + sz.height() > ynext) ynext = ytmp + sz.height(); } } } else { // Move all windows to the top left corner for (int i = 0, end = mWindowList.count(); i < end; ++i) mWindowList[i]->move(desk.topLeft()); } return scatter; } /****************************************************************************** * Check whether message windows are all piled up, or are spread out. * Reply = true if windows are currently spread, false if piled up. */ bool MessageWin::isSpread(const QPoint& topLeft) { for (int i = 0, end = mWindowList.count(); i < end; ++i) { if (mWindowList[i]->pos() != topLeft) return true; } return false; } /****************************************************************************** * Returns the existing message window (if any) which is displaying the event * with the specified ID. */ MessageWin* MessageWin::findEvent(const EventId& eventId, MessageWin* exclude) { if (!eventId.isEmpty()) { for (int i = 0, end = mWindowList.count(); i < end; ++i) { MessageWin* w = mWindowList[i]; if (w != exclude && w->mEventId == eventId && !w->mErrorWindow) return w; } } return nullptr; } /****************************************************************************** * Beep and play the audio file, as appropriate. */ void MessageWin::playAudio() { if (mBeep) { // Beep using two methods, in case the sound card/speakers are switched off or not working QApplication::beep(); // beep through the internal speaker KNotification::beep(); // beep through the sound card & speakers } if (!mAudioFile.isEmpty()) { if (!mVolume && mFadeVolume <= 0) return; // ensure zero volume doesn't play anything startAudio(); // play the audio file } else if (mSpeak) { // The message is to be spoken. In case of error messges, // call it on a timer to allow the window to display first. QTimer::singleShot(0, this, &MessageWin::slotSpeak); } } /****************************************************************************** * Speak the message. * Called asynchronously to avoid delaying the display of the message. */ void MessageWin::slotSpeak() { KPIMTextEdit::TextToSpeech *tts = KPIMTextEdit::TextToSpeech::self(); if (!tts->isReady()) { KAMessageBox::detailedError(MainWindow::mainMainWindow(), i18nc("@info", "Unable to speak message"), i18nc("@info", "Text-to-speech subsystem is not available")); clearErrorMessage(ErrMsg_Speak); return; } tts->say(mMessage); } /****************************************************************************** * Called when another window's audio thread has been destructed. * Start playing this window's audio file. Because initialising the sound system * and loading the file may take some time, it is called in a separate thread to * allow the window to display first. */ void MessageWin::startAudio() { if (mAudioThread) { // An audio file is already playing for another message // window, so wait until it has finished. connect(mAudioThread.data(), &QObject::destroyed, this, &MessageWin::audioTerminating); } else { qCDebug(KALARM_LOG) << "MessageWin::startAudio:" << QThread::currentThread(); mAudioThread = new AudioThread(this, mAudioFile, mVolume, mFadeVolume, mFadeSeconds, mAudioRepeatPause); connect(mAudioThread.data(), &AudioThread::readyToPlay, this, &MessageWin::playReady); connect(mAudioThread.data(), &QThread::finished, this, &MessageWin::playFinished); if (mSilenceButton) connect(mSilenceButton, &QAbstractButton::clicked, mAudioThread.data(), &QThread::quit); // Notify after creating mAudioThread, so that isAudioPlaying() will // return the correct value. theApp()->notifyAudioPlaying(true); mAudioThread->start(); } } /****************************************************************************** * Return whether audio playback is currently active. */ bool MessageWin::isAudioPlaying() { return mAudioThread; } /****************************************************************************** * Stop audio playback. */ void MessageWin::stopAudio(bool wait) { qCDebug(KALARM_LOG) << "MessageWin::stopAudio"; if (mAudioThread) mAudioThread->stop(wait); } /****************************************************************************** * Called when another window's audio thread is being destructed. * Wait until the destructor has finished. */ void MessageWin::audioTerminating() { QTimer::singleShot(0, this, &MessageWin::startAudio); } /****************************************************************************** * Called when the audio file is ready to start playing. */ void MessageWin::playReady() { if (mSilenceButton) mSilenceButton->setEnabled(true); } /****************************************************************************** * Called when the audio file thread finishes. */ void MessageWin::playFinished() { if (mSilenceButton) mSilenceButton->setEnabled(false); if (mAudioThread) // mAudioThread can actually be null here! { const QString errmsg = mAudioThread->error(); if (!errmsg.isEmpty() && !haveErrorMessage(ErrMsg_AudioFile)) { KAMessageBox::error(this, errmsg); clearErrorMessage(ErrMsg_AudioFile); } } delete mAudioThread.data(); if (mAlwaysHide) close(); } /****************************************************************************** * Constructor for audio thread. */ AudioThread::AudioThread(MessageWin* parent, const QString& audioFile, float volume, float fadeVolume, int fadeSeconds, int repeatPause) : QThread(parent), mFile(audioFile), mVolume(volume), mFadeVolume(fadeVolume), mFadeSeconds(fadeSeconds), mRepeatPause(repeatPause), mAudioObject(nullptr) { if (mAudioOwner) qCCritical(KALARM_LOG) << "MessageWin::AudioThread: mAudioOwner already set"; mAudioOwner = parent; } /****************************************************************************** * Destructor for audio thread. Waits for thread completion and tidies up. * Note that this destructor is executed in the parent thread. */ AudioThread::~AudioThread() { qCDebug(KALARM_LOG) << "~MessageWin::AudioThread"; stop(true); // stop playing and tidy up (timeout 3 seconds) delete mAudioObject; mAudioObject = nullptr; if (mAudioOwner == parent()) mAudioOwner = nullptr; // Notify after deleting mAudioThread, so that isAudioPlaying() will // return the correct value. QTimer::singleShot(0, theApp(), &KAlarmApp::notifyAudioStopped); } /****************************************************************************** * Quits the thread and waits for thread completion and tidies up. */ void AudioThread::stop(bool waiT) { qCDebug(KALARM_LOG) << "MessageWin::AudioThread::stop"; quit(); // stop playing and tidy up wait(3000); // wait for run() to exit (timeout 3 seconds) if (!isFinished()) { // Something has gone wrong - forcibly kill the thread terminate(); if (waiT) wait(); } } /****************************************************************************** * Kick off the thread to play the audio file. */ void AudioThread::run() { mMutex.lock(); if (mAudioObject) { mMutex.unlock(); return; } qCDebug(KALARM_LOG) << "MessageWin::AudioThread::run:" << QThread::currentThread() << mFile; const QString audioFile = mFile; const QUrl url = QUrl::fromUserInput(mFile); mFile = url.isLocalFile() ? url.toLocalFile() : url.toString(); Phonon::MediaSource source(url); if (source.type() == Phonon::MediaSource::Invalid) { mError = xi18nc("@info", "Cannot open audio file: %1", audioFile); mMutex.unlock(); qCCritical(KALARM_LOG) << "MessageWin::AudioThread::run: Open failure:" << audioFile; return; } mAudioObject = new Phonon::MediaObject(); mAudioObject->setCurrentSource(source); mAudioObject->setTransitionTime(100); // workaround to prevent clipping of end of files in Xine backend Phonon::AudioOutput* output = new Phonon::AudioOutput(Phonon::NotificationCategory, mAudioObject); mPath = Phonon::createPath(mAudioObject, output); if (mVolume >= 0 || mFadeVolume >= 0) { const float vol = (mVolume >= 0) ? mVolume : output->volume(); const float maxvol = qMax(vol, mFadeVolume); output->setVolume(maxvol); if (mFadeVolume >= 0 && mFadeSeconds > 0) { Phonon::VolumeFaderEffect* fader = new Phonon::VolumeFaderEffect(mAudioObject); fader->setVolume(mFadeVolume / maxvol); fader->fadeTo(mVolume / maxvol, mFadeSeconds * 1000); mPath.insertEffect(fader); } } connect(mAudioObject, &Phonon::MediaObject::stateChanged, this, &AudioThread::playStateChanged, Qt::DirectConnection); connect(mAudioObject, &Phonon::MediaObject::finished, this, &AudioThread::checkAudioPlay, Qt::DirectConnection); mPlayedOnce = false; mPausing = false; mMutex.unlock(); Q_EMIT readyToPlay(); checkAudioPlay(); // Start an event loop. // The function will exit once exit() or quit() is called. // First, ensure that the thread object is deleted once it has completed. connect(this, &QThread::finished, this, &QObject::deleteLater); exec(); stopPlay(); } /****************************************************************************** * Called when the audio file has loaded and is ready to play, or when play * has completed. * If it is ready to play, start playing it (for the first time or repeated). * If play has not yet completed, wait a bit longer. */ void AudioThread::checkAudioPlay() { mMutex.lock(); if (!mAudioObject) { mMutex.unlock(); return; } if (mPausing) mPausing = false; else { // The file has loaded and is ready to play, or play has completed if (mPlayedOnce) { if (mRepeatPause < 0) { // Play has completed mMutex.unlock(); stopPlay(); return; } if (mRepeatPause > 0) { // Pause before playing the file again mPausing = true; QTimer::singleShot(mRepeatPause * 1000, this, &AudioThread::checkAudioPlay); mMutex.unlock(); return; } } mPlayedOnce = true; } // Start playing the file, either for the first time or again qCDebug(KALARM_LOG) << "MessageWin::AudioThread::checkAudioPlay: start"; mAudioObject->play(); mMutex.unlock(); } /****************************************************************************** * Called when the playback object changes state. * If an error has occurred, quit and return the error to the caller. */ void AudioThread::playStateChanged(Phonon::State newState) { if (newState == Phonon::ErrorState) { QMutexLocker locker(&mMutex); const QString err = mAudioObject->errorString(); if (!err.isEmpty()) { qCCritical(KALARM_LOG) << "MessageWin::AudioThread::playStateChanged: Play failure:" << mFile << ":" << err; mError = xi18nc("@info", "Error playing audio file: %1%2", mFile, err); exit(1); } } } /****************************************************************************** * Called when play completes, the Silence button is clicked, or the window is * closed, to terminate audio access. */ void AudioThread::stopPlay() { mMutex.lock(); if (mAudioObject) { mAudioObject->stop(); const QList effects = mPath.effects(); for (int i = 0; i < effects.count(); ++i) { mPath.removeEffect(effects[i]); delete effects[i]; } delete mAudioObject; mAudioObject = nullptr; } mMutex.unlock(); quit(); // exit the event loop, if it's still running } QString AudioThread::error() const { QMutexLocker locker(&mMutex); return mError; } /****************************************************************************** * Raise the alarm window, re-output any required audio notification, and * reschedule the alarm in the calendar file. */ void MessageWin::repeat(const KAAlarm& alarm) { if (!mInitialised) return; if (mDeferDlg) { // Cancel any deferral dialog so that the user notices something's going on, // and also because the deferral time limit will have changed. delete mDeferDlg; mDeferDlg = nullptr; } KAEvent* event = mEventId.isEmpty() ? nullptr : AlarmCalendar::resources()->event(mEventId); if (event) { mAlarmType = alarm.type(); // store new alarm type for use if it is later deferred if (mAlwaysHide) playAudio(); else { if (!mDeferDlg || Preferences::modalMessages()) { raise(); playAudio(); } if (mDeferButton->isVisible()) { mDeferButton->setEnabled(true); setDeferralLimit(*event); // ensure that button is disabled when alarm can't be deferred any more } } alarmShowing(*event); } } /****************************************************************************** * Display the window. * If windows are being positioned away from the mouse cursor, it is initially * positioned at the top left to slightly reduce the number of times the * windows need to be moved in showEvent(). */ void MessageWin::show() { if (mCloseTime.isValid()) { // Set a timer to auto-close the window int delay = QDateTime::currentDateTimeUtc().secsTo(mCloseTime); if (delay < 0) delay = 0; QTimer::singleShot(delay * 1000, this, &QWidget::close); if (!delay) return; // don't show the window if auto-closing is already due } if (Preferences::messageButtonDelay() == 0) move(0, 0); MainWindowBase::show(); } /****************************************************************************** * Returns the window's recommended size exclusive of its frame. */ QSize MessageWin::sizeHint() const { QSize desired; switch (mAction) { case KAEvent::MESSAGE: desired = MainWindowBase::sizeHint(); break; case KAEvent::COMMAND: if (mShown) { // For command output, expand the window to accommodate the text const QSize texthint = mCommandText->sizeHint(); int w = texthint.width() + 2 * style()->pixelMetric(QStyle::PM_DefaultChildMargin); if (w < width()) w = width(); const int ypadding = height() - mCommandText->height(); desired = QSize(w, texthint.height() + ypadding); break; } // fall through to default Q_FALLTHROUGH(); default: return MainWindowBase::sizeHint(); } // Limit the size to fit inside the working area of the desktop const QSize desktop = Desktop::workArea(mScreenNumber).size(); const QSize frameThickness = frameGeometry().size() - geometry().size(); // title bar & window frame return desired.boundedTo(desktop - frameThickness); } /****************************************************************************** * Called when the window is shown. * The first time, output any required audio notification, and reschedule or * delete the event from the calendar file. */ void MessageWin::showEvent(QShowEvent* se) { MainWindowBase::showEvent(se); if (mShown || !mInitialised) return; if (mErrorWindow || mAlarmType == KAAlarm::INVALID_ALARM) { // Don't bother repositioning error messages, // and invalid alarms should be deleted anyway. enableButtons(); } else { /* Set the window size. * Note that the frame thickness is not yet known when this * method is called, so for large windows the size needs to be * set again later. */ bool execComplete = true; QSize s = sizeHint(); // fit the window round the message if (mAction == KAEvent::FILE && !mErrorMsgs.count()) Config::readWindowSize("FileMessage", s); resize(s); const QRect desk = Desktop::workArea(mScreenNumber); const QRect frame = frameGeometry(); mButtonDelay = Preferences::messageButtonDelay() * 1000; if (mButtonDelay) { // Position the window in the middle of the screen, and // delay enabling the buttons. mPositioning = true; move((desk.width() - frame.width())/2, (desk.height() - frame.height())/2); execComplete = false; } else { /* Try to ensure that the window can't accidentally be acknowledged * by the user clicking the mouse just as it appears. * To achieve this, move the window so that the OK button is as far away * from the cursor as possible. If the buttons are still too close to the * cursor, disable the buttons for a short time. * N.B. This can't be done in show(), since the geometry of the window * is not known until it is displayed. Unfortunately by moving the * window in showEvent(), a flicker is unavoidable. * See the Qt documentation on window geometry for more details. */ // PROBLEM: The frame size is not known yet! const QPoint cursor = QCursor::pos(); const QRect rect = geometry(); // Find the offsets from the outside of the frame to the edges of the OK button const QRect button(mOkButton->mapToParent(QPoint(0, 0)), mOkButton->mapToParent(mOkButton->rect().bottomRight())); const int buttonLeft = button.left() + rect.left() - frame.left(); const int buttonRight = width() - button.right() + frame.right() - rect.right(); const int buttonTop = button.top() + rect.top() - frame.top(); const int buttonBottom = height() - button.bottom() + frame.bottom() - rect.bottom(); const int centrex = (desk.width() + buttonLeft - buttonRight) / 2; const int centrey = (desk.height() + buttonTop - buttonBottom) / 2; const int x = (cursor.x() < centrex) ? desk.right() - frame.width() : desk.left(); const int y = (cursor.y() < centrey) ? desk.bottom() - frame.height() : desk.top(); // Find the enclosing rectangle for the new button positions // and check if the cursor is too near QRect buttons = mOkButton->geometry().united(mKAlarmButton->geometry()); buttons.translate(rect.left() + x - frame.left(), rect.top() + y - frame.top()); const int minDistance = proximityMultiple * mOkButton->height(); if ((abs(cursor.x() - buttons.left()) < minDistance || abs(cursor.x() - buttons.right()) < minDistance) && (abs(cursor.y() - buttons.top()) < minDistance || abs(cursor.y() - buttons.bottom()) < minDistance)) mButtonDelay = proximityButtonDelay; // too near - disable buttons initially if (x != frame.left() || y != frame.top()) { mPositioning = true; move(x, y); execComplete = false; } } if (execComplete) displayComplete(); // play audio, etc. } // Set the window size etc. once the frame size is known QTimer::singleShot(0, this, &MessageWin::frameDrawn); mShown = true; } /****************************************************************************** * Called when the window has been moved. */ void MessageWin::moveEvent(QMoveEvent* e) { MainWindowBase::moveEvent(e); theApp()->setSpreadWindowsState(isSpread(Desktop::workArea(mScreenNumber).topLeft())); if (mPositioning) { // The window has just been initially positioned mPositioning = false; displayComplete(); // play audio, etc. } } /****************************************************************************** * Called after (hopefully) the window frame size is known. * Reset the initial window size if it exceeds the working area of the desktop. * Set the 'spread windows' menu item status. */ void MessageWin::frameDrawn() { if (!mErrorWindow && mAction == KAEvent::MESSAGE) { const QSize s = sizeHint(); if (width() > s.width() || height() > s.height()) resize(s); } theApp()->setSpreadWindowsState(isSpread(Desktop::workArea(mScreenNumber).topLeft())); } /****************************************************************************** * Called when the window has been displayed properly (in its correct position), * to play sounds and reschedule the event. */ void MessageWin::displayComplete() { delete mTempFile; mTempFile = nullptr; playAudio(); if (mRescheduleEvent) alarmShowing(mEvent); if (!mAlwaysHide) { // Enable the window's buttons either now or after the configured delay if (mButtonDelay > 0) QTimer::singleShot(mButtonDelay, this, &MessageWin::enableButtons); else enableButtons(); } } /****************************************************************************** * Enable the window's buttons. */ void MessageWin::enableButtons() { mOkButton->setEnabled(true); mKAlarmButton->setEnabled(true); if (mDeferButton->isVisible() && !mDisableDeferral) mDeferButton->setEnabled(true); if (mEditButton) mEditButton->setEnabled(true); if (mKMailButton) mKMailButton->setEnabled(true); } /****************************************************************************** * Called when the window's size has changed (before it is painted). */ void MessageWin::resizeEvent(QResizeEvent* re) { if (mRestoreHeight) { // Restore the window height on session restoration if (mRestoreHeight != re->size().height()) { QSize size = re->size(); size.setHeight(mRestoreHeight); resize(size); } else if (isVisible()) mRestoreHeight = 0; } else { if (mShown && mAction == KAEvent::FILE && !mErrorMsgs.count()) Config::writeWindowSize("FileMessage", re->size()); MainWindowBase::resizeEvent(re); } } /****************************************************************************** * Called when a close event is received. * Only quits the application if there is no system tray icon displayed. */ void MessageWin::closeEvent(QCloseEvent* ce) { // Don't prompt or delete the alarm from the display calendar if the session is closing if (!mErrorWindow && !qApp->isSavingSession()) { if (mConfirmAck && !mNoCloseConfirm) { // Ask for confirmation of acknowledgement. Use warningYesNo() because its default is No. if (KAMessageBox::warningYesNo(this, i18nc("@info", "Do you really want to acknowledge this alarm?"), i18nc("@action:button", "Acknowledge Alarm"), KGuiItem(i18nc("@action:button", "Acknowledge")), KStandardGuiItem::cancel()) != KMessageBox::Yes) { ce->ignore(); return; } } if (!mEventId.isEmpty()) { // Delete from the display calendar KAlarm::deleteDisplayEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING)); } } MainWindowBase::closeEvent(ce); } /****************************************************************************** * Called when the OK button is clicked. */ void MessageWin::slotOk() { if (mDontShowAgainCheck && mDontShowAgainCheck->isChecked()) KAlarm::setDontShowErrors(mEventId, mDontShowAgain); close(); } /****************************************************************************** * Called when the KMail button is clicked. * Tells KMail to display the email message displayed in this message window. */ void MessageWin::slotShowKMailMessage() { qCDebug(KALARM_LOG) << "MessageWin::slotShowKMailMessage"; if (mAkonadiItemId < 0) return; const QString err = KAlarm::runKMail(); if (!err.isNull()) { KAMessageBox::sorry(this, err); return; } org::kde::kmail::kmail kmail(KMAIL_DBUS_SERVICE, KMAIL_DBUS_PATH, QDBusConnection::sessionBus()); // Display the message contents QDBusReply reply = kmail.showMail(mAkonadiItemId); bool failed1 = true; bool failed2 = true; if (!reply.isValid()) qCCritical(KALARM_LOG) << "kmail 'showMail' D-Bus call failed:" << reply.error().message(); else if (reply.value()) failed1 = false; // Select the mail folder containing the message Akonadi::ItemFetchJob* job = new Akonadi::ItemFetchJob(Akonadi::Item(mAkonadiItemId)); job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent); Akonadi::Item::List items; if (job->exec()) items = job->items(); if (items.isEmpty() || !items.at(0).isValid()) qCWarning(KALARM_LOG) << "MessageWin::slotShowKMailMessage: No parent found for item" << mAkonadiItemId; else { const Akonadi::Item& it = items.at(0); const Akonadi::Collection::Id colId = it.parentCollection().id(); reply = kmail.selectFolder(QString::number(colId)); if (!reply.isValid()) qCCritical(KALARM_LOG) << "kmail 'selectFolder' D-Bus call failed:" << reply.error().message(); else if (reply.value()) failed2 = false; } if (failed1 || failed2) KAMessageBox::sorry(this, xi18nc("@info", "Unable to locate this email in KMail")); } /****************************************************************************** * Called when the Edit... button is clicked. * Displays the alarm edit dialog. * * NOTE: The alarm edit dialog is made a child of the main window, not this * window, so that if this window closes before the dialog (e.g. on * auto-close), KAlarm doesn't crash. The dialog is set non-modal so that * the main window is unaffected, but modal mode is simulated so that * this window is inactive while the dialog is open. */ void MessageWin::slotEdit() { qCDebug(KALARM_LOG) << "MessageWin::slotEdit"; MainWindow* mainWin = MainWindow::mainMainWindow(); mEditDlg = EditAlarmDlg::create(false, &mOriginalEvent, false, mainWin, EditAlarmDlg::RES_IGNORE); mEditDlg->setAttribute(Qt::WA_NativeWindow, true); KWindowSystem::setMainWindow(mEditDlg->windowHandle(), winId()); KWindowSystem::setOnAllDesktops(mEditDlg->winId(), false); setButtonsReadOnly(true); connect(mEditDlg, &QDialog::accepted, this, &MessageWin::editCloseOk); connect(mEditDlg, &QDialog::rejected, this, &MessageWin::editCloseCancel); connect(mEditDlg, &QObject::destroyed, this, &MessageWin::editCloseCancel); connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, this, &MessageWin::activeWindowChanged); mainWin->editAlarm(mEditDlg, mOriginalEvent); } /****************************************************************************** * Called when OK is clicked in the alarm edit dialog invoked by the Edit button. * Closes the window. */ void MessageWin::editCloseOk() { mEditDlg = nullptr; mNoCloseConfirm = true; // allow window to close without confirmation prompt close(); } /****************************************************************************** * Called when Cancel is clicked in the alarm edit dialog invoked by the Edit * button, or when the dialog is deleted. */ void MessageWin::editCloseCancel() { mEditDlg = nullptr; setButtonsReadOnly(false); } /****************************************************************************** * Called when the active window has changed. If this window has become the * active window and there is an alarm edit dialog, simulate a modal dialog by * making the alarm edit dialog the active window instead. */ void MessageWin::activeWindowChanged(WId win) { if (mEditDlg && win == winId()) KWindowSystem::activateWindow(mEditDlg->winId()); } /****************************************************************************** * Set or clear the read-only state of the dialog buttons. */ void MessageWin::setButtonsReadOnly(bool ro) { mOkButton->setReadOnly(ro, true); mDeferButton->setReadOnly(ro, true); mEditButton->setReadOnly(ro, true); if (mSilenceButton) mSilenceButton->setReadOnly(ro, true); if (mKMailButton) mKMailButton->setReadOnly(ro, true); mKAlarmButton->setReadOnly(ro, true); } /****************************************************************************** * Set up to disable the defer button when the deferral limit is reached. */ void MessageWin::setDeferralLimit(const KAEvent& event) { mDeferLimit = event.deferralLimit().effectiveKDateTime().toUtc().qDateTime(); MidnightTimer::connect(this, SLOT(checkDeferralLimit())); // check every day mDisableDeferral = false; checkDeferralLimit(); } /****************************************************************************** * Check whether the deferral limit has been reached. * If so, disable the Defer button. * N.B. Ideally, just a single QTimer::singleShot() call would be made to disable * the defer button at the corret time. But for a 32-bit integer, the * milliseconds parameter overflows in about 25 days, so instead a daily * check is done until the day when the deferral limit is reached, followed * by a non-overflowing QTimer::singleShot() call. */ void MessageWin::checkDeferralLimit() { if (!mDeferButton->isEnabled() || !mDeferLimit.isValid()) return; int n = KADateTime::currentLocalDate().daysTo(KADateTime(mDeferLimit, KADateTime::LocalZone).date()); if (n > 0) return; MidnightTimer::disconnect(this, SLOT(checkDeferralLimit())); if (n == 0) { // The deferral limit will be reached today n = QDateTime::currentDateTimeUtc().secsTo(mDeferLimit); if (n > 0) { QTimer::singleShot(n * 1000, this, &MessageWin::checkDeferralLimit); return; } } mDeferButton->setEnabled(false); mDisableDeferral = true; } /****************************************************************************** * Called when the Defer... button is clicked. * Displays the defer message dialog. */ void MessageWin::slotDefer() { mDeferDlg = new DeferAlarmDlg(KADateTime::currentDateTime(Preferences::timeSpec()).addSecs(60), mDateTime.isDateOnly(), false, this); mDeferDlg->setObjectName(QStringLiteral("DeferDlg")); // used by LikeBack mDeferDlg->setDeferMinutes(mDefaultDeferMinutes > 0 ? mDefaultDeferMinutes : Preferences::defaultDeferTime()); mDeferDlg->setLimit(mEvent); if (!Preferences::modalMessages()) lower(); if (mDeferDlg->exec() == QDialog::Accepted) { const DateTime dateTime = mDeferDlg->getDateTime(); const int delayMins = mDeferDlg->deferMinutes(); // Fetch the up-to-date alarm from the calendar. Note that it could have // changed since it was displayed. const KAEvent* event = mEventId.isEmpty() ? nullptr : AlarmCalendar::resources()->event(mEventId); if (event) { // The event still exists in the active calendar qCDebug(KALARM_LOG) << "MessageWin::slotDefer: Deferring event" << mEventId; KAEvent newev(*event); newev.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true); newev.setDeferDefaultMinutes(delayMins); KAlarm::updateEvent(newev, mDeferDlg, true); if (newev.deferred()) mNoPostAction = true; } else { // Try to retrieve the event from the displaying or archive calendars Resource resource; KAEvent event; bool showEdit, showDefer; if (!retrieveEvent(event, resource, showEdit, showDefer)) { // The event doesn't exist any more !?!, so recurrence data, // flags, and more, have been lost. KAMessageBox::error(this, xi18nc("@info", "Cannot defer alarm:Alarm not found.")); raise(); delete mDeferDlg; mDeferDlg = nullptr; mDeferButton->setEnabled(false); mEditButton->setEnabled(false); return; } qCDebug(KALARM_LOG) << "MessageWin::slotDefer: Deferring retrieved event" << mEventId; event.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true); event.setDeferDefaultMinutes(delayMins); event.setCommandError(mCommandError); // Add the event back into the calendar file, retaining its ID // and not updating KOrganizer. KAlarm::addEvent(event, &resource, mDeferDlg, KAlarm::USE_EVENT_ID); if (event.deferred()) mNoPostAction = true; // Finally delete it from the archived calendar now that it has // been reactivated. event.setCategory(CalEvent::ARCHIVED); KAlarm::deleteEvent(event, false); } if (theApp()->wantShowInSystemTray()) { // Alarms are to be displayed only if the system tray icon is running, // so start it if necessary so that the deferred alarm will be shown. theApp()->displayTrayIcon(true); } mNoCloseConfirm = true; // allow window to close without confirmation prompt close(); } else raise(); delete mDeferDlg; mDeferDlg = nullptr; } /****************************************************************************** * Called when the KAlarm icon button in the message window is clicked. * Displays the main window, with the appropriate alarm selected. */ void MessageWin::displayMainWindow() { KAlarm::displayMainWindowSelected(mEventId.eventId()); } /****************************************************************************** * Check whether the specified error message is already displayed for this * alarm, and note that it will now be displayed. * Reply = true if message is already displayed. */ bool MessageWin::haveErrorMessage(unsigned msg) const { if (!mErrorMessages.contains(mEventId)) mErrorMessages.insert(mEventId, 0); const bool result = (mErrorMessages[mEventId] & msg); mErrorMessages[mEventId] |= msg; return result; } void MessageWin::clearErrorMessage(unsigned msg) const { if (mErrorMessages.contains(mEventId)) { if (mErrorMessages[mEventId] == msg) mErrorMessages.remove(mEventId); else mErrorMessages[mEventId] &= ~msg; } } /****************************************************************************** * Check whether the message window should be modal, i.e. with title bar etc. * Normally this follows the Preferences setting, but if there is a full screen * window displayed, on X11 the message window has to bypass the window manager * in order to display on top of it (which has the side effect that it will have * no window decoration). * * Also find the usable area of the desktop (excluding panel etc.), on the * appropriate screen if there are multiple screens. */ bool MessageWin::getWorkAreaAndModal() { mScreenNumber = -1; const bool modal = Preferences::modalMessages(); #if KDEPIM_HAVE_X11 const QList screens = QGuiApplication::screens(); const int numScreens = screens.count(); if (numScreens > 1) { // There are multiple screens. // Check for any full screen windows, even if they are not the active // window, and try not to show the alarm message their screens. mScreenNumber = QApplication::desktop()->screenNumber(MainWindow::mainMainWindow()); // default = KAlarm's screen if (QGuiApplication::primaryScreen()->virtualSiblings().size() > 1) { // The screens form a single virtual desktop. // Xinerama, for example, uses this scheme. QVector screenTypes(numScreens); QVector screenRects(numScreens); for (int s = 0; s < numScreens; ++s) screenRects[s] = screens[s]->geometry(); const FullScreenType full = findFullScreenWindows(screenRects, screenTypes); if (full == NoFullScreen || screenTypes[mScreenNumber] == NoFullScreen) return modal; for (int s = 0; s < numScreens; ++s) { if (screenTypes[s] == NoFullScreen) { // There is no full screen window on this screen mScreenNumber = s; return modal; } } // All screens contain a full screen window: use one without // an active full screen window. for (int s = 0; s < numScreens; ++s) { if (screenTypes[s] == FullScreen) { mScreenNumber = s; return modal; } } } else { // The screens are completely separate from each other. int inactiveScreen = -1; FullScreenType full = haveFullScreenWindow(mScreenNumber); //qCDebug(KALARM_LOG)<<"full="<& screenRects, QVector& screenTypes) { FullScreenType result = NoFullScreen; screenTypes.fill(NoFullScreen); xcb_connection_t* connection = QX11Info::connection(); const NETRootInfo rootInfo(connection, NET::ClientList | NET::ActiveWindow, NET::Properties2()); const xcb_window_t rootWindow = rootInfo.rootWindow(); const xcb_window_t activeWindow = rootInfo.activeWindow(); const xcb_window_t* windows = rootInfo.clientList(); const int windowCount = rootInfo.clientListCount(); //qCDebug(KALARM_LOG)<<"Virtual desktops: Window count="< * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "traywindow.h" #include "alarmcalendar.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "messagewin.h" #include "newalarmaction.h" #include "prefdlg.h" #include "preferences.h" #include "templatemenuaction.h" #include "resources/datamodel.h" #include "resources/eventmodel.h" #include "lib/synchtimer.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include -#include #include #include #include #include #include #include #include #include using namespace KAlarmCal; struct TipItem { QDateTime dateTime; QString text; }; /*============================================================================= = Class: TrayWindow = The KDE system tray window. =============================================================================*/ TrayWindow::TrayWindow(MainWindow* parent) : KStatusNotifierItem(parent) , mAssocMainWindow(parent) , mStatusUpdateTimer(new QTimer(this)) { qCDebug(KALARM_LOG) << "TrayWindow:"; setToolTipIconByName(QStringLiteral("kalarm")); setToolTipTitle(KAboutData::applicationData().displayName()); setIconByName(QStringLiteral("kalarm")); setStatus(KStatusNotifierItem::Active); // Set up the context menu mActionEnabled = KAlarm::createAlarmEnableAction(this); addAction(QStringLiteral("tAlarmsEnable"), mActionEnabled); contextMenu()->addAction(mActionEnabled); connect(theApp(), &KAlarmApp::alarmEnabledToggled, this, &TrayWindow::setEnabledStatus); contextMenu()->addSeparator(); mActionNew = new NewAlarmAction(false, i18nc("@action", "&New Alarm"), this); addAction(QStringLiteral("tNew"), mActionNew); contextMenu()->addAction(mActionNew); connect(mActionNew, &NewAlarmAction::selected, this, &TrayWindow::slotNewAlarm); connect(mActionNew->fromTemplateAlarmAction(QString()), &TemplateMenuAction::selected, this, &TrayWindow::slotNewFromTemplate); contextMenu()->addSeparator(); QAction* a = KAlarm::createStopPlayAction(this); addAction(QStringLiteral("tStopPlay"), a); contextMenu()->addAction(a); QObject::connect(theApp(), &KAlarmApp::audioPlaying, a, &QAction::setVisible); QObject::connect(theApp(), &KAlarmApp::audioPlaying, this, &TrayWindow::updateStatus); a = KAlarm::createSpreadWindowsAction(this); addAction(QStringLiteral("tSpread"), a); contextMenu()->addAction(a); contextMenu()->addSeparator(); contextMenu()->addAction(KStandardAction::preferences(this, SLOT(slotPreferences()), this)); // Disable standard quit behaviour. We have to intercept the quit even, // if the main window is hidden. QAction* act = action(QStringLiteral("quit")); if (act) { act->disconnect(SIGNAL(triggered(bool)), this, SLOT(maybeQuit())); connect(act, &QAction::triggered, this, &TrayWindow::slotQuit); } // Set icon to correspond with the alarms enabled menu status setEnabledStatus(theApp()->alarmsEnabled()); connect(AlarmCalendar::resources(), &AlarmCalendar::haveDisabledAlarmsChanged, this, &TrayWindow::slotHaveDisabledAlarms); connect(this, &TrayWindow::activateRequested, this, &TrayWindow::slotActivateRequested); connect(this, &TrayWindow::secondaryActivateRequested, this, &TrayWindow::slotSecondaryActivateRequested); slotHaveDisabledAlarms(AlarmCalendar::resources()->haveDisabledAlarms()); // Hack: KSNI does not let us know when it is about to show the tooltip, // so we need to update it whenever something change in it. // This timer ensures that updateToolTip() is not called several times in a row mToolTipUpdateTimer = new QTimer(this); mToolTipUpdateTimer->setInterval(0); mToolTipUpdateTimer->setSingleShot(true); connect(mToolTipUpdateTimer, &QTimer::timeout, this, &TrayWindow::updateToolTip); // Update every minute to show accurate deadlines MinuteTimer::connect(mToolTipUpdateTimer, SLOT(start())); // Update when alarms are modified AlarmListModel* all = DataModel::allAlarmListModel(); connect(all, SIGNAL(dataChanged(QModelIndex,QModelIndex)), mToolTipUpdateTimer, SLOT(start())); connect(all, SIGNAL(rowsInserted(QModelIndex,int,int)), mToolTipUpdateTimer, SLOT(start())); connect(all, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), mToolTipUpdateTimer, SLOT(start())); connect(all, SIGNAL(rowsRemoved(QModelIndex,int,int)), mToolTipUpdateTimer, SLOT(start())); connect(all, SIGNAL(modelReset()), mToolTipUpdateTimer, SLOT(start())); // Set auto-hide status when next alarm or preferences change mStatusUpdateTimer->setSingleShot(true); connect(mStatusUpdateTimer, &QTimer::timeout, this, &TrayWindow::updateStatus); connect(AlarmCalendar::resources(), &AlarmCalendar::earliestAlarmChanged, this, &TrayWindow::updateStatus); Preferences::connect(SIGNAL(autoHideSystemTrayChanged(int)), this, SLOT(updateStatus())); updateStatus(); // Update when tooltip preferences are modified Preferences::connect(SIGNAL(tooltipPreferencesChanged()), mToolTipUpdateTimer, SLOT(start())); } TrayWindow::~TrayWindow() { qCDebug(KALARM_LOG) << "~TrayWindow"; theApp()->removeWindow(this); Q_EMIT deleted(); } /****************************************************************************** * Called when the "New Alarm" menu item is selected to edit a new alarm. */ void TrayWindow::slotNewAlarm(EditAlarmDlg::Type type) { KAlarm::editNewAlarm(type); } /****************************************************************************** * Called when the "New Alarm" menu item is selected to edit a new alarm from a * template. */ void TrayWindow::slotNewFromTemplate(const KAEvent* event) { KAlarm::editNewAlarm(event); } /****************************************************************************** * Called when the "Configure KAlarm" menu item is selected. */ void TrayWindow::slotPreferences() { KAlarmPrefDlg::display(); } /****************************************************************************** * Called when the Quit context menu item is selected. * Note that KAlarmApp::doQuit() must be called by the event loop, not directly * from the menu item, since otherwise the tray icon will be deleted while still * processing the menu, resulting in a crash. * Ideally, the connect() call setting up this slot in the constructor would use * Qt::QueuedConnection, but the slot is never called in that case. */ void TrayWindow::slotQuit() { // Note: QTimer::singleShot(0, ...) never calls the slot. QTimer::singleShot(1, this, &TrayWindow::slotQuitAfter); } void TrayWindow::slotQuitAfter() { theApp()->doQuit(static_cast(parent())); } /****************************************************************************** * Called when the Alarms Enabled action status has changed. * Updates the alarms enabled menu item check state, and the icon pixmap. */ void TrayWindow::setEnabledStatus(bool status) { qCDebug(KALARM_LOG) << "TrayWindow::setEnabledStatus:" << status; updateIcon(); updateStatus(); updateToolTip(); } /****************************************************************************** * Called when individual alarms are enabled or disabled. * Set the enabled icon to show or hide a disabled indication. */ void TrayWindow::slotHaveDisabledAlarms(bool haveDisabled) { qCDebug(KALARM_LOG) << "TrayWindow::slotHaveDisabledAlarms:" << haveDisabled; mHaveDisabledAlarms = haveDisabled; updateIcon(); updateToolTip(); } /****************************************************************************** * Show the associated main window. */ void TrayWindow::showAssocMainWindow() { if (mAssocMainWindow) { mAssocMainWindow->show(); mAssocMainWindow->raise(); mAssocMainWindow->activateWindow(); } } /****************************************************************************** * A left click displays the KAlarm main window. */ void TrayWindow::slotActivateRequested() { // Left click: display/hide the first main window if (mAssocMainWindow && mAssocMainWindow->isVisible()) { mAssocMainWindow->raise(); mAssocMainWindow->activateWindow(); } } /****************************************************************************** * A middle button click displays the New Alarm window. */ void TrayWindow::slotSecondaryActivateRequested() { if (mActionNew->isEnabled()) mActionNew->trigger(); // display a New Alarm dialog } /****************************************************************************** * Adjust icon auto-hide status according to when the next alarm is due. * The icon is always shown if audio is playing, to give access to the 'stop' * menu option. */ void TrayWindow::updateStatus() { mStatusUpdateTimer->stop(); int period = Preferences::autoHideSystemTray(); // If the icon is always to be shown (AutoHideSystemTray = 0), // or audio is playing, show the icon. bool active = !period || MessageWin::isAudioPlaying(); if (!active) { // Show the icon only if the next active alarm complies active = theApp()->alarmsEnabled(); if (active) { KAEvent* event = AlarmCalendar::resources()->earliestAlarm(); active = static_cast(event); if (event && period > 0) { const KADateTime dt = event->nextTrigger(KAEvent::ALL_TRIGGER).effectiveKDateTime(); qint64 delay = KADateTime::currentLocalDateTime().secsTo(dt); delay -= static_cast(period) * 60; // delay until icon to be shown active = (delay <= 0); if (!active) { // First alarm trigger is too far in future, so tray icon is to // be auto-hidden. Set timer for when it should be shown again. delay *= 1000; // convert to msec int delay_int = static_cast(delay); if (delay_int != delay) delay_int = INT_MAX; mStatusUpdateTimer->setInterval(delay_int); mStatusUpdateTimer->start(); } } } } setStatus(active ? Active : Passive); } /****************************************************************************** * Adjust tooltip according to the app state. * The tooltip text shows alarms due in the next 24 hours. The limit of 24 * hours is because only times, not dates, are displayed. */ void TrayWindow::updateToolTip() { bool enabled = theApp()->alarmsEnabled(); QString subTitle; if (enabled && Preferences::tooltipAlarmCount()) subTitle = tooltipAlarmText(); if (!enabled) subTitle = i18n("Disabled"); else if (mHaveDisabledAlarms) { if (!subTitle.isEmpty()) subTitle += QLatin1String("
"); subTitle += i18nc("@info:tooltip Brief: some alarms are disabled", "(Some alarms disabled)"); } setToolTipSubTitle(subTitle); } /****************************************************************************** * Adjust icon according to the app state. */ void TrayWindow::updateIcon() { setIconByName(!theApp()->alarmsEnabled() ? QStringLiteral("kalarm-disabled") : mHaveDisabledAlarms ? QStringLiteral("kalarm-partdisabled") : QStringLiteral("kalarm")); } /****************************************************************************** * Return the tooltip text showing alarms due in the next 24 hours. * The limit of 24 hours is because only times, not dates, are displayed. */ QString TrayWindow::tooltipAlarmText() const { KAEvent event; const QString& prefix = Preferences::tooltipTimeToPrefix(); int maxCount = Preferences::tooltipAlarmCount(); const KADateTime now = KADateTime::currentLocalDateTime(); const KADateTime tomorrow = now.addDays(1); // Get today's and tomorrow's alarms, sorted in time order int i, iend; QList items; QVector events = KAlarm::getSortedActiveEvents(const_cast(this), &mAlarmsModel); for (i = 0, iend = events.count(); i < iend; ++i) { KAEvent* event = &events[i]; if (event->actionSubType() == KAEvent::MESSAGE) { TipItem item; QDateTime dateTime = event->nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone().qDateTime(); if (dateTime > tomorrow.qDateTime()) break; // ignore alarms after tomorrow at the current clock time item.dateTime = dateTime; // The alarm is due today, or early tomorrow if (Preferences::showTooltipAlarmTime()) { item.text += QLocale().toString(item.dateTime.time(), QLocale::ShortFormat); item.text += QLatin1Char(' '); } if (Preferences::showTooltipTimeToAlarm()) { int mins = (now.qDateTime().secsTo(item.dateTime) + 59) / 60; if (mins < 0) mins = 0; char minutes[3] = "00"; minutes[0] = static_cast((mins%60) / 10 + '0'); minutes[1] = static_cast((mins%60) % 10 + '0'); if (Preferences::showTooltipAlarmTime()) item.text += i18nc("@info prefix + hours:minutes", "(%1%2:%3)", prefix, mins/60, QLatin1String(minutes)); else item.text += i18nc("@info prefix + hours:minutes", "%1%2:%3", prefix, mins/60, QLatin1String(minutes)); item.text += QLatin1Char(' '); } item.text += AlarmText::summary(*event); // Insert the item into the list in time-sorted order int it = 0; for (int itend = items.count(); it < itend; ++it) { if (item.dateTime <= items[it].dateTime) break; } items.insert(it, item); } } qCDebug(KALARM_LOG) << "TrayWindow::tooltipAlarmText"; QString text; int count = 0; for (i = 0, iend = items.count(); i < iend; ++i) { qCDebug(KALARM_LOG) << "TrayWindow::tooltipAlarmText: --" << (count+1) << ")" << items[i].text; if (i > 0) text += QLatin1String("
"); text += items[i].text; if (++count == maxCount) break; } return text; } /****************************************************************************** * Called when the associated main window is closed. */ void TrayWindow::removeWindow(MainWindow* win) { if (win == mAssocMainWindow) mAssocMainWindow = nullptr; } // vim: et sw=4: