SVN_SILENT made messages (after extraction)
[kdepim.git] / kalarm / messagewin.cpp
blobca551b3692c055b59b4ef7f8e3b0d831c10d15b4
1 /*
2 * messagewin.cpp - displays an alarm message
3 * Program: kalarm
4 * Copyright © 2001-2016 by David Jarvie <djarvie@kde.org>
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 #include "config-kdepim.h"
22 #include "kalarm.h"
23 #include "messagewin_p.h"
24 #include "messagewin.h"
26 #include "alarmcalendar.h"
27 #include "autoqpointer.h"
28 #include "collectionmodel.h"
29 #include "deferdlg.h"
30 #include "desktop.h"
31 #include "editdlg.h"
32 #include "functions.h"
33 #include "kalarmapp.h"
34 #include "mainwindow.h"
35 #include "messagebox.h"
36 #include "preferences.h"
37 #include "pushbutton.h"
38 #include "shellprocess.h"
39 #include "synchtimer.h"
41 #include <kpimtextedit/texttospeech.h>
42 //QT5 reactivate after porting (activated by config-kdepim.h include in texttospeech.h)
43 #define KDEPIM_HAVE_X11 0
45 #include <KAboutData>
46 #include <KLocale>
47 #include <kstandardguiitem.h>
48 #include <KLocalizedString>
49 #include <kconfig.h>
50 #include <kiconloader.h>
51 #include <ksystemtimezone.h>
52 #include <ktextedit.h>
53 #include <kwindowsystem.h>
54 #include <KIO/StoredTransferJob>
55 #include <KJobWidgets>
56 #include <knotification.h>
57 #include <ksqueezedtextlabel.h>
58 #include <phonon/mediaobject.h>
59 #include <phonon/audiooutput.h>
60 #include <phonon/volumefadereffect.h>
61 #if KDEPIM_HAVE_X11
62 #include <netwm.h>
63 #include <qx11info_x11.h>
64 #endif
66 #include <qtextbrowser.h>
67 #include <QPushButton>
68 #include <QScrollBar>
69 #include <QtDBus/QtDBus>
70 #include <QFile>
71 #include <QFileInfo>
72 #include <QCheckBox>
73 #include <QLabel>
74 #include <QPalette>
75 #include <QTimer>
76 #include <QPixmap>
77 #include <QByteArray>
78 #include <QFrame>
79 #include <QGridLayout>
80 #include <QVBoxLayout>
81 #include <QHBoxLayout>
82 #include <QResizeEvent>
83 #include <QCloseEvent>
84 #include <QDesktopWidget>
85 #include <QMutexLocker>
86 #include <QMimeDatabase>
87 #include <QUrl>
88 #include "kalarm_debug.h"
90 #include <stdlib.h>
91 #include <string.h>
93 using namespace KCalCore;
94 using namespace KAlarmCal;
96 #if KDEPIM_HAVE_X11
97 enum FullScreenType { NoFullScreen = 0, FullScreen = 1, FullScreenActive = 2 };
98 static FullScreenType haveFullScreenWindow(int screen);
99 static FullScreenType findFullScreenWindows(const QVector<QRect>& screenRects, QVector<FullScreenType>& screenTypes);
100 #endif
102 #include "kmailinterface.h"
103 static const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail");
104 static const QLatin1String KMAIL_DBUS_PATH("/KMail");
106 // The delay for enabling message window buttons if a zero delay is
107 // configured, i.e. the windows are placed far from the cursor.
108 static const int proximityButtonDelay = 1000; // (milliseconds)
109 static const int proximityMultiple = 10; // multiple of button height distance from cursor for proximity
111 // A text label widget which can be scrolled and copied with the mouse
112 class MessageText : public KTextEdit
114 public:
115 MessageText(QWidget* parent = Q_NULLPTR)
116 : KTextEdit(parent),
117 mNewLine(false)
119 setReadOnly(true);
120 setFrameStyle(NoFrame);
121 setLineWrapMode(NoWrap);
123 int scrollBarHeight() const { return horizontalScrollBar()->height(); }
124 int scrollBarWidth() const { return verticalScrollBar()->width(); }
125 void setBackgroundColour(const QColor& c)
127 QPalette pal = viewport()->palette();
128 pal.setColor(viewport()->backgroundRole(), c);
129 viewport()->setPalette(pal);
131 QSize sizeHint() const Q_DECL_OVERRIDE
133 const QSizeF docsize = document()->size();
134 return QSize(static_cast<int>(docsize.width() + 0.99) + verticalScrollBar()->width(),
135 static_cast<int>(docsize.height() + 0.99) + horizontalScrollBar()->height());
137 bool newLine() const { return mNewLine; }
138 void setNewLine(bool nl) { mNewLine = nl; }
139 private:
140 bool mNewLine;
144 // Basic flags for the window
145 static const Qt::WindowFlags WFLAGS = Qt::WindowStaysOnTopHint;
146 static const Qt::WindowFlags WFLAGS2 = Qt::WindowContextHelpButtonHint;
147 static const Qt::WidgetAttribute WidgetFlags = Qt::WA_DeleteOnClose;
149 // Error message bit masks
150 enum {
151 ErrMsg_Speak = 0x01,
152 ErrMsg_AudioFile = 0x02
156 QList<MessageWin*> MessageWin::mWindowList;
157 QMap<EventId, unsigned> MessageWin::mErrorMessages;
158 bool MessageWin::mRedisplayed = false;
159 // There can only be one audio thread at a time: trying to play multiple
160 // sound files simultaneously would result in a cacophony, and besides
161 // that, Phonon currently crashes...
162 QPointer<AudioThread> MessageWin::mAudioThread;
163 MessageWin* AudioThread::mAudioOwner = Q_NULLPTR;
165 /******************************************************************************
166 * Construct the message window for the specified alarm.
167 * Other alarms in the supplied event may have been updated by the caller, so
168 * the whole event needs to be stored for updating the calendar file when it is
169 * displayed.
171 MessageWin::MessageWin(const KAEvent* event, const KAAlarm& alarm, int flags)
172 : MainWindowBase(Q_NULLPTR, static_cast<Qt::WindowFlags>(WFLAGS | WFLAGS2 | ((flags & ALWAYS_HIDE) || getWorkAreaAndModal() ? Qt::WindowType(0) : Qt::X11BypassWindowManagerHint))),
173 mMessage(event->cleanText()),
174 mFont(event->font()),
175 mBgColour(event->bgColour()),
176 mFgColour(event->fgColour()),
177 mEventItemId(event->itemId()),
178 mEventId(*event),
179 mAudioFile(event->audioFile()),
180 mVolume(event->soundVolume()),
181 mFadeVolume(event->fadeVolume()),
182 mFadeSeconds(qMin(event->fadeSeconds(), 86400)),
183 mDefaultDeferMinutes(event->deferDefaultMinutes()),
184 mAlarmType(alarm.type()),
185 mAction(event->actionSubType()),
186 mKMailSerialNumber(event->kmailSerialNumber()),
187 mCommandError(event->commandError()),
188 mRestoreHeight(0),
189 mAudioRepeatPause(event->repeatSoundPause()),
190 mConfirmAck(event->confirmAck()),
191 mNoDefer(true),
192 mInvalid(false),
193 mEvent(*event),
194 mOriginalEvent(*event),
195 mCollection(AlarmCalendar::resources()->collectionForEvent(mEventItemId)),
196 mTimeLabel(Q_NULLPTR),
197 mRemainingText(Q_NULLPTR),
198 mEditButton(Q_NULLPTR),
199 mDeferButton(Q_NULLPTR),
200 mSilenceButton(Q_NULLPTR),
201 mKMailButton(Q_NULLPTR),
202 mCommandText(Q_NULLPTR),
203 mDontShowAgainCheck(Q_NULLPTR),
204 mEditDlg(Q_NULLPTR),
205 mDeferDlg(Q_NULLPTR),
206 mAlwaysHide(flags & ALWAYS_HIDE),
207 mErrorWindow(false),
208 mInitialised(false),
209 mNoPostAction(alarm.type() & KAAlarm::REMINDER_ALARM),
210 mRecreating(false),
211 mBeep(event->beep()),
212 mSpeak(event->speak()),
213 mRescheduleEvent(!(flags & NO_RESCHEDULE)),
214 mShown(false),
215 mPositioning(false),
216 mNoCloseConfirm(false),
217 mDisableDeferral(false)
219 qCDebug(KALARM_LOG) << (void*)this << "event" << mEventId;
220 setAttribute(static_cast<Qt::WidgetAttribute>(WidgetFlags));
221 setWindowModality(Qt::WindowModal);
222 setObjectName(QStringLiteral("MessageWin")); // used by LikeBack
223 if (alarm.type() & KAAlarm::REMINDER_ALARM)
225 if (event->reminderMinutes() < 0)
227 event->previousOccurrence(alarm.dateTime(false).effectiveKDateTime(), mDateTime, false);
228 if (!mDateTime.isValid() && event->repeatAtLogin())
229 mDateTime = alarm.dateTime().addSecs(event->reminderMinutes() * 60);
231 else
232 mDateTime = event->mainDateTime(true);
234 else
235 mDateTime = alarm.dateTime(true);
236 if (!(flags & (NO_INIT_VIEW | ALWAYS_HIDE)))
238 const bool readonly = AlarmCalendar::resources()->eventReadOnly(mEventItemId);
239 mShowEdit = !mEventId.isEmpty() && !readonly;
240 mNoDefer = readonly || (flags & NO_DEFER) || alarm.repeatAtLogin();
241 initView();
243 // Set to save settings automatically, but don't save window size.
244 // File alarm window size is saved elsewhere.
245 setAutoSaveSettings(QStringLiteral("MessageWin"), false);
246 mWindowList.append(this);
247 if (event->autoClose())
248 mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().dateTime().addSecs(event->lateCancel() * 60);
249 if (mAlwaysHide)
251 hide();
252 displayComplete(); // play audio, etc.
256 /******************************************************************************
257 * Display an error message window.
258 * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note
259 * that the option is specific to 'event'.
261 void MessageWin::showError(const KAEvent& event, const DateTime& alarmDateTime,
262 const QStringList& errmsgs, const QString& dontShowAgain)
264 if (!dontShowAgain.isEmpty()
265 && KAlarm::dontShowErrors(EventId(event), dontShowAgain))
266 return;
268 // Don't pile up duplicate error messages for the same alarm
269 for (int i = 0, end = mWindowList.count(); i < end; ++i)
271 const MessageWin* w = mWindowList[i];
272 if (w->mErrorWindow && w->mEventId == EventId(event)
273 && w->mErrorMsgs == errmsgs && w->mDontShowAgain == dontShowAgain)
274 return;
277 (new MessageWin(&event, alarmDateTime, errmsgs, dontShowAgain))->show();
280 /******************************************************************************
281 * Construct the message window for a specified error message.
282 * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note
283 * that the option is specific to 'event'.
285 MessageWin::MessageWin(const KAEvent* event, const DateTime& alarmDateTime,
286 const QStringList& errmsgs, const QString& dontShowAgain)
287 : MainWindowBase(Q_NULLPTR, WFLAGS | WFLAGS2),
288 mMessage(event->cleanText()),
289 mDateTime(alarmDateTime),
290 mEventItemId(event->itemId()),
291 mEventId(*event),
292 mAlarmType(KAAlarm::MAIN_ALARM),
293 mAction(event->actionSubType()),
294 mKMailSerialNumber(0),
295 mCommandError(KAEvent::CMD_NO_ERROR),
296 mErrorMsgs(errmsgs),
297 mDontShowAgain(dontShowAgain),
298 mRestoreHeight(0),
299 mConfirmAck(false),
300 mShowEdit(false),
301 mNoDefer(true),
302 mInvalid(false),
303 mEvent(*event),
304 mOriginalEvent(*event),
305 mTimeLabel(Q_NULLPTR),
306 mRemainingText(Q_NULLPTR),
307 mEditButton(Q_NULLPTR),
308 mDeferButton(Q_NULLPTR),
309 mSilenceButton(Q_NULLPTR),
310 mKMailButton(Q_NULLPTR),
311 mCommandText(Q_NULLPTR),
312 mDontShowAgainCheck(Q_NULLPTR),
313 mEditDlg(Q_NULLPTR),
314 mDeferDlg(Q_NULLPTR),
315 mAlwaysHide(false),
316 mErrorWindow(true),
317 mInitialised(false),
318 mNoPostAction(true),
319 mRecreating(false),
320 mRescheduleEvent(false),
321 mShown(false),
322 mPositioning(false),
323 mNoCloseConfirm(false),
324 mDisableDeferral(false)
326 qCDebug(KALARM_LOG) << "errmsg";
327 setAttribute(static_cast<Qt::WidgetAttribute>(WidgetFlags));
328 setWindowModality(Qt::WindowModal);
329 setObjectName(QStringLiteral("ErrorWin")); // used by LikeBack
330 getWorkAreaAndModal();
331 initView();
332 mWindowList.append(this);
335 /******************************************************************************
336 * Construct the message window for restoration by session management.
337 * The window is initialised by readProperties().
339 MessageWin::MessageWin()
340 : MainWindowBase(Q_NULLPTR, WFLAGS),
341 mTimeLabel(Q_NULLPTR),
342 mRemainingText(Q_NULLPTR),
343 mEditButton(Q_NULLPTR),
344 mDeferButton(Q_NULLPTR),
345 mSilenceButton(Q_NULLPTR),
346 mKMailButton(Q_NULLPTR),
347 mCommandText(Q_NULLPTR),
348 mDontShowAgainCheck(Q_NULLPTR),
349 mEditDlg(Q_NULLPTR),
350 mDeferDlg(Q_NULLPTR),
351 mAlwaysHide(false),
352 mErrorWindow(false),
353 mInitialised(false),
354 mRecreating(false),
355 mRescheduleEvent(false),
356 mShown(false),
357 mPositioning(false),
358 mNoCloseConfirm(false),
359 mDisableDeferral(false)
361 qCDebug(KALARM_LOG) << (void*)this << "restore";
362 setAttribute(WidgetFlags);
363 setWindowModality(Qt::WindowModal);
364 setObjectName(QStringLiteral("RestoredMsgWin")); // used by LikeBack
365 getWorkAreaAndModal();
366 mWindowList.append(this);
369 /******************************************************************************
370 * Destructor. Perform any post-alarm actions before tidying up.
372 MessageWin::~MessageWin()
374 qCDebug(KALARM_LOG) << (void*)this << mEventId;
375 if (AudioThread::mAudioOwner == this && !mAudioThread.isNull())
376 mAudioThread->quit();
377 mErrorMessages.remove(mEventId);
378 mWindowList.removeAll(this);
379 if (!mRecreating)
381 if (!mNoPostAction && !mEvent.postAction().isEmpty())
382 theApp()->alarmCompleted(mEvent);
383 if (!instanceCount(true))
384 theApp()->quitIf(); // no visible windows remain - check whether to quit
388 /******************************************************************************
389 * Construct the message window.
391 void MessageWin::initView()
393 const bool reminder = (!mErrorWindow && (mAlarmType & KAAlarm::REMINDER_ALARM));
394 const int leading = fontMetrics().leading();
395 setCaption((mAlarmType & KAAlarm::REMINDER_ALARM) ? i18nc("@title:window", "Reminder") : i18nc("@title:window", "Message"));
396 QWidget* topWidget = new QWidget(this);
397 setCentralWidget(topWidget);
398 QVBoxLayout* topLayout = new QVBoxLayout(topWidget);
399 topLayout->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin));
400 topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing));
402 QPalette labelPalette = palette();
403 labelPalette.setColor(backgroundRole(), labelPalette.color(QPalette::Window));
405 // Show the alarm date/time, together with a reminder text where appropriate.
406 // Alarm date/time: display time zone if not local time zone.
407 mTimeLabel = new QLabel(topWidget);
408 mTimeLabel->setText(dateTimeToDisplay());
409 mTimeLabel->setFrameStyle(QFrame::StyledPanel);
410 mTimeLabel->setPalette(labelPalette);
411 mTimeLabel->setAutoFillBackground(true);
412 topLayout->addWidget(mTimeLabel, 0, Qt::AlignHCenter);
413 mTimeLabel->setWhatsThis(i18nc("@info:whatsthis", "The scheduled date/time for the message (as opposed to the actual time of display)."));
415 if (mDateTime.isValid())
417 // Reminder
418 if (reminder)
420 // Create a label "time\nReminder" by inserting the time at the
421 // start of the translated string, allowing for possible HTML tags
422 // enclosing "Reminder".
423 QString s = i18nc("@info", "Reminder");
424 QRegExp re(QStringLiteral("^(<[^>]+>)*"));
425 re.indexIn(s);
426 s.insert(re.matchedLength(), mTimeLabel->text() + QLatin1String("<br/>"));
427 mTimeLabel->setText(s);
428 mTimeLabel->setAlignment(Qt::AlignHCenter);
431 else
432 mTimeLabel->hide();
434 if (!mErrorWindow)
436 // It's a normal alarm message window
437 switch (mAction)
439 case KAEvent::FILE:
441 // Display the file name
442 KSqueezedTextLabel* label = new KSqueezedTextLabel(mMessage, topWidget);
443 label->setFrameStyle(QFrame::StyledPanel);
444 label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
445 label->setPalette(labelPalette);
446 label->setAutoFillBackground(true);
447 label->setWhatsThis(i18nc("@info:whatsthis", "The file whose contents are displayed below"));
448 topLayout->addWidget(label, 0, Qt::AlignHCenter);
450 // Display contents of file
451 const QUrl url = QUrl::fromUserInput(mMessage, QString(), QUrl::AssumeLocalFile);
453 auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 0, KIO::HideProgressInfo);
454 const bool exists = statJob->exec();
455 const bool isDir = statJob->statResult().isDir();
457 bool opened = false;
458 if (exists && !isDir) {
459 auto job = KIO::storedGet(url);
460 KJobWidgets::setWindow(job, MainWindow::mainMainWindow());
461 if (job->exec()) {
462 opened = true;
463 const QByteArray data = job->data();
464 QTemporaryFile tmpFile;
465 tmpFile.write(data);
466 tmpFile.seek(0);
468 QTextBrowser* view = new QTextBrowser(topWidget);
469 view->setFrameStyle(QFrame::NoFrame);
470 view->setWordWrapMode(QTextOption::NoWrap);
471 QPalette pal = view->viewport()->palette();
472 pal.setColor(view->viewport()->backgroundRole(), mBgColour);
473 view->viewport()->setPalette(pal);
474 view->setTextColor(mFgColour);
475 view->setCurrentFont(mFont);
476 QMimeDatabase db;
477 QMimeType mime = db.mimeTypeForUrl(url);
478 if (mime.name() == QLatin1String("application/octet-stream"))
479 mime = db.mimeTypeForData(&tmpFile);
480 switch (KAlarm::fileType(mime))
482 case KAlarm::Image:
483 view->setHtml(QLatin1String("<img source=\"") + tmpFile.fileName() + QLatin1String("\">"));
484 break;
485 case KAlarm::TextFormatted:
486 view->QTextBrowser::setSource(QUrl::fromLocalFile(tmpFile.fileName())); //krazy:exclude=qclasses
487 break;
488 default:
490 view->setPlainText(QString::fromUtf8(data));
491 break;
494 view->setMinimumSize(view->sizeHint());
495 topLayout->addWidget(view);
497 // Set the default size to 20 lines square.
498 // Note that after the first file has been displayed, this size
499 // is overridden by the user-set default stored in the config file.
500 // So there is no need to calculate an accurate size.
501 int h = 20*view->fontMetrics().lineSpacing() + 2*view->frameWidth();
502 view->resize(QSize(h, h).expandedTo(view->sizeHint()));
503 view->setWhatsThis(i18nc("@info:whatsthis", "The contents of the file to be displayed"));
507 if (!exists || isDir || !opened) {
508 mErrorMsgs += isDir ? i18nc("@info", "File is a folder") : exists ? i18nc("@info", "Failed to open file") : i18nc("@info", "File not found");
510 break;
512 case KAEvent::MESSAGE:
514 // Message label
515 // Using MessageText instead of QLabel allows scrolling and mouse copying
516 MessageText* text = new MessageText(topWidget);
517 text->setAutoFillBackground(true);
518 text->setBackgroundColour(mBgColour);
519 text->setTextColor(mFgColour);
520 text->setCurrentFont(mFont);
521 text->insertPlainText(mMessage);
522 const int lineSpacing = text->fontMetrics().lineSpacing();
523 const QSize s = text->sizeHint();
524 const int h = s.height();
525 text->setMaximumHeight(h + text->scrollBarHeight());
526 text->setMinimumHeight(qMin(h, lineSpacing*4));
527 text->setMaximumWidth(s.width() + text->scrollBarWidth());
528 text->setWhatsThis(i18nc("@info:whatsthis", "The alarm message"));
529 const int vspace = lineSpacing/2;
530 const int hspace = lineSpacing - style()->pixelMetric(QStyle::PM_DefaultChildMargin);
531 topLayout->addSpacing(vspace);
532 topLayout->addStretch();
533 // Don't include any horizontal margins if message is 2/3 screen width
534 if (text->sizeHint().width() >= KAlarm::desktopWorkArea(mScreenNumber).width()*2/3)
535 topLayout->addWidget(text, 1, Qt::AlignHCenter);
536 else
538 QHBoxLayout* layout = new QHBoxLayout();
539 layout->addSpacing(hspace);
540 layout->addWidget(text, 1, Qt::AlignHCenter);
541 layout->addSpacing(hspace);
542 topLayout->addLayout(layout);
544 if (!reminder)
545 topLayout->addStretch();
546 break;
548 case KAEvent::COMMAND:
550 mCommandText = new MessageText(topWidget);
551 mCommandText->setBackgroundColour(mBgColour);
552 mCommandText->setTextColor(mFgColour);
553 mCommandText->setCurrentFont(mFont);
554 topLayout->addWidget(mCommandText);
555 mCommandText->setWhatsThis(i18nc("@info:whatsthis", "The output of the alarm's command"));
556 theApp()->execCommandAlarm(mEvent, mEvent.alarm(mAlarmType), this, SLOT(readProcessOutput(ShellProcess*)));
557 break;
559 case KAEvent::EMAIL:
560 default:
561 break;
564 if (reminder && mEvent.reminderMinutes() > 0)
566 // Advance reminder: show remaining time until the actual alarm
567 mRemainingText = new QLabel(topWidget);
568 mRemainingText->setFrameStyle(QFrame::Box | QFrame::Raised);
569 mRemainingText->setMargin(leading);
570 mRemainingText->setPalette(labelPalette);
571 mRemainingText->setAutoFillBackground(true);
572 if (mDateTime.isDateOnly() || KDateTime::currentLocalDate().daysTo(mDateTime.date()) > 0)
574 setRemainingTextDay();
575 MidnightTimer::connect(this, SLOT(setRemainingTextDay())); // update every day
577 else
579 setRemainingTextMinute();
580 MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute
582 topLayout->addWidget(mRemainingText, 0, Qt::AlignHCenter);
583 topLayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing));
584 topLayout->addStretch();
587 else
589 // It's an error message
590 switch (mAction)
592 case KAEvent::EMAIL:
594 // Display the email addresses and subject.
595 QFrame* frame = new QFrame(topWidget);
596 frame->setFrameStyle(QFrame::Box | QFrame::Raised);
597 frame->setWhatsThis(i18nc("@info:whatsthis", "The email to send"));
598 topLayout->addWidget(frame, 0, Qt::AlignHCenter);
599 QGridLayout* grid = new QGridLayout(frame);
600 grid->setMargin(style()->pixelMetric(QStyle::PM_DefaultChildMargin));
601 grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing));
603 QLabel* label = new QLabel(i18nc("@info Email addressee", "To:"), frame);
604 label->setFixedSize(label->sizeHint());
605 grid->addWidget(label, 0, 0, Qt::AlignLeft);
606 label = new QLabel(mEvent.emailAddresses(QStringLiteral("\n")), frame);
607 label->setFixedSize(label->sizeHint());
608 grid->addWidget(label, 0, 1, Qt::AlignLeft);
610 label = new QLabel(i18nc("@info Email subject", "Subject:"), frame);
611 label->setFixedSize(label->sizeHint());
612 grid->addWidget(label, 1, 0, Qt::AlignLeft);
613 label = new QLabel(mEvent.emailSubject(), frame);
614 label->setFixedSize(label->sizeHint());
615 grid->addWidget(label, 1, 1, Qt::AlignLeft);
616 break;
618 case KAEvent::COMMAND:
619 case KAEvent::FILE:
620 case KAEvent::MESSAGE:
621 default:
622 // Just display the error message strings
623 break;
627 if (!mErrorMsgs.count())
629 topWidget->setAutoFillBackground(true);
630 QPalette palette = topWidget->palette();
631 palette.setColor(topWidget->backgroundRole(), mBgColour);
632 topWidget->setPalette(palette);
634 else
636 setCaption(i18nc("@title:window", "Error"));
637 QHBoxLayout* layout = new QHBoxLayout();
638 layout->setMargin(2 * style()->pixelMetric(QStyle::PM_DefaultChildMargin));
639 layout->addStretch();
640 topLayout->addLayout(layout);
641 QLabel* label = new QLabel(topWidget);
642 label->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-error")).pixmap(IconSize(KIconLoader::Desktop), IconSize(KIconLoader::Desktop)));
643 label->setFixedSize(label->sizeHint());
644 layout->addWidget(label, 0, Qt::AlignRight);
645 QVBoxLayout* vlayout = new QVBoxLayout();
646 layout->addLayout(vlayout);
647 for (QStringList::ConstIterator it = mErrorMsgs.constBegin(); it != mErrorMsgs.constEnd(); ++it)
649 label = new QLabel(*it, topWidget);
650 label->setFixedSize(label->sizeHint());
651 vlayout->addWidget(label, 0, Qt::AlignLeft);
653 layout->addStretch();
654 if (!mDontShowAgain.isEmpty())
656 mDontShowAgainCheck = new QCheckBox(i18nc("@option:check", "Do not display this error message again for this alarm"), topWidget);
657 mDontShowAgainCheck->setFixedSize(mDontShowAgainCheck->sizeHint());
658 topLayout->addWidget(mDontShowAgainCheck, 0, Qt::AlignLeft);
662 QGridLayout* grid = new QGridLayout();
663 grid->setColumnStretch(0, 1); // keep the buttons right-adjusted in the window
664 topLayout->addLayout(grid);
665 int gridIndex = 1;
667 // Close button
668 mOkButton = new PushButton(KStandardGuiItem::close(), topWidget);
669 // Prevent accidental acknowledgement of the message if the user is typing when the window appears
670 mOkButton->clearFocus();
671 mOkButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection
672 mOkButton->setFixedSize(mOkButton->sizeHint());
673 connect(mOkButton, &QAbstractButton::clicked, this, &MessageWin::slotOk);
674 grid->addWidget(mOkButton, 0, gridIndex++, Qt::AlignHCenter);
675 mOkButton->setWhatsThis(i18nc("@info:whatsthis", "Acknowledge the alarm"));
677 if (mShowEdit)
679 // Edit button
680 mEditButton = new PushButton(i18nc("@action:button", "&Edit..."), topWidget);
681 mEditButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection
682 mEditButton->setFixedSize(mEditButton->sizeHint());
683 connect(mEditButton, &QAbstractButton::clicked, this, &MessageWin::slotEdit);
684 grid->addWidget(mEditButton, 0, gridIndex++, Qt::AlignHCenter);
685 mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the alarm."));
688 // Defer button
689 mDeferButton = new PushButton(i18nc("@action:button", "&Defer..."), topWidget);
690 mDeferButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection
691 mDeferButton->setFixedSize(mDeferButton->sizeHint());
692 connect(mDeferButton, &QAbstractButton::clicked, this, &MessageWin::slotDefer);
693 grid->addWidget(mDeferButton, 0, gridIndex++, Qt::AlignHCenter);
694 mDeferButton->setWhatsThis(xi18nc("@info:whatsthis", "<para>Defer the alarm until later.</para>"
695 "<para>You will be prompted to specify when the alarm should be redisplayed.</para>"));
697 if (mNoDefer)
698 mDeferButton->hide();
699 else
700 setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more
702 if (!mAudioFile.isEmpty() && (mVolume || mFadeVolume > 0))
704 // Silence button to stop sound repetition
705 const QPixmap pixmap = MainBarIcon(QStringLiteral("media-playback-stop"));
706 mSilenceButton = new PushButton(topWidget);
707 mSilenceButton->setIcon(pixmap);
708 grid->addWidget(mSilenceButton, 0, gridIndex++, Qt::AlignHCenter);
709 mSilenceButton->setToolTip(i18nc("@info:tooltip", "Stop sound"));
710 mSilenceButton->setWhatsThis(i18nc("@info:whatsthis", "Stop playing the sound"));
711 // To avoid getting in a mess, disable the button until sound playing has been set up
712 mSilenceButton->setEnabled(false);
715 KIconLoader iconLoader;
716 if (mKMailSerialNumber)
718 // KMail button
719 const QPixmap pixmap = iconLoader.loadIcon(QStringLiteral("internet-mail"), KIconLoader::MainToolbar);
720 mKMailButton = new PushButton(topWidget);
721 mKMailButton->setIcon(pixmap);
722 connect(mKMailButton, &QAbstractButton::clicked, this, &MessageWin::slotShowKMailMessage);
723 grid->addWidget(mKMailButton, 0, gridIndex++, Qt::AlignHCenter);
724 mKMailButton->setToolTip(xi18nc("@info:tooltip Locate this email in KMail", "Locate in <application>KMail</application>"));
725 mKMailButton->setWhatsThis(xi18nc("@info:whatsthis", "Locate and highlight this email in <application>KMail</application>"));
728 // KAlarm button
729 const QPixmap pixmap = iconLoader.loadIcon(KAboutData::applicationData().componentName(), KIconLoader::MainToolbar);
730 mKAlarmButton = new PushButton(topWidget);
731 mKAlarmButton->setIcon(pixmap);
732 connect(mKAlarmButton, &QAbstractButton::clicked, this, &MessageWin::displayMainWindow);
733 grid->addWidget(mKAlarmButton, 0, gridIndex++, Qt::AlignHCenter);
734 mKAlarmButton->setToolTip(xi18nc("@info:tooltip", "Activate <application>KAlarm</application>"));
735 mKAlarmButton->setWhatsThis(xi18nc("@info:whatsthis", "Activate <application>KAlarm</application>"));
737 int butsize = mKAlarmButton->sizeHint().height();
738 if (mSilenceButton)
739 butsize = qMax(butsize, mSilenceButton->sizeHint().height());
740 if (mKMailButton)
741 butsize = qMax(butsize, mKMailButton->sizeHint().height());
742 mKAlarmButton->setFixedSize(butsize, butsize);
743 if (mSilenceButton)
744 mSilenceButton->setFixedSize(butsize, butsize);
745 if (mKMailButton)
746 mKMailButton->setFixedSize(butsize, butsize);
748 // Disable all buttons initially, to prevent accidental clicking on if they happen to be
749 // under the mouse just as the window appears.
750 mOkButton->setEnabled(false);
751 if (mDeferButton->isVisible())
752 mDeferButton->setEnabled(false);
753 if (mEditButton)
754 mEditButton->setEnabled(false);
755 if (mKMailButton)
756 mKMailButton->setEnabled(false);
757 mKAlarmButton->setEnabled(false);
759 topLayout->activate();
760 setMinimumSize(QSize(grid->sizeHint().width() + 2 * style()->pixelMetric(QStyle::PM_DefaultChildMargin),
761 sizeHint().height()));
762 const bool modal = !(windowFlags() & Qt::X11BypassWindowManagerHint);
763 const unsigned long wstate = (modal ? NET::Modal : 0) | NET::Sticky | NET::StaysOnTop;
764 WId winid = winId();
765 //QT5 KWindowSystem::setState(winid, wstate);
766 KWindowSystem::setOnAllDesktops(winid, true);
768 mInitialised = true; // the window's widgets have been created
771 /******************************************************************************
772 * Return the number of message windows, optionally excluding always-hidden ones.
774 int MessageWin::instanceCount(bool excludeAlwaysHidden)
776 int count = mWindowList.count();
777 if (excludeAlwaysHidden)
779 foreach (MessageWin* win, mWindowList)
781 if (win->mAlwaysHide)
782 --count;
785 return count;
788 bool MessageWin::hasDefer() const
790 return mDeferButton && mDeferButton->isVisible();
793 /******************************************************************************
794 * Show the Defer button when it was previously hidden.
796 void MessageWin::showDefer()
798 if (mDeferButton)
800 mNoDefer = false;
801 mDeferButton->show();
802 setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more
803 resize(sizeHint());
807 /******************************************************************************
808 * Convert a reminder window into a normal alarm window.
810 void MessageWin::cancelReminder(const KAEvent& event, const KAAlarm& alarm)
812 if (!mInitialised)
813 return;
814 mDateTime = alarm.dateTime(true);
815 mNoPostAction = false;
816 mAlarmType = alarm.type();
817 if (event.autoClose())
818 mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().dateTime().addSecs(event.lateCancel() * 60);
819 setCaption(i18nc("@title:window", "Message"));
820 mTimeLabel->setText(dateTimeToDisplay());
821 if (mRemainingText)
822 mRemainingText->hide();
823 MidnightTimer::disconnect(this, SLOT(setRemainingTextDay()));
824 MinuteTimer::disconnect(this, SLOT(setRemainingTextMinute()));
825 setMinimumHeight(0);
826 centralWidget()->layout()->activate();
827 setMinimumHeight(sizeHint().height());
828 resize(sizeHint());
831 /******************************************************************************
832 * Show the alarm's trigger time.
833 * This is assumed to have previously been hidden.
835 void MessageWin::showDateTime(const KAEvent& event, const KAAlarm& alarm)
837 if (!mTimeLabel)
838 return;
839 mDateTime = (alarm.type() & KAAlarm::REMINDER_ALARM) ? event.mainDateTime(true) : alarm.dateTime(true);
840 if (mDateTime.isValid())
842 mTimeLabel->setText(dateTimeToDisplay());
843 mTimeLabel->show();
847 /******************************************************************************
848 * Get the trigger time to display.
850 QString MessageWin::dateTimeToDisplay()
852 QString tm;
853 if (mDateTime.isValid())
855 if (mDateTime.isDateOnly())
856 tm = KLocale::global()->formatDate(mDateTime.date(), KLocale::ShortDate);
857 else
859 bool showZone = false;
860 if (mDateTime.timeType() == KDateTime::UTC
861 || (mDateTime.timeType() == KDateTime::TimeZone && !mDateTime.isLocalZone()))
863 // Display time zone abbreviation if it's different from the local
864 // zone. Note that the iCalendar time zone might represent the local
865 // time zone in a slightly different way from the system time zone,
866 // so the zone comparison above might not produce the desired result.
867 const QString tz = mDateTime.kDateTime().toString(QStringLiteral("%Z"));
868 KDateTime local = mDateTime.kDateTime();
869 local.setTimeSpec(KDateTime::Spec::LocalZone());
870 showZone = (local.toString(QStringLiteral("%Z")) != tz);
872 tm = KLocale::global()->formatDateTime(mDateTime.kDateTime(), KLocale::ShortDate, KLocale::DateTimeFormatOptions(showZone ? KLocale::TimeZone : 0));
875 return tm;
878 /******************************************************************************
879 * Set the remaining time text in a reminder window.
880 * Called at the start of every day (at the user-defined start-of-day time).
882 void MessageWin::setRemainingTextDay()
884 QString text;
885 const int days = KDateTime::currentLocalDate().daysTo(mDateTime.date());
886 if (days <= 0 && !mDateTime.isDateOnly())
888 // The alarm is due today, so start refreshing every minute
889 MidnightTimer::disconnect(this, SLOT(setRemainingTextDay()));
890 setRemainingTextMinute();
891 MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute
893 else
895 if (days <= 0)
896 text = i18nc("@info", "Today");
897 else if (days % 7)
898 text = i18ncp("@info", "Tomorrow", "in %1 days' time", days);
899 else
900 text = i18ncp("@info", "in 1 week's time", "in %1 weeks' time", days/7);
902 mRemainingText->setText(text);
905 /******************************************************************************
906 * Set the remaining time text in a reminder window.
907 * Called on every minute boundary.
909 void MessageWin::setRemainingTextMinute()
911 QString text;
912 const int mins = (KDateTime::currentUtcDateTime().secsTo(mDateTime.effectiveKDateTime()) + 59) / 60;
913 if (mins < 60)
914 text = i18ncp("@info", "in 1 minute's time", "in %1 minutes' time", (mins > 0 ? mins : 0));
915 else if (mins % 60 == 0)
916 text = i18ncp("@info", "in 1 hour's time", "in %1 hours' time", mins/60);
917 else
919 QString hourText = i18ncp("@item:intext inserted into 'in ... %1 minute's time' below", "1 hour", "%1 hours", mins/60);
920 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);
922 mRemainingText->setText(text);
925 /******************************************************************************
926 * Called when output is available from the command which is providing the text
927 * for this window. Add the output and resize the window to show it.
929 void MessageWin::readProcessOutput(ShellProcess* proc)
931 const QByteArray data = proc->readAll();
932 if (!data.isEmpty())
934 // Strip any trailing newline, to avoid showing trailing blank line
935 // in message window.
936 if (mCommandText->newLine())
937 mCommandText->append(QStringLiteral("\n"));
938 const int nl = data.endsWith('\n') ? 1 : 0;
939 mCommandText->setNewLine(nl);
940 mCommandText->insertPlainText(QString::fromLocal8Bit(data.data(), data.length() - nl));
941 resize(sizeHint());
945 /******************************************************************************
946 * Save settings to the session managed config file, for restoration
947 * when the program is restored.
949 void MessageWin::saveProperties(KConfigGroup& config)
951 if (mShown && !mErrorWindow && !mAlwaysHide)
953 config.writeEntry("EventID", mEventId.eventId());
954 config.writeEntry("EventItemID", mEventItemId);
955 config.writeEntry("AlarmType", static_cast<int>(mAlarmType));
956 if (mAlarmType == KAAlarm::INVALID_ALARM)
957 qCCritical(KALARM_LOG) << "Invalid alarm: id=" << mEventId << ", alarm count=" << mEvent.alarmCount();
958 config.writeEntry("Message", mMessage);
959 config.writeEntry("Type", static_cast<int>(mAction));
960 config.writeEntry("Font", mFont);
961 config.writeEntry("BgColour", mBgColour);
962 config.writeEntry("FgColour", mFgColour);
963 config.writeEntry("ConfirmAck", mConfirmAck);
964 if (mDateTime.isValid())
966 //TODO: Write KDateTime when it becomes possible
967 config.writeEntry("Time", mDateTime.effectiveDateTime());
968 config.writeEntry("DateOnly", mDateTime.isDateOnly());
969 QString zone;
970 if (mDateTime.isUtc())
971 zone = QStringLiteral("UTC");
972 else
974 const KTimeZone tz = mDateTime.timeZone();
975 if (tz.isValid())
976 zone = tz.name();
978 config.writeEntry("TimeZone", zone);
980 if (mCloseTime.isValid())
981 config.writeEntry("Expiry", mCloseTime);
982 if (mAudioRepeatPause >= 0 && mSilenceButton && mSilenceButton->isEnabled())
984 // Only need to restart sound file playing if it's being repeated
985 config.writePathEntry("AudioFile", mAudioFile);
986 config.writeEntry("Volume", static_cast<int>(mVolume * 100));
987 config.writeEntry("AudioPause", mAudioRepeatPause);
989 config.writeEntry("Speak", mSpeak);
990 config.writeEntry("Height", height());
991 config.writeEntry("DeferMins", mDefaultDeferMinutes);
992 config.writeEntry("NoDefer", mNoDefer);
993 config.writeEntry("NoPostAction", mNoPostAction);
994 config.writeEntry("KMailSerial", static_cast<qulonglong>(mKMailSerialNumber));
995 config.writeEntry("CmdErr", static_cast<int>(mCommandError));
996 config.writeEntry("DontShowAgain", mDontShowAgain);
998 else
999 config.writeEntry("Invalid", true);
1002 /******************************************************************************
1003 * Read settings from the session managed config file.
1004 * This function is automatically called whenever the app is being restored.
1005 * Read in whatever was saved in saveProperties().
1007 void MessageWin::readProperties(const KConfigGroup& config)
1009 mInvalid = config.readEntry("Invalid", false);
1010 QString eventId = config.readEntry("EventID");
1011 mEventItemId = config.readEntry("EventItemID", Akonadi::Item::Id(-1));
1012 mAlarmType = static_cast<KAAlarm::Type>(config.readEntry("AlarmType", 0));
1013 if (mAlarmType == KAAlarm::INVALID_ALARM)
1015 mInvalid = true;
1016 qCCritical(KALARM_LOG) << "Invalid alarm: id=" << eventId;
1018 mMessage = config.readEntry("Message");
1019 mAction = static_cast<KAEvent::SubAction>(config.readEntry("Type", 0));
1020 mFont = config.readEntry("Font", QFont());
1021 mBgColour = config.readEntry("BgColour", QColor(Qt::white));
1022 mFgColour = config.readEntry("FgColour", QColor(Qt::black));
1023 mConfirmAck = config.readEntry("ConfirmAck", false);
1024 QDateTime invalidDateTime;
1025 QDateTime dt = config.readEntry("Time", invalidDateTime);
1026 const QString zone = config.readEntry("TimeZone");
1027 if (zone.isEmpty())
1028 mDateTime = KDateTime(dt, KDateTime::ClockTime);
1029 else if (zone == QStringLiteral("UTC"))
1031 dt.setTimeSpec(Qt::UTC);
1032 mDateTime = KDateTime(dt, KDateTime::UTC);
1034 else
1036 KTimeZone tz = KSystemTimeZones::zone(zone);
1037 mDateTime = KDateTime(dt, (tz.isValid() ? tz : KSystemTimeZones::local()));
1039 const bool dateOnly = config.readEntry("DateOnly", false);
1040 if (dateOnly)
1041 mDateTime.setDateOnly(true);
1042 mCloseTime = config.readEntry("Expiry", invalidDateTime);
1043 mCloseTime.setTimeSpec(Qt::UTC);
1044 mAudioFile = config.readPathEntry("AudioFile", QString());
1045 mVolume = static_cast<float>(config.readEntry("Volume", 0)) / 100;
1046 mFadeVolume = -1;
1047 mFadeSeconds = 0;
1048 if (!mAudioFile.isEmpty()) // audio file URL was only saved if it repeats
1049 mAudioRepeatPause = config.readEntry("AudioPause", 0);
1050 mBeep = false; // don't beep after restart (similar to not playing non-repeated sound file)
1051 mSpeak = config.readEntry("Speak", false);
1052 mRestoreHeight = config.readEntry("Height", 0);
1053 mDefaultDeferMinutes = config.readEntry("DeferMins", 0);
1054 mNoDefer = config.readEntry("NoDefer", false);
1055 mNoPostAction = config.readEntry("NoPostAction", true);
1056 mKMailSerialNumber = static_cast<unsigned long>(config.readEntry("KMailSerial", QVariant(QVariant::ULongLong)).toULongLong());
1057 mCommandError = KAEvent::CmdErrType(config.readEntry("CmdErr", static_cast<int>(KAEvent::CMD_NO_ERROR)));
1058 mDontShowAgain = config.readEntry("DontShowAgain", QString());
1059 mShowEdit = false;
1060 // Temporarily initialise mCollection and mEventId - they will be set by redisplayAlarm()
1061 mCollection = Akonadi::Collection();
1062 mEventId = EventId(mCollection.id(), eventId);
1063 qCDebug(KALARM_LOG) << eventId;
1064 if (mAlarmType != KAAlarm::INVALID_ALARM)
1066 // Recreate the event from the calendar file (if possible)
1067 if (eventId.isEmpty())
1068 initView();
1069 else
1071 // Close any other window for this alarm which has already been restored by redisplayAlarms()
1072 if (!AkonadiModel::instance()->isCollectionTreeFetched())
1074 connect(AkonadiModel::instance(), &Akonadi::EntityTreeModel::collectionTreeFetched,
1075 this, &MessageWin::showRestoredAlarm);
1076 return;
1078 redisplayAlarm();
1083 /******************************************************************************
1084 * Fetch the restored alarm from the calendar and redisplay it in this window.
1086 void MessageWin::showRestoredAlarm()
1088 qCDebug(KALARM_LOG) << mEventId;
1089 redisplayAlarm();
1090 show();
1093 /******************************************************************************
1094 * Fetch the restored alarm from the calendar and redisplay it in this window.
1096 void MessageWin::redisplayAlarm()
1098 mCollection = AkonadiModel::instance()->collectionForItem(mEventItemId);
1099 mEventId.setCollectionId(mCollection.id());
1100 qCDebug(KALARM_LOG) << mEventId;
1101 // Delete any already existing window for the same event
1102 MessageWin* duplicate = findEvent(mEventId, this);
1103 if (duplicate)
1104 qCDebug(KALARM_LOG) << "Deleting duplicate window:" << mEventId;
1105 delete duplicate;
1107 KAEvent* event = AlarmCalendar::resources()->event(mEventId);
1108 if (event)
1110 mEvent = *event;
1111 mShowEdit = true;
1113 else
1115 // It's not in the active calendar, so try the displaying or archive calendars
1116 retrieveEvent(mEvent, mCollection, mShowEdit, mNoDefer);
1117 mNoDefer = !mNoDefer;
1119 initView();
1122 /******************************************************************************
1123 * Redisplay alarms which were being shown when the program last exited.
1124 * Normally, these alarms will have been displayed by session restoration, but
1125 * if the program crashed or was killed, we can redisplay them here so that
1126 * they won't be lost.
1128 void MessageWin::redisplayAlarms()
1130 if (mRedisplayed)
1131 return;
1132 qCDebug(KALARM_LOG);
1133 mRedisplayed = true;
1134 AlarmCalendar* cal = AlarmCalendar::displayCalendar();
1135 if (cal && cal->isOpen())
1137 KAEvent event;
1138 Akonadi::Collection collection;
1139 const Event::List events = cal->kcalEvents();
1140 for (int i = 0, end = events.count(); i < end; ++i)
1142 bool showDefer, showEdit;
1143 reinstateFromDisplaying(events[i], event, collection, showEdit, showDefer);
1144 Akonadi::Item::Id id = AkonadiModel::instance()->findItemId(event);
1145 if (id >= 0)
1146 event.setItemId(id);
1147 const EventId eventId(event);
1148 if (findEvent(eventId))
1149 qCDebug(KALARM_LOG) << "Message window already exists:" << eventId;
1150 else
1152 // This event should be displayed, but currently isn't being
1153 const KAAlarm alarm = event.convertDisplayingAlarm();
1154 if (alarm.type() == KAAlarm::INVALID_ALARM)
1156 qCCritical(KALARM_LOG) << "Invalid alarm: id=" << eventId;
1157 continue;
1159 qCDebug(KALARM_LOG) << eventId;
1160 const bool login = alarm.repeatAtLogin();
1161 const int flags = NO_RESCHEDULE | (login ? NO_DEFER : 0) | NO_INIT_VIEW;
1162 MessageWin* win = new MessageWin(&event, alarm, flags);
1163 win->mCollection = collection;
1164 const bool rw = CollectionControlModel::isWritableEnabled(collection, event.category()) > 0;
1165 win->mShowEdit = rw ? showEdit : false;
1166 win->mNoDefer = (rw && !login) ? !showDefer : true;
1167 win->initView();
1168 win->show();
1174 /******************************************************************************
1175 * Retrieves the event with the current ID from the displaying calendar file,
1176 * or if not found there, from the archive calendar.
1178 bool MessageWin::retrieveEvent(KAEvent& event, Akonadi::Collection& resource, bool& showEdit, bool& showDefer)
1180 const Event::Ptr kcalEvent = AlarmCalendar::displayCalendar()->kcalEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING));
1181 if (!reinstateFromDisplaying(kcalEvent, event, resource, showEdit, showDefer))
1183 // The event isn't in the displaying calendar.
1184 // Try to retrieve it from the archive calendar.
1185 KAEvent* ev = Q_NULLPTR;
1186 Akonadi::Collection archiveCol = CollectionControlModel::getStandard(CalEvent::ARCHIVED);
1187 if (archiveCol.isValid())
1188 ev = AlarmCalendar::resources()->event(EventId(archiveCol.id(), CalEvent::uid(mEventId.eventId(), CalEvent::ARCHIVED)));
1189 if (!ev)
1190 return false;
1191 event = *ev;
1192 event.setArchive(); // ensure that it gets re-archived if it's saved
1193 event.setCategory(CalEvent::ACTIVE);
1194 if (mEventId.eventId() != event.id())
1195 qCCritical(KALARM_LOG) << "Wrong event ID";
1196 event.setEventId(mEventId.eventId());
1197 resource = Akonadi::Collection();
1198 showEdit = true;
1199 showDefer = true;
1200 qCDebug(KALARM_LOG) << event.id() << ": success";
1202 return true;
1205 /******************************************************************************
1206 * Retrieves the displayed event from the calendar file, or if not found there,
1207 * from the displaying calendar.
1209 bool MessageWin::reinstateFromDisplaying(const Event::Ptr& kcalEvent, KAEvent& event, Akonadi::Collection& collection, bool& showEdit, bool& showDefer)
1211 if (!kcalEvent)
1212 return false;
1213 Akonadi::Collection::Id collectionId;
1214 event.reinstateFromDisplaying(kcalEvent, collectionId, showEdit, showDefer);
1215 event.setCollectionId(collectionId);
1216 collection = AkonadiModel::instance()->collectionById(collectionId);
1217 qCDebug(KALARM_LOG) << EventId(event) << ": success";
1218 return true;
1221 /******************************************************************************
1222 * Called when an alarm is currently being displayed, to store a copy of the
1223 * alarm in the displaying calendar, and to reschedule it for its next repetition.
1224 * If no repetitions remain, cancel it.
1226 void MessageWin::alarmShowing(KAEvent& event)
1228 qCDebug(KALARM_LOG) << event.id() << "," << KAAlarm::debugType(mAlarmType);
1229 const KAAlarm alarm = event.alarm(mAlarmType);
1230 if (!alarm.isValid())
1232 qCCritical(KALARM_LOG) << "Alarm type not found:" << event.id() << ":" << mAlarmType;
1233 return;
1235 if (!mAlwaysHide)
1237 // Copy the alarm to the displaying calendar in case of a crash, etc.
1238 KAEvent dispEvent;
1239 const Akonadi::Collection collection = AkonadiModel::instance()->collectionForItem(event.itemId());
1240 dispEvent.setDisplaying(event, mAlarmType, collection.id(),
1241 mDateTime.effectiveKDateTime(), mShowEdit, !mNoDefer);
1242 AlarmCalendar* cal = AlarmCalendar::displayCalendarOpen();
1243 if (cal)
1245 cal->deleteDisplayEvent(dispEvent.id()); // in case it already exists
1246 cal->addEvent(dispEvent);
1247 cal->save();
1250 theApp()->rescheduleAlarm(event, alarm);
1253 /******************************************************************************
1254 * Spread alarm windows over the screen so that they are all visible, or pile
1255 * them on top of each other again.
1256 * Reply = true if windows are now scattered, false if piled up.
1258 bool MessageWin::spread(bool scatter)
1260 if (instanceCount(true) <= 1) // ignore always-hidden windows
1261 return false;
1263 const QRect desk = KAlarm::desktopWorkArea(); // get the usable area of the desktop
1264 if (scatter == isSpread(desk.topLeft()))
1265 return scatter;
1267 if (scatter)
1269 // Usually there won't be many windows, so a crude
1270 // scattering algorithm should suffice.
1271 int x = desk.left();
1272 int y = desk.top();
1273 int ynext = y;
1274 for (int errmsgs = 0; errmsgs < 2; ++errmsgs)
1276 // Display alarm messages first, then error messages, since most
1277 // error messages tend to be the same height.
1278 for (int i = 0, end = mWindowList.count(); i < end; ++i)
1280 MessageWin* w = mWindowList[i];
1281 if ((!errmsgs && w->mErrorWindow)
1282 || (errmsgs && !w->mErrorWindow))
1283 continue;
1284 const QSize sz = w->frameGeometry().size();
1285 if (x + sz.width() > desk.right())
1287 x = desk.left();
1288 y = ynext;
1290 int ytmp = y;
1291 if (y + sz.height() > desk.bottom())
1293 ytmp = desk.bottom() - sz.height();
1294 if (ytmp < desk.top())
1295 ytmp = desk.top();
1297 w->move(x, ytmp);
1298 x += sz.width();
1299 if (ytmp + sz.height() > ynext)
1300 ynext = ytmp + sz.height();
1304 else
1306 // Move all windows to the top left corner
1307 for (int i = 0, end = mWindowList.count(); i < end; ++i)
1308 mWindowList[i]->move(desk.topLeft());
1310 return scatter;
1313 /******************************************************************************
1314 * Check whether message windows are all piled up, or are spread out.
1315 * Reply = true if windows are currently spread, false if piled up.
1317 bool MessageWin::isSpread(const QPoint& topLeft)
1319 for (int i = 0, end = mWindowList.count(); i < end; ++i)
1321 if (mWindowList[i]->pos() != topLeft)
1322 return true;
1324 return false;
1327 /******************************************************************************
1328 * Returns the existing message window (if any) which is displaying the event
1329 * with the specified ID.
1331 MessageWin* MessageWin::findEvent(const EventId& eventId, MessageWin* exclude)
1333 if (!eventId.isEmpty())
1335 for (int i = 0, end = mWindowList.count(); i < end; ++i)
1337 MessageWin* w = mWindowList[i];
1338 if (w != exclude && w->mEventId == eventId && !w->mErrorWindow)
1339 return w;
1342 return Q_NULLPTR;
1345 /******************************************************************************
1346 * Beep and play the audio file, as appropriate.
1348 void MessageWin::playAudio()
1350 if (mBeep)
1352 // Beep using two methods, in case the sound card/speakers are switched off or not working
1353 QApplication::beep(); // beep through the internal speaker
1354 KNotification::beep(); // beep through the sound card & speakers
1356 if (!mAudioFile.isEmpty())
1358 if (!mVolume && mFadeVolume <= 0)
1359 return; // ensure zero volume doesn't play anything
1360 startAudio(); // play the audio file
1362 else if (mSpeak)
1364 // The message is to be spoken. In case of error messges,
1365 // call it on a timer to allow the window to display first.
1366 QTimer::singleShot(0, this, &MessageWin::slotSpeak);
1370 /******************************************************************************
1371 * Speak the message.
1372 * Called asynchronously to avoid delaying the display of the message.
1374 void MessageWin::slotSpeak()
1376 KPIMTextEdit::TextToSpeech *tts = KPIMTextEdit::TextToSpeech::self();
1377 if (!tts->isReady()) {
1378 KAMessageBox::detailedError(MainWindow::mainMainWindow(), i18nc("@info", "Unable to speak message"), i18nc("@info", "Text-to-speech subsystem is not available"));
1379 clearErrorMessage(ErrMsg_Speak);
1380 return;
1383 tts->say(mMessage);
1386 /******************************************************************************
1387 * Called when another window's audio thread has been destructed.
1388 * Start playing this window's audio file. Because initialising the sound system
1389 * and loading the file may take some time, it is called in a separate thread to
1390 * allow the window to display first.
1392 void MessageWin::startAudio()
1394 if (mAudioThread)
1396 // An audio file is already playing for another message
1397 // window, so wait until it has finished.
1398 connect(mAudioThread.data(), &QObject::destroyed, this, &MessageWin::audioTerminating);
1400 else
1402 qCDebug(KALARM_LOG) << QThread::currentThread();
1403 mAudioThread = new AudioThread(this, mAudioFile, mVolume, mFadeVolume, mFadeSeconds, mAudioRepeatPause);
1404 connect(mAudioThread.data(), &AudioThread::readyToPlay, this, &MessageWin::playReady);
1405 connect(mAudioThread.data(), &QThread::finished, this, &MessageWin::playFinished);
1406 if (mSilenceButton)
1407 connect(mSilenceButton, &QAbstractButton::clicked, mAudioThread.data(), &QThread::quit);
1408 // Notify after creating mAudioThread, so that isAudioPlaying() will
1409 // return the correct value.
1410 theApp()->notifyAudioPlaying(true);
1411 mAudioThread->start();
1415 /******************************************************************************
1416 * Return whether audio playback is currently active.
1418 bool MessageWin::isAudioPlaying()
1420 return mAudioThread;
1423 /******************************************************************************
1424 * Stop audio playback.
1426 void MessageWin::stopAudio(bool wait)
1428 qCDebug(KALARM_LOG);
1429 if (mAudioThread)
1430 mAudioThread->stop(wait);
1433 /******************************************************************************
1434 * Called when another window's audio thread is being destructed.
1435 * Wait until the destructor has finished.
1437 void MessageWin::audioTerminating()
1439 QTimer::singleShot(0, this, &MessageWin::startAudio);
1442 /******************************************************************************
1443 * Called when the audio file is ready to start playing.
1445 void MessageWin::playReady()
1447 if (mSilenceButton)
1448 mSilenceButton->setEnabled(true);
1451 /******************************************************************************
1452 * Called when the audio file thread finishes.
1454 void MessageWin::playFinished()
1456 if (mSilenceButton)
1457 mSilenceButton->setEnabled(false);
1458 if (mAudioThread) // mAudioThread can actually be null here!
1460 const QString errmsg = mAudioThread->error();
1461 if (!errmsg.isEmpty() && !haveErrorMessage(ErrMsg_AudioFile))
1463 KAMessageBox::error(this, errmsg);
1464 clearErrorMessage(ErrMsg_AudioFile);
1467 delete mAudioThread.data();
1468 if (mAlwaysHide)
1469 close();
1472 /******************************************************************************
1473 * Constructor for audio thread.
1475 AudioThread::AudioThread(MessageWin* parent, const QString& audioFile, float volume, float fadeVolume, int fadeSeconds, int repeatPause)
1476 : QThread(parent),
1477 mFile(audioFile),
1478 mVolume(volume),
1479 mFadeVolume(fadeVolume),
1480 mFadeSeconds(fadeSeconds),
1481 mRepeatPause(repeatPause),
1482 mAudioObject(Q_NULLPTR)
1484 if (mAudioOwner)
1485 qCCritical(KALARM_LOG) << "mAudioOwner already set";
1486 mAudioOwner = parent;
1489 /******************************************************************************
1490 * Destructor for audio thread. Waits for thread completion and tidies up.
1491 * Note that this destructor is executed in the parent thread.
1493 AudioThread::~AudioThread()
1495 qCDebug(KALARM_LOG);
1496 stop(true); // stop playing and tidy up (timeout 3 seconds)
1497 delete mAudioObject;
1498 mAudioObject = Q_NULLPTR;
1499 if (mAudioOwner == parent())
1500 mAudioOwner = Q_NULLPTR;
1501 // Notify after deleting mAudioThread, so that isAudioPlaying() will
1502 // return the correct value.
1503 QTimer::singleShot(0, theApp(), &KAlarmApp::notifyAudioStopped);
1506 /******************************************************************************
1507 * Quits the thread and waits for thread completion and tidies up.
1509 void AudioThread::stop(bool waiT)
1511 qCDebug(KALARM_LOG);
1512 quit(); // stop playing and tidy up
1513 wait(3000); // wait for run() to exit (timeout 3 seconds)
1514 if (!isFinished())
1516 // Something has gone wrong - forcibly kill the thread
1517 terminate();
1518 if (waiT)
1519 wait();
1523 /******************************************************************************
1524 * Kick off the thread to play the audio file.
1526 void AudioThread::run()
1528 mMutex.lock();
1529 if (mAudioObject)
1531 mMutex.unlock();
1532 return;
1534 qCDebug(KALARM_LOG) << QThread::currentThread() << mFile;
1535 const QString audioFile = mFile;
1536 const QUrl url = QUrl::fromUserInput(mFile);
1537 mFile = url.isLocalFile() ? url.toLocalFile() : url.toString();
1538 Phonon::MediaSource source(url);
1539 if (source.type() == Phonon::MediaSource::Invalid)
1541 mError = xi18nc("@info", "Cannot open audio file: <filename>%1</filename>", audioFile);
1542 mMutex.unlock();
1543 qCCritical(KALARM_LOG) << "Open failure:" << audioFile;
1544 return;
1546 mAudioObject = new Phonon::MediaObject();
1547 mAudioObject->setCurrentSource(source);
1548 mAudioObject->setTransitionTime(100); // workaround to prevent clipping of end of files in Xine backend
1549 Phonon::AudioOutput* output = new Phonon::AudioOutput(Phonon::NotificationCategory, mAudioObject);
1550 mPath = Phonon::createPath(mAudioObject, output);
1551 if (mVolume >= 0 || mFadeVolume >= 0)
1553 const float vol = (mVolume >= 0) ? mVolume : output->volume();
1554 const float maxvol = qMax(vol, mFadeVolume);
1555 output->setVolume(maxvol);
1556 if (mFadeVolume >= 0 && mFadeSeconds > 0)
1558 Phonon::VolumeFaderEffect* fader = new Phonon::VolumeFaderEffect(mAudioObject);
1559 fader->setVolume(mFadeVolume / maxvol);
1560 fader->fadeTo(mVolume / maxvol, mFadeSeconds * 1000);
1561 mPath.insertEffect(fader);
1564 connect(mAudioObject, &Phonon::MediaObject::stateChanged, this, &AudioThread::playStateChanged, Qt::DirectConnection);
1565 connect(mAudioObject, &Phonon::MediaObject::finished, this, &AudioThread::checkAudioPlay, Qt::DirectConnection);
1566 mPlayedOnce = false;
1567 mPausing = false;
1568 mMutex.unlock();
1569 Q_EMIT readyToPlay();
1570 checkAudioPlay();
1572 // Start an event loop.
1573 // The function will exit once exit() or quit() is called.
1574 // First, ensure that the thread object is deleted once it has completed.
1575 connect(this, &QThread::finished, this, &QObject::deleteLater);
1576 exec();
1577 stopPlay();
1580 /******************************************************************************
1581 * Called when the audio file has loaded and is ready to play, or when play
1582 * has completed.
1583 * If it is ready to play, start playing it (for the first time or repeated).
1584 * If play has not yet completed, wait a bit longer.
1586 void AudioThread::checkAudioPlay()
1588 mMutex.lock();
1589 if (!mAudioObject)
1591 mMutex.unlock();
1592 return;
1594 if (mPausing)
1595 mPausing = false;
1596 else
1598 // The file has loaded and is ready to play, or play has completed
1599 if (mPlayedOnce)
1601 if (mRepeatPause < 0)
1603 // Play has completed
1604 mMutex.unlock();
1605 stopPlay();
1606 return;
1608 if (mRepeatPause > 0)
1610 // Pause before playing the file again
1611 mPausing = true;
1612 QTimer::singleShot(mRepeatPause * 1000, this, &AudioThread::checkAudioPlay);
1613 mMutex.unlock();
1614 return;
1617 mPlayedOnce = true;
1620 // Start playing the file, either for the first time or again
1621 qCDebug(KALARM_LOG) << "start";
1622 mAudioObject->play();
1623 mMutex.unlock();
1626 /******************************************************************************
1627 * Called when the playback object changes state.
1628 * If an error has occurred, quit and return the error to the caller.
1630 void AudioThread::playStateChanged(Phonon::State newState)
1632 if (newState == Phonon::ErrorState)
1634 QMutexLocker locker(&mMutex);
1635 const QString err = mAudioObject->errorString();
1636 if (!err.isEmpty())
1638 qCCritical(KALARM_LOG) << "Play failure:" << mFile << ":" << err;
1639 mError = xi18nc("@info", "<para>Error playing audio file: <filename>%1</filename></para><para>%2</para>", mFile, err);
1640 exit(1);
1645 /******************************************************************************
1646 * Called when play completes, the Silence button is clicked, or the window is
1647 * closed, to terminate audio access.
1649 void AudioThread::stopPlay()
1651 mMutex.lock();
1652 if (mAudioObject)
1654 mAudioObject->stop();
1655 const QList<Phonon::Effect*> effects = mPath.effects();
1656 for (int i = 0; i < effects.count(); ++i)
1658 mPath.removeEffect(effects[i]);
1659 delete effects[i];
1661 delete mAudioObject;
1662 mAudioObject = Q_NULLPTR;
1664 mMutex.unlock();
1665 quit(); // exit the event loop, if it's still running
1668 QString AudioThread::error() const
1670 QMutexLocker locker(&mMutex);
1671 return mError;
1674 /******************************************************************************
1675 * Raise the alarm window, re-output any required audio notification, and
1676 * reschedule the alarm in the calendar file.
1678 void MessageWin::repeat(const KAAlarm& alarm)
1680 if (!mInitialised)
1681 return;
1682 if (mDeferDlg)
1684 // Cancel any deferral dialog so that the user notices something's going on,
1685 // and also because the deferral time limit will have changed.
1686 delete mDeferDlg;
1687 mDeferDlg = Q_NULLPTR;
1689 KAEvent* event = mEventId.isEmpty() ? Q_NULLPTR : AlarmCalendar::resources()->event(mEventId);
1690 if (event)
1692 mAlarmType = alarm.type(); // store new alarm type for use if it is later deferred
1693 if (mAlwaysHide)
1694 playAudio();
1695 else
1697 if (!mDeferDlg || Preferences::modalMessages())
1699 raise();
1700 playAudio();
1702 if (mDeferButton->isVisible())
1704 mDeferButton->setEnabled(true);
1705 setDeferralLimit(*event); // ensure that button is disabled when alarm can't be deferred any more
1708 alarmShowing(*event);
1712 /******************************************************************************
1713 * Display the window.
1714 * If windows are being positioned away from the mouse cursor, it is initially
1715 * positioned at the top left to slightly reduce the number of times the
1716 * windows need to be moved in showEvent().
1718 void MessageWin::show()
1720 if (mCloseTime.isValid())
1722 // Set a timer to auto-close the window
1723 int delay = KDateTime::currentUtcDateTime().dateTime().secsTo(mCloseTime);
1724 if (delay < 0)
1725 delay = 0;
1726 QTimer::singleShot(delay * 1000, this, &QWidget::close);
1727 if (!delay)
1728 return; // don't show the window if auto-closing is already due
1730 if (Preferences::messageButtonDelay() == 0)
1731 move(0, 0);
1732 MainWindowBase::show();
1735 /******************************************************************************
1736 * Returns the window's recommended size exclusive of its frame.
1738 QSize MessageWin::sizeHint() const
1740 QSize desired;
1741 switch (mAction)
1743 case KAEvent::MESSAGE:
1744 desired = MainWindowBase::sizeHint();
1745 break;
1746 case KAEvent::COMMAND:
1747 if (mShown)
1749 // For command output, expand the window to accommodate the text
1750 const QSize texthint = mCommandText->sizeHint();
1751 int w = texthint.width() + 2 * style()->pixelMetric(QStyle::PM_DefaultChildMargin);
1752 if (w < width())
1753 w = width();
1754 const int ypadding = height() - mCommandText->height();
1755 desired = QSize(w, texthint.height() + ypadding);
1756 break;
1758 // fall through to default
1759 default:
1760 return MainWindowBase::sizeHint();
1763 // Limit the size to fit inside the working area of the desktop
1764 const QSize desktop = KAlarm::desktopWorkArea(mScreenNumber).size();
1765 const QSize frameThickness = frameGeometry().size() - geometry().size(); // title bar & window frame
1766 return desired.boundedTo(desktop - frameThickness);
1769 /******************************************************************************
1770 * Called when the window is shown.
1771 * The first time, output any required audio notification, and reschedule or
1772 * delete the event from the calendar file.
1774 void MessageWin::showEvent(QShowEvent* se)
1776 MainWindowBase::showEvent(se);
1777 if (mShown || !mInitialised)
1778 return;
1779 if (mErrorWindow || mAlarmType == KAAlarm::INVALID_ALARM)
1781 // Don't bother repositioning error messages,
1782 // and invalid alarms should be deleted anyway.
1783 enableButtons();
1785 else
1787 /* Set the window size.
1788 * Note that the frame thickness is not yet known when this
1789 * method is called, so for large windows the size needs to be
1790 * set again later.
1792 bool execComplete = true;
1793 QSize s = sizeHint(); // fit the window round the message
1794 if (mAction == KAEvent::FILE && !mErrorMsgs.count())
1795 KAlarm::readConfigWindowSize("FileMessage", s);
1796 resize(s);
1798 const QRect desk = KAlarm::desktopWorkArea(mScreenNumber);
1799 const QRect frame = frameGeometry();
1801 mButtonDelay = Preferences::messageButtonDelay() * 1000;
1802 if (mButtonDelay)
1804 // Position the window in the middle of the screen, and
1805 // delay enabling the buttons.
1806 mPositioning = true;
1807 move((desk.width() - frame.width())/2, (desk.height() - frame.height())/2);
1808 execComplete = false;
1810 else
1812 /* Try to ensure that the window can't accidentally be acknowledged
1813 * by the user clicking the mouse just as it appears.
1814 * To achieve this, move the window so that the OK button is as far away
1815 * from the cursor as possible. If the buttons are still too close to the
1816 * cursor, disable the buttons for a short time.
1817 * N.B. This can't be done in show(), since the geometry of the window
1818 * is not known until it is displayed. Unfortunately by moving the
1819 * window in showEvent(), a flicker is unavoidable.
1820 * See the Qt documentation on window geometry for more details.
1822 // PROBLEM: The frame size is not known yet!
1823 const QPoint cursor = QCursor::pos();
1824 const QRect rect = geometry();
1825 // Find the offsets from the outside of the frame to the edges of the OK button
1826 const QRect button(mOkButton->mapToParent(QPoint(0, 0)), mOkButton->mapToParent(mOkButton->rect().bottomRight()));
1827 const int buttonLeft = button.left() + rect.left() - frame.left();
1828 const int buttonRight = width() - button.right() + frame.right() - rect.right();
1829 const int buttonTop = button.top() + rect.top() - frame.top();
1830 const int buttonBottom = height() - button.bottom() + frame.bottom() - rect.bottom();
1832 const int centrex = (desk.width() + buttonLeft - buttonRight) / 2;
1833 const int centrey = (desk.height() + buttonTop - buttonBottom) / 2;
1834 const int x = (cursor.x() < centrex) ? desk.right() - frame.width() : desk.left();
1835 const int y = (cursor.y() < centrey) ? desk.bottom() - frame.height() : desk.top();
1837 // Find the enclosing rectangle for the new button positions
1838 // and check if the cursor is too near
1839 QRect buttons = mOkButton->geometry().unite(mKAlarmButton->geometry());
1840 buttons.translate(rect.left() + x - frame.left(), rect.top() + y - frame.top());
1841 const int minDistance = proximityMultiple * mOkButton->height();
1842 if ((abs(cursor.x() - buttons.left()) < minDistance
1843 || abs(cursor.x() - buttons.right()) < minDistance)
1844 && (abs(cursor.y() - buttons.top()) < minDistance
1845 || abs(cursor.y() - buttons.bottom()) < minDistance))
1846 mButtonDelay = proximityButtonDelay; // too near - disable buttons initially
1848 if (x != frame.left() || y != frame.top())
1850 mPositioning = true;
1851 move(x, y);
1852 execComplete = false;
1855 if (execComplete)
1856 displayComplete(); // play audio, etc.
1859 // Set the window size etc. once the frame size is known
1860 QTimer::singleShot(0, this, &MessageWin::frameDrawn);
1862 mShown = true;
1865 /******************************************************************************
1866 * Called when the window has been moved.
1868 void MessageWin::moveEvent(QMoveEvent* e)
1870 MainWindowBase::moveEvent(e);
1871 theApp()->setSpreadWindowsState(isSpread(KAlarm::desktopWorkArea(mScreenNumber).topLeft()));
1872 if (mPositioning)
1874 // The window has just been initially positioned
1875 mPositioning = false;
1876 displayComplete(); // play audio, etc.
1880 /******************************************************************************
1881 * Called after (hopefully) the window frame size is known.
1882 * Reset the initial window size if it exceeds the working area of the desktop.
1883 * Set the 'spread windows' menu item status.
1885 void MessageWin::frameDrawn()
1887 if (!mErrorWindow && mAction == KAEvent::MESSAGE)
1889 const QSize s = sizeHint();
1890 if (width() > s.width() || height() > s.height())
1891 resize(s);
1893 theApp()->setSpreadWindowsState(isSpread(KAlarm::desktopWorkArea(mScreenNumber).topLeft()));
1896 /******************************************************************************
1897 * Called when the window has been displayed properly (in its correct position),
1898 * to play sounds and reschedule the event.
1900 void MessageWin::displayComplete()
1902 playAudio();
1903 if (mRescheduleEvent)
1904 alarmShowing(mEvent);
1906 if (!mAlwaysHide)
1908 // Enable the window's buttons either now or after the configured delay
1909 if (mButtonDelay > 0)
1910 QTimer::singleShot(mButtonDelay, this, &MessageWin::enableButtons);
1911 else
1912 enableButtons();
1916 /******************************************************************************
1917 * Enable the window's buttons.
1919 void MessageWin::enableButtons()
1921 mOkButton->setEnabled(true);
1922 mKAlarmButton->setEnabled(true);
1923 if (mDeferButton->isVisible() && !mDisableDeferral)
1924 mDeferButton->setEnabled(true);
1925 if (mEditButton)
1926 mEditButton->setEnabled(true);
1927 if (mKMailButton)
1928 mKMailButton->setEnabled(true);
1931 /******************************************************************************
1932 * Called when the window's size has changed (before it is painted).
1934 void MessageWin::resizeEvent(QResizeEvent* re)
1936 if (mRestoreHeight)
1938 // Restore the window height on session restoration
1939 if (mRestoreHeight != re->size().height())
1941 QSize size = re->size();
1942 size.setHeight(mRestoreHeight);
1943 resize(size);
1945 else if (isVisible())
1946 mRestoreHeight = 0;
1948 else
1950 if (mShown && mAction == KAEvent::FILE && !mErrorMsgs.count())
1951 KAlarm::writeConfigWindowSize("FileMessage", re->size());
1952 MainWindowBase::resizeEvent(re);
1956 /******************************************************************************
1957 * Called when a close event is received.
1958 * Only quits the application if there is no system tray icon displayed.
1960 void MessageWin::closeEvent(QCloseEvent* ce)
1962 // Don't prompt or delete the alarm from the display calendar if the session is closing
1963 if (!mErrorWindow && !qApp->isSavingSession())
1965 if (mConfirmAck && !mNoCloseConfirm)
1967 // Ask for confirmation of acknowledgement. Use warningYesNo() because its default is No.
1968 if (KAMessageBox::warningYesNo(this, i18nc("@info", "Do you really want to acknowledge this alarm?"),
1969 i18nc("@action:button", "Acknowledge Alarm"), KGuiItem(i18nc("@action:button", "Acknowledge")), KStandardGuiItem::cancel())
1970 != KMessageBox::Yes)
1972 ce->ignore();
1973 return;
1976 if (!mEventId.isEmpty())
1978 // Delete from the display calendar
1979 KAlarm::deleteDisplayEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING));
1982 MainWindowBase::closeEvent(ce);
1985 /******************************************************************************
1986 * Called when the OK button is clicked.
1988 void MessageWin::slotOk()
1990 if (mDontShowAgainCheck && mDontShowAgainCheck->isChecked())
1991 KAlarm::setDontShowErrors(mEventId, mDontShowAgain);
1992 close();
1995 /******************************************************************************
1996 * Called when the KMail button is clicked.
1997 * Tells KMail to display the email message displayed in this message window.
1999 void MessageWin::slotShowKMailMessage()
2001 qCDebug(KALARM_LOG);
2002 if (!mKMailSerialNumber)
2003 return;
2004 const QString err = KAlarm::runKMail(false);
2005 if (!err.isNull())
2007 KAMessageBox::sorry(this, err);
2008 return;
2010 org::kde::kmail::kmail kmail(KMAIL_DBUS_SERVICE, KMAIL_DBUS_PATH, QDBusConnection::sessionBus());
2011 QDBusReply<bool> reply = kmail.showMail((qint64)mKMailSerialNumber);
2012 if (!reply.isValid())
2013 qCCritical(KALARM_LOG) << "kmail D-Bus call failed:" << reply.error().message();
2014 else if (!reply.value())
2015 KAMessageBox::sorry(this, xi18nc("@info", "Unable to locate this email in <application>KMail</application>"));
2018 /******************************************************************************
2019 * Called when the Edit... button is clicked.
2020 * Displays the alarm edit dialog.
2022 * NOTE: The alarm edit dialog is made a child of the main window, not this
2023 * window, so that if this window closes before the dialog (e.g. on
2024 * auto-close), KAlarm doesn't crash. The dialog is set non-modal so that
2025 * the main window is unaffected, but modal mode is simulated so that
2026 * this window is inactive while the dialog is open.
2028 void MessageWin::slotEdit()
2030 qCDebug(KALARM_LOG);
2031 MainWindow* mainWin = MainWindow::mainMainWindow();
2032 mEditDlg = EditAlarmDlg::create(false, &mOriginalEvent, false, mainWin, EditAlarmDlg::RES_IGNORE);
2033 KWindowSystem::setMainWindow(mEditDlg, winId());
2034 KWindowSystem::setOnAllDesktops(mEditDlg->winId(), false);
2035 setButtonsReadOnly(true);
2036 connect(mEditDlg, &QDialog::accepted, this, &MessageWin::editCloseOk);
2037 connect(mEditDlg, &QDialog::rejected, this, &MessageWin::editCloseCancel);
2038 connect(mEditDlg, &QObject::destroyed, this, &MessageWin::editCloseCancel);
2039 connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, this, &MessageWin::activeWindowChanged);
2040 mainWin->editAlarm(mEditDlg, mOriginalEvent);
2043 /******************************************************************************
2044 * Called when OK is clicked in the alarm edit dialog invoked by the Edit button.
2045 * Closes the window.
2047 void MessageWin::editCloseOk()
2049 mEditDlg = Q_NULLPTR;
2050 mNoCloseConfirm = true; // allow window to close without confirmation prompt
2051 close();
2054 /******************************************************************************
2055 * Called when Cancel is clicked in the alarm edit dialog invoked by the Edit
2056 * button, or when the dialog is deleted.
2058 void MessageWin::editCloseCancel()
2060 mEditDlg = Q_NULLPTR;
2061 setButtonsReadOnly(false);
2064 /******************************************************************************
2065 * Called when the active window has changed. If this window has become the
2066 * active window and there is an alarm edit dialog, simulate a modal dialog by
2067 * making the alarm edit dialog the active window instead.
2069 void MessageWin::activeWindowChanged(WId win)
2071 if (mEditDlg && win == winId())
2072 KWindowSystem::activateWindow(mEditDlg->winId());
2075 /******************************************************************************
2076 * Set or clear the read-only state of the dialog buttons.
2078 void MessageWin::setButtonsReadOnly(bool ro)
2080 mOkButton->setReadOnly(ro, true);
2081 mDeferButton->setReadOnly(ro, true);
2082 mEditButton->setReadOnly(ro, true);
2083 if (mSilenceButton)
2084 mSilenceButton->setReadOnly(ro, true);
2085 if (mKMailButton)
2086 mKMailButton->setReadOnly(ro, true);
2087 mKAlarmButton->setReadOnly(ro, true);
2090 /******************************************************************************
2091 * Set up to disable the defer button when the deferral limit is reached.
2093 void MessageWin::setDeferralLimit(const KAEvent& event)
2095 mDeferLimit = event.deferralLimit().effectiveKDateTime().toUtc().dateTime();
2096 MidnightTimer::connect(this, SLOT(checkDeferralLimit())); // check every day
2097 mDisableDeferral = false;
2098 checkDeferralLimit();
2101 /******************************************************************************
2102 * Check whether the deferral limit has been reached.
2103 * If so, disable the Defer button.
2104 * N.B. Ideally, just a single QTimer::singleShot() call would be made to disable
2105 * the defer button at the corret time. But for a 32-bit integer, the
2106 * milliseconds parameter overflows in about 25 days, so instead a daily
2107 * check is done until the day when the deferral limit is reached, followed
2108 * by a non-overflowing QTimer::singleShot() call.
2110 void MessageWin::checkDeferralLimit()
2112 if (!mDeferButton->isEnabled() || !mDeferLimit.isValid())
2113 return;
2114 int n = KDateTime::currentLocalDate().daysTo(KDateTime(mDeferLimit, KDateTime::LocalZone).date());
2115 if (n > 0)
2116 return;
2117 MidnightTimer::disconnect(this, SLOT(checkDeferralLimit()));
2118 if (n == 0)
2120 // The deferral limit will be reached today
2121 n = KDateTime::currentUtcDateTime().dateTime().secsTo(mDeferLimit);
2122 if (n > 0)
2124 QTimer::singleShot(n * 1000, this, &MessageWin::checkDeferralLimit);
2125 return;
2128 mDeferButton->setEnabled(false);
2129 mDisableDeferral = true;
2132 /******************************************************************************
2133 * Called when the Defer... button is clicked.
2134 * Displays the defer message dialog.
2136 void MessageWin::slotDefer()
2138 mDeferDlg = new DeferAlarmDlg(KDateTime::currentDateTime(Preferences::timeZone()).addSecs(60), mDateTime.isDateOnly(), false, this);
2139 mDeferDlg->setObjectName(QStringLiteral("DeferDlg")); // used by LikeBack
2140 mDeferDlg->setDeferMinutes(mDefaultDeferMinutes > 0 ? mDefaultDeferMinutes : Preferences::defaultDeferTime());
2141 mDeferDlg->setLimit(mEvent);
2142 if (!Preferences::modalMessages())
2143 lower();
2144 if (mDeferDlg->exec() == QDialog::Accepted)
2146 const DateTime dateTime = mDeferDlg->getDateTime();
2147 const int delayMins = mDeferDlg->deferMinutes();
2148 // Fetch the up-to-date alarm from the calendar. Note that it could have
2149 // changed since it was displayed.
2150 const KAEvent* event = mEventId.isEmpty() ? Q_NULLPTR : AlarmCalendar::resources()->event(mEventId);
2151 if (event)
2153 // The event still exists in the active calendar
2154 qCDebug(KALARM_LOG) << "Deferring event" << mEventId;
2155 KAEvent newev(*event);
2156 newev.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true);
2157 newev.setDeferDefaultMinutes(delayMins);
2158 KAlarm::updateEvent(newev, mDeferDlg, true);
2159 if (newev.deferred())
2160 mNoPostAction = true;
2162 else
2164 // Try to retrieve the event from the displaying or archive calendars
2165 Akonadi::Collection collection;
2166 KAEvent event;
2167 bool showEdit, showDefer;
2168 if (!retrieveEvent(event, collection, showEdit, showDefer))
2170 // The event doesn't exist any more !?!, so recurrence data,
2171 // flags, and more, have been lost.
2172 KAMessageBox::error(this, xi18nc("@info", "<para>Cannot defer alarm:</para><para>Alarm not found.</para>"));
2173 raise();
2174 delete mDeferDlg;
2175 mDeferDlg = Q_NULLPTR;
2176 mDeferButton->setEnabled(false);
2177 mEditButton->setEnabled(false);
2178 return;
2180 qCDebug(KALARM_LOG) << "Deferring retrieved event" << mEventId;
2181 event.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true);
2182 event.setDeferDefaultMinutes(delayMins);
2183 event.setCommandError(mCommandError);
2184 // Add the event back into the calendar file, retaining its ID
2185 // and not updating KOrganizer.
2186 KAlarm::addEvent(event, &collection, mDeferDlg, KAlarm::USE_EVENT_ID);
2187 if (event.deferred())
2188 mNoPostAction = true;
2189 // Finally delete it from the archived calendar now that it has
2190 // been reactivated.
2191 event.setCategory(CalEvent::ARCHIVED);
2192 KAlarm::deleteEvent(event, false);
2194 if (theApp()->wantShowInSystemTray())
2196 // Alarms are to be displayed only if the system tray icon is running,
2197 // so start it if necessary so that the deferred alarm will be shown.
2198 theApp()->displayTrayIcon(true);
2200 mNoCloseConfirm = true; // allow window to close without confirmation prompt
2201 close();
2203 else
2204 raise();
2205 delete mDeferDlg;
2206 mDeferDlg = Q_NULLPTR;
2209 /******************************************************************************
2210 * Called when the KAlarm icon button in the message window is clicked.
2211 * Displays the main window, with the appropriate alarm selected.
2213 void MessageWin::displayMainWindow()
2215 KAlarm::displayMainWindowSelected(mEventItemId);
2218 /******************************************************************************
2219 * Check whether the specified error message is already displayed for this
2220 * alarm, and note that it will now be displayed.
2221 * Reply = true if message is already displayed.
2223 bool MessageWin::haveErrorMessage(unsigned msg) const
2225 if (!mErrorMessages.contains(mEventId))
2226 mErrorMessages.insert(mEventId, 0);
2227 const bool result = (mErrorMessages[mEventId] & msg);
2228 mErrorMessages[mEventId] |= msg;
2229 return result;
2232 void MessageWin::clearErrorMessage(unsigned msg) const
2234 if (mErrorMessages.contains(mEventId))
2236 if (mErrorMessages[mEventId] == msg)
2237 mErrorMessages.remove(mEventId);
2238 else
2239 mErrorMessages[mEventId] &= ~msg;
2244 /******************************************************************************
2245 * Check whether the message window should be modal, i.e. with title bar etc.
2246 * Normally this follows the Preferences setting, but if there is a full screen
2247 * window displayed, on X11 the message window has to bypass the window manager
2248 * in order to display on top of it (which has the side effect that it will have
2249 * no window decoration).
2251 * Also find the usable area of the desktop (excluding panel etc.), on the
2252 * appropriate screen if there are multiple screens.
2254 bool MessageWin::getWorkAreaAndModal()
2256 mScreenNumber = -1;
2257 const bool modal = Preferences::modalMessages();
2258 #if KDEPIM_HAVE_X11
2259 const QDesktopWidget* desktop = qApp->desktop();
2260 const int numScreens = desktop->numScreens();
2261 if (numScreens > 1)
2263 // There are multiple screens.
2264 // Check for any full screen windows, even if they are not the active
2265 // window, and try not to show the alarm message their screens.
2266 mScreenNumber = desktop->screenNumber(MainWindow::mainMainWindow()); // default = KAlarm's screen
2267 if (desktop->isVirtualDesktop())
2269 // The screens form a single virtual desktop.
2270 // Xinerama, for example, uses this scheme.
2271 QVector<FullScreenType> screenTypes(numScreens);
2272 QVector<QRect> screenRects(numScreens);
2273 for (int s = 0; s < numScreens; ++s)
2274 screenRects[s] = desktop->screenGeometry(s);
2275 const FullScreenType full = findFullScreenWindows(screenRects, screenTypes);
2276 if (full == NoFullScreen || screenTypes[mScreenNumber] == NoFullScreen)
2277 return modal;
2278 for (int s = 0; s < numScreens; ++s)
2280 if (screenTypes[s] == NoFullScreen)
2283 // There is no full screen window on this screen
2284 mScreenNumber = s;
2285 return modal;
2288 // All screens contain a full screen window: use one without
2289 // an active full screen window.
2290 for (int s = 0; s < numScreens; ++s)
2292 if (screenTypes[s] == FullScreen)
2294 mScreenNumber = s;
2295 return modal;
2299 else
2301 // The screens are completely separate from each other.
2302 int inactiveScreen = -1;
2303 FullScreenType full = haveFullScreenWindow(mScreenNumber);
2304 qCDebug(KALARM_LOG)<<"full="<<full<<", screen="<<mScreenNumber;
2305 if (full == NoFullScreen)
2306 return modal; // KAlarm's screen doesn't contain a full screen window
2307 if (full == FullScreen)
2308 inactiveScreen = mScreenNumber;
2309 for (int s = 0; s < numScreens; ++s)
2311 if (s != mScreenNumber)
2313 full = haveFullScreenWindow(s);
2314 if (full == NoFullScreen)
2316 // There is no full screen window on this screen
2317 mScreenNumber = s;
2318 return modal;
2320 if (full == FullScreen && inactiveScreen < 0)
2321 inactiveScreen = s;
2324 if (inactiveScreen >= 0)
2326 // All screens contain a full screen window: use one without
2327 // an active full screen window.
2328 mScreenNumber = inactiveScreen;
2329 return modal;
2332 return false; // can't logically get here, since there can only be one active window...
2334 #endif
2335 if (modal)
2337 const WId activeId = KWindowSystem::activeWindow();
2338 const KWindowInfo wi = KWindowInfo(activeId, NET::WMState);
2339 if (wi.valid() && wi.hasState(NET::FullScreen))
2340 return false; // the active window is full screen.
2342 return modal;
2345 #if KDEPIM_HAVE_X11
2346 /******************************************************************************
2347 * In a multi-screen setup (not a single virtual desktop), find whether the
2348 * specified screen has a full screen window on it.
2350 FullScreenType haveFullScreenWindow(int screen)
2352 FullScreenType type = NoFullScreen;
2353 Display* display = QX11Info::display();
2354 const NETRootInfo rootInfo(display, NET::ClientList | NET::ActiveWindow, screen);
2355 const Window rootWindow = rootInfo.rootWindow();
2356 const Window activeWindow = rootInfo.activeWindow();
2357 const Window* windows = rootInfo.clientList();
2358 const int windowCount = rootInfo.clientListCount();
2359 qCDebug(KALARM_LOG)<<"Screen"<<screen<<": Window count="<<windowCount<<", active="<<activeWindow<<", geom="<<qApp->desktop()->screenGeometry(screen);
2360 NETRect geom;
2361 NETRect frame;
2362 for (int w = 0; w < windowCount; ++w)
2364 NETWinInfo winInfo(display, windows[w], rootWindow, NET::WMState|NET::WMGeometry);
2365 winInfo.kdeGeometry(frame, geom);
2366 const QRect fr(frame.pos.x, frame.pos.y, frame.size.width, frame.size.height);
2367 const QRect gm(geom.pos.x, geom.pos.y, geom.size.width, geom.size.height);
2368 if (winInfo.state() & NET::FullScreen)
2370 qCDebug(KALARM_LOG)<<"Found FULL SCREEN: "<<windows[w]<<", geom="<<gm<<", frame="<<fr;
2371 type = FullScreen;
2372 if (windows[w] == activeWindow)
2373 return FullScreenActive;
2375 //else { qCDebug(KALARM_LOG)<<"Found normal: "<<windows[w]<<", geom="<<gm<<", frame="<<fr; }
2377 return type;
2380 /******************************************************************************
2381 * In a multi-screen setup (single virtual desktop, e.g. Xinerama), find which
2382 * screens have full screen windows on them.
2384 FullScreenType findFullScreenWindows(const QVector<QRect>& screenRects, QVector<FullScreenType>& screenTypes)
2386 FullScreenType result = NoFullScreen;
2387 screenTypes.fill(NoFullScreen);
2388 Display* display = QX11Info::display();
2389 const NETRootInfo rootInfo(display, NET::ClientList | NET::ActiveWindow, 0);
2390 const Window rootWindow = rootInfo.rootWindow();
2391 const Window activeWindow = rootInfo.activeWindow();
2392 const Window* windows = rootInfo.clientList();
2393 const int windowCount = rootInfo.clientListCount();
2394 qCDebug(KALARM_LOG)<<"Virtual desktops: Window count="<<windowCount<<", active="<<activeWindow<<", geom="<<qApp->desktop()->screenGeometry(0);
2395 NETRect netgeom;
2396 NETRect netframe;
2397 for (int w = 0; w < windowCount; ++w)
2399 NETWinInfo winInfo(display, windows[w], rootWindow, NET::WMState | NET::WMGeometry);
2400 if (winInfo.state() & NET::FullScreen)
2402 // Found a full screen window - find which screen it's on
2403 const bool active = (windows[w] == activeWindow);
2404 winInfo.kdeGeometry(netframe, netgeom);
2405 const QRect winRect(netgeom.pos.x, netgeom.pos.y, netgeom.size.width, netgeom.size.height);
2406 qCDebug(KALARM_LOG)<<"Found FULL SCREEN: "<<windows[w]<<", geom="<<winRect;
2407 for (int s = 0, count = screenRects.count(); s < count; ++s)
2409 if (screenRects[s].contains(winRect))
2411 qCDebug(KALARM_LOG)<<"FULL SCREEN on screen"<<s<<", active="<<active;
2412 if (active)
2413 screenTypes[s] = result = FullScreenActive;
2414 else
2416 if (screenTypes[s] == NoFullScreen)
2417 screenTypes[s] = FullScreen;
2418 if (result == NoFullScreen)
2419 result = FullScreen;
2421 break;
2426 return result;
2428 #endif
2430 #include "moc_messagewin_p.cpp"
2431 #include "moc_messagewin.cpp"
2433 // vim: et sw=4: