2 * messagewin.cpp - displays an alarm message
4 * Copyright © 2001-2014 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 //QT5 reactivate after porting #include "config-kdepim.h"
23 #include "messagewin_p.h"
24 #include "messagewin.h"
26 #include "alarmcalendar.h"
27 #include "autoqpointer.h"
28 #include "collectionmodel.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 <PimCommon/TextToSpeech>
42 //QT5 reactivate after porting (activated by config-kdepim.h include in texttospeech.h)
43 #undef KDEPIM_HAVE_X11
45 #include <K4AboutData>
47 #include <kstandardguiitem.h>
48 #include <KLocalizedString>
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>
63 #include <qx11info_x11.h>
66 #include <qtextbrowser.h>
67 #include <QPushButton>
69 #include <QtDBus/QtDBus>
79 #include <QGridLayout>
80 #include <QVBoxLayout>
81 #include <QHBoxLayout>
82 #include <QResizeEvent>
83 #include <QCloseEvent>
84 #include <QDesktopWidget>
85 #include <QMutexLocker>
86 #include <QMimeDatabase>
88 #include "kalarm_debug.h"
93 using namespace KCalCore
;
94 using namespace KAlarmCal
;
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
);
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
115 MessageText(QWidget
* parent
= Q_NULLPTR
)
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
; }
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
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
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()),
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()),
189 mAudioRepeatPause(event
->repeatSoundPause()),
190 mConfirmAck(event
->confirmAck()),
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
),
205 mDeferDlg(Q_NULLPTR
),
206 mAlwaysHide(flags
& ALWAYS_HIDE
),
209 mNoPostAction(alarm
.type() & KAAlarm::REMINDER_ALARM
),
211 mBeep(event
->beep()),
212 mSpeak(event
->speak()),
213 mRescheduleEvent(!(flags
& NO_RESCHEDULE
)),
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);
232 mDateTime
= event
->mainDateTime(true);
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();
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);
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
))
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
)
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()),
292 mAlarmType(KAAlarm::MAIN_ALARM
),
293 mAction(event
->actionSubType()),
294 mKMailSerialNumber(0),
295 mCommandError(KAEvent::CMD_NO_ERROR
),
297 mDontShowAgain(dontShowAgain
),
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
),
314 mDeferDlg(Q_NULLPTR
),
320 mRescheduleEvent(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();
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
),
350 mDeferDlg(Q_NULLPTR
),
355 mRescheduleEvent(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);
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())
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("^(<[^>]+>)*"));
426 s
.insert(re
.matchedLength(), mTimeLabel
->text() + QLatin1String("<br/>"));
427 mTimeLabel
->setText(s
);
428 mTimeLabel
->setAlignment(Qt::AlignHCenter
);
436 // It's a normal alarm message window
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();
458 if (exists
&& !isDir
) {
459 auto job
= KIO::storedGet(url
);
460 KJobWidgets::setWindow(job
, MainWindow::mainMainWindow());
463 const QByteArray data
= job
->data();
464 QTemporaryFile tmpFile
;
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
);
477 QMimeType mime
= db
.mimeTypeForUrl(url
);
478 if (mime
.name() == QLatin1String("application/octet-stream"))
479 mime
= db
.mimeTypeForData(&tmpFile
);
480 switch (KAlarm::fileType(mime
))
483 view
->setHtml(QLatin1String("<img source=\"") + tmpFile
.fileName() + QLatin1String("\">"));
485 case KAlarm::TextFormatted
:
486 view
->QTextBrowser::setSource(QUrl::fromLocalFile(tmpFile
.fileName())); //krazy:exclude=qclasses
490 view
->setPlainText(QString::fromUtf8(data
));
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");
512 case KAEvent::MESSAGE
:
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
);
538 QHBoxLayout
* layout
= new QHBoxLayout();
539 layout
->addSpacing(hspace
);
540 layout
->addWidget(text
, 1, Qt::AlignHCenter
);
541 layout
->addSpacing(hspace
);
542 topLayout
->addLayout(layout
);
545 topLayout
->addStretch();
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
*)));
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
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();
589 // It's an error message
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
);
618 case KAEvent::COMMAND
:
620 case KAEvent::MESSAGE
:
622 // Just display the error message strings
627 if (!mErrorMsgs
.count())
629 topWidget
->setAutoFillBackground(true);
630 QPalette palette
= topWidget
->palette();
631 palette
.setColor(topWidget
->backgroundRole(), mBgColour
);
632 topWidget
->setPalette(palette
);
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(QLatin1String("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
);
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
, SIGNAL(clicked()), SLOT(slotOk()));
674 grid
->addWidget(mOkButton
, 0, gridIndex
++, Qt::AlignHCenter
);
675 mOkButton
->setWhatsThis(i18nc("@info:whatsthis", "Acknowledge the alarm"));
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
, SIGNAL(clicked()), SLOT(slotEdit()));
684 grid
->addWidget(mEditButton
, 0, gridIndex
++, Qt::AlignHCenter
);
685 mEditButton
->setWhatsThis(i18nc("@info:whatsthis", "Edit the alarm."));
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
, SIGNAL(clicked()), SLOT(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>"));
698 mDeferButton
->hide();
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
)
719 const QPixmap pixmap
= iconLoader
.loadIcon(QStringLiteral("internet-mail"), KIconLoader::MainToolbar
);
720 mKMailButton
= new PushButton(topWidget
);
721 mKMailButton
->setIcon(pixmap
);
722 connect(mKMailButton
, SIGNAL(clicked()), SLOT(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>"));
729 const QPixmap pixmap
= iconLoader
.loadIcon(KComponentData::mainComponent().aboutData()->appName(), KIconLoader::MainToolbar
);
730 mKAlarmButton
= new PushButton(topWidget
);
731 mKAlarmButton
->setIcon(pixmap
);
732 connect(mKAlarmButton
, SIGNAL(clicked()), SLOT(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();
739 butsize
= qMax(butsize
, mSilenceButton
->sizeHint().height());
741 butsize
= qMax(butsize
, mKMailButton
->sizeHint().height());
742 mKAlarmButton
->setFixedSize(butsize
, butsize
);
744 mSilenceButton
->setFixedSize(butsize
, butsize
);
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);
754 mEditButton
->setEnabled(false);
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
;
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
)
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()
801 mDeferButton
->show();
802 setDeferralLimit(mEvent
); // ensure that button is disabled when alarm can't be deferred any more
807 /******************************************************************************
808 * Convert a reminder window into a normal alarm window.
810 void MessageWin::cancelReminder(const KAEvent
& event
, const KAAlarm
& alarm
)
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());
822 mRemainingText
->hide();
823 MidnightTimer::disconnect(this, SLOT(setRemainingTextDay()));
824 MinuteTimer::disconnect(this, SLOT(setRemainingTextMinute()));
826 centralWidget()->layout()->activate();
827 setMinimumHeight(sizeHint().height());
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
)
839 mDateTime
= (alarm
.type() & KAAlarm::REMINDER_ALARM
) ? event
.mainDateTime(true) : alarm
.dateTime(true);
840 if (mDateTime
.isValid())
842 mTimeLabel
->setText(dateTimeToDisplay());
847 /******************************************************************************
848 * Get the trigger time to display.
850 QString
MessageWin::dateTimeToDisplay()
853 if (mDateTime
.isValid())
855 if (mDateTime
.isDateOnly())
856 tm
= KLocale::global()->formatDate(mDateTime
.date(), KLocale::ShortDate
);
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));
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()
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
896 text
= i18nc("@info", "Today");
898 text
= i18ncp("@info", "Tomorrow", "in %1 days' time", days
);
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()
912 const int mins
= (KDateTime::currentUtcDateTime().secsTo(mDateTime
.effectiveKDateTime()) + 59) / 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);
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();
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
));
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());
970 if (mDateTime
.isUtc())
971 zone
= QStringLiteral("UTC");
974 const KTimeZone tz
= mDateTime
.timeZone();
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
);
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
)
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");
1028 mDateTime
= KDateTime(dt
, KDateTime::ClockTime
);
1029 else if (zone
== QStringLiteral("UTC"))
1031 dt
.setTimeSpec(Qt::UTC
);
1032 mDateTime
= KDateTime(dt
, KDateTime::UTC
);
1036 KTimeZone tz
= KSystemTimeZones::zone(zone
);
1037 mDateTime
= KDateTime(dt
, (tz
.isValid() ? tz
: KSystemTimeZones::local()));
1039 const bool dateOnly
= config
.readEntry("DateOnly", false);
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;
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());
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())
1071 // Close any other window for this alarm which has already been restored by redisplayAlarms()
1072 if (!AkonadiModel::instance()->isCollectionTreeFetched())
1074 connect(AkonadiModel::instance(), SIGNAL(collectionTreeFetched(Akonadi::Collection::List
)),
1075 SLOT(showRestoredAlarm()));
1083 /******************************************************************************
1084 * Fetch the restored alarm from the calendar and redisplay it in this window.
1086 void MessageWin::showRestoredAlarm()
1088 qCDebug(KALARM_LOG
) << mEventId
;
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);
1104 qCDebug(KALARM_LOG
) << "Deleting duplicate window:" << mEventId
;
1107 KAEvent
* event
= AlarmCalendar::resources()->event(mEventId
);
1115 // It's not in the active calendar, so try the displaying or archive calendars
1116 retrieveEvent(mEvent
, mCollection
, mShowEdit
, mNoDefer
);
1117 mNoDefer
= !mNoDefer
;
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()
1132 qCDebug(KALARM_LOG
);
1133 mRedisplayed
= true;
1134 AlarmCalendar
* cal
= AlarmCalendar::displayCalendar();
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
);
1146 event
.setItemId(id
);
1147 const EventId
eventId(event
);
1148 if (findEvent(eventId
))
1149 qCDebug(KALARM_LOG
) << "Message window already exists:" << eventId
;
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
;
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;
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
)));
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();
1200 qCDebug(KALARM_LOG
) << event
.id() << ": success";
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
)
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";
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
;
1237 // Copy the alarm to the displaying calendar in case of a crash, etc.
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();
1245 cal
->deleteDisplayEvent(dispEvent
.id()); // in case it already exists
1246 cal
->addEvent(dispEvent
);
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
1263 const QRect desk
= KAlarm::desktopWorkArea(); // get the usable area of the desktop
1264 if (scatter
== isSpread(desk
.topLeft()))
1269 // Usually there won't be many windows, so a crude
1270 // scattering algorithm should suffice.
1271 int x
= desk
.left();
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
))
1284 const QSize sz
= w
->frameGeometry().size();
1285 if (x
+ sz
.width() > desk
.right())
1291 if (y
+ sz
.height() > desk
.bottom())
1293 ytmp
= desk
.bottom() - sz
.height();
1294 if (ytmp
< desk
.top())
1299 if (ytmp
+ sz
.height() > ynext
)
1300 ynext
= ytmp
+ sz
.height();
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());
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
)
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
)
1345 /******************************************************************************
1346 * Beep and play the audio file, as appropriate.
1348 void MessageWin::playAudio()
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
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, SLOT(slotSpeak()));
1370 /******************************************************************************
1371 * Speak the message.
1372 * Called asynchronously to avoid delaying the display of the message.
1374 void MessageWin::slotSpeak()
1376 PimCommon::TextToSpeech
*tts
= PimCommon::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
);
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()
1396 // An audio file is already playing for another message
1397 // window, so wait until it has finished.
1398 connect(mAudioThread
, SIGNAL(destroyed(QObject
*)), SLOT(audioTerminating()));
1402 qCDebug(KALARM_LOG
) << QThread::currentThread();
1403 mAudioThread
= new AudioThread(this, mAudioFile
, mVolume
, mFadeVolume
, mFadeSeconds
, mAudioRepeatPause
);
1404 connect(mAudioThread
, SIGNAL(readyToPlay()), SLOT(playReady()));
1405 connect(mAudioThread
, SIGNAL(finished()), SLOT(playFinished()));
1407 connect(mSilenceButton
, SIGNAL(clicked()), mAudioThread
, SLOT(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
);
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, SLOT(startAudio()));
1442 /******************************************************************************
1443 * Called when the audio file is ready to start playing.
1445 void MessageWin::playReady()
1448 mSilenceButton
->setEnabled(true);
1451 /******************************************************************************
1452 * Called when the audio file thread finishes.
1454 void MessageWin::playFinished()
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();
1472 /******************************************************************************
1473 * Constructor for audio thread.
1475 AudioThread::AudioThread(MessageWin
* parent
, const QString
& audioFile
, float volume
, float fadeVolume
, int fadeSeconds
, int repeatPause
)
1479 mFadeVolume(fadeVolume
),
1480 mFadeSeconds(fadeSeconds
),
1481 mRepeatPause(repeatPause
),
1482 mAudioObject(Q_NULLPTR
)
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(), SLOT(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)
1516 // Something has gone wrong - forcibly kill the thread
1523 /******************************************************************************
1524 * Kick off the thread to play the audio file.
1526 void AudioThread::run()
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
);
1543 qCCritical(KALARM_LOG
) << "Open failure:" << audioFile
;
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
, SIGNAL(stateChanged(Phonon::State
,Phonon::State
)), SLOT(playStateChanged(Phonon::State
)), Qt::DirectConnection
);
1565 connect(mAudioObject
, SIGNAL(finished()), SLOT(checkAudioPlay()), Qt::DirectConnection
);
1566 mPlayedOnce
= false;
1569 Q_EMIT
readyToPlay();
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, SIGNAL(finished()), this, SLOT(deleteLater()));
1580 /******************************************************************************
1581 * Called when the audio file has loaded and is ready to play, or when play
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()
1598 // The file has loaded and is ready to play, or play has completed
1601 if (mRepeatPause
< 0)
1603 // Play has completed
1608 if (mRepeatPause
> 0)
1610 // Pause before playing the file again
1612 QTimer::singleShot(mRepeatPause
* 1000, this, SLOT(checkAudioPlay()));
1620 // Start playing the file, either for the first time or again
1621 qCDebug(KALARM_LOG
) << "start";
1622 mAudioObject
->play();
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();
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
);
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()
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
]);
1661 delete mAudioObject
;
1662 mAudioObject
= Q_NULLPTR
;
1665 quit(); // exit the event loop, if it's still running
1668 QString
AudioThread::error() const
1670 QMutexLocker
locker(&mMutex
);
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
)
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.
1687 mDeferDlg
= Q_NULLPTR
;
1689 KAEvent
* event
= mEventId
.isEmpty() ? Q_NULLPTR
: AlarmCalendar::resources()->event(mEventId
);
1692 mAlarmType
= alarm
.type(); // store new alarm type for use if it is later deferred
1697 if (!mDeferDlg
|| Preferences::modalMessages())
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
);
1726 QTimer::singleShot(delay
* 1000, this, SLOT(close()));
1728 return; // don't show the window if auto-closing is already due
1730 if (Preferences::messageButtonDelay() == 0)
1732 MainWindowBase::show();
1735 /******************************************************************************
1736 * Returns the window's recommended size exclusive of its frame.
1738 QSize
MessageWin::sizeHint() const
1743 case KAEvent::MESSAGE
:
1744 desired
= MainWindowBase::sizeHint();
1746 case KAEvent::COMMAND
:
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
);
1754 const int ypadding
= height() - mCommandText
->height();
1755 desired
= QSize(w
, texthint
.height() + ypadding
);
1758 // fall through to 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
)
1779 if (mErrorWindow
|| mAlarmType
== KAAlarm::INVALID_ALARM
)
1781 // Don't bother repositioning error messages,
1782 // and invalid alarms should be deleted anyway.
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
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
);
1798 const QRect desk
= KAlarm::desktopWorkArea(mScreenNumber
);
1799 const QRect frame
= frameGeometry();
1801 mButtonDelay
= Preferences::messageButtonDelay() * 1000;
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;
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;
1852 execComplete
= false;
1856 displayComplete(); // play audio, etc.
1859 // Set the window size etc. once the frame size is known
1860 QTimer::singleShot(0, this, SLOT(frameDrawn()));
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()));
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())
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()
1903 if (mRescheduleEvent
)
1904 alarmShowing(mEvent
);
1908 // Enable the window's buttons either now or after the configured delay
1909 if (mButtonDelay
> 0)
1910 QTimer::singleShot(mButtonDelay
, this, SLOT(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);
1926 mEditButton
->setEnabled(true);
1928 mKMailButton
->setEnabled(true);
1931 /******************************************************************************
1932 * Called when the window's size has changed (before it is painted).
1934 void MessageWin::resizeEvent(QResizeEvent
* re
)
1938 // Restore the window height on session restoration
1939 if (mRestoreHeight
!= re
->size().height())
1941 QSize size
= re
->size();
1942 size
.setHeight(mRestoreHeight
);
1945 else if (isVisible())
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
&& !theApp()->sessionClosingDown())
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
)
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
);
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
)
2004 const QString err
= KAlarm::runKMail(false);
2007 KAMessageBox::sorry(this, err
);
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
, SIGNAL(accepted()), SLOT(editCloseOk()));
2037 connect(mEditDlg
, SIGNAL(rejected()), SLOT(editCloseCancel()));
2038 connect(mEditDlg
, SIGNAL(destroyed(QObject
*)), SLOT(editCloseCancel()));
2039 connect(KWindowSystem::self(), SIGNAL(activeWindowChanged(WId
)), SLOT(activeWindowChanged(WId
)));
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
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);
2084 mSilenceButton
->setReadOnly(ro
, true);
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())
2114 int n
= KDateTime::currentLocalDate().daysTo(KDateTime(mDeferLimit
, KDateTime::LocalZone
).date());
2117 MidnightTimer::disconnect(this, SLOT(checkDeferralLimit()));
2120 // The deferral limit will be reached today
2121 n
= KDateTime::currentUtcDateTime().dateTime().secsTo(mDeferLimit
);
2124 QTimer::singleShot(n
* 1000, this, SLOT(checkDeferralLimit()));
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())
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
);
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;
2164 // Try to retrieve the event from the displaying or archive calendars
2165 Akonadi::Collection collection
;
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>"));
2175 mDeferDlg
= Q_NULLPTR
;
2176 mDeferButton
->setEnabled(false);
2177 mEditButton
->setEnabled(false);
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
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
;
2232 void MessageWin::clearErrorMessage(unsigned msg
) const
2234 if (mErrorMessages
.contains(mEventId
))
2236 if (mErrorMessages
[mEventId
] == msg
)
2237 mErrorMessages
.remove(mEventId
);
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()
2257 const bool modal
= Preferences::modalMessages();
2259 const QDesktopWidget
* desktop
= qApp
->desktop();
2260 const int numScreens
= desktop
->numScreens();
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
)
2278 for (int s
= 0; s
< numScreens
; ++s
)
2280 if (screenTypes
[s
] == NoFullScreen
)
2283 // There is no full screen window on this screen
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
)
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
2320 if (full
== FullScreen
&& inactiveScreen
< 0)
2324 if (inactiveScreen
>= 0)
2326 // All screens contain a full screen window: use one without
2327 // an active full screen window.
2328 mScreenNumber
= inactiveScreen
;
2332 return false; // can't logically get here, since there can only be one active window...
2337 const WId activeId
= KWindowSystem::activeWindow();
2338 const KWindowInfo wi
= KWindowSystem::windowInfo(activeId
, NET::WMState
);
2339 if (wi
.valid() && wi
.hasState(NET::FullScreen
))
2340 return false; // the active window is full screen.
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
);
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
;
2372 if (windows
[w
] == activeWindow
)
2373 return FullScreenActive
;
2375 //else { qCDebug(KALARM_LOG)<<"Found normal: "<<windows[w]<<", geom="<<gm<<", frame="<<fr; }
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);
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
;
2413 screenTypes
[s
] = result
= FullScreenActive
;
2416 if (screenTypes
[s
] == NoFullScreen
)
2417 screenTypes
[s
] = FullScreen
;
2418 if (result
== NoFullScreen
)
2419 result
= FullScreen
;
2430 #include "moc_messagewin_p.cpp"
2431 #include "moc_messagewin.cpp"