Updated the Tools menu to reflect current build.
[kdepim.git] / kalarm / messagewin.cpp
blobd8a4b724a80bf865b9eecf76a112886510a3c26b
1 /*
2 * messagewin.cpp - displays an alarm message
3 * Program: kalarm
4 * Copyright © 2001-2013 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 "kalarm.h"
22 #include "messagewin_p.moc"
23 #include "messagewin.moc"
25 #include "alarmcalendar.h"
26 #include "autoqpointer.h"
27 #ifdef USE_AKONADI
28 #include "collectionmodel.h"
29 #endif
30 #include "deferdlg.h"
31 #include "desktop.h"
32 #include "editdlg.h"
33 #include "functions.h"
34 #include "kalarmapp.h"
35 #include "mainwindow.h"
36 #include "messagebox.h"
37 #include "preferences.h"
38 #include "pushbutton.h"
39 #include "shellprocess.h"
40 #include "synchtimer.h"
42 #include "kspeechinterface.h"
44 #include <kstandarddirs.h>
45 #include <kaction.h>
46 #include <kstandardguiitem.h>
47 #include <kaboutdata.h>
48 #include <klocale.h>
49 #include <kconfig.h>
50 #include <kiconloader.h>
51 #include <kdialog.h>
52 #include <ktextbrowser.h>
53 #include <ksystemtimezone.h>
54 #include <kglobalsettings.h>
55 #include <kmimetype.h>
56 #include <ktextedit.h>
57 #include <kwindowsystem.h>
58 #include <kio/netaccess.h>
59 #include <knotification.h>
60 #include <kpushbutton.h>
61 #include <ksqueezedtextlabel.h>
62 #include <phonon/mediaobject.h>
63 #include <phonon/audiooutput.h>
64 #include <phonon/volumefadereffect.h>
65 #include <kdebug.h>
66 #include <ktoolinvocation.h>
67 #ifdef Q_WS_X11
68 #include <netwm.h>
69 #include <QX11Info>
70 #endif
72 #include <QScrollBar>
73 #include <QtDBus/QtDBus>
74 #include <QFile>
75 #include <QFileInfo>
76 #include <QCheckBox>
77 #include <QLabel>
78 #include <QPalette>
79 #include <QTimer>
80 #include <QPixmap>
81 #include <QByteArray>
82 #include <QFrame>
83 #include <QGridLayout>
84 #include <QVBoxLayout>
85 #include <QHBoxLayout>
86 #include <QResizeEvent>
87 #include <QCloseEvent>
88 #include <QDesktopWidget>
89 #include <QMutexLocker>
91 #include <stdlib.h>
92 #include <string.h>
94 #ifdef USE_AKONADI
95 using namespace KCalCore;
96 #else
97 using namespace KCal;
98 #endif
99 using namespace KAlarmCal;
101 #ifdef Q_WS_X11
102 enum FullScreenType { NoFullScreen = 0, FullScreen = 1, FullScreenActive = 2 };
103 static FullScreenType haveFullScreenWindow(int screen);
104 static FullScreenType findFullScreenWindows(const QVector<QRect>& screenRects, QVector<FullScreenType>& screenTypes);
105 #endif
107 #ifdef KMAIL_SUPPORTED
108 #include "kmailinterface.h"
109 static const char* KMAIL_DBUS_SERVICE = "org.kde.kmail";
110 static const char* KMAIL_DBUS_PATH = "/KMail";
111 #endif
113 // The delay for enabling message window buttons if a zero delay is
114 // configured, i.e. the windows are placed far from the cursor.
115 static const int proximityButtonDelay = 1000; // (milliseconds)
116 static const int proximityMultiple = 10; // multiple of button height distance from cursor for proximity
118 // A text label widget which can be scrolled and copied with the mouse
119 class MessageText : public KTextEdit
121 public:
122 MessageText(QWidget* parent = 0)
123 : KTextEdit(parent),
124 mNewLine(false)
126 setReadOnly(true);
127 setFrameStyle(NoFrame);
128 setLineWrapMode(NoWrap);
130 int scrollBarHeight() const { return horizontalScrollBar()->height(); }
131 int scrollBarWidth() const { return verticalScrollBar()->width(); }
132 void setBackgroundColour(const QColor& c)
134 QPalette pal = viewport()->palette();
135 pal.setColor(viewport()->backgroundRole(), c);
136 viewport()->setPalette(pal);
138 virtual QSize sizeHint() const
140 QSizeF docsize = document()->size();
141 return QSize(static_cast<int>(docsize.width() + 0.99) + verticalScrollBar()->width(),
142 static_cast<int>(docsize.height() + 0.99) + horizontalScrollBar()->height());
144 bool newLine() const { return mNewLine; }
145 void setNewLine(bool nl) { mNewLine = nl; }
146 private:
147 bool mNewLine;
151 // Basic flags for the window
152 static const Qt::WindowFlags WFLAGS = Qt::WindowStaysOnTopHint;
153 static const Qt::WindowFlags WFLAGS2 = Qt::WindowContextHelpButtonHint;
154 static const Qt::WidgetAttribute WidgetFlags = Qt::WA_DeleteOnClose;
156 // Error message bit masks
157 enum {
158 ErrMsg_Speak = 0x01,
159 ErrMsg_AudioFile = 0x02
163 QList<MessageWin*> MessageWin::mWindowList;
164 #ifdef USE_AKONADI
165 QMap<EventId, unsigned> MessageWin::mErrorMessages;
166 #else
167 QMap<QString, unsigned> MessageWin::mErrorMessages;
168 #endif
169 // There can only be one audio thread at a time: trying to play multiple
170 // sound files simultaneously would result in a cacophony, and besides
171 // that, Phonon currently crashes...
172 QPointer<AudioThread> MessageWin::mAudioThread;
173 MessageWin* AudioThread::mAudioOwner = 0;
175 /******************************************************************************
176 * Construct the message window for the specified alarm.
177 * Other alarms in the supplied event may have been updated by the caller, so
178 * the whole event needs to be stored for updating the calendar file when it is
179 * displayed.
181 MessageWin::MessageWin(const KAEvent* event, const KAAlarm& alarm, int flags)
182 : MainWindowBase(0, static_cast<Qt::WindowFlags>(WFLAGS | WFLAGS2 | ((flags & ALWAYS_HIDE) || getWorkAreaAndModal() ? Qt::WindowType(0) : Qt::X11BypassWindowManagerHint))),
183 mMessage(event->cleanText()),
184 mFont(event->font()),
185 mBgColour(event->bgColour()),
186 mFgColour(event->fgColour()),
187 #ifdef USE_AKONADI
188 mEventItemId(event->itemId()),
189 mEventId(*event),
190 #else
191 mEventId(event->id()),
192 #endif
193 mAudioFile(event->audioFile()),
194 mVolume(event->soundVolume()),
195 mFadeVolume(event->fadeVolume()),
196 mFadeSeconds(qMin(event->fadeSeconds(), 86400)),
197 mDefaultDeferMinutes(event->deferDefaultMinutes()),
198 mAlarmType(alarm.type()),
199 mAction(event->actionSubType()),
200 #ifdef KMAIL_SUPPORTED
201 mKMailSerialNumber(event->kmailSerialNumber()),
202 #else
203 mKMailSerialNumber(0),
204 #endif
205 mCommandError(event->commandError()),
206 mRestoreHeight(0),
207 mAudioRepeatPause(event->repeatSoundPause()),
208 mConfirmAck(event->confirmAck()),
209 mNoDefer(true),
210 mInvalid(false),
211 mEvent(*event),
212 mOriginalEvent(*event),
213 #ifdef USE_AKONADI
214 mCollection(AlarmCalendar::resources()->collectionForEvent(mEventItemId)),
215 #else
216 mResource(AlarmCalendar::resources()->resourceForEvent(mEventId)),
217 #endif
218 mTimeLabel(0),
219 mRemainingText(0),
220 mEditButton(0),
221 mDeferButton(0),
222 mSilenceButton(0),
223 mKMailButton(0),
224 mCommandText(0),
225 mDontShowAgainCheck(0),
226 mEditDlg(0),
227 mDeferDlg(0),
228 mAlwaysHide(flags & ALWAYS_HIDE),
229 mErrorWindow(false),
230 mInitialised(false),
231 mNoPostAction(alarm.type() & KAAlarm::REMINDER_ALARM),
232 mRecreating(false),
233 mBeep(event->beep()),
234 mSpeak(event->speak()),
235 mRescheduleEvent(!(flags & NO_RESCHEDULE)),
236 mShown(false),
237 mPositioning(false),
238 mNoCloseConfirm(false),
239 mDisableDeferral(false)
241 kDebug() << "event";
242 setAttribute(static_cast<Qt::WidgetAttribute>(WidgetFlags));
243 setWindowModality(Qt::WindowModal);
244 setObjectName("MessageWin"); // used by LikeBack
245 if (alarm.type() & KAAlarm::REMINDER_ALARM)
247 if (event->reminderMinutes() < 0)
249 event->previousOccurrence(alarm.dateTime(false).effectiveKDateTime(), mDateTime, false);
250 if (!mDateTime.isValid() && event->repeatAtLogin())
251 mDateTime = alarm.dateTime().addSecs(event->reminderMinutes() * 60);
253 else
254 mDateTime = event->mainDateTime(true);
256 else
257 mDateTime = alarm.dateTime(true);
258 if (!(flags & (NO_INIT_VIEW | ALWAYS_HIDE)))
260 #ifdef USE_AKONADI
261 bool readonly = AlarmCalendar::resources()->eventReadOnly(mEventItemId);
262 #else
263 bool readonly = AlarmCalendar::resources()->eventReadOnly(mEventId);
264 #endif
265 mShowEdit = !mEventId.isEmpty() && !readonly;
266 mNoDefer = readonly || (flags & NO_DEFER) || alarm.repeatAtLogin();
267 initView();
269 // Set to save settings automatically, but don't save window size.
270 // File alarm window size is saved elsewhere.
271 setAutoSaveSettings(QLatin1String("MessageWin"), false);
272 mWindowList.append(this);
273 if (event->autoClose())
274 mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().dateTime().addSecs(event->lateCancel() * 60);
275 if (mAlwaysHide)
277 hide();
278 displayComplete(); // play audio, etc.
282 /******************************************************************************
283 * Display an error message window.
284 * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note
285 * that the option is specific to 'event'.
287 void MessageWin::showError(const KAEvent& event, const DateTime& alarmDateTime,
288 const QStringList& errmsgs, const QString& dontShowAgain)
290 #ifdef USE_AKONADI
291 if (!dontShowAgain.isEmpty()
292 && KAlarm::dontShowErrors(EventId(event), dontShowAgain))
293 #else
294 if (!dontShowAgain.isEmpty()
295 && KAlarm::dontShowErrors(event.id(), dontShowAgain))
296 #endif
297 return;
299 // Don't pile up duplicate error messages for the same alarm
300 for (int i = 0, end = mWindowList.count(); i < end; ++i)
302 MessageWin* w = mWindowList[i];
303 #ifdef USE_AKONADI
304 if (w->mErrorWindow && w->mEventId == EventId(event)
305 && w->mErrorMsgs == errmsgs && w->mDontShowAgain == dontShowAgain)
306 #else
307 if (w->mErrorWindow && w->mEventId == event.id()
308 && w->mErrorMsgs == errmsgs && w->mDontShowAgain == dontShowAgain)
309 #endif
310 return;
313 (new MessageWin(&event, alarmDateTime, errmsgs, dontShowAgain))->show();
316 /******************************************************************************
317 * Construct the message window for a specified error message.
318 * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note
319 * that the option is specific to 'event'.
321 MessageWin::MessageWin(const KAEvent* event, const DateTime& alarmDateTime,
322 const QStringList& errmsgs, const QString& dontShowAgain)
323 : MainWindowBase(0, WFLAGS | WFLAGS2),
324 mMessage(event->cleanText()),
325 mDateTime(alarmDateTime),
326 #ifdef USE_AKONADI
327 mEventItemId(event->itemId()),
328 mEventId(*event),
329 #else
330 mEventId(event->id()),
331 #endif
332 mAlarmType(KAAlarm::MAIN_ALARM),
333 mAction(event->actionSubType()),
334 mKMailSerialNumber(0),
335 mCommandError(KAEvent::CMD_NO_ERROR),
336 mErrorMsgs(errmsgs),
337 mDontShowAgain(dontShowAgain),
338 mRestoreHeight(0),
339 mConfirmAck(false),
340 mShowEdit(false),
341 mNoDefer(true),
342 mInvalid(false),
343 mEvent(*event),
344 mOriginalEvent(*event),
345 #ifndef USE_AKONADI
346 mResource(0),
347 #endif
348 mTimeLabel(0),
349 mRemainingText(0),
350 mEditButton(0),
351 mDeferButton(0),
352 mSilenceButton(0),
353 mKMailButton(0),
354 mCommandText(0),
355 mDontShowAgainCheck(0),
356 mEditDlg(0),
357 mDeferDlg(0),
358 mAlwaysHide(false),
359 mErrorWindow(true),
360 mInitialised(false),
361 mNoPostAction(true),
362 mRecreating(false),
363 mRescheduleEvent(false),
364 mShown(false),
365 mPositioning(false),
366 mNoCloseConfirm(false),
367 mDisableDeferral(false)
369 kDebug() << "errmsg";
370 setAttribute(static_cast<Qt::WidgetAttribute>(WidgetFlags));
371 setWindowModality(Qt::WindowModal);
372 setObjectName("ErrorWin"); // used by LikeBack
373 getWorkAreaAndModal();
374 initView();
375 mWindowList.append(this);
378 /******************************************************************************
379 * Construct the message window for restoration by session management.
380 * The window is initialised by readProperties().
382 MessageWin::MessageWin()
383 : MainWindowBase(0, WFLAGS),
384 mTimeLabel(0),
385 mRemainingText(0),
386 mEditButton(0),
387 mDeferButton(0),
388 mSilenceButton(0),
389 mKMailButton(0),
390 mCommandText(0),
391 mDontShowAgainCheck(0),
392 mEditDlg(0),
393 mDeferDlg(0),
394 mAlwaysHide(false),
395 mErrorWindow(false),
396 mInitialised(false),
397 mRecreating(false),
398 mRescheduleEvent(false),
399 mShown(false),
400 mPositioning(false),
401 mNoCloseConfirm(false),
402 mDisableDeferral(false)
404 kDebug() << "restore";
405 setAttribute(WidgetFlags);
406 setWindowModality(Qt::WindowModal);
407 setObjectName("RestoredMsgWin"); // used by LikeBack
408 getWorkAreaAndModal();
409 mWindowList.append(this);
412 /******************************************************************************
413 * Destructor. Perform any post-alarm actions before tidying up.
415 MessageWin::~MessageWin()
417 kDebug() << mEventId;
418 if (AudioThread::mAudioOwner == this && !mAudioThread.isNull())
419 mAudioThread->quit();
420 mErrorMessages.remove(mEventId);
421 mWindowList.removeAll(this);
422 if (!mRecreating)
424 if (!mNoPostAction && !mEvent.postAction().isEmpty())
425 theApp()->alarmCompleted(mEvent);
426 if (!instanceCount(true))
427 theApp()->quitIf(); // no visible windows remain - check whether to quit
431 /******************************************************************************
432 * Construct the message window.
434 void MessageWin::initView()
436 bool reminder = (!mErrorWindow && (mAlarmType & KAAlarm::REMINDER_ALARM));
437 int leading = fontMetrics().leading();
438 setCaption((mAlarmType & KAAlarm::REMINDER_ALARM) ? i18nc("@title:window", "Reminder") : i18nc("@title:window", "Message"));
439 QWidget* topWidget = new QWidget(this);
440 setCentralWidget(topWidget);
441 QVBoxLayout* topLayout = new QVBoxLayout(topWidget);
442 topLayout->setMargin(KDialog::marginHint());
443 topLayout->setSpacing(KDialog::spacingHint());
445 QPalette labelPalette = palette();
446 labelPalette.setColor(backgroundRole(), labelPalette.color(QPalette::Window));
448 // Show the alarm date/time, together with a reminder text where appropriate.
449 // Alarm date/time: display time zone if not local time zone.
450 mTimeLabel = new QLabel(topWidget);
451 mTimeLabel->setText(dateTimeToDisplay());
452 mTimeLabel->setFrameStyle(QFrame::StyledPanel);
453 mTimeLabel->setPalette(labelPalette);
454 mTimeLabel->setAutoFillBackground(true);
455 topLayout->addWidget(mTimeLabel, 0, Qt::AlignHCenter);
456 mTimeLabel->setWhatsThis(i18nc("@info:whatsthis", "The scheduled date/time for the message (as opposed to the actual time of display)."));
458 if (mDateTime.isValid())
460 // Reminder
461 if (reminder)
463 QString s = i18nc("@info", "Reminder");
464 QRegExp re("^(<[^>]+>)*");
465 re.indexIn(s);
466 s.insert(re.matchedLength(), mTimeLabel->text() + "<br/>");
467 mTimeLabel->setText(s);
468 mTimeLabel->setAlignment(Qt::AlignHCenter);
471 else
472 mTimeLabel->hide();
474 if (!mErrorWindow)
476 // It's a normal alarm message window
477 switch (mAction)
479 case KAEvent::FILE:
481 // Display the file name
482 KSqueezedTextLabel* label = new KSqueezedTextLabel(mMessage, topWidget);
483 label->setFrameStyle(QFrame::StyledPanel);
484 label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
485 label->setPalette(labelPalette);
486 label->setAutoFillBackground(true);
487 label->setWhatsThis(i18nc("@info:whatsthis", "The file whose contents are displayed below"));
488 topLayout->addWidget(label, 0, Qt::AlignHCenter);
490 // Display contents of file
491 bool opened = false;
492 bool dir = false;
493 QString tmpFile;
494 KUrl url(mMessage);
495 if (KIO::NetAccess::download(url, tmpFile, MainWindow::mainMainWindow()))
497 QFile qfile(tmpFile);
498 QFileInfo info(qfile);
499 if (!(dir = info.isDir()))
501 opened = true;
502 KTextBrowser* view = new KTextBrowser(topWidget);
503 view->setFrameStyle(QFrame::NoFrame);
504 view->setWordWrapMode(QTextOption::NoWrap);
505 QPalette pal = view->viewport()->palette();
506 pal.setColor(view->viewport()->backgroundRole(), mBgColour);
507 view->viewport()->setPalette(pal);
508 view->setTextColor(mFgColour);
509 view->setCurrentFont(mFont);
510 KMimeType::Ptr mime = KMimeType::findByUrl(url);
511 if (mime->is("application/octet-stream"))
512 mime = KMimeType::findByFileContent(tmpFile);
513 switch (KAlarm::fileType(mime))
515 case KAlarm::Image:
516 view->setHtml("<img source=\"" + tmpFile + "\">");
517 break;
518 case KAlarm::TextFormatted:
519 view->QTextBrowser::setSource(tmpFile); //krazy:exclude=qclasses
520 break;
521 default:
523 // Assume a plain text file
524 if (qfile.open(QIODevice::ReadOnly))
526 QTextStream str(&qfile);
528 view->setPlainText(str.readAll());
529 qfile.close();
531 break;
534 view->setMinimumSize(view->sizeHint());
535 topLayout->addWidget(view);
537 // Set the default size to 20 lines square.
538 // Note that after the first file has been displayed, this size
539 // is overridden by the user-set default stored in the config file.
540 // So there is no need to calculate an accurate size.
541 int h = 20*view->fontMetrics().lineSpacing() + 2*view->frameWidth();
542 view->resize(QSize(h, h).expandedTo(view->sizeHint()));
543 view->setWhatsThis(i18nc("@info:whatsthis", "The contents of the file to be displayed"));
545 KIO::NetAccess::removeTempFile(tmpFile);
547 if (!opened)
549 // File couldn't be opened
550 bool exists = KIO::NetAccess::exists(url, KIO::NetAccess::SourceSide, MainWindow::mainMainWindow());
551 mErrorMsgs += dir ? i18nc("@info", "File is a folder") : exists ? i18nc("@info", "Failed to open file") : i18nc("@info", "File not found");
553 break;
555 case KAEvent::MESSAGE:
557 // Message label
558 // Using MessageText instead of QLabel allows scrolling and mouse copying
559 MessageText* text = new MessageText(topWidget);
560 text->setAutoFillBackground(true);
561 text->setBackgroundColour(mBgColour);
562 text->setTextColor(mFgColour);
563 text->setCurrentFont(mFont);
564 text->insertPlainText(mMessage);
565 int lineSpacing = text->fontMetrics().lineSpacing();
566 QSize s = text->sizeHint();
567 int h = s.height();
568 text->setMaximumHeight(h + text->scrollBarHeight());
569 text->setMinimumHeight(qMin(h, lineSpacing*4));
570 text->setMaximumWidth(s.width() + text->scrollBarWidth());
571 text->setWhatsThis(i18nc("@info:whatsthis", "The alarm message"));
572 int vspace = lineSpacing/2;
573 int hspace = lineSpacing - KDialog::marginHint();
574 topLayout->addSpacing(vspace);
575 topLayout->addStretch();
576 // Don't include any horizontal margins if message is 2/3 screen width
577 if (text->sizeHint().width() >= KAlarm::desktopWorkArea(mScreenNumber).width()*2/3)
578 topLayout->addWidget(text, 1, Qt::AlignHCenter);
579 else
581 QHBoxLayout* layout = new QHBoxLayout();
582 layout->addSpacing(hspace);
583 layout->addWidget(text, 1, Qt::AlignHCenter);
584 layout->addSpacing(hspace);
585 topLayout->addLayout(layout);
587 if (!reminder)
588 topLayout->addStretch();
589 break;
591 case KAEvent::COMMAND:
593 mCommandText = new MessageText(topWidget);
594 mCommandText->setBackgroundColour(mBgColour);
595 mCommandText->setTextColor(mFgColour);
596 mCommandText->setCurrentFont(mFont);
597 topLayout->addWidget(mCommandText);
598 mCommandText->setWhatsThis(i18nc("@info:whatsthis", "The output of the alarm's command"));
599 theApp()->execCommandAlarm(mEvent, mEvent.alarm(mAlarmType), this, SLOT(readProcessOutput(ShellProcess*)));
600 break;
602 case KAEvent::EMAIL:
603 default:
604 break;
607 if (reminder && mEvent.reminderMinutes() > 0)
609 // Advance reminder: show remaining time until the actual alarm
610 mRemainingText = new QLabel(topWidget);
611 mRemainingText->setFrameStyle(QFrame::Box | QFrame::Raised);
612 mRemainingText->setMargin(leading);
613 mRemainingText->setPalette(labelPalette);
614 mRemainingText->setAutoFillBackground(true);
615 if (mDateTime.isDateOnly() || KDateTime::currentLocalDate().daysTo(mDateTime.date()) > 0)
617 setRemainingTextDay();
618 MidnightTimer::connect(this, SLOT(setRemainingTextDay())); // update every day
620 else
622 setRemainingTextMinute();
623 MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute
625 topLayout->addWidget(mRemainingText, 0, Qt::AlignHCenter);
626 topLayout->addSpacing(KDialog::spacingHint());
627 topLayout->addStretch();
630 else
632 // It's an error message
633 switch (mAction)
635 case KAEvent::EMAIL:
637 // Display the email addresses and subject.
638 QFrame* frame = new QFrame(topWidget);
639 frame->setFrameStyle(QFrame::Box | QFrame::Raised);
640 frame->setWhatsThis(i18nc("@info:whatsthis", "The email to send"));
641 topLayout->addWidget(frame, 0, Qt::AlignHCenter);
642 QGridLayout* grid = new QGridLayout(frame);
643 grid->setMargin(KDialog::marginHint());
644 grid->setSpacing(KDialog::spacingHint());
646 QLabel* label = new QLabel(i18nc("@info Email addressee", "To:"), frame);
647 label->setFixedSize(label->sizeHint());
648 grid->addWidget(label, 0, 0, Qt::AlignLeft);
649 label = new QLabel(mEvent.emailAddresses("\n"), frame);
650 label->setFixedSize(label->sizeHint());
651 grid->addWidget(label, 0, 1, Qt::AlignLeft);
653 label = new QLabel(i18nc("@info Email subject", "Subject:"), frame);
654 label->setFixedSize(label->sizeHint());
655 grid->addWidget(label, 1, 0, Qt::AlignLeft);
656 label = new QLabel(mEvent.emailSubject(), frame);
657 label->setFixedSize(label->sizeHint());
658 grid->addWidget(label, 1, 1, Qt::AlignLeft);
659 break;
661 case KAEvent::COMMAND:
662 case KAEvent::FILE:
663 case KAEvent::MESSAGE:
664 default:
665 // Just display the error message strings
666 break;
670 if (!mErrorMsgs.count())
672 topWidget->setAutoFillBackground(true);
673 QPalette palette = topWidget->palette();
674 palette.setColor(topWidget->backgroundRole(), mBgColour);
675 topWidget->setPalette(palette);
677 else
679 setCaption(i18nc("@title:window", "Error"));
680 QHBoxLayout* layout = new QHBoxLayout();
681 layout->setMargin(2*KDialog::marginHint());
682 layout->addStretch();
683 topLayout->addLayout(layout);
684 QLabel* label = new QLabel(topWidget);
685 label->setPixmap(DesktopIcon("dialog-error"));
686 label->setFixedSize(label->sizeHint());
687 layout->addWidget(label, 0, Qt::AlignRight);
688 QVBoxLayout* vlayout = new QVBoxLayout();
689 layout->addLayout(vlayout);
690 for (QStringList::Iterator it = mErrorMsgs.begin(); it != mErrorMsgs.end(); ++it)
692 label = new QLabel(*it, topWidget);
693 label->setFixedSize(label->sizeHint());
694 vlayout->addWidget(label, 0, Qt::AlignLeft);
696 layout->addStretch();
697 if (!mDontShowAgain.isEmpty())
699 mDontShowAgainCheck = new QCheckBox(i18nc("@option:check", "Do not display this error message again for this alarm"), topWidget);
700 mDontShowAgainCheck->setFixedSize(mDontShowAgainCheck->sizeHint());
701 topLayout->addWidget(mDontShowAgainCheck, 0, Qt::AlignLeft);
705 QGridLayout* grid = new QGridLayout();
706 grid->setColumnStretch(0, 1); // keep the buttons right-adjusted in the window
707 topLayout->addLayout(grid);
708 int gridIndex = 1;
710 // Close button
711 mOkButton = new PushButton(KStandardGuiItem::close(), topWidget);
712 // Prevent accidental acknowledgement of the message if the user is typing when the window appears
713 mOkButton->clearFocus();
714 mOkButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection
715 mOkButton->setFixedSize(mOkButton->sizeHint());
716 connect(mOkButton, SIGNAL(clicked()), SLOT(slotOk()));
717 grid->addWidget(mOkButton, 0, gridIndex++, Qt::AlignHCenter);
718 mOkButton->setWhatsThis(i18nc("@info:whatsthis", "Acknowledge the alarm"));
720 if (mShowEdit)
722 // Edit button
723 mEditButton = new PushButton(i18nc("@action:button", "&Edit..."), topWidget);
724 mEditButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection
725 mEditButton->setFixedSize(mEditButton->sizeHint());
726 connect(mEditButton, SIGNAL(clicked()), SLOT(slotEdit()));
727 grid->addWidget(mEditButton, 0, gridIndex++, Qt::AlignHCenter);
728 mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the alarm."));
731 // Defer button
732 mDeferButton = new PushButton(i18nc("@action:button", "&Defer..."), topWidget);
733 mDeferButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection
734 mDeferButton->setFixedSize(mDeferButton->sizeHint());
735 connect(mDeferButton, SIGNAL(clicked()), SLOT(slotDefer()));
736 grid->addWidget(mDeferButton, 0, gridIndex++, Qt::AlignHCenter);
737 mDeferButton->setWhatsThis(i18nc("@info:whatsthis", "<para>Defer the alarm until later.</para>"
738 "<para>You will be prompted to specify when the alarm should be redisplayed.</para>"));
740 if (mNoDefer)
741 mDeferButton->hide();
742 else
743 setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more
745 if (!mAudioFile.isEmpty() && (mVolume || mFadeVolume > 0))
747 // Silence button to stop sound repetition
748 QPixmap pixmap = MainBarIcon("media-playback-stop");
749 mSilenceButton = new PushButton(topWidget);
750 mSilenceButton->setIcon(KIcon(pixmap));
751 grid->addWidget(mSilenceButton, 0, gridIndex++, Qt::AlignHCenter);
752 mSilenceButton->setToolTip(i18nc("@info:tooltip", "Stop sound"));
753 mSilenceButton->setWhatsThis(i18nc("@info:whatsthis", "Stop playing the sound"));
754 // To avoid getting in a mess, disable the button until sound playing has been set up
755 mSilenceButton->setEnabled(false);
758 KIconLoader iconLoader;
759 if (mKMailSerialNumber)
761 // KMail button
762 QPixmap pixmap = iconLoader.loadIcon(QLatin1String("internet-mail"), KIconLoader::MainToolbar);
763 mKMailButton = new PushButton(topWidget);
764 mKMailButton->setIcon(KIcon(pixmap));
765 connect(mKMailButton, SIGNAL(clicked()), SLOT(slotShowKMailMessage()));
766 grid->addWidget(mKMailButton, 0, gridIndex++, Qt::AlignHCenter);
767 mKMailButton->setToolTip(i18nc("@info:tooltip Locate this email in KMail", "Locate in <application>KMail</application>"));
768 mKMailButton->setWhatsThis(i18nc("@info:whatsthis", "Locate and highlight this email in <application>KMail</application>"));
771 // KAlarm button
772 QPixmap pixmap = iconLoader.loadIcon(KGlobal::mainComponent().aboutData()->appName(), KIconLoader::MainToolbar);
773 mKAlarmButton = new PushButton(topWidget);
774 mKAlarmButton->setIcon(KIcon(pixmap));
775 connect(mKAlarmButton, SIGNAL(clicked()), SLOT(displayMainWindow()));
776 grid->addWidget(mKAlarmButton, 0, gridIndex++, Qt::AlignHCenter);
777 mKAlarmButton->setToolTip(i18nc("@info:tooltip", "Activate <application>KAlarm</application>"));
778 mKAlarmButton->setWhatsThis(i18nc("@info:whatsthis", "Activate <application>KAlarm</application>"));
780 int butsize = mKAlarmButton->sizeHint().height();
781 if (mSilenceButton)
782 butsize = qMax(butsize, mSilenceButton->sizeHint().height());
783 if (mKMailButton)
784 butsize = qMax(butsize, mKMailButton->sizeHint().height());
785 mKAlarmButton->setFixedSize(butsize, butsize);
786 if (mSilenceButton)
787 mSilenceButton->setFixedSize(butsize, butsize);
788 if (mKMailButton)
789 mKMailButton->setFixedSize(butsize, butsize);
791 // Disable all buttons initially, to prevent accidental clicking on if they happen to be
792 // under the mouse just as the window appears.
793 mOkButton->setEnabled(false);
794 if (mDeferButton->isVisible())
795 mDeferButton->setEnabled(false);
796 if (mEditButton)
797 mEditButton->setEnabled(false);
798 if (mKMailButton)
799 mKMailButton->setEnabled(false);
800 mKAlarmButton->setEnabled(false);
802 topLayout->activate();
803 setMinimumSize(QSize(grid->sizeHint().width() + 2*KDialog::marginHint(), sizeHint().height()));
804 bool modal = !(windowFlags() & Qt::X11BypassWindowManagerHint);
805 unsigned long wstate = (modal ? NET::Modal : 0) | NET::Sticky | NET::StaysOnTop;
806 WId winid = winId();
807 KWindowSystem::setState(winid, wstate);
808 KWindowSystem::setOnAllDesktops(winid, true);
810 mInitialised = true; // the window's widgets have been created
813 /******************************************************************************
814 * Return the number of message windows, optionally excluding always-hidden ones.
816 int MessageWin::instanceCount(bool excludeAlwaysHidden)
818 int count = mWindowList.count();
819 if (excludeAlwaysHidden)
821 foreach (MessageWin* win, mWindowList)
823 if (win->mAlwaysHide)
824 --count;
827 return count;
830 bool MessageWin::hasDefer() const
832 return mDeferButton && mDeferButton->isVisible();
835 /******************************************************************************
836 * Show the Defer button when it was previously hidden.
838 void MessageWin::showDefer()
840 if (mDeferButton)
842 mNoDefer = false;
843 mDeferButton->show();
844 setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more
845 resize(sizeHint());
849 /******************************************************************************
850 * Convert a reminder window into a normal alarm window.
852 void MessageWin::cancelReminder(const KAEvent& event, const KAAlarm& alarm)
854 if (!mInitialised)
855 return;
856 mDateTime = alarm.dateTime(true);
857 mNoPostAction = false;
858 mAlarmType = alarm.type();
859 if (event.autoClose())
860 mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().dateTime().addSecs(event.lateCancel() * 60);
861 setCaption(i18nc("@title:window", "Message"));
862 mTimeLabel->setText(dateTimeToDisplay());
863 if (mRemainingText)
864 mRemainingText->hide();
865 MidnightTimer::disconnect(this, SLOT(setRemainingTextDay()));
866 MinuteTimer::disconnect(this, SLOT(setRemainingTextMinute()));
867 setMinimumHeight(0);
868 centralWidget()->layout()->activate();
869 setMinimumHeight(sizeHint().height());
870 resize(sizeHint());
873 /******************************************************************************
874 * Show the alarm's trigger time.
875 * This is assumed to have previously been hidden.
877 void MessageWin::showDateTime(const KAEvent& event, const KAAlarm& alarm)
879 if (!mTimeLabel)
880 return;
881 mDateTime = (alarm.type() & KAAlarm::REMINDER_ALARM) ? event.mainDateTime(true) : alarm.dateTime(true);
882 if (mDateTime.isValid())
884 mTimeLabel->setText(dateTimeToDisplay());
885 mTimeLabel->show();
889 /******************************************************************************
890 * Get the trigger time to display.
892 QString MessageWin::dateTimeToDisplay()
894 QString tm;
895 if (mDateTime.isValid())
897 if (mDateTime.isDateOnly())
898 tm = KGlobal::locale()->formatDate(mDateTime.date(), KLocale::ShortDate);
899 else
901 bool showZone = false;
902 if (mDateTime.timeType() == KDateTime::UTC
903 || (mDateTime.timeType() == KDateTime::TimeZone && !mDateTime.isLocalZone()))
905 // Display time zone abbreviation if it's different from the local
906 // zone. Note that the iCalendar time zone might represent the local
907 // time zone in a slightly different way from the system time zone,
908 // so the zone comparison above might not produce the desired result.
909 QString tz = mDateTime.kDateTime().toString(QString::fromLatin1("%Z"));
910 KDateTime local = mDateTime.kDateTime();
911 local.setTimeSpec(KDateTime::Spec::LocalZone());
912 showZone = (local.toString(QString::fromLatin1("%Z")) != tz);
914 tm = KGlobal::locale()->formatDateTime(mDateTime.kDateTime(), KLocale::ShortDate, KLocale::DateTimeFormatOptions(showZone ? KLocale::TimeZone : 0));
917 return tm;
920 /******************************************************************************
921 * Set the remaining time text in a reminder window.
922 * Called at the start of every day (at the user-defined start-of-day time).
924 void MessageWin::setRemainingTextDay()
926 QString text;
927 int days = KDateTime::currentLocalDate().daysTo(mDateTime.date());
928 if (days <= 0 && !mDateTime.isDateOnly())
930 // The alarm is due today, so start refreshing every minute
931 MidnightTimer::disconnect(this, SLOT(setRemainingTextDay()));
932 setRemainingTextMinute();
933 MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute
935 else
937 if (days <= 0)
938 text = i18nc("@info", "Today");
939 else if (days % 7)
940 text = i18ncp("@info", "Tomorrow", "in %1 days' time", days);
941 else
942 text = i18ncp("@info", "in 1 week's time", "in %1 weeks' time", days/7);
944 mRemainingText->setText(text);
947 /******************************************************************************
948 * Set the remaining time text in a reminder window.
949 * Called on every minute boundary.
951 void MessageWin::setRemainingTextMinute()
953 QString text;
954 int mins = (KDateTime::currentUtcDateTime().secsTo(mDateTime.effectiveKDateTime()) + 59) / 60;
955 if (mins < 60)
956 text = i18ncp("@info", "in 1 minute's time", "in %1 minutes' time", (mins > 0 ? mins : 0));
957 else if (mins % 60 == 0)
958 text = i18ncp("@info", "in 1 hour's time", "in %1 hours' time", mins/60);
959 else
961 QString hourText = i18ncp("@item:intext inserted into 'in ... %1 minute's time' below", "1 hour", "%1 hours", mins/60);
962 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);
964 mRemainingText->setText(text);
967 /******************************************************************************
968 * Called when output is available from the command which is providing the text
969 * for this window. Add the output and resize the window to show it.
971 void MessageWin::readProcessOutput(ShellProcess* proc)
973 QByteArray data = proc->readAll();
974 if (!data.isEmpty())
976 // Strip any trailing newline, to avoid showing trailing blank line
977 // in message window.
978 if (mCommandText->newLine())
979 mCommandText->append("\n");
980 int nl = data.endsWith('\n') ? 1 : 0;
981 mCommandText->setNewLine(nl);
982 mCommandText->insertPlainText(QString::fromLocal8Bit(data.data(), data.length() - nl));
983 resize(sizeHint());
987 /******************************************************************************
988 * Save settings to the session managed config file, for restoration
989 * when the program is restored.
991 void MessageWin::saveProperties(KConfigGroup& config)
993 if (mShown && !mErrorWindow && !mAlwaysHide)
995 #ifdef USE_AKONADI
996 config.writeEntry("EventID", mEventId.eventId());
997 config.writeEntry("EventItemID", mEventItemId);
998 #else
999 config.writeEntry("EventID", mEventId);
1000 #endif
1001 config.writeEntry("AlarmType", static_cast<int>(mAlarmType));
1002 if (mAlarmType == KAAlarm::INVALID_ALARM)
1003 kError() << "Invalid alarm: id=" << mEventId << ", alarm count=" << mEvent.alarmCount();
1004 config.writeEntry("Message", mMessage);
1005 config.writeEntry("Type", static_cast<int>(mAction));
1006 config.writeEntry("Font", mFont);
1007 config.writeEntry("BgColour", mBgColour);
1008 config.writeEntry("FgColour", mFgColour);
1009 config.writeEntry("ConfirmAck", mConfirmAck);
1010 if (mDateTime.isValid())
1012 //TODO: Write KDateTime when it becomes possible
1013 config.writeEntry("Time", mDateTime.effectiveDateTime());
1014 config.writeEntry("DateOnly", mDateTime.isDateOnly());
1015 QString zone;
1016 if (mDateTime.isUtc())
1017 zone = QLatin1String("UTC");
1018 else
1020 KTimeZone tz = mDateTime.timeZone();
1021 if (tz.isValid())
1022 zone = tz.name();
1024 config.writeEntry("TimeZone", zone);
1026 if (mCloseTime.isValid())
1027 config.writeEntry("Expiry", mCloseTime);
1028 if (mAudioRepeatPause >= 0 && mSilenceButton && mSilenceButton->isEnabled())
1030 // Only need to restart sound file playing if it's being repeated
1031 config.writePathEntry("AudioFile", mAudioFile);
1032 config.writeEntry("Volume", static_cast<int>(mVolume * 100));
1033 config.writeEntry("AudioPause", mAudioRepeatPause);
1035 config.writeEntry("Speak", mSpeak);
1036 config.writeEntry("Height", height());
1037 config.writeEntry("DeferMins", mDefaultDeferMinutes);
1038 config.writeEntry("NoDefer", mNoDefer);
1039 config.writeEntry("NoPostAction", mNoPostAction);
1040 config.writeEntry("KMailSerial", static_cast<qulonglong>(mKMailSerialNumber));
1041 config.writeEntry("CmdErr", static_cast<int>(mCommandError));
1042 config.writeEntry("DontShowAgain", mDontShowAgain);
1044 else
1045 config.writeEntry("Invalid", true);
1048 /******************************************************************************
1049 * Read settings from the session managed config file.
1050 * This function is automatically called whenever the app is being restored.
1051 * Read in whatever was saved in saveProperties().
1053 void MessageWin::readProperties(const KConfigGroup& config)
1055 mInvalid = config.readEntry("Invalid", false);
1056 #ifdef USE_AKONADI
1057 mEventItemId = config.readEntry("EventItemID", Akonadi::Item::Id(-1));
1058 mCollection = AkonadiModel::instance()->collectionForItem(mEventItemId);
1059 mEventId = EventId(mCollection.id(), config.readEntry("EventID"));
1060 #else
1061 mEventId = config.readEntry("EventID");
1062 #endif
1063 mAlarmType = static_cast<KAAlarm::Type>(config.readEntry("AlarmType", 0));
1064 if (mAlarmType == KAAlarm::INVALID_ALARM)
1066 mInvalid = true;
1067 kError() << "Invalid alarm: id=" << mEventId;
1069 mMessage = config.readEntry("Message");
1070 mAction = static_cast<KAEvent::SubAction>(config.readEntry("Type", 0));
1071 mFont = config.readEntry("Font", QFont());
1072 mBgColour = config.readEntry("BgColour", QColor(Qt::white));
1073 mFgColour = config.readEntry("FgColour", QColor(Qt::black));
1074 mConfirmAck = config.readEntry("ConfirmAck", false);
1075 QDateTime invalidDateTime;
1076 QDateTime dt = config.readEntry("Time", invalidDateTime);
1077 QString zone = config.readEntry("TimeZone");
1078 if (zone.isEmpty())
1079 mDateTime = KDateTime(dt, KDateTime::ClockTime);
1080 else if (zone == QLatin1String("UTC"))
1082 dt.setTimeSpec(Qt::UTC);
1083 mDateTime = KDateTime(dt, KDateTime::UTC);
1085 else
1087 KTimeZone tz = KSystemTimeZones::zone(zone);
1088 mDateTime = KDateTime(dt, (tz.isValid() ? tz : KSystemTimeZones::local()));
1090 bool dateOnly = config.readEntry("DateOnly", false);
1091 if (dateOnly)
1092 mDateTime.setDateOnly(true);
1093 mCloseTime = config.readEntry("Expiry", invalidDateTime);
1094 mCloseTime.setTimeSpec(Qt::UTC);
1095 mAudioFile = config.readPathEntry("AudioFile", QString());
1096 mVolume = static_cast<float>(config.readEntry("Volume", 0)) / 100;
1097 mFadeVolume = -1;
1098 mFadeSeconds = 0;
1099 if (!mAudioFile.isEmpty()) // audio file URL was only saved if it repeats
1100 mAudioRepeatPause = config.readEntry("AudioPause", 0);
1101 mBeep = false; // don't beep after restart (similar to not playing non-repeated sound file)
1102 mSpeak = config.readEntry("Speak", false);
1103 mRestoreHeight = config.readEntry("Height", 0);
1104 mDefaultDeferMinutes = config.readEntry("DeferMins", 0);
1105 mNoDefer = config.readEntry("NoDefer", false);
1106 mNoPostAction = config.readEntry("NoPostAction", true);
1107 mKMailSerialNumber = static_cast<unsigned long>(config.readEntry("KMailSerial", QVariant(QVariant::ULongLong)).toULongLong());
1108 mCommandError = KAEvent::CmdErrType(config.readEntry("CmdErr", static_cast<int>(KAEvent::CMD_NO_ERROR)));
1109 mDontShowAgain = config.readEntry("DontShowAgain", QString());
1110 mShowEdit = false;
1111 #ifdef USE_AKONADI
1112 mCollection = Akonadi::Collection();
1113 #else
1114 mResource = 0;
1115 #endif
1116 kDebug() << mEventId;
1117 if (mAlarmType != KAAlarm::INVALID_ALARM)
1119 // Recreate the event from the calendar file (if possible)
1120 if (!mEventId.isEmpty())
1122 KAEvent* event = AlarmCalendar::resources()->event(mEventId);
1123 if (event)
1125 mEvent = *event;
1126 #ifndef USE_AKONADI
1127 mResource = AlarmCalendar::resources()->resourceForEvent(mEventId);
1128 #endif
1129 mShowEdit = true;
1131 else
1133 // It's not in the active calendar, so try the displaying or archive calendars
1134 #ifdef USE_AKONADI
1135 retrieveEvent(mEvent, mCollection, mShowEdit, mNoDefer);
1136 #else
1137 retrieveEvent(mEvent, mResource, mShowEdit, mNoDefer);
1138 #endif
1139 mNoDefer = !mNoDefer;
1142 initView();
1146 /******************************************************************************
1147 * Redisplay alarms which were being shown when the program last exited.
1148 * Normally, these alarms will have been displayed by session restoration, but
1149 * if the program crashed or was killed, we can redisplay them here so that
1150 * they won't be lost.
1152 void MessageWin::redisplayAlarms()
1154 AlarmCalendar* cal = AlarmCalendar::displayCalendar();
1155 if (cal->isOpen())
1157 KAEvent event;
1158 #ifdef USE_AKONADI
1159 Akonadi::Collection collection;
1160 #else
1161 AlarmResource* resource;
1162 #endif
1163 Event::List events = cal->kcalEvents();
1164 for (int i = 0, end = events.count(); i < end; ++i)
1166 bool showDefer, showEdit;
1167 #ifdef USE_AKONADI
1168 reinstateFromDisplaying(events[i], event, collection, showEdit, showDefer);
1169 if (!findEvent(EventId(event)))
1170 #else
1171 reinstateFromDisplaying(events[i], event, resource, showEdit, showDefer);
1172 if (!findEvent(event.id()))
1173 #endif
1175 // This event should be displayed, but currently isn't being
1176 KAAlarm alarm = event.convertDisplayingAlarm();
1177 if (alarm.type() == KAAlarm::INVALID_ALARM)
1179 kError() << "Invalid alarm: id=" << event.id();
1180 continue;
1182 kDebug() << event.id();
1183 bool login = alarm.repeatAtLogin();
1184 int flags = NO_RESCHEDULE | (login ? NO_DEFER : 0) | NO_INIT_VIEW;
1185 MessageWin* win = new MessageWin(&event, alarm, flags);
1186 #ifdef USE_AKONADI
1187 win->mCollection = collection;
1188 bool rw = CollectionControlModel::isWritableEnabled(collection, event.category()) > 0;
1189 #else
1190 win->mResource = resource;
1191 bool rw = resource && resource->writable();
1192 #endif
1193 win->mShowEdit = rw ? showEdit : false;
1194 win->mNoDefer = (rw && !login) ? !showDefer : true;
1195 win->initView();
1196 win->show();
1202 /******************************************************************************
1203 * Retrieves the event with the current ID from the displaying calendar file,
1204 * or if not found there, from the archive calendar.
1206 #ifdef USE_AKONADI
1207 bool MessageWin::retrieveEvent(KAEvent& event, Akonadi::Collection& resource, bool& showEdit, bool& showDefer)
1208 #else
1209 bool MessageWin::retrieveEvent(KAEvent& event, AlarmResource*& resource, bool& showEdit, bool& showDefer)
1210 #endif
1212 #ifdef USE_AKONADI
1213 Event::Ptr kcalEvent = AlarmCalendar::displayCalendar()->kcalEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING));
1214 #else
1215 const Event* kcalEvent = AlarmCalendar::displayCalendar()->kcalEvent(CalEvent::uid(mEventId, CalEvent::DISPLAYING));
1216 #endif
1217 if (!reinstateFromDisplaying(kcalEvent, event, resource, showEdit, showDefer))
1219 // The event isn't in the displaying calendar.
1220 // Try to retrieve it from the archive calendar.
1221 #ifdef USE_AKONADI
1222 KAEvent* ev = 0;
1223 Akonadi::Collection archiveCol = CollectionControlModel::getStandard(CalEvent::ARCHIVED);
1224 if (archiveCol.isValid())
1225 ev = AlarmCalendar::resources()->event(EventId(archiveCol.id(), CalEvent::uid(mEventId.eventId(), CalEvent::ARCHIVED)));
1226 #else
1227 KAEvent* ev = AlarmCalendar::resources()->event(CalEvent::uid(mEventId, CalEvent::ARCHIVED));
1228 #endif
1229 if (!ev)
1230 return false;
1231 event = *ev;
1232 event.setArchive(); // ensure that it gets re-archived if it's saved
1233 event.setCategory(CalEvent::ACTIVE);
1234 #ifdef USE_AKONADI
1235 if (mEventId.eventId() != event.id())
1236 kError() << "Wrong event ID";
1237 event.setEventId(mEventId.eventId());
1238 resource = Akonadi::Collection();
1239 #else
1240 if (mEventId != event.id())
1241 kError() << "Wrong event ID";
1242 event.setEventId(mEventId);
1243 resource = 0;
1244 #endif
1245 showEdit = true;
1246 showDefer = true;
1247 kDebug() << event.id() << ": success";
1249 return true;
1252 /******************************************************************************
1253 * Retrieves the displayed event from the calendar file, or if not found there,
1254 * from the displaying calendar.
1256 #ifdef USE_AKONADI
1257 bool MessageWin::reinstateFromDisplaying(const Event::Ptr& kcalEvent, KAEvent& event, Akonadi::Collection& collection, bool& showEdit, bool& showDefer)
1258 #else
1259 bool MessageWin::reinstateFromDisplaying(const Event* kcalEvent, KAEvent& event, AlarmResource*& resource, bool& showEdit, bool& showDefer)
1260 #endif
1262 if (!kcalEvent)
1263 return false;
1264 #ifdef USE_AKONADI
1265 Akonadi::Collection::Id collectionId;
1266 event.reinstateFromDisplaying(kcalEvent, collectionId, showEdit, showDefer);
1267 collection = AkonadiModel::instance()->collectionById(collectionId);
1268 #else
1269 QString resourceID;
1270 event.reinstateFromDisplaying(kcalEvent, resourceID, showEdit, showDefer);
1271 resource = AlarmResources::instance()->resourceWithId(resourceID);
1272 if (resource && !resource->isOpen())
1273 resource = 0;
1274 #endif
1275 kDebug() << event.id() << ": success";
1276 return true;
1279 /******************************************************************************
1280 * Called when an alarm is currently being displayed, to store a copy of the
1281 * alarm in the displaying calendar, and to reschedule it for its next repetition.
1282 * If no repetitions remain, cancel it.
1284 void MessageWin::alarmShowing(KAEvent& event)
1286 kDebug() << event.id() << "," << KAAlarm::debugType(mAlarmType);
1287 #ifndef USE_AKONADI
1288 const KCal::Event* kcalEvent = AlarmCalendar::resources()->kcalEvent(event.id());
1289 if (!kcalEvent)
1291 kError() << "Event ID not found:" << event.id();
1292 return;
1294 #endif
1295 KAAlarm alarm = event.alarm(mAlarmType);
1296 if (!alarm.isValid())
1298 kError() << "Alarm type not found:" << event.id() << ":" << mAlarmType;
1299 return;
1301 if (!mAlwaysHide)
1303 // Copy the alarm to the displaying calendar in case of a crash, etc.
1304 #ifdef USE_AKONADI
1305 KAEvent dispEvent;
1306 Akonadi::Collection collection = AkonadiModel::instance()->collectionForItem(event.itemId());
1307 dispEvent.setDisplaying(event, mAlarmType, collection.id(),
1308 mDateTime.effectiveKDateTime(), mShowEdit, !mNoDefer);
1309 #else
1310 KAEvent* dispEvent = new KAEvent;
1311 AlarmResource* resource = AlarmResources::instance()->resource(kcalEvent);
1312 dispEvent->setDisplaying(event, mAlarmType, (resource ? resource->identifier() : QString()),
1313 mDateTime.effectiveKDateTime(), mShowEdit, !mNoDefer);
1314 #endif
1315 AlarmCalendar* cal = AlarmCalendar::displayCalendarOpen();
1316 if (cal)
1318 #ifdef USE_AKONADI
1319 cal->deleteDisplayEvent(dispEvent.id()); // in case it already exists
1320 cal->addEvent(dispEvent);
1321 #else
1322 cal->deleteEvent(dispEvent->id()); // in case it already exists
1323 if (!cal->addEvent(dispEvent))
1324 delete dispEvent;
1325 #endif
1326 cal->save();
1328 #ifndef USE_AKONADI
1329 else
1330 delete dispEvent;
1331 #endif
1333 theApp()->rescheduleAlarm(event, alarm);
1336 /******************************************************************************
1337 * Spread alarm windows over the screen so that they are all visible, or pile
1338 * them on top of each other again.
1339 * Reply = true if windows are now scattered, false if piled up.
1341 bool MessageWin::spread(bool scatter)
1343 if (instanceCount(true) <= 1) // ignore always-hidden windows
1344 return false;
1346 QRect desk = KAlarm::desktopWorkArea(); // get the usable area of the desktop
1347 if (scatter == isSpread(desk.topLeft()))
1348 return scatter;
1350 if (scatter)
1352 // Usually there won't be many windows, so a crude
1353 // scattering algorithm should suffice.
1354 int x = desk.left();
1355 int y = desk.top();
1356 int ynext = y;
1357 for (int errmsgs = 0; errmsgs < 2; ++errmsgs)
1359 // Display alarm messages first, then error messages, since most
1360 // error messages tend to be the same height.
1361 for (int i = 0, end = mWindowList.count(); i < end; ++i)
1363 MessageWin* w = mWindowList[i];
1364 if ((!errmsgs && w->mErrorWindow)
1365 || (errmsgs && !w->mErrorWindow))
1366 continue;
1367 QSize sz = w->frameGeometry().size();
1368 if (x + sz.width() > desk.right())
1370 x = desk.left();
1371 y = ynext;
1373 int ytmp = y;
1374 if (y + sz.height() > desk.bottom())
1376 ytmp = desk.bottom() - sz.height();
1377 if (ytmp < desk.top())
1378 ytmp = desk.top();
1380 w->move(x, ytmp);
1381 x += sz.width();
1382 if (ytmp + sz.height() > ynext)
1383 ynext = ytmp + sz.height();
1387 else
1389 // Move all windows to the top left corner
1390 for (int i = 0, end = mWindowList.count(); i < end; ++i)
1391 mWindowList[i]->move(desk.topLeft());
1393 return scatter;
1396 /******************************************************************************
1397 * Check whether message windows are all piled up, or are spread out.
1398 * Reply = true if windows are currently spread, false if piled up.
1400 bool MessageWin::isSpread(const QPoint& topLeft)
1402 for (int i = 0, end = mWindowList.count(); i < end; ++i)
1404 if (mWindowList[i]->pos() != topLeft)
1405 return true;
1407 return false;
1410 /******************************************************************************
1411 * Returns the existing message window (if any) which is displaying the event
1412 * with the specified ID.
1414 #ifdef USE_AKONADI
1415 MessageWin* MessageWin::findEvent(const EventId& eventId)
1416 #else
1417 MessageWin* MessageWin::findEvent(const QString& eventId)
1418 #endif
1420 if (!eventId.isEmpty())
1422 for (int i = 0, end = mWindowList.count(); i < end; ++i)
1424 MessageWin* w = mWindowList[i];
1425 if (w->mEventId == eventId && !w->mErrorWindow)
1426 return w;
1429 return 0;
1432 /******************************************************************************
1433 * Beep and play the audio file, as appropriate.
1435 void MessageWin::playAudio()
1437 if (mBeep)
1439 // Beep using two methods, in case the sound card/speakers are switched off or not working
1440 QApplication::beep(); // beep through the internal speaker
1441 KNotification::beep(); // beep through the sound card & speakers
1443 if (!mAudioFile.isEmpty())
1445 if (!mVolume && mFadeVolume <= 0)
1446 return; // ensure zero volume doesn't play anything
1447 startAudio(); // play the audio file
1449 else if (mSpeak)
1451 // The message is to be spoken. In case of error messges,
1452 // call it on a timer to allow the window to display first.
1453 QTimer::singleShot(0, this, SLOT(slotSpeak()));
1457 /******************************************************************************
1458 * Speak the message.
1459 * Called asynchronously to avoid delaying the display of the message.
1461 void MessageWin::slotSpeak()
1463 QString error;
1464 OrgKdeKSpeechInterface* kspeech = theApp()->kspeechInterface(error);
1465 if (!kspeech)
1467 if (!haveErrorMessage(ErrMsg_Speak))
1469 KAMessageBox::detailedError(MainWindow::mainMainWindow(), i18nc("@info", "Unable to speak message"), error);
1470 clearErrorMessage(ErrMsg_Speak);
1472 return;
1474 if (!kspeech->say(mMessage, 0))
1476 kDebug() << "SayMessage() D-Bus error";
1477 if (!haveErrorMessage(ErrMsg_Speak))
1479 KAMessageBox::detailedError(MainWindow::mainMainWindow(), i18nc("@info", "Unable to speak message"), i18nc("@info", "D-Bus call say() failed"));
1480 clearErrorMessage(ErrMsg_Speak);
1485 /******************************************************************************
1486 * Called when another window's audio thread has been destructed.
1487 * Start playing this window's audio file. Because initialising the sound system
1488 * and loading the file may take some time, it is called in a separate thread to
1489 * allow the window to display first.
1491 void MessageWin::startAudio()
1493 if (mAudioThread)
1495 // An audio file is already playing for another message
1496 // window, so wait until it has finished.
1497 connect(mAudioThread, SIGNAL(destroyed(QObject*)), SLOT(audioTerminating()));
1499 else
1501 kDebug() << QThread::currentThread();
1502 mAudioThread = new AudioThread(this, mAudioFile, mVolume, mFadeVolume, mFadeSeconds, mAudioRepeatPause);
1503 connect(mAudioThread, SIGNAL(readyToPlay()), SLOT(playReady()));
1504 connect(mAudioThread, SIGNAL(finished()), SLOT(playFinished()));
1505 if (mSilenceButton)
1506 connect(mSilenceButton, SIGNAL(clicked()), mAudioThread, SLOT(quit()));
1507 // Notify after creating mAudioThread, so that isAudioPlaying() will
1508 // return the correct value.
1509 theApp()->notifyAudioPlaying(true);
1510 mAudioThread->start();
1514 /******************************************************************************
1515 * Return whether audio playback is currently active.
1517 bool MessageWin::isAudioPlaying()
1519 return mAudioThread;
1522 /******************************************************************************
1523 * Stop audio playback.
1525 void MessageWin::stopAudio(bool wait)
1527 kDebug();
1528 if (mAudioThread)
1529 mAudioThread->stop(wait);
1532 /******************************************************************************
1533 * Called when another window's audio thread is being destructed.
1534 * Wait until the destructor has finished.
1536 void MessageWin::audioTerminating()
1538 QTimer::singleShot(0, this, SLOT(startAudio()));
1541 /******************************************************************************
1542 * Called when the audio file is ready to start playing.
1544 void MessageWin::playReady()
1546 if (mSilenceButton)
1547 mSilenceButton->setEnabled(true);
1550 /******************************************************************************
1551 * Called when the audio file thread finishes.
1553 void MessageWin::playFinished()
1555 if (mSilenceButton)
1556 mSilenceButton->setEnabled(false);
1557 if (mAudioThread) // mAudioThread can actually be null here!
1559 QString errmsg = mAudioThread->error();
1560 if (!errmsg.isEmpty() && !haveErrorMessage(ErrMsg_AudioFile))
1562 KAMessageBox::error(this, errmsg);
1563 clearErrorMessage(ErrMsg_AudioFile);
1566 delete mAudioThread.data();
1567 if (mAlwaysHide)
1568 close();
1571 /******************************************************************************
1572 * Constructor for audio thread.
1574 AudioThread::AudioThread(MessageWin* parent, const QString& audioFile, float volume, float fadeVolume, int fadeSeconds, int repeatPause)
1575 : QThread(parent),
1576 mFile(audioFile),
1577 mVolume(volume),
1578 mFadeVolume(fadeVolume),
1579 mFadeSeconds(fadeSeconds),
1580 mRepeatPause(repeatPause),
1581 mAudioObject(0)
1583 if (mAudioOwner)
1584 kError() << "mAudioOwner already set";
1585 mAudioOwner = parent;
1588 /******************************************************************************
1589 * Destructor for audio thread. Waits for thread completion and tidies up.
1590 * Note that this destructor is executed in the parent thread.
1592 AudioThread::~AudioThread()
1594 kDebug();
1595 stop(true); // stop playing and tidy up (timeout 3 seconds)
1596 delete mAudioObject;
1597 mAudioObject = 0;
1598 if (mAudioOwner == parent())
1599 mAudioOwner = 0;
1600 // Notify after deleting mAudioThread, so that isAudioPlaying() will
1601 // return the correct value.
1602 QTimer::singleShot(0, theApp(), SLOT(notifyAudioStopped()));
1605 /******************************************************************************
1606 * Quits the thread and waits for thread completion and tidies up.
1608 void AudioThread::stop(bool waiT)
1610 kDebug();
1611 quit(); // stop playing and tidy up
1612 wait(3000); // wait for run() to exit (timeout 3 seconds)
1613 if (!isFinished())
1615 // Something has gone wrong - forcibly kill the thread
1616 terminate();
1617 if (waiT)
1618 wait();
1622 /******************************************************************************
1623 * Kick off the thread to play the audio file.
1625 void AudioThread::run()
1627 mMutex.lock();
1628 if (mAudioObject)
1630 mMutex.unlock();
1631 return;
1633 kDebug() << QThread::currentThread() << mFile;
1634 QString audioFile = mFile;
1635 mFile = KAlarm::pathOrUrl(mFile);
1636 Phonon::MediaSource source(audioFile);
1637 if (source.type() == Phonon::MediaSource::Invalid)
1639 mError = i18nc("@info", "Cannot open audio file: <filename>%1</filename>", audioFile);
1640 mMutex.unlock();
1641 kError() << "Open failure:" << audioFile;
1642 return;
1644 mAudioObject = new Phonon::MediaObject();
1645 mAudioObject->setCurrentSource(source);
1646 mAudioObject->setTransitionTime(100); // workaround to prevent clipping of end of files in Xine backend
1647 Phonon::AudioOutput* output = new Phonon::AudioOutput(Phonon::NotificationCategory, mAudioObject);
1648 mPath = Phonon::createPath(mAudioObject, output);
1649 if (mVolume >= 0 || mFadeVolume >= 0)
1651 float vol = (mVolume >= 0) ? mVolume : output->volume();
1652 float maxvol = qMax(vol, mFadeVolume);
1653 output->setVolume(maxvol);
1654 if (mFadeVolume >= 0 && mFadeSeconds > 0)
1656 Phonon::VolumeFaderEffect* fader = new Phonon::VolumeFaderEffect(mAudioObject);
1657 fader->setVolume(mFadeVolume / maxvol);
1658 fader->fadeTo(mVolume / maxvol, mFadeSeconds * 1000);
1659 mPath.insertEffect(fader);
1662 connect(mAudioObject, SIGNAL(stateChanged(Phonon::State,Phonon::State)), SLOT(playStateChanged(Phonon::State)), Qt::DirectConnection);
1663 connect(mAudioObject, SIGNAL(finished()), SLOT(checkAudioPlay()), Qt::DirectConnection);
1664 mPlayedOnce = false;
1665 mPausing = false;
1666 mMutex.unlock();
1667 emit readyToPlay();
1668 checkAudioPlay();
1670 // Start an event loop.
1671 // The function will exit once exit() or quit() is called.
1672 // First, ensure that the thread object is deleted once it has completed.
1673 connect(this, SIGNAL(finished()), this, SLOT(deleteLater()));
1674 exec();
1675 stopPlay();
1678 /******************************************************************************
1679 * Called when the audio file has loaded and is ready to play, or when play
1680 * has completed.
1681 * If it is ready to play, start playing it (for the first time or repeated).
1682 * If play has not yet completed, wait a bit longer.
1684 void AudioThread::checkAudioPlay()
1686 mMutex.lock();
1687 if (!mAudioObject)
1689 mMutex.unlock();
1690 return;
1692 if (mPausing)
1693 mPausing = false;
1694 else
1696 // The file has loaded and is ready to play, or play has completed
1697 if (mPlayedOnce)
1699 if (mRepeatPause < 0)
1701 // Play has completed
1702 mMutex.unlock();
1703 stopPlay();
1704 return;
1706 if (mRepeatPause > 0)
1708 // Pause before playing the file again
1709 mPausing = true;
1710 QTimer::singleShot(mRepeatPause * 1000, this, SLOT(checkAudioPlay()));
1711 mMutex.unlock();
1712 return;
1715 mPlayedOnce = true;
1718 // Start playing the file, either for the first time or again
1719 kDebug() << "start";
1720 mAudioObject->play();
1721 mMutex.unlock();
1724 /******************************************************************************
1725 * Called when the playback object changes state.
1726 * If an error has occurred, quit and return the error to the caller.
1728 void AudioThread::playStateChanged(Phonon::State newState)
1730 if (newState == Phonon::ErrorState)
1732 QMutexLocker locker(&mMutex);
1733 QString err = mAudioObject->errorString();
1734 if (!err.isEmpty())
1736 kError() << "Play failure:" << mFile << ":" << err;
1737 mError = i18nc("@info", "<para>Error playing audio file: <filename>%1</filename></para><para>%2</para>", mFile, err);
1738 exit(1);
1743 /******************************************************************************
1744 * Called when play completes, the Silence button is clicked, or the window is
1745 * closed, to terminate audio access.
1747 void AudioThread::stopPlay()
1749 mMutex.lock();
1750 if (mAudioObject)
1752 mAudioObject->stop();
1753 QList<Phonon::Effect*> effects = mPath.effects();
1754 for (int i = 0; i < effects.count(); ++i)
1756 mPath.removeEffect(effects[i]);
1757 delete effects[i];
1759 delete mAudioObject;
1760 mAudioObject = 0;
1762 mMutex.unlock();
1763 quit(); // exit the event loop, if it's still running
1766 QString AudioThread::error() const
1768 QMutexLocker locker(&mMutex);
1769 return mError;
1772 /******************************************************************************
1773 * Raise the alarm window, re-output any required audio notification, and
1774 * reschedule the alarm in the calendar file.
1776 void MessageWin::repeat(const KAAlarm& alarm)
1778 if (!mInitialised)
1779 return;
1780 if (mDeferDlg)
1782 // Cancel any deferral dialog so that the user notices something's going on,
1783 // and also because the deferral time limit will have changed.
1784 delete mDeferDlg;
1785 mDeferDlg = 0;
1787 KAEvent* event = mEventId.isEmpty() ? 0 : AlarmCalendar::resources()->event(mEventId);
1788 if (event)
1790 mAlarmType = alarm.type(); // store new alarm type for use if it is later deferred
1791 if (mAlwaysHide)
1792 playAudio();
1793 else
1795 if (!mDeferDlg || Preferences::modalMessages())
1797 raise();
1798 playAudio();
1800 if (mDeferButton->isVisible())
1802 mDeferButton->setEnabled(true);
1803 setDeferralLimit(*event); // ensure that button is disabled when alarm can't be deferred any more
1806 alarmShowing(*event);
1810 /******************************************************************************
1811 * Display the window.
1812 * If windows are being positioned away from the mouse cursor, it is initially
1813 * positioned at the top left to slightly reduce the number of times the
1814 * windows need to be moved in showEvent().
1816 void MessageWin::show()
1818 if (mCloseTime.isValid())
1820 // Set a timer to auto-close the window
1821 int delay = KDateTime::currentUtcDateTime().dateTime().secsTo(mCloseTime);
1822 if (delay < 0)
1823 delay = 0;
1824 QTimer::singleShot(delay * 1000, this, SLOT(close()));
1825 if (!delay)
1826 return; // don't show the window if auto-closing is already due
1828 if (Preferences::messageButtonDelay() == 0)
1829 move(0, 0);
1830 MainWindowBase::show();
1833 /******************************************************************************
1834 * Returns the window's recommended size exclusive of its frame.
1836 QSize MessageWin::sizeHint() const
1838 QSize desired;
1839 switch (mAction)
1841 case KAEvent::MESSAGE:
1842 desired = MainWindowBase::sizeHint();
1843 break;
1844 case KAEvent::COMMAND:
1845 if (mShown)
1847 // For command output, expand the window to accommodate the text
1848 QSize texthint = mCommandText->sizeHint();
1849 int w = texthint.width() + 2*KDialog::marginHint();
1850 if (w < width())
1851 w = width();
1852 int ypadding = height() - mCommandText->height();
1853 desired = QSize(w, texthint.height() + ypadding);
1854 break;
1856 // fall through to default
1857 default:
1858 return MainWindowBase::sizeHint();
1861 // Limit the size to fit inside the working area of the desktop
1862 QSize desktop = KAlarm::desktopWorkArea(mScreenNumber).size();
1863 QSize frameThickness = frameGeometry().size() - geometry().size(); // title bar & window frame
1864 return desired.boundedTo(desktop - frameThickness);
1867 /******************************************************************************
1868 * Called when the window is shown.
1869 * The first time, output any required audio notification, and reschedule or
1870 * delete the event from the calendar file.
1872 void MessageWin::showEvent(QShowEvent* se)
1874 MainWindowBase::showEvent(se);
1875 if (mShown)
1876 return;
1877 if (mErrorWindow || mAlarmType == KAAlarm::INVALID_ALARM)
1879 // Don't bother repositioning error messages,
1880 // and invalid alarms should be deleted anyway.
1881 enableButtons();
1883 else
1885 /* Set the window size.
1886 * Note that the frame thickness is not yet known when this
1887 * method is called, so for large windows the size needs to be
1888 * set again later.
1890 bool execComplete = true;
1891 QSize s = sizeHint(); // fit the window round the message
1892 if (mAction == KAEvent::FILE && !mErrorMsgs.count())
1893 KAlarm::readConfigWindowSize("FileMessage", s);
1894 resize(s);
1896 QRect desk = KAlarm::desktopWorkArea(mScreenNumber);
1897 QRect frame = frameGeometry();
1899 mButtonDelay = Preferences::messageButtonDelay() * 1000;
1900 if (mButtonDelay)
1902 // Position the window in the middle of the screen, and
1903 // delay enabling the buttons.
1904 mPositioning = true;
1905 move((desk.width() - frame.width())/2, (desk.height() - frame.height())/2);
1906 execComplete = false;
1908 else
1910 /* Try to ensure that the window can't accidentally be acknowledged
1911 * by the user clicking the mouse just as it appears.
1912 * To achieve this, move the window so that the OK button is as far away
1913 * from the cursor as possible. If the buttons are still too close to the
1914 * cursor, disable the buttons for a short time.
1915 * N.B. This can't be done in show(), since the geometry of the window
1916 * is not known until it is displayed. Unfortunately by moving the
1917 * window in showEvent(), a flicker is unavoidable.
1918 * See the Qt documentation on window geometry for more details.
1920 // PROBLEM: The frame size is not known yet!
1921 QPoint cursor = QCursor::pos();
1922 QRect rect = geometry();
1923 // Find the offsets from the outside of the frame to the edges of the OK button
1924 QRect button(mOkButton->mapToParent(QPoint(0, 0)), mOkButton->mapToParent(mOkButton->rect().bottomRight()));
1925 int buttonLeft = button.left() + rect.left() - frame.left();
1926 int buttonRight = width() - button.right() + frame.right() - rect.right();
1927 int buttonTop = button.top() + rect.top() - frame.top();
1928 int buttonBottom = height() - button.bottom() + frame.bottom() - rect.bottom();
1930 int centrex = (desk.width() + buttonLeft - buttonRight) / 2;
1931 int centrey = (desk.height() + buttonTop - buttonBottom) / 2;
1932 int x = (cursor.x() < centrex) ? desk.right() - frame.width() : desk.left();
1933 int y = (cursor.y() < centrey) ? desk.bottom() - frame.height() : desk.top();
1935 // Find the enclosing rectangle for the new button positions
1936 // and check if the cursor is too near
1937 QRect buttons = mOkButton->geometry().unite(mKAlarmButton->geometry());
1938 buttons.translate(rect.left() + x - frame.left(), rect.top() + y - frame.top());
1939 int minDistance = proximityMultiple * mOkButton->height();
1940 if ((abs(cursor.x() - buttons.left()) < minDistance
1941 || abs(cursor.x() - buttons.right()) < minDistance)
1942 && (abs(cursor.y() - buttons.top()) < minDistance
1943 || abs(cursor.y() - buttons.bottom()) < minDistance))
1944 mButtonDelay = proximityButtonDelay; // too near - disable buttons initially
1946 if (x != frame.left() || y != frame.top())
1948 mPositioning = true;
1949 move(x, y);
1950 execComplete = false;
1953 if (execComplete)
1954 displayComplete(); // play audio, etc.
1957 // Set the window size etc. once the frame size is known
1958 QTimer::singleShot(0, this, SLOT(frameDrawn()));
1960 mShown = true;
1963 /******************************************************************************
1964 * Called when the window has been moved.
1966 void MessageWin::moveEvent(QMoveEvent* e)
1968 MainWindowBase::moveEvent(e);
1969 theApp()->setSpreadWindowsState(isSpread(KAlarm::desktopWorkArea(mScreenNumber).topLeft()));
1970 if (mPositioning)
1972 // The window has just been initially positioned
1973 mPositioning = false;
1974 displayComplete(); // play audio, etc.
1978 /******************************************************************************
1979 * Called after (hopefully) the window frame size is known.
1980 * Reset the initial window size if it exceeds the working area of the desktop.
1981 * Set the 'spread windows' menu item status.
1983 void MessageWin::frameDrawn()
1985 if (!mErrorWindow && mAction == KAEvent::MESSAGE)
1987 QSize s = sizeHint();
1988 if (width() > s.width() || height() > s.height())
1989 resize(s);
1991 theApp()->setSpreadWindowsState(isSpread(KAlarm::desktopWorkArea(mScreenNumber).topLeft()));
1994 /******************************************************************************
1995 * Called when the window has been displayed properly (in its correct position),
1996 * to play sounds and reschedule the event.
1998 void MessageWin::displayComplete()
2000 playAudio();
2001 if (mRescheduleEvent)
2002 alarmShowing(mEvent);
2004 if (!mAlwaysHide)
2006 // Enable the window's buttons either now or after the configured delay
2007 if (mButtonDelay > 0)
2008 QTimer::singleShot(mButtonDelay, this, SLOT(enableButtons()));
2009 else
2010 enableButtons();
2014 /******************************************************************************
2015 * Enable the window's buttons.
2017 void MessageWin::enableButtons()
2019 mOkButton->setEnabled(true);
2020 mKAlarmButton->setEnabled(true);
2021 if (mDeferButton->isVisible() && !mDisableDeferral)
2022 mDeferButton->setEnabled(true);
2023 if (mEditButton)
2024 mEditButton->setEnabled(true);
2025 if (mKMailButton)
2026 mKMailButton->setEnabled(true);
2029 /******************************************************************************
2030 * Called when the window's size has changed (before it is painted).
2032 void MessageWin::resizeEvent(QResizeEvent* re)
2034 if (mRestoreHeight)
2036 // Restore the window height on session restoration
2037 if (mRestoreHeight != re->size().height())
2039 QSize size = re->size();
2040 size.setHeight(mRestoreHeight);
2041 resize(size);
2043 else if (isVisible())
2044 mRestoreHeight = 0;
2046 else
2048 if (mShown && mAction == KAEvent::FILE && !mErrorMsgs.count())
2049 KAlarm::writeConfigWindowSize("FileMessage", re->size());
2050 MainWindowBase::resizeEvent(re);
2054 /******************************************************************************
2055 * Called when a close event is received.
2056 * Only quits the application if there is no system tray icon displayed.
2058 void MessageWin::closeEvent(QCloseEvent* ce)
2060 // Don't prompt or delete the alarm from the display calendar if the session is closing
2061 if (!mErrorWindow && !theApp()->sessionClosingDown())
2063 if (mConfirmAck && !mNoCloseConfirm)
2065 // Ask for confirmation of acknowledgement. Use warningYesNo() because its default is No.
2066 if (KAMessageBox::warningYesNo(this, i18nc("@info", "Do you really want to acknowledge this alarm?"),
2067 i18nc("@action:button", "Acknowledge Alarm"), KGuiItem(i18nc("@action:button", "Acknowledge")), KStandardGuiItem::cancel())
2068 != KMessageBox::Yes)
2070 ce->ignore();
2071 return;
2074 if (!mEventId.isEmpty())
2076 // Delete from the display calendar
2077 #ifdef USE_AKONADI
2078 KAlarm::deleteDisplayEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING));
2079 #else
2080 KAlarm::deleteDisplayEvent(CalEvent::uid(mEventId, CalEvent::DISPLAYING));
2081 #endif
2084 MainWindowBase::closeEvent(ce);
2087 /******************************************************************************
2088 * Called when the OK button is clicked.
2090 void MessageWin::slotOk()
2092 if (mDontShowAgainCheck && mDontShowAgainCheck->isChecked())
2093 KAlarm::setDontShowErrors(mEventId, mDontShowAgain);
2094 close();
2097 #ifdef KMAIL_SUPPORTED
2098 /******************************************************************************
2099 * Called when the KMail button is clicked.
2100 * Tells KMail to display the email message displayed in this message window.
2102 void MessageWin::slotShowKMailMessage()
2104 kDebug();
2105 if (!mKMailSerialNumber)
2106 return;
2107 QString err = KAlarm::runKMail(false);
2108 if (!err.isNull())
2110 KAMessageBox::sorry(this, err);
2111 return;
2113 org::kde::kmail::kmail kmail(KMAIL_DBUS_SERVICE, KMAIL_DBUS_PATH, QDBusConnection::sessionBus());
2114 QDBusReply<bool> reply = kmail.showMail((qulonglong)mKMailSerialNumber, QString());
2115 if (!reply.isValid())
2116 kError() << "kmail D-Bus call failed:" << reply.error().message();
2117 else if (!reply.value())
2118 KAMessageBox::sorry(this, i18nc("@info", "Unable to locate this email in <application>KMail</application>"));
2120 #endif
2122 /******************************************************************************
2123 * Called when the Edit... button is clicked.
2124 * Displays the alarm edit dialog.
2126 * NOTE: The alarm edit dialog is made a child of the main window, not this
2127 * window, so that if this window closes before the dialog (e.g. on
2128 * auto-close), KAlarm doesn't crash. The dialog is set non-modal so that
2129 * the main window is unaffected, but modal mode is simulated so that
2130 * this window is inactive while the dialog is open.
2132 void MessageWin::slotEdit()
2134 kDebug();
2135 MainWindow* mainWin = MainWindow::mainMainWindow();
2136 mEditDlg = EditAlarmDlg::create(false, &mOriginalEvent, false, mainWin, EditAlarmDlg::RES_IGNORE);
2137 KWindowSystem::setMainWindow(mEditDlg, winId());
2138 KWindowSystem::setOnAllDesktops(mEditDlg->winId(), false);
2139 setButtonsReadOnly(true);
2140 connect(mEditDlg, SIGNAL(accepted()), SLOT(editCloseOk()));
2141 connect(mEditDlg, SIGNAL(rejected()), SLOT(editCloseCancel()));
2142 connect(mEditDlg, SIGNAL(destroyed(QObject*)), SLOT(editCloseCancel()));
2143 connect(KWindowSystem::self(), SIGNAL(activeWindowChanged(WId)), SLOT(activeWindowChanged(WId)));
2144 #ifdef USE_AKONADI
2145 mainWin->editAlarm(mEditDlg, mOriginalEvent);
2146 #else
2147 mainWin->editAlarm(mEditDlg, mOriginalEvent, mResource);
2148 #endif
2151 /******************************************************************************
2152 * Called when OK is clicked in the alarm edit dialog invoked by the Edit button.
2153 * Closes the window.
2155 void MessageWin::editCloseOk()
2157 mEditDlg = 0;
2158 mNoCloseConfirm = true; // allow window to close without confirmation prompt
2159 close();
2162 /******************************************************************************
2163 * Called when Cancel is clicked in the alarm edit dialog invoked by the Edit
2164 * button, or when the dialog is deleted.
2166 void MessageWin::editCloseCancel()
2168 mEditDlg = 0;
2169 setButtonsReadOnly(false);
2172 /******************************************************************************
2173 * Called when the active window has changed. If this window has become the
2174 * active window and there is an alarm edit dialog, simulate a modal dialog by
2175 * making the alarm edit dialog the active window instead.
2177 void MessageWin::activeWindowChanged(WId win)
2179 if (mEditDlg && win == winId())
2180 KWindowSystem::activateWindow(mEditDlg->winId());
2183 /******************************************************************************
2184 * Set or clear the read-only state of the dialog buttons.
2186 void MessageWin::setButtonsReadOnly(bool ro)
2188 mOkButton->setReadOnly(ro, true);
2189 mDeferButton->setReadOnly(ro, true);
2190 mEditButton->setReadOnly(ro, true);
2191 if (mSilenceButton)
2192 mSilenceButton->setReadOnly(ro, true);
2193 if (mKMailButton)
2194 mKMailButton->setReadOnly(ro, true);
2195 mKAlarmButton->setReadOnly(ro, true);
2198 /******************************************************************************
2199 * Set up to disable the defer button when the deferral limit is reached.
2201 void MessageWin::setDeferralLimit(const KAEvent& event)
2203 mDeferLimit = event.deferralLimit().effectiveKDateTime().toUtc().dateTime();
2204 MidnightTimer::connect(this, SLOT(checkDeferralLimit())); // check every day
2205 mDisableDeferral = false;
2206 checkDeferralLimit();
2209 /******************************************************************************
2210 * Check whether the deferral limit has been reached.
2211 * If so, disable the Defer button.
2212 * N.B. Ideally, just a single QTimer::singleShot() call would be made to disable
2213 * the defer button at the corret time. But for a 32-bit integer, the
2214 * milliseconds parameter overflows in about 25 days, so instead a daily
2215 * check is done until the day when the deferral limit is reached, followed
2216 * by a non-overflowing QTimer::singleShot() call.
2218 void MessageWin::checkDeferralLimit()
2220 if (!mDeferButton->isEnabled() || !mDeferLimit.isValid())
2221 return;
2222 int n = KDateTime::currentLocalDate().daysTo(KDateTime(mDeferLimit, KDateTime::LocalZone).date());
2223 if (n > 0)
2224 return;
2225 MidnightTimer::disconnect(this, SLOT(checkDeferralLimit()));
2226 if (n == 0)
2228 // The deferral limit will be reached today
2229 n = KDateTime::currentUtcDateTime().dateTime().secsTo(mDeferLimit);
2230 if (n > 0)
2232 QTimer::singleShot(n * 1000, this, SLOT(checkDeferralLimit()));
2233 return;
2236 mDeferButton->setEnabled(false);
2237 mDisableDeferral = true;
2240 /******************************************************************************
2241 * Called when the Defer... button is clicked.
2242 * Displays the defer message dialog.
2244 void MessageWin::slotDefer()
2246 mDeferDlg = new DeferAlarmDlg(KDateTime::currentDateTime(Preferences::timeZone()).addSecs(60), mDateTime.isDateOnly(), false, this);
2247 mDeferDlg->setObjectName("DeferDlg"); // used by LikeBack
2248 mDeferDlg->setDeferMinutes(mDefaultDeferMinutes > 0 ? mDefaultDeferMinutes : Preferences::defaultDeferTime());
2249 mDeferDlg->setLimit(mEvent);
2250 if (!Preferences::modalMessages())
2251 lower();
2252 if (mDeferDlg->exec() == QDialog::Accepted)
2254 DateTime dateTime = mDeferDlg->getDateTime();
2255 int delayMins = mDeferDlg->deferMinutes();
2256 // Fetch the up-to-date alarm from the calendar. Note that it could have
2257 // changed since it was displayed.
2258 const KAEvent* event = mEventId.isEmpty() ? 0 : AlarmCalendar::resources()->event(mEventId);
2259 if (event)
2261 // The event still exists in the active calendar
2262 kDebug() << "Deferring event" << mEventId;
2263 KAEvent newev(*event);
2264 newev.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true);
2265 newev.setDeferDefaultMinutes(delayMins);
2266 KAlarm::updateEvent(newev, mDeferDlg, true);
2267 if (newev.deferred())
2268 mNoPostAction = true;
2270 else
2272 // Try to retrieve the event from the displaying or archive calendars
2273 #ifdef USE_AKONADI
2274 Akonadi::Collection collection;
2275 #else
2276 AlarmResource* resource = 0;
2277 #endif
2278 KAEvent event;
2279 bool showEdit, showDefer;
2280 #ifdef USE_AKONADI
2281 if (!retrieveEvent(event, collection, showEdit, showDefer))
2282 #else
2283 if (!retrieveEvent(event, resource, showEdit, showDefer))
2284 #endif
2286 // The event doesn't exist any more !?!, so recurrence data,
2287 // flags, and more, have been lost.
2288 KAMessageBox::error(this, i18nc("@info", "<para>Cannot defer alarm:</para><para>Alarm not found.</para>"));
2289 raise();
2290 delete mDeferDlg;
2291 mDeferDlg = 0;
2292 mDeferButton->setEnabled(false);
2293 mEditButton->setEnabled(false);
2294 return;
2296 kDebug() << "Deferring retrieved event" << mEventId;
2297 event.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true);
2298 event.setDeferDefaultMinutes(delayMins);
2299 event.setCommandError(mCommandError);
2300 // Add the event back into the calendar file, retaining its ID
2301 // and not updating KOrganizer.
2302 #ifdef USE_AKONADI
2303 KAlarm::addEvent(event, &collection, mDeferDlg, KAlarm::USE_EVENT_ID);
2304 #else
2305 KAlarm::addEvent(event, resource, mDeferDlg, KAlarm::USE_EVENT_ID);
2306 #endif
2307 if (event.deferred())
2308 mNoPostAction = true;
2309 // Finally delete it from the archived calendar now that it has
2310 // been reactivated.
2311 event.setCategory(CalEvent::ARCHIVED);
2312 KAlarm::deleteEvent(event, false);
2314 if (theApp()->wantShowInSystemTray())
2316 // Alarms are to be displayed only if the system tray icon is running,
2317 // so start it if necessary so that the deferred alarm will be shown.
2318 theApp()->displayTrayIcon(true);
2320 mNoCloseConfirm = true; // allow window to close without confirmation prompt
2321 close();
2323 else
2324 raise();
2325 delete mDeferDlg;
2326 mDeferDlg = 0;
2329 /******************************************************************************
2330 * Called when the KAlarm icon button in the message window is clicked.
2331 * Displays the main window, with the appropriate alarm selected.
2333 void MessageWin::displayMainWindow()
2335 #ifdef USE_AKONADI
2336 KAlarm::displayMainWindowSelected(mEventItemId);
2337 #else
2338 KAlarm::displayMainWindowSelected(mEventId);
2339 #endif
2342 /******************************************************************************
2343 * Check whether the specified error message is already displayed for this
2344 * alarm, and note that it will now be displayed.
2345 * Reply = true if message is already displayed.
2347 bool MessageWin::haveErrorMessage(unsigned msg) const
2349 if (!mErrorMessages.contains(mEventId))
2350 mErrorMessages.insert(mEventId, 0);
2351 bool result = (mErrorMessages[mEventId] & msg);
2352 mErrorMessages[mEventId] |= msg;
2353 return result;
2356 void MessageWin::clearErrorMessage(unsigned msg) const
2358 if (mErrorMessages.contains(mEventId))
2360 if (mErrorMessages[mEventId] == msg)
2361 mErrorMessages.remove(mEventId);
2362 else
2363 mErrorMessages[mEventId] &= ~msg;
2368 /******************************************************************************
2369 * Check whether the message window should be modal, i.e. with title bar etc.
2370 * Normally this follows the Preferences setting, but if there is a full screen
2371 * window displayed, on X11 the message window has to bypass the window manager
2372 * in order to display on top of it (which has the side effect that it will have
2373 * no window decoration).
2375 * Also find the usable area of the desktop (excluding panel etc.), on the
2376 * appropriate screen if there are multiple screens.
2378 bool MessageWin::getWorkAreaAndModal()
2380 mScreenNumber = -1;
2381 bool modal = Preferences::modalMessages();
2382 #ifdef Q_WS_X11
2383 QDesktopWidget* desktop = qApp->desktop();
2384 int numScreens = desktop->numScreens();
2385 if (numScreens > 1)
2387 // There are multiple screens.
2388 // Check for any full screen windows, even if they are not the active
2389 // window, and try not to show the alarm message their screens.
2390 mScreenNumber = desktop->screenNumber(MainWindow::mainMainWindow()); // default = KAlarm's screen
2391 if (desktop->isVirtualDesktop())
2393 // The screens form a single virtual desktop.
2394 // Xinerama, for example, uses this scheme.
2395 QVector<FullScreenType> screenTypes(numScreens);
2396 QVector<QRect> screenRects(numScreens);
2397 for (int s = 0; s < numScreens; ++s)
2398 screenRects[s] = desktop->screenGeometry(s);
2399 FullScreenType full = findFullScreenWindows(screenRects, screenTypes);
2400 if (full == NoFullScreen || screenTypes[mScreenNumber] == NoFullScreen)
2401 return modal;
2402 for (int s = 0; s < numScreens; ++s)
2404 if (screenTypes[s] == NoFullScreen)
2407 // There is no full screen window on this screen
2408 mScreenNumber = s;
2409 return modal;
2412 // All screens contain a full screen window: use one without
2413 // an active full screen window.
2414 for (int s = 0; s < numScreens; ++s)
2416 if (screenTypes[s] == FullScreen)
2418 mScreenNumber = s;
2419 return modal;
2423 else
2425 // The screens are completely separate from each other.
2426 int inactiveScreen = -1;
2427 FullScreenType full = haveFullScreenWindow(mScreenNumber);
2428 kDebug()<<"full="<<full<<", screen="<<mScreenNumber;
2429 if (full == NoFullScreen)
2430 return modal; // KAlarm's screen doesn't contain a full screen window
2431 if (full == FullScreen)
2432 inactiveScreen = mScreenNumber;
2433 for (int s = 0; s < numScreens; ++s)
2435 if (s != mScreenNumber)
2437 full = haveFullScreenWindow(s);
2438 if (full == NoFullScreen)
2440 // There is no full screen window on this screen
2441 mScreenNumber = s;
2442 return modal;
2444 if (full == FullScreen && inactiveScreen < 0)
2445 inactiveScreen = s;
2448 if (inactiveScreen >= 0)
2450 // All screens contain a full screen window: use one without
2451 // an active full screen window.
2452 mScreenNumber = inactiveScreen;
2453 return modal;
2456 return false; // can't logically get here, since there can only be one active window...
2458 #endif
2459 if (modal)
2461 WId activeId = KWindowSystem::activeWindow();
2462 KWindowInfo wi = KWindowSystem::windowInfo(activeId, NET::WMState);
2463 if (wi.valid() && wi.hasState(NET::FullScreen))
2464 return false; // the active window is full screen.
2466 return modal;
2469 #ifdef Q_WS_X11
2470 /******************************************************************************
2471 * In a multi-screen setup (not a single virtual desktop), find whether the
2472 * specified screen has a full screen window on it.
2474 FullScreenType haveFullScreenWindow(int screen)
2476 FullScreenType type = NoFullScreen;
2477 Display* display = QX11Info::display();
2478 NETRootInfo rootInfo(display, NET::ClientList | NET::ActiveWindow, screen);
2479 Window rootWindow = rootInfo.rootWindow();
2480 Window activeWindow = rootInfo.activeWindow();
2481 const Window* windows = rootInfo.clientList();
2482 int windowCount = rootInfo.clientListCount();
2483 kDebug()<<"Screen"<<screen<<": Window count="<<windowCount<<", active="<<activeWindow<<", geom="<<qApp->desktop()->screenGeometry(screen);
2484 NETRect geom;
2485 NETRect frame;
2486 for (int w = 0; w < windowCount; ++w)
2488 NETWinInfo winInfo(display, windows[w], rootWindow, NET::WMState|NET::WMGeometry);
2489 winInfo.kdeGeometry(frame, geom);
2490 QRect fr(frame.pos.x, frame.pos.y, frame.size.width, frame.size.height);
2491 QRect gm(geom.pos.x, geom.pos.y, geom.size.width, geom.size.height);
2492 if (winInfo.state() & NET::FullScreen)
2494 kDebug()<<"Found FULL SCREEN: "<<windows[w]<<", geom="<<gm<<", frame="<<fr;
2495 type = FullScreen;
2496 if (windows[w] == activeWindow)
2497 return FullScreenActive;
2499 //else { kDebug()<<"Found normal: "<<windows[w]<<", geom="<<gm<<", frame="<<fr; }
2501 return type;
2504 /******************************************************************************
2505 * In a multi-screen setup (single virtual desktop, e.g. Xinerama), find which
2506 * screens have full screen windows on them.
2508 FullScreenType findFullScreenWindows(const QVector<QRect>& screenRects, QVector<FullScreenType>& screenTypes)
2510 FullScreenType result = NoFullScreen;
2511 screenTypes.fill(NoFullScreen);
2512 Display* display = QX11Info::display();
2513 NETRootInfo rootInfo(display, NET::ClientList | NET::ActiveWindow, 0);
2514 Window rootWindow = rootInfo.rootWindow();
2515 Window activeWindow = rootInfo.activeWindow();
2516 const Window* windows = rootInfo.clientList();
2517 int windowCount = rootInfo.clientListCount();
2518 kDebug()<<"Virtual desktops: Window count="<<windowCount<<", active="<<activeWindow<<", geom="<<qApp->desktop()->screenGeometry(0);
2519 NETRect netgeom;
2520 NETRect netframe;
2521 for (int w = 0; w < windowCount; ++w)
2523 NETWinInfo winInfo(display, windows[w], rootWindow, NET::WMState | NET::WMGeometry);
2524 if (winInfo.state() & NET::FullScreen)
2526 // Found a full screen window - find which screen it's on
2527 bool active = (windows[w] == activeWindow);
2528 winInfo.kdeGeometry(netframe, netgeom);
2529 QRect winRect(netgeom.pos.x, netgeom.pos.y, netgeom.size.width, netgeom.size.height);
2530 kDebug()<<"Found FULL SCREEN: "<<windows[w]<<", geom="<<winRect;
2531 for (int s = 0, count = screenRects.count(); s < count; ++s)
2533 if (screenRects[s].contains(winRect))
2535 kDebug()<<"FULL SCREEN on screen"<<s<<", active="<<active;
2536 if (active)
2537 screenTypes[s] = result = FullScreenActive;
2538 else
2540 if (screenTypes[s] == NoFullScreen)
2541 screenTypes[s] = FullScreen;
2542 if (result == NoFullScreen)
2543 result = FullScreen;
2545 break;
2550 return result;
2552 #endif
2554 // vim: et sw=4: