/*
 *  messagewin.cpp  -  displays an alarm message
 *  Program:  kalarm
 *  Copyright © 2001-2020 David Jarvie <djarvie@kde.org>
 *
 *  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 "deferdlg.h"
#include "displaycalendar.h"
#include "editdlg.h"
#include "functions.h"
#include "kalarmapp.h"
#include "mainwindow.h"
#include "preferences.h"
#include "resourcescalendar.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 <KPIMTextEdit/TextToSpeech>
#include <AkonadiCore/ItemFetchJob>
#include <AkonadiCore/ItemFetchScope>

#include <kwindowsystem_version.h>
#include <KAboutData>
#include <KStandardGuiItem>
#include <KLocalizedString>
#include <KConfig>
#include <KTextEdit>
#include <KWindowSystem>
#include <KIO/StatJob>
#include <KIO/StoredTransferJob>
#include <KJobWidgets>
#include <KNotification>
#include <KSqueezedTextLabel>
#include <phonon/mediaobject.h>
#include <phonon/audiooutput.h>
#include <phonon/volumefadereffect.h>
#if KDEPIM_HAVE_X11
#include <KWindowInfo>
#include <netwm.h>
#include <qx11info_x11.h>
#include <QScreen>
#endif

#include <QTextBrowser>
#include <QPushButton>
#include <QScrollBar>
#include <QLocale>
#include <QCheckBox>
#include <QLabel>
#include <QPalette>
#include <QTimer>
#include <QByteArray>
#include <QFrame>
#include <QGridLayout>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QResizeEvent>
#include <QCloseEvent>
#include <QDesktopWidget>
#include <QMutexLocker>
#include <QMimeDatabase>
#include <QUrl>
#include <QStyle>

#include <stdlib.h>
#include <string.h>

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<QRect>& screenRects, QVector<FullScreenType>& 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<int>(docsize.width() + 0.99) + verticalScrollBar()->width(),
                         static_cast<int>(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*>       MessageWin::mWindowList;
QHash<EventId, unsigned> 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<AudioThread> 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<Qt::WindowFlags>(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<Qt::WidgetAttribute>(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 = ResourcesCalendar::instance()->eventReadOnly(mEventId.eventId());
        mShowEdit = !mEventId.isEmpty()  &&  !readonly;
        mNoDefer  = readonly || (flags & NO_DEFER) || alarm.repeatAtLogin();
        initView();
    }
    // Set to save settings automatically, but don't save window size.
    // File alarm window size is saved elsewhere.
    setAutoSaveSettings(QStringLiteral("MessageWin"), false);
    mWindowList.append(this);
    if (event->autoClose())
        mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().qDateTime().addSecs(event->lateCancel() * 60);
    if (mAlwaysHide)
    {
        hide();
        displayComplete();    // play audio, etc.
    }
}

/******************************************************************************
* Display an error message window.
* If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note
* that the option is specific to 'event'.
*/
void MessageWin::showError(const KAEvent& event, const DateTime& alarmDateTime,
                           const QStringList& errmsgs, const QString& dontShowAgain)
{
    if (!dontShowAgain.isEmpty()
    &&  KAlarm::dontShowErrors(EventId(event), dontShowAgain))
        return;

    // Don't pile up duplicate error messages for the same alarm
    for (int i = 0, end = mWindowList.count();  i < end;  ++i)
    {
        const MessageWin* w = mWindowList[i];
        if (w->mErrorWindow  &&  w->mEventId == EventId(event)
        &&  w->mErrorMsgs == errmsgs  &&  w->mDontShowAgain == dontShowAgain)
            return;
    }

    (new MessageWin(&event, alarmDateTime, errmsgs, dontShowAgain))->show();
}

/******************************************************************************
* Construct the message window for a specified error message.
* If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note
* that the option is specific to 'event'.
*/
MessageWin::MessageWin(const KAEvent* event, const DateTime& alarmDateTime,
                       const QStringList& errmsgs, const QString& dontShowAgain)
    : MainWindowBase(nullptr, WFLAGS | WFLAGS2)
    , mMessage(event->cleanText())
    , mDateTime(alarmDateTime)
    , mEventId(*event)
    , mAlarmType(KAAlarm::MAIN_ALARM)
    , mAction(event->actionSubType())
    , mAkonadiItemId(-1)
    , mCommandError(KAEvent::CMD_NO_ERROR)
    , mErrorMsgs(errmsgs)
    , mDontShowAgain(dontShowAgain)
    , mRestoreHeight(0)
    , mConfirmAck(false)
    , mShowEdit(false)
    , mNoDefer(true)
    , mInvalid(false)
    , mEvent(*event)
    , mOriginalEvent(*event)
    , mErrorWindow(true)
    , mNoPostAction(true)
{
    qCDebug(KALARM_LOG) << "MessageWin: errmsg";
    setAttribute(static_cast<Qt::WidgetAttribute>(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);
    const int dcmLeft   = style()->pixelMetric(QStyle::PM_LayoutLeftMargin);
    const int dcmTop    = style()->pixelMetric(QStyle::PM_LayoutTopMargin);
    const int dcmRight  = style()->pixelMetric(QStyle::PM_LayoutRightMargin);
    const int dcmBottom = style()->pixelMetric(QStyle::PM_LayoutBottomMargin);

    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("<br/>"));
            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::statDetails(url, KIO::StatJob::SourceSide, KIO::StatBasic, 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("<div align=\"center\"><img src=\"") + mTempFile->fileName() + QLatin1String("\"></div>"));
                                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 - (dcmLeft + dcmRight)/2;
                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_LayoutVerticalSpacing));
            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);

                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.isEmpty())
    {
        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();
        layout->setContentsMargins(2 * dcmLeft, 2 * dcmTop, 2 * dcmRight, 2 * dcmBottom);
        layout->addStretch();
        topLayout->addLayout(layout);
        QLabel* label = new QLabel(topWidget);
        label->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-error")).pixmap(style()->pixelMetric(QStyle::PM_MessageBoxIconSize)));
        label->setFixedSize(label->sizeHint());
        layout->addWidget(label, 0, Qt::AlignRight);
        QVBoxLayout* vlayout = new QVBoxLayout();
        layout->addLayout(vlayout);
        for (QStringList::ConstIterator it = mErrorMsgs.constBegin();  it != mErrorMsgs.constEnd();  ++it)
        {
            label = new QLabel(*it, topWidget);
            label->setFixedSize(label->sizeHint());
            vlayout->addWidget(label, 0, Qt::AlignLeft);
        }
        layout->addStretch();
        if (!mDontShowAgain.isEmpty())
        {
            mDontShowAgainCheck = new QCheckBox(i18nc("@option:check", "Do not display this error message again for this alarm"), topWidget);
            mDontShowAgainCheck->setFixedSize(mDontShowAgainCheck->sizeHint());
            topLayout->addWidget(mDontShowAgainCheck, 0, Qt::AlignLeft);
        }
    }

    QGridLayout* grid = new QGridLayout();
    grid->setColumnStretch(0, 1);     // keep the buttons right-adjusted in the window
    topLayout->addLayout(grid);
    int gridIndex = 1;

    // Close button
    mOkButton = new PushButton(KStandardGuiItem::close(), topWidget);
    // Prevent accidental acknowledgement of the message if the user is typing when the window appears
    mOkButton->clearFocus();
    mOkButton->setFocusPolicy(Qt::ClickFocus);    // don't allow keyboard selection
    mOkButton->setFixedSize(mOkButton->sizeHint());
    connect(mOkButton, &QAbstractButton::clicked, this, &MessageWin::slotOk);
    grid->addWidget(mOkButton, 0, gridIndex++, Qt::AlignHCenter);
    mOkButton->setWhatsThis(i18nc("@info:whatsthis", "Acknowledge the alarm"));

    if (mShowEdit)
    {
        // Edit button
        mEditButton = new PushButton(i18nc("@action:button", "&Edit..."), topWidget);
        mEditButton->setFocusPolicy(Qt::ClickFocus);    // don't allow keyboard selection
        mEditButton->setFixedSize(mEditButton->sizeHint());
        connect(mEditButton, &QAbstractButton::clicked, this, &MessageWin::slotEdit);
        grid->addWidget(mEditButton, 0, gridIndex++, Qt::AlignHCenter);
        mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the alarm."));
    }

    // Defer button
    mDeferButton = new PushButton(i18nc("@action:button", "&Defer..."), topWidget);
    mDeferButton->setFocusPolicy(Qt::ClickFocus);    // don't allow keyboard selection
    mDeferButton->setFixedSize(mDeferButton->sizeHint());
    connect(mDeferButton, &QAbstractButton::clicked, this, &MessageWin::slotDefer);
    grid->addWidget(mDeferButton, 0, gridIndex++, Qt::AlignHCenter);
    mDeferButton->setWhatsThis(xi18nc("@info:whatsthis", "<para>Defer the alarm until later.</para>"
                                    "<para>You will be prompted to specify when the alarm should be redisplayed.</para>"));

    if (mNoDefer)
        mDeferButton->hide();
    else
        setDeferralLimit(mEvent);    // ensure that button is disabled when alarm can't be deferred any more

    if (!mAudioFile.isEmpty()  &&  (mVolume || mFadeVolume > 0))
    {
        // Silence button to stop sound repetition
        mSilenceButton = new PushButton(topWidget);
        mSilenceButton->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-stop")));
        grid->addWidget(mSilenceButton, 0, gridIndex++, Qt::AlignHCenter);
        mSilenceButton->setToolTip(i18nc("@info:tooltip", "Stop sound"));
        mSilenceButton->setWhatsThis(i18nc("@info:whatsthis", "Stop playing the sound"));
        // To avoid getting in a mess, disable the button until sound playing has been set up
        mSilenceButton->setEnabled(false);
    }

    if (mAkonadiItemId >= 0)
    {
        // KMail button
        mKMailButton = new PushButton(topWidget);
        mKMailButton->setIcon(QIcon::fromTheme(QStringLiteral("internet-mail")));
        connect(mKMailButton, &QAbstractButton::clicked, this, &MessageWin::slotShowKMailMessage);
        grid->addWidget(mKMailButton, 0, gridIndex++, Qt::AlignHCenter);
        mKMailButton->setToolTip(xi18nc("@info:tooltip Locate this email in KMail", "Locate in <application>KMail</application>"));
        mKMailButton->setWhatsThis(xi18nc("@info:whatsthis", "Locate and highlight this email in <application>KMail</application>"));
    }

    // KAlarm button
    mKAlarmButton = new PushButton(topWidget);
    mKAlarmButton->setIcon(QIcon::fromTheme(KAboutData::applicationData().componentName()));
    connect(mKAlarmButton, &QAbstractButton::clicked, this, &MessageWin::displayMainWindow);
    grid->addWidget(mKAlarmButton, 0, gridIndex++, Qt::AlignHCenter);
    mKAlarmButton->setToolTip(xi18nc("@info:tooltip", "Activate <application>KAlarm</application>"));
    mKAlarmButton->setWhatsThis(xi18nc("@info:whatsthis", "Activate <application>KAlarm</application>"));

    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() + dcmLeft + dcmRight,
                         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<int>(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<int>(mAction));
        config.writeEntry("Font", mFont);
        config.writeEntry("BgColour", mBgColour);
        config.writeEntry("FgColour", mFgColour);
        config.writeEntry("ConfirmAck", mConfirmAck);
        if (mDateTime.isValid())
        {
            config.writeEntry("Time", mDateTime.effectiveDateTime());
            config.writeEntry("DateOnly", mDateTime.isDateOnly());
            QByteArray zone;
            if (mDateTime.isUtc())
                zone = "UTC";
            else if (mDateTime.isOffsetFromUtc())
            {
                int offset = mDateTime.utcOffset();
                if (offset >= 0)
                    zone = '+' + QByteArray::number(offset);
                else
                    zone = QByteArray::number(offset);
            }
            else if (mDateTime.timeType() == KADateTime::TimeZone)
            {
                const QTimeZone tz = mDateTime.timeZone();
                if (tz.isValid())
                    zone = tz.id();
            }
            config.writeEntry("TimeZone", zone);
        }
        if (mCloseTime.isValid())
            config.writeEntry("Expiry", mCloseTime);
        if (mAudioRepeatPause >= 0  &&  mSilenceButton  &&  mSilenceButton->isEnabled())
        {
            // Only need to restart sound file playing if it's being repeated
            config.writePathEntry("AudioFile", mAudioFile);
            config.writeEntry("Volume", static_cast<int>(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<int>(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 resourceId = config.readEntry("CollectionID", ResourceId(-1));
    mAlarmType           = static_cast<KAAlarm::Type>(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<KAEvent::SubAction>(config.readEntry("Type", 0));
    mFont                = config.readEntry("Font", QFont());
    mBgColour            = config.readEntry("BgColour", QColor(Qt::white));
    mFgColour            = config.readEntry("FgColour", QColor(Qt::black));
    mConfirmAck          = config.readEntry("ConfirmAck", false);
    QDateTime invalidDateTime;
    QDateTime dt         = config.readEntry("Time", invalidDateTime);
    const QByteArray zoneId = config.readEntry("TimeZone").toLatin1();
    KADateTime::Spec timeSpec;
    if (zoneId.isEmpty())
        timeSpec = KADateTime::LocalZone;
    else if (zoneId == "UTC")
        timeSpec = KADateTime::UTC;
    else if (zoneId.startsWith('+')  ||  zoneId.startsWith('-'))
        timeSpec.setType(KADateTime::OffsetFromUTC, zoneId.toInt());
    else
        timeSpec = QTimeZone(zoneId);
    mDateTime = KADateTime(dt.date(), dt.time(), timeSpec);
    const bool dateOnly  = config.readEntry("DateOnly", false);
    if (dateOnly)
        mDateTime.setDateOnly(true);
    mCloseTime           = config.readEntry("Expiry", invalidDateTime);
    mCloseTime.setTimeSpec(Qt::UTC);
    mAudioFile           = config.readPathEntry("AudioFile", QString());
    mVolume              = static_cast<float>(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<int>(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(resourceId);
    mEventId             = EventId(resourceId, eventId);
    qCDebug(KALARM_LOG) << "MessageWin::readProperties:" << eventId;
    if (mAlarmType != KAAlarm::INVALID_ALARM)
    {
        // Recreate the event from the calendar file (if possible)
        if (eventId.isEmpty())
            initView();
        else
        {
            // Close any other window for this alarm which has already been restored by redisplayAlarms()
            if (!Resources::allCreated())
            {
                connect(Resources::instance(), &Resources::resourcesCreated,
                                         this, &MessageWin::showRestoredAlarm);
                return;
            }
            redisplayAlarm();
        }
    }
}

/******************************************************************************
* Fetch the restored alarm from the calendar and redisplay it in this window.
*/
void MessageWin::showRestoredAlarm()
{
    qCDebug(KALARM_LOG) << "MessageWin::showRestoredAlarm:" << mEventId;
    redisplayAlarm();
    show();
}

/******************************************************************************
* Fetch the restored alarm from the calendar and redisplay it in this window.
*/
void MessageWin::redisplayAlarm()
{
    mResource = Resources::resourceForEvent(mEventId.eventId());
    mEventId.setResourceId(mResource.id());
    qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarm:" << mEventId;
    // Delete any already existing window for the same event
    MessageWin* duplicate = findEvent(mEventId, this);
    if (duplicate)
        qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarm: Deleting duplicate window:" << mEventId;
    delete duplicate;

    KAEvent* event = ResourcesCalendar::instance()->event(mEventId);
    if (event)
    {
        mEvent = *event;
        mShowEdit = true;
    }
    else
    {
        // It's not in the active calendar, so try the displaying or archive calendars
        retrieveEvent(mEvent, mResource, mShowEdit, mNoDefer);
        mNoDefer = !mNoDefer;
    }
    initView();
}

/******************************************************************************
* Redisplay alarms which were being shown when the program last exited.
* Normally, these alarms will have been displayed by session restoration, but
* if the program crashed or was killed, we can redisplay them here so that
* they won't be lost.
*/
void MessageWin::redisplayAlarms()
{
    if (mRedisplayed)
        return;
    qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms";
    mRedisplayed = true;
    DisplayCalendar* cal = DisplayCalendar::instance();
    if (cal  &&  cal->isOpen())
    {
        KAEvent event;
        Resource resource;
        const Event::List events = cal->kcalEvents();
        for (int i = 0, end = events.count();  i < end;  ++i)
        {
            bool showDefer, showEdit;
            reinstateFromDisplaying(events[i], event, resource, showEdit, showDefer);
            const EventId eventId(event);
            if (findEvent(eventId))
                qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms: Message window already exists:" << eventId;
            else
            {
                // This event should be displayed, but currently isn't being
                const KAAlarm alarm = event.convertDisplayingAlarm();
                if (alarm.type() == KAAlarm::INVALID_ALARM)
                {
                    qCCritical(KALARM_LOG) << "MessageWin::redisplayAlarms: Invalid alarm: id=" << eventId;
                    continue;
                }
                qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms:" << eventId;
                const bool login = alarm.repeatAtLogin();
                const int flags = NO_RESCHEDULE | (login ? NO_DEFER : 0) | NO_INIT_VIEW;
                MessageWin* win = new MessageWin(&event, alarm, flags);
                win->mResource = resource;
                const bool rw = resource.isWritable(event.category());
                win->mShowEdit = rw ? showEdit : false;
                win->mNoDefer  = (rw && !login) ? !showDefer : true;
                win->initView();
                win->show();
            }
        }
    }
}

/******************************************************************************
* Retrieves the event with the current ID from the displaying calendar file,
* or if not found there, from the archive calendar. 'resource' is set to the
* resource which originally contained the event, or invalid if not known.
*/
bool MessageWin::retrieveEvent(KAEvent& event, Resource& resource, bool& showEdit, bool& showDefer)
{
    const Event::Ptr kcalEvent = DisplayCalendar::instance()->kcalEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING));
    if (!reinstateFromDisplaying(kcalEvent, event, resource, showEdit, showDefer))
    {
        // The event isn't in the displaying calendar.
        // Try to retrieve it from the archive calendar.
        KAEvent* ev = nullptr;
        Resource archiveRes = Resources::getStandard(CalEvent::ARCHIVED);
        if (archiveRes.isValid())
            ev = ResourcesCalendar::instance()->event(EventId(archiveRes.id(), CalEvent::uid(mEventId.eventId(), CalEvent::ARCHIVED)));
        if (!ev)
            return false;
        event = *ev;
        event.setArchive();     // ensure that it gets re-archived if it's saved
        event.setCategory(CalEvent::ACTIVE);
        if (mEventId.eventId() != event.id())
            qCCritical(KALARM_LOG) << "MessageWin::retrieveEvent: Wrong event ID";
        event.setEventId(mEventId.eventId());
        resource  = Resource();
        showEdit  = true;
        showDefer = true;
        qCDebug(KALARM_LOG) << "MessageWin::retrieveEvent:" << event.id() << ": success";
    }
    return true;
}

/******************************************************************************
* Retrieves the displayed event from the calendar file, or if not found there,
* from the displaying calendar. 'resource' is set to the resource which
* originally contained the event.
*/
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.setResourceId(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);
        DisplayCalendar* cal = DisplayCalendar::instanceOpen();
        if (cal)
        {
            cal->deleteEvent(dispEvent.id());   // in case it already exists
            cal->addEvent(dispEvent);
            cal->save();
        }
    }
    theApp()->rescheduleAlarm(event, alarm);
}

/******************************************************************************
* Spread alarm windows over the screen so that they are all visible, or pile
* them on top of each other again.
* Reply = true if windows are now scattered, false if piled up.
*/
bool MessageWin::spread(bool scatter)
{
    if (instanceCount(true) <= 1)    // ignore always-hidden windows
        return false;

    const QRect desk = Desktop::workArea();   // get the usable area of the desktop
    if (scatter == isSpread(desk.topLeft()))
        return scatter;

    if (scatter)
    {
        // Usually there won't be many windows, so a crude
        // scattering algorithm should suffice.
        int x = desk.left();
        int y = desk.top();
        int ynext = y;
        for (int errmsgs = 0;  errmsgs < 2;  ++errmsgs)
        {
            // Display alarm messages first, then error messages, since most
            // error messages tend to be the same height.
            for (int i = 0, end = mWindowList.count();  i < end;  ++i)
            {
                MessageWin* w = mWindowList[i];
                if ((!errmsgs && w->mErrorWindow)
                ||  (errmsgs && !w->mErrorWindow))
                    continue;
                const QSize sz = w->frameGeometry().size();
                if (x + sz.width() > desk.right())
                {
                    x = desk.left();
                    y = ynext;
                }
                int ytmp = y;
                if (y + sz.height() > desk.bottom())
                {
                    ytmp = desk.bottom() - sz.height();
                    if (ytmp < desk.top())
                        ytmp = desk.top();
                }
                w->move(x, ytmp);
                x += sz.width();
                if (ytmp + sz.height() > ynext)
                    ynext = ytmp + sz.height();
            }
        }
    }
    else
    {
        // Move all windows to the top left corner
        for (int i = 0, end = mWindowList.count();  i < end;  ++i)
            mWindowList[i]->move(desk.topLeft());
    }
    return scatter;
}

/******************************************************************************
* Check whether message windows are all piled up, or are spread out.
* Reply = true if windows are currently spread, false if piled up.
*/
bool MessageWin::isSpread(const QPoint& topLeft)
{
    for (int i = 0, end = mWindowList.count();  i < end;  ++i)
    {
        if (mWindowList[i]->pos() != topLeft)
            return true;
    }
    return false;
}

/******************************************************************************
* Returns the existing message window (if any) which is displaying the event
* with the specified ID.
*/
MessageWin* MessageWin::findEvent(const EventId& eventId, MessageWin* exclude)
{
    if (!eventId.isEmpty())
    {
        for (int i = 0, end = mWindowList.count();  i < end;  ++i)
        {
            MessageWin* w = mWindowList[i];
            if (w != exclude  &&  w->mEventId == eventId  &&  !w->mErrorWindow)
                return w;
        }
    }
    return nullptr;
}

/******************************************************************************
* Beep and play the audio file, as appropriate.
*/
void MessageWin::playAudio()
{
    if (mBeep)
    {
        // Beep using two methods, in case the sound card/speakers are switched off or not working
        QApplication::beep();      // beep through the internal speaker
        KNotification::beep();     // beep through the sound card & speakers
    }
    if (!mAudioFile.isEmpty())
    {
        if (!mVolume  &&  mFadeVolume <= 0)
            return;    // ensure zero volume doesn't play anything
        startAudio();    // play the audio file
    }
    else if (mSpeak)
    {
        // The message is to be spoken. In case of error messges,
        // call it on a timer to allow the window to display first.
        QTimer::singleShot(0, this, &MessageWin::slotSpeak);
    }
}

/******************************************************************************
* Speak the message.
* Called asynchronously to avoid delaying the display of the message.
*/
void MessageWin::slotSpeak()
{
    KPIMTextEdit::TextToSpeech *tts = KPIMTextEdit::TextToSpeech::self();
    if (!tts->isReady())
    {
        KAMessageBox::detailedError(MainWindow::mainMainWindow(), i18nc("@info", "Unable to speak message"), i18nc("@info", "Text-to-speech subsystem is not available"));
        clearErrorMessage(ErrMsg_Speak);
        return;
    }

    tts->say(mMessage);
}

/******************************************************************************
* Called when another window's audio thread has been destructed.
* Start playing this window's audio file. Because initialising the sound system
* and loading the file may take some time, it is called in a separate thread to
* allow the window to display first.
*/
void MessageWin::startAudio()
{
    if (mAudioThread)
    {
        // An audio file is already playing for another message
        // window, so wait until it has finished.
        connect(mAudioThread.data(), &QObject::destroyed, this, &MessageWin::audioTerminating);
    }
    else
    {
        qCDebug(KALARM_LOG) << "MessageWin::startAudio:" << QThread::currentThread();
        mAudioThread = new AudioThread(this, mAudioFile, mVolume, mFadeVolume, mFadeSeconds, mAudioRepeatPause);
        connect(mAudioThread.data(), &AudioThread::readyToPlay, this, &MessageWin::playReady);
        connect(mAudioThread.data(), &QThread::finished, this, &MessageWin::playFinished);
        if (mSilenceButton)
            connect(mSilenceButton, &QAbstractButton::clicked, mAudioThread.data(), &QThread::quit);
        // Notify after creating mAudioThread, so that isAudioPlaying() will
        // return the correct value.
        theApp()->notifyAudioPlaying(true);
        mAudioThread->start();
    }
}

/******************************************************************************
* Return whether audio playback is currently active.
*/
bool MessageWin::isAudioPlaying()
{
    return mAudioThread;
}

/******************************************************************************
* Stop audio playback.
*/
void MessageWin::stopAudio(bool wait)
{
    qCDebug(KALARM_LOG) << "MessageWin::stopAudio";
    if (mAudioThread)
        mAudioThread->stop(wait);
}

/******************************************************************************
* Called when another window's audio thread is being destructed.
* Wait until the destructor has finished.
*/
void MessageWin::audioTerminating()
{
    QTimer::singleShot(0, this, &MessageWin::startAudio);
}

/******************************************************************************
* Called when the audio file is ready to start playing.
*/
void MessageWin::playReady()
{
    if (mSilenceButton)
        mSilenceButton->setEnabled(true);
}

/******************************************************************************
* Called when the audio file thread finishes.
*/
void MessageWin::playFinished()
{
    if (mSilenceButton)
        mSilenceButton->setEnabled(false);
    if (mAudioThread)   // mAudioThread can actually be null here!
    {
        const QString errmsg = mAudioThread->error();
        if (!errmsg.isEmpty()  &&  !haveErrorMessage(ErrMsg_AudioFile))
        {
            KAMessageBox::error(this, errmsg);
            clearErrorMessage(ErrMsg_AudioFile);
        }
    }
    delete mAudioThread.data();
    if (mAlwaysHide)
        close();
}

/******************************************************************************
* Constructor for audio thread.
*/
AudioThread::AudioThread(MessageWin* parent, const QString& audioFile, float volume, float fadeVolume, int fadeSeconds, int repeatPause)
    : QThread(parent),
      mFile(audioFile),
      mVolume(volume),
      mFadeVolume(fadeVolume),
      mFadeSeconds(fadeSeconds),
      mRepeatPause(repeatPause),
      mAudioObject(nullptr)
{
    if (mAudioOwner)
        qCCritical(KALARM_LOG) << "MessageWin::AudioThread: mAudioOwner already set";
    mAudioOwner = parent;
}

/******************************************************************************
* Destructor for audio thread. Waits for thread completion and tidies up.
* Note that this destructor is executed in the parent thread.
*/
AudioThread::~AudioThread()
{
    qCDebug(KALARM_LOG) << "~MessageWin::AudioThread";
    stop(true);   // stop playing and tidy up (timeout 3 seconds)
    delete mAudioObject;
    mAudioObject = nullptr;
    if (mAudioOwner == parent())
        mAudioOwner = nullptr;
    // Notify after deleting mAudioThread, so that isAudioPlaying() will
    // return the correct value.
    QTimer::singleShot(0, theApp(), &KAlarmApp::notifyAudioStopped);
}

/******************************************************************************
* Quits the thread and waits for thread completion and tidies up.
*/
void AudioThread::stop(bool waiT)
{
    qCDebug(KALARM_LOG) << "MessageWin::AudioThread::stop";
    quit();       // stop playing and tidy up
    wait(3000);   // wait for run() to exit (timeout 3 seconds)
    if (!isFinished())
    {
        // Something has gone wrong - forcibly kill the thread
        terminate();
        if (waiT)
            wait();
    }
}

/******************************************************************************
* Kick off the thread to play the audio file.
*/
void AudioThread::run()
{
    mMutex.lock();
    if (mAudioObject)
    {
        mMutex.unlock();
        return;
    }
    qCDebug(KALARM_LOG) << "MessageWin::AudioThread::run:" << QThread::currentThread() << mFile;
    const QString audioFile = mFile;
    const QUrl url = QUrl::fromUserInput(mFile);
    mFile = url.isLocalFile() ? url.toLocalFile() : url.toString();
    Phonon::MediaSource source(url);
    if (source.type() == Phonon::MediaSource::Invalid)
    {
        mError = xi18nc("@info", "Cannot open audio file: <filename>%1</filename>", 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", "<para>Error playing audio file: <filename>%1</filename></para><para>%2</para>", 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<Phonon::Effect*> 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 : ResourcesCalendar::instance()->event(mEventId);
    if (event)
    {
        mAlarmType = alarm.type();    // store new alarm type for use if it is later deferred
        if (mAlwaysHide)
            playAudio();
        else
        {
            if (!mDeferDlg  ||  Preferences::modalMessages())
            {
                raise();
                playAudio();
            }
            if (mDeferButton->isVisible())
            {
                mDeferButton->setEnabled(true);
                setDeferralLimit(*event);    // ensure that button is disabled when alarm can't be deferred any more
            }
        }
        alarmShowing(*event);
    }
}

/******************************************************************************
* Display the window.
* If windows are being positioned away from the mouse cursor, it is initially
* positioned at the top left to slightly reduce the number of times the
* windows need to be moved in showEvent().
*/
void MessageWin::show()
{
    if (mCloseTime.isValid())
    {
        // Set a timer to auto-close the window
        int delay = QDateTime::currentDateTimeUtc().secsTo(mCloseTime);
        if (delay < 0)
            delay = 0;
        QTimer::singleShot(delay * 1000, this, &QWidget::close);
        if (!delay)
            return;    // don't show the window if auto-closing is already due
    }
    if (Preferences::messageButtonDelay() == 0)
        move(0, 0);
    MainWindowBase::show();
}

/******************************************************************************
* Returns the window's recommended size exclusive of its frame.
*/
QSize MessageWin::sizeHint() const
{
    QSize desired;
    switch (mAction)
    {
        case KAEvent::MESSAGE:
            desired = MainWindowBase::sizeHint();
            break;
        case KAEvent::COMMAND:
            if (mShown)
            {
                // For command output, expand the window to accommodate the text
                const QSize texthint = mCommandText->sizeHint();
                int w = texthint.width() + style()->pixelMetric(QStyle::PM_LayoutLeftMargin)
                                         + style()->pixelMetric(QStyle::PM_LayoutRightMargin);
                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<bool> 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 <application>KMail</application>"));
}

/******************************************************************************
* 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);
    if (!mEditDlg)
        return;
    mEditDlg->setAttribute(Qt::WA_NativeWindow, true);
    KWindowSystem::setMainWindow(mEditDlg->windowHandle(), winId());
    KWindowSystem::setOnAllDesktops(mEditDlg->winId(), false);
    setButtonsReadOnly(true);
    connect(mEditDlg, &QDialog::accepted, this, &MessageWin::editCloseOk);
    connect(mEditDlg, &QDialog::rejected, this, &MessageWin::editCloseCancel);
    connect(mEditDlg, &QObject::destroyed, this, &MessageWin::editCloseCancel);
    connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, this, &MessageWin::activeWindowChanged);
    mainWin->editAlarm(mEditDlg, mOriginalEvent);
}

/******************************************************************************
* Called when OK is clicked in the alarm edit dialog invoked by the Edit button.
* Closes the window.
*/
void MessageWin::editCloseOk()
{
    mEditDlg = nullptr;
    mNoCloseConfirm = true;   // allow window to close without confirmation prompt
    close();
}

/******************************************************************************
* Called when Cancel is clicked in the alarm edit dialog invoked by the Edit
* button, or when the dialog is deleted.
*/
void MessageWin::editCloseCancel()
{
    mEditDlg = nullptr;
    setButtonsReadOnly(false);
}

/******************************************************************************
* Called when the active window has changed. If this window has become the
* active window and there is an alarm edit dialog, simulate a modal dialog by
* making the alarm edit dialog the active window instead.
*/
void MessageWin::activeWindowChanged(WId win)
{
    if (mEditDlg  &&  win == winId())
        KWindowSystem::activateWindow(mEditDlg->winId());
}

/******************************************************************************
* Set or clear the read-only state of the dialog buttons.
*/
void MessageWin::setButtonsReadOnly(bool ro)
{
    mOkButton->setReadOnly(ro, true);
    mDeferButton->setReadOnly(ro, true);
    mEditButton->setReadOnly(ro, true);
    if (mSilenceButton)
        mSilenceButton->setReadOnly(ro, true);
    if (mKMailButton)
        mKMailButton->setReadOnly(ro, true);
    mKAlarmButton->setReadOnly(ro, true);
}

/******************************************************************************
* Set up to disable the defer button when the deferral limit is reached.
*/
void MessageWin::setDeferralLimit(const KAEvent& event)
{
    mDeferLimit = event.deferralLimit().effectiveKDateTime().toUtc().qDateTime();
    MidnightTimer::connect(this, SLOT(checkDeferralLimit()));   // check every day
    mDisableDeferral = false;
    checkDeferralLimit();
}

/******************************************************************************
* Check whether the deferral limit has been reached.
* If so, disable the Defer button.
* N.B. Ideally, just a single QTimer::singleShot() call would be made to disable
*      the defer button at the corret time. But for a 32-bit integer, the
*      milliseconds parameter overflows in about 25 days, so instead a daily
*      check is done until the day when the deferral limit is reached, followed
*      by a non-overflowing QTimer::singleShot() call.
*/
void MessageWin::checkDeferralLimit()
{
    if (!mDeferButton->isEnabled()  ||  !mDeferLimit.isValid())
        return;
    int n = KADateTime::currentLocalDate().daysTo(KADateTime(mDeferLimit, KADateTime::LocalZone).date());
    if (n > 0)
        return;
    MidnightTimer::disconnect(this, SLOT(checkDeferralLimit()));
    if (n == 0)
    {
        // The deferral limit will be reached today
        n = QDateTime::currentDateTimeUtc().secsTo(mDeferLimit);
        if (n > 0)
        {
            QTimer::singleShot(n * 1000, this, &MessageWin::checkDeferralLimit);
            return;
        }
    }
    mDeferButton->setEnabled(false);
    mDisableDeferral = true;
}

/******************************************************************************
* Called when the Defer... button is clicked.
* Displays the defer message dialog.
*/
void MessageWin::slotDefer()
{
    mDeferDlg = new DeferAlarmDlg(KADateTime::currentDateTime(Preferences::timeSpec()).addSecs(60), mDateTime.isDateOnly(), false, this);
    if (windowFlags() & Qt::X11BypassWindowManagerHint)
        mDeferDlg->setWindowFlags(mDeferDlg->windowFlags() | Qt::X11BypassWindowManagerHint);
    mDeferDlg->setObjectName(QStringLiteral("DeferDlg"));    // used by LikeBack
    mDeferDlg->setDeferMinutes(mDefaultDeferMinutes > 0 ? mDefaultDeferMinutes : Preferences::defaultDeferTime());
    mDeferDlg->setLimit(mEvent);
    if (!Preferences::modalMessages())
        lower();
    if (mDeferDlg->exec() == QDialog::Accepted)
    {
        const DateTime dateTime  = mDeferDlg->getDateTime();
        const int      delayMins = mDeferDlg->deferMinutes();
        // Fetch the up-to-date alarm from the calendar. Note that it could have
        // changed since it was displayed.
        const KAEvent* event = mEventId.isEmpty() ? nullptr : ResourcesCalendar::instance()->event(mEventId);
        if (event)
        {
            // The event still exists in the active calendar
            qCDebug(KALARM_LOG) << "MessageWin::slotDefer: Deferring event" << mEventId;
            KAEvent newev(*event);
            newev.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true);
            newev.setDeferDefaultMinutes(delayMins);
            KAlarm::updateEvent(newev, mDeferDlg, true);
            if (newev.deferred())
                mNoPostAction = true;
        }
        else
        {
            // Try to retrieve the event from the displaying or archive calendars
            Resource resource;   // receives the event's original resource, if known
            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", "<para>Cannot defer alarm:</para><para>Alarm not found.</para>"));
                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);
            Resource res;
            KAlarm::deleteEvent(event, res, 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<QScreen*> 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<FullScreenType> screenTypes(numScreens);
            QVector<QRect> 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="<<full<<", screen="<<mScreenNumber;
            if (full == NoFullScreen)
                return modal;   // KAlarm's screen doesn't contain a full screen window
            if (full == FullScreen)
                inactiveScreen = mScreenNumber;
            for (int s = 0;  s < numScreens;  ++s)
            {
                if (s != mScreenNumber)
                {
                    full = haveFullScreenWindow(s);
                    if (full == NoFullScreen)
                    {
                        // There is no full screen window on this screen
                        mScreenNumber = s;
                        return modal;
                    }
                    if (full == FullScreen  &&  inactiveScreen < 0)
                        inactiveScreen = s;
                }
            }
            if (inactiveScreen >= 0)
            {
                // All screens contain a full screen window: use one without
                // an active full screen window.
                mScreenNumber = inactiveScreen;
                return modal;
            }
        }
        return false;  // can't logically get here, since there can only be one active window...
    }
#endif
    if (modal)
    {
        const WId activeId = KWindowSystem::activeWindow();
        const KWindowInfo wi = KWindowInfo(activeId, NET::WMState);
        if (wi.valid()  &&  wi.hasState(NET::FullScreen))
            return false;    // the active window is full screen.
    }
    return modal;
}

#if KDEPIM_HAVE_X11
/******************************************************************************
* In a multi-screen setup (not a single virtual desktop), find whether the
* specified screen has a full screen window on it.
*/
FullScreenType haveFullScreenWindow(int screen)
{
    FullScreenType type = NoFullScreen;
    xcb_connection_t* connection = QX11Info::connection();
    const NETRootInfo rootInfo(connection, NET::ClientList | NET::ActiveWindow, NET::Properties2(), screen);
    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();
    for (int w = 0;  w < windowCount;  ++w)
    {
        NETWinInfo winInfo(connection, windows[w], rootWindow, NET::WMState|NET::WMGeometry, NET::Properties2());
        if (winInfo.state() & NET::FullScreen)
        {
//qCDebug(KALARM_LOG)<<"Found FULL SCREEN: " << windows[w];
            type = FullScreen;
            if (windows[w] == activeWindow)
                return FullScreenActive;
        }
    }
    return type;
}

/******************************************************************************
* In a multi-screen setup (single virtual desktop, e.g. Xinerama), find which
* screens have full screen windows on them.
*/
FullScreenType findFullScreenWindows(const QVector<QRect>& screenRects, QVector<FullScreenType>& 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="<<windowCount<<", active="<<activeWindow<<", geom="<<QApplication::desktop()->screenGeometry(0);
    NETRect netgeom;
    NETRect netframe;
    for (int w = 0;  w < windowCount;  ++w)
    {
        NETWinInfo winInfo(connection, windows[w], rootWindow, NET::WMState | NET::WMGeometry, NET::Properties2());
        if (winInfo.state() & NET::FullScreen)
        {
            // Found a full screen window - find which screen it's on
            const bool active = (windows[w] == activeWindow);
            winInfo.kdeGeometry(netframe, netgeom);
            const QRect winRect(netgeom.pos.x, netgeom.pos.y, netgeom.size.width, netgeom.size.height);
//qCDebug(KALARM_LOG)<<"Found FULL SCREEN: "<<windows[w]<<", geom="<<winRect;
            for (int s = 0, count = screenRects.count();  s < count;  ++s)
            {
                if (screenRects[s].contains(winRect))
                {
//qCDebug(KALARM_LOG)<<"FULL SCREEN on screen"<<s<<", active="<<active;
                    if (active)
                        screenTypes[s] = result = FullScreenActive;
                    else
                    {
                        if (screenTypes[s] == NoFullScreen)
                            screenTypes[s] = FullScreen;
                        if (result == NoFullScreen)
                            result = FullScreen;
                    }
                    break;
                }
            }
        }
    }
    return result;
}
#endif

#include "moc_messagewin_p.cpp"
#include "moc_messagewin.cpp"

// vim: et sw=4:
