summaryrefslogtreecommitdiffstats
path: root/kalarm/messagewin.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'kalarm/messagewin.cpp')
-rw-r--r--kalarm/messagewin.cpp1727
1 files changed, 1727 insertions, 0 deletions
diff --git a/kalarm/messagewin.cpp b/kalarm/messagewin.cpp
new file mode 100644
index 000000000..acee6b9b3
--- /dev/null
+++ b/kalarm/messagewin.cpp
@@ -0,0 +1,1727 @@
+/*
+ * messagewin.cpp - displays an alarm message
+ * Program: kalarm
+ * Copyright © 2001-2009 by David Jarvie <djarvie@kde.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "kalarm.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <qfile.h>
+#include <qfileinfo.h>
+#include <qlayout.h>
+#include <qpushbutton.h>
+#include <qlabel.h>
+#include <qwhatsthis.h>
+#include <qtooltip.h>
+#include <qdragobject.h>
+#include <qtextedit.h>
+#include <qtimer.h>
+
+#include <kstandarddirs.h>
+#include <kaction.h>
+#include <kstdguiitem.h>
+#include <kaboutdata.h>
+#include <klocale.h>
+#include <kconfig.h>
+#include <kiconloader.h>
+#include <kdialog.h>
+#include <ktextbrowser.h>
+#include <kglobalsettings.h>
+#include <kmimetype.h>
+#include <kmessagebox.h>
+#include <kwin.h>
+#include <kwinmodule.h>
+#include <kprocess.h>
+#include <kio/netaccess.h>
+#include <knotifyclient.h>
+#include <kpushbutton.h>
+#ifdef WITHOUT_ARTS
+#include <kaudioplayer.h>
+#else
+#include <arts/kartsdispatcher.h>
+#include <arts/kartsserver.h>
+#include <arts/kplayobjectfactory.h>
+#include <arts/kplayobject.h>
+#endif
+#include <dcopclient.h>
+#include <kdebug.h>
+
+#include "alarmcalendar.h"
+#include "deferdlg.h"
+#include "editdlg.h"
+#include "functions.h"
+#include "kalarmapp.h"
+#include "mainwindow.h"
+#include "preferences.h"
+#include "synchtimer.h"
+#include "messagewin.moc"
+
+using namespace KCal;
+
+#ifndef WITHOUT_ARTS
+static const char* KMIX_APP_NAME = "kmix";
+static const char* KMIX_DCOP_OBJECT = "Mixer0";
+static const char* KMIX_DCOP_WINDOW = "kmix-mainwindow#1";
+#endif
+static const char* KMAIL_DCOP_OBJECT = "KMailIface";
+
+// The delay for enabling message window buttons if a zero delay is
+// configured, i.e. the windows are placed far from the cursor.
+static const int proximityButtonDelay = 1000; // (milliseconds)
+static const int proximityMultiple = 10; // multiple of button height distance from cursor for proximity
+
+static bool wantModal();
+
+// A text label widget which can be scrolled and copied with the mouse
+class MessageText : public QTextEdit
+{
+ public:
+ MessageText(const QString& text, const QString& context = QString::null, QWidget* parent = 0, const char* name = 0)
+ : QTextEdit(text, context, parent, name)
+ {
+ setReadOnly(true);
+ setWordWrap(QTextEdit::NoWrap);
+ }
+ int scrollBarHeight() const { return horizontalScrollBar()->height(); }
+ int scrollBarWidth() const { return verticalScrollBar()->width(); }
+ virtual QSize sizeHint() const { return QSize(contentsWidth() + scrollBarWidth(), contentsHeight() + scrollBarHeight()); }
+};
+
+
+class MWMimeSourceFactory : public QMimeSourceFactory
+{
+ public:
+ MWMimeSourceFactory(const QString& absPath, KTextBrowser*);
+ virtual ~MWMimeSourceFactory();
+ virtual const QMimeSource* data(const QString& abs_name) const;
+ private:
+ // Prohibit the following methods
+ virtual void setData(const QString&, QMimeSource*) {}
+ virtual void setExtensionType(const QString&, const char*) {}
+
+ QString mTextFile;
+ QCString mMimeType;
+ mutable const QMimeSource* mLast;
+};
+
+
+// Basic flags for the window
+static const Qt::WFlags WFLAGS = Qt::WStyle_StaysOnTop | Qt::WDestructiveClose;
+
+// Error message bit masks
+enum {
+ ErrMsg_Speak = 0x01,
+ ErrMsg_AudioFile = 0x02,
+ ErrMsg_Volume = 0x04
+};
+
+
+QValueList<MessageWin*> MessageWin::mWindowList;
+QMap<QString, unsigned> MessageWin::mErrorMessages;
+
+
+/******************************************************************************
+* Construct the message window for the specified alarm.
+* Other alarms in the supplied event may have been updated by the caller, so
+* the whole event needs to be stored for updating the calendar file when it is
+* displayed.
+*/
+MessageWin::MessageWin(const KAEvent& event, const KAAlarm& alarm, bool reschedule_event, bool allowDefer)
+ : MainWindowBase(0, "MessageWin", WFLAGS | Qt::WGroupLeader | Qt::WStyle_ContextHelp
+ | (wantModal() ? 0 : Qt::WX11BypassWM)),
+ mMessage(event.cleanText()),
+ mFont(event.font()),
+ mBgColour(event.bgColour()),
+ mFgColour(event.fgColour()),
+ mDateTime((alarm.type() & KAAlarm::REMINDER_ALARM) ? event.mainDateTime(true) : alarm.dateTime(true)),
+ mEventID(event.id()),
+ mAudioFile(event.audioFile()),
+ mVolume(event.soundVolume()),
+ mFadeVolume(event.fadeVolume()),
+ mFadeSeconds(QMIN(event.fadeSeconds(), 86400)),
+ mDefaultDeferMinutes(event.deferDefaultMinutes()),
+ mAlarmType(alarm.type()),
+ mAction(event.action()),
+ mKMailSerialNumber(event.kmailSerialNumber()),
+ mRestoreHeight(0),
+ mAudioRepeat(event.repeatSound()),
+ mConfirmAck(event.confirmAck()),
+ mShowEdit(!mEventID.isEmpty()),
+ mNoDefer(!allowDefer || alarm.repeatAtLogin()),
+ mInvalid(false),
+ mArtsDispatcher(0),
+ mPlayObject(0),
+ mOldVolume(-1),
+ mEvent(event),
+ mEditButton(0),
+ mDeferButton(0),
+ mSilenceButton(0),
+ mDeferDlg(0),
+ mWinModule(0),
+ mFlags(event.flags()),
+ mLateCancel(event.lateCancel()),
+ mErrorWindow(false),
+ mNoPostAction(alarm.type() & KAAlarm::REMINDER_ALARM),
+ mRecreating(false),
+ mBeep(event.beep()),
+ mSpeak(event.speak()),
+ mRescheduleEvent(reschedule_event),
+ mShown(false),
+ mPositioning(false),
+ mNoCloseConfirm(false),
+ mDisableDeferral(false)
+{
+ kdDebug(5950) << "MessageWin::MessageWin(event)" << endl;
+ // Set to save settings automatically, but don't save window size.
+ // File alarm window size is saved elsewhere.
+ setAutoSaveSettings(QString::fromLatin1("MessageWin"), false);
+ initView();
+ mWindowList.append(this);
+ if (event.autoClose())
+ mCloseTime = alarm.dateTime().dateTime().addSecs(event.lateCancel() * 60);
+}
+
+/******************************************************************************
+* Construct the message window for a specified error message.
+*/
+MessageWin::MessageWin(const KAEvent& event, const DateTime& alarmDateTime, const QStringList& errmsgs)
+ : MainWindowBase(0, "MessageWin", WFLAGS | Qt::WGroupLeader | Qt::WStyle_ContextHelp),
+ mMessage(event.cleanText()),
+ mDateTime(alarmDateTime),
+ mEventID(event.id()),
+ mAlarmType(KAAlarm::MAIN_ALARM),
+ mAction(event.action()),
+ mKMailSerialNumber(0),
+ mErrorMsgs(errmsgs),
+ mRestoreHeight(0),
+ mConfirmAck(false),
+ mShowEdit(false),
+ mNoDefer(true),
+ mInvalid(false),
+ mArtsDispatcher(0),
+ mPlayObject(0),
+ mEvent(event),
+ mEditButton(0),
+ mDeferButton(0),
+ mSilenceButton(0),
+ mDeferDlg(0),
+ mWinModule(0),
+ mErrorWindow(true),
+ mNoPostAction(true),
+ mRecreating(false),
+ mRescheduleEvent(false),
+ mShown(false),
+ mPositioning(false),
+ mNoCloseConfirm(false),
+ mDisableDeferral(false)
+{
+ kdDebug(5950) << "MessageWin::MessageWin(errmsg)" << endl;
+ initView();
+ mWindowList.append(this);
+}
+
+/******************************************************************************
+* Construct the message window for restoration by session management.
+* The window is initialised by readProperties().
+*/
+MessageWin::MessageWin()
+ : MainWindowBase(0, "MessageWin", WFLAGS),
+ mArtsDispatcher(0),
+ mPlayObject(0),
+ mEditButton(0),
+ mDeferButton(0),
+ mSilenceButton(0),
+ mDeferDlg(0),
+ mWinModule(0),
+ mErrorWindow(false),
+ mRecreating(false),
+ mRescheduleEvent(false),
+ mShown(false),
+ mPositioning(false),
+ mNoCloseConfirm(false),
+ mDisableDeferral(false)
+{
+ kdDebug(5950) << "MessageWin::MessageWin(restore)\n";
+ mWindowList.append(this);
+}
+
+/******************************************************************************
+* Destructor. Perform any post-alarm actions before tidying up.
+*/
+MessageWin::~MessageWin()
+{
+ kdDebug(5950) << "MessageWin::~MessageWin(" << mEventID << ")" << endl;
+ stopPlay();
+ delete mWinModule;
+ mWinModule = 0;
+ mErrorMessages.remove(mEventID);
+ mWindowList.remove(this);
+ if (!mRecreating)
+ {
+ if (!mNoPostAction && !mEvent.postAction().isEmpty())
+ theApp()->alarmCompleted(mEvent);
+ if (!mWindowList.count())
+ theApp()->quitIf();
+ }
+}
+
+/******************************************************************************
+* Construct the message window.
+*/
+void MessageWin::initView()
+{
+ bool reminder = (!mErrorWindow && (mAlarmType & KAAlarm::REMINDER_ALARM));
+ int leading = fontMetrics().leading();
+ setCaption((mAlarmType & KAAlarm::REMINDER_ALARM) ? i18n("Reminder") : i18n("Message"));
+ QWidget* topWidget = new QWidget(this, "messageWinTop");
+ setCentralWidget(topWidget);
+ QVBoxLayout* topLayout = new QVBoxLayout(topWidget, KDialog::marginHint(), KDialog::spacingHint());
+
+ if (mDateTime.isValid())
+ {
+ // Show the alarm date/time, together with an "Advance reminder" text where appropriate
+ QFrame* frame = 0;
+ QVBoxLayout* layout = topLayout;
+ if (reminder)
+ {
+ frame = new QFrame(topWidget);
+ frame->setFrameStyle(QFrame::Box | QFrame::Raised);
+ topLayout->addWidget(frame, 0, Qt::AlignHCenter);
+ layout = new QVBoxLayout(frame, leading + frame->frameWidth(), leading);
+ }
+
+ // Alarm date/time
+ QLabel* label = new QLabel(frame ? frame : topWidget);
+ label->setText(mDateTime.isDateOnly()
+ ? KGlobal::locale()->formatDate(mDateTime.date(), true)
+ : KGlobal::locale()->formatDateTime(mDateTime.dateTime()));
+ if (!frame)
+ label->setFrameStyle(QFrame::Box | QFrame::Raised);
+ label->setFixedSize(label->sizeHint());
+ layout->addWidget(label, 0, Qt::AlignHCenter);
+ QWhatsThis::add(label,
+ i18n("The scheduled date/time for the message (as opposed to the actual time of display)."));
+
+ if (frame)
+ {
+ label = new QLabel(frame);
+ label->setText(i18n("Reminder"));
+ label->setFixedSize(label->sizeHint());
+ layout->addWidget(label, 0, Qt::AlignHCenter);
+ frame->setFixedSize(frame->sizeHint());
+ }
+ }
+
+ if (!mErrorWindow)
+ {
+ // It's a normal alarm message window
+ switch (mAction)
+ {
+ case KAEvent::FILE:
+ {
+ // Display the file name
+ QLabel* label = new QLabel(mMessage, topWidget);
+ label->setFrameStyle(QFrame::Box | QFrame::Raised);
+ label->setFixedSize(label->sizeHint());
+ QWhatsThis::add(label, i18n("The file whose contents are displayed below"));
+ topLayout->addWidget(label, 0, Qt::AlignHCenter);
+
+ // Display contents of file
+ bool opened = false;
+ bool dir = false;
+ QString tmpFile;
+ KURL url(mMessage);
+ if (KIO::NetAccess::download(url, tmpFile, MainWindow::mainMainWindow()))
+ {
+ QFile qfile(tmpFile);
+ QFileInfo info(qfile);
+ if (!(dir = info.isDir()))
+ {
+ opened = true;
+ KTextBrowser* view = new KTextBrowser(topWidget, "fileContents");
+ MWMimeSourceFactory msf(tmpFile, view);
+ view->setMinimumSize(view->sizeHint());
+ topLayout->addWidget(view);
+
+ // Set the default size to 20 lines square.
+ // Note that after the first file has been displayed, this size
+ // is overridden by the user-set default stored in the config file.
+ // So there is no need to calculate an accurate size.
+ int h = 20*view->fontMetrics().lineSpacing() + 2*view->frameWidth();
+ view->resize(QSize(h, h).expandedTo(view->sizeHint()));
+ QWhatsThis::add(view, i18n("The contents of the file to be displayed"));
+ }
+ KIO::NetAccess::removeTempFile(tmpFile);
+ }
+ if (!opened)
+ {
+ // File couldn't be opened
+ bool exists = KIO::NetAccess::exists(url, true, MainWindow::mainMainWindow());
+ mErrorMsgs += dir ? i18n("File is a folder") : exists ? i18n("Failed to open file") : i18n("File not found");
+ }
+ break;
+ }
+ case KAEvent::MESSAGE:
+ {
+ // Message label
+ // Using MessageText instead of QLabel allows scrolling and mouse copying
+ MessageText* text = new MessageText(mMessage, QString::null, topWidget);
+ text->setFrameStyle(QFrame::NoFrame);
+ text->setPaper(mBgColour);
+ text->setPaletteForegroundColor(mFgColour);
+ text->setFont(mFont);
+ int lineSpacing = text->fontMetrics().lineSpacing();
+ QSize s = text->sizeHint();
+ int h = s.height();
+ text->setMaximumHeight(h + text->scrollBarHeight());
+ text->setMinimumHeight(QMIN(h, lineSpacing*4));
+ text->setMaximumWidth(s.width() + text->scrollBarWidth());
+ QWhatsThis::add(text, i18n("The alarm message"));
+ int vspace = lineSpacing/2;
+ int hspace = lineSpacing - KDialog::marginHint();
+ topLayout->addSpacing(vspace);
+ topLayout->addStretch();
+ // Don't include any horizontal margins if message is 2/3 screen width
+ if (!mWinModule)
+ mWinModule = new KWinModule(0, KWinModule::INFO_DESKTOP);
+ if (text->sizeHint().width() >= mWinModule->workArea().width()*2/3)
+ topLayout->addWidget(text, 1, Qt::AlignHCenter);
+ else
+ {
+ QBoxLayout* layout = new QHBoxLayout(topLayout);
+ layout->addSpacing(hspace);
+ layout->addWidget(text, 1, Qt::AlignHCenter);
+ layout->addSpacing(hspace);
+ }
+ if (!reminder)
+ topLayout->addStretch();
+ break;
+ }
+ case KAEvent::COMMAND:
+ case KAEvent::EMAIL:
+ default:
+ break;
+ }
+
+ if (reminder)
+ {
+ // Reminder: show remaining time until the actual alarm
+ mRemainingText = new QLabel(topWidget);
+ mRemainingText->setFrameStyle(QFrame::Box | QFrame::Raised);
+ mRemainingText->setMargin(leading);
+ if (mDateTime.isDateOnly() || QDate::currentDate().daysTo(mDateTime.date()) > 0)
+ {
+ setRemainingTextDay();
+ MidnightTimer::connect(this, SLOT(setRemainingTextDay())); // update every day
+ }
+ else
+ {
+ setRemainingTextMinute();
+ MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute
+ }
+ topLayout->addWidget(mRemainingText, 0, Qt::AlignHCenter);
+ topLayout->addSpacing(KDialog::spacingHint());
+ topLayout->addStretch();
+ }
+ }
+ else
+ {
+ // It's an error message
+ switch (mAction)
+ {
+ case KAEvent::EMAIL:
+ {
+ // Display the email addresses and subject.
+ QFrame* frame = new QFrame(topWidget);
+ frame->setFrameStyle(QFrame::Box | QFrame::Raised);
+ QWhatsThis::add(frame, i18n("The email to send"));
+ topLayout->addWidget(frame, 0, Qt::AlignHCenter);
+ QGridLayout* grid = new QGridLayout(frame, 2, 2, KDialog::marginHint(), KDialog::spacingHint());
+
+ QLabel* label = new QLabel(i18n("Email addressee", "To:"), frame);
+ label->setFixedSize(label->sizeHint());
+ grid->addWidget(label, 0, 0, Qt::AlignLeft);
+ label = new QLabel(mEvent.emailAddresses("\n"), frame);
+ label->setFixedSize(label->sizeHint());
+ grid->addWidget(label, 0, 1, Qt::AlignLeft);
+
+ label = new QLabel(i18n("Email subject", "Subject:"), frame);
+ label->setFixedSize(label->sizeHint());
+ grid->addWidget(label, 1, 0, Qt::AlignLeft);
+ label = new QLabel(mEvent.emailSubject(), frame);
+ label->setFixedSize(label->sizeHint());
+ grid->addWidget(label, 1, 1, Qt::AlignLeft);
+ break;
+ }
+ case KAEvent::COMMAND:
+ case KAEvent::FILE:
+ case KAEvent::MESSAGE:
+ default:
+ // Just display the error message strings
+ break;
+ }
+ }
+
+ if (!mErrorMsgs.count())
+ topWidget->setBackgroundColor(mBgColour);
+ else
+ {
+ setCaption(i18n("Error"));
+ QBoxLayout* layout = new QHBoxLayout(topLayout);
+ layout->setMargin(2*KDialog::marginHint());
+ layout->addStretch();
+ QLabel* label = new QLabel(topWidget);
+ label->setPixmap(DesktopIcon("error"));
+ label->setFixedSize(label->sizeHint());
+ layout->addWidget(label, 0, Qt::AlignRight);
+ QBoxLayout* vlayout = new QVBoxLayout(layout);
+ for (QStringList::Iterator it = mErrorMsgs.begin(); it != mErrorMsgs.end(); ++it)
+ {
+ label = new QLabel(*it, topWidget);
+ label->setFixedSize(label->sizeHint());
+ vlayout->addWidget(label, 0, Qt::AlignLeft);
+ }
+ layout->addStretch();
+ }
+
+ QGridLayout* grid = new QGridLayout(1, 4);
+ topLayout->addLayout(grid);
+ grid->setColStretch(0, 1); // keep the buttons right-adjusted in the window
+ int gridIndex = 1;
+
+ // Close button
+ mOkButton = new KPushButton(KStdGuiItem::close(), topWidget);
+ // Prevent accidental acknowledgement of the message if the user is typing when the window appears
+ mOkButton->clearFocus();
+ mOkButton->setFocusPolicy(QWidget::ClickFocus); // don't allow keyboard selection
+ mOkButton->setFixedSize(mOkButton->sizeHint());
+ connect(mOkButton, SIGNAL(clicked()), SLOT(close()));
+ grid->addWidget(mOkButton, 0, gridIndex++, AlignHCenter);
+ QWhatsThis::add(mOkButton, i18n("Acknowledge the alarm"));
+
+ if (mShowEdit)
+ {
+ // Edit button
+ mEditButton = new QPushButton(i18n("&Edit..."), topWidget);
+ mEditButton->setFocusPolicy(QWidget::ClickFocus); // don't allow keyboard selection
+ mEditButton->setFixedSize(mEditButton->sizeHint());
+ connect(mEditButton, SIGNAL(clicked()), SLOT(slotEdit()));
+ grid->addWidget(mEditButton, 0, gridIndex++, AlignHCenter);
+ QWhatsThis::add(mEditButton, i18n("Edit the alarm."));
+ }
+
+ if (!mNoDefer)
+ {
+ // Defer button
+ mDeferButton = new QPushButton(i18n("&Defer..."), topWidget);
+ mDeferButton->setFocusPolicy(QWidget::ClickFocus); // don't allow keyboard selection
+ mDeferButton->setFixedSize(mDeferButton->sizeHint());
+ connect(mDeferButton, SIGNAL(clicked()), SLOT(slotDefer()));
+ grid->addWidget(mDeferButton, 0, gridIndex++, AlignHCenter);
+ QWhatsThis::add(mDeferButton,
+ i18n("Defer the alarm until later.\n"
+ "You will be prompted to specify when the alarm should be redisplayed."));
+
+ setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more
+ }
+
+#ifndef WITHOUT_ARTS
+ if (!mAudioFile.isEmpty() && (mVolume || mFadeVolume > 0))
+ {
+ // Silence button to stop sound repetition
+ QPixmap pixmap = MainBarIcon("player_stop");
+ mSilenceButton = new QPushButton(topWidget);
+ mSilenceButton->setPixmap(pixmap);
+ mSilenceButton->setFixedSize(mSilenceButton->sizeHint());
+ connect(mSilenceButton, SIGNAL(clicked()), SLOT(stopPlay()));
+ grid->addWidget(mSilenceButton, 0, gridIndex++, AlignHCenter);
+ QToolTip::add(mSilenceButton, i18n("Stop sound"));
+ QWhatsThis::add(mSilenceButton, i18n("Stop playing the sound"));
+ // To avoid getting in a mess, disable the button until sound playing has been set up
+ mSilenceButton->setEnabled(false);
+ }
+#endif
+
+ KIconLoader iconLoader;
+ if (mKMailSerialNumber)
+ {
+ // KMail button
+ QPixmap pixmap = iconLoader.loadIcon(QString::fromLatin1("kmail"), KIcon::MainToolbar);
+ mKMailButton = new QPushButton(topWidget);
+ mKMailButton->setPixmap(pixmap);
+ mKMailButton->setFixedSize(mKMailButton->sizeHint());
+ connect(mKMailButton, SIGNAL(clicked()), SLOT(slotShowKMailMessage()));
+ grid->addWidget(mKMailButton, 0, gridIndex++, AlignHCenter);
+ QToolTip::add(mKMailButton, i18n("Locate this email in KMail", "Locate in KMail"));
+ QWhatsThis::add(mKMailButton, i18n("Locate and highlight this email in KMail"));
+ }
+ else
+ mKMailButton = 0;
+
+ // KAlarm button
+ QPixmap pixmap = iconLoader.loadIcon(QString::fromLatin1(kapp->aboutData()->appName()), KIcon::MainToolbar);
+ mKAlarmButton = new QPushButton(topWidget);
+ mKAlarmButton->setPixmap(pixmap);
+ mKAlarmButton->setFixedSize(mKAlarmButton->sizeHint());
+ connect(mKAlarmButton, SIGNAL(clicked()), SLOT(displayMainWindow()));
+ grid->addWidget(mKAlarmButton, 0, gridIndex++, AlignHCenter);
+ QString actKAlarm = i18n("Activate KAlarm");
+ QToolTip::add(mKAlarmButton, actKAlarm);
+ QWhatsThis::add(mKAlarmButton, actKAlarm);
+
+ // Disable all buttons initially, to prevent accidental clicking on if they happen to be
+ // under the mouse just as the window appears.
+ mOkButton->setEnabled(false);
+ if (mDeferButton)
+ mDeferButton->setEnabled(false);
+ if (mEditButton)
+ mEditButton->setEnabled(false);
+ if (mKMailButton)
+ mKMailButton->setEnabled(false);
+ mKAlarmButton->setEnabled(false);
+
+ topLayout->activate();
+ setMinimumSize(QSize(grid->sizeHint().width() + 2*KDialog::marginHint(), sizeHint().height()));
+
+ bool modal = !(getWFlags() & Qt::WX11BypassWM);
+
+ unsigned long wstate = (modal ? NET::Modal : 0) | NET::Sticky | NET::KeepAbove;
+ WId winid = winId();
+ KWin::setState(winid, wstate);
+ KWin::setOnAllDesktops(winid, true);
+}
+
+/******************************************************************************
+* Set the remaining time text in a reminder window.
+* Called at the start of every day (at the user-defined start-of-day time).
+*/
+void MessageWin::setRemainingTextDay()
+{
+ QString text;
+ int days = QDate::currentDate().daysTo(mDateTime.date());
+ if (days <= 0 && !mDateTime.isDateOnly())
+ {
+ // The alarm is due today, so start refreshing every minute
+ MidnightTimer::disconnect(this, SLOT(setRemainingTextDay()));
+ setRemainingTextMinute();
+ MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute
+ }
+ else
+ {
+ if (days <= 0)
+ text = i18n("Today");
+ else if (days % 7)
+ text = i18n("Tomorrow", "in %n days' time", days);
+ else
+ text = i18n("in 1 week's time", "in %n weeks' time", days/7);
+ }
+ mRemainingText->setText(text);
+}
+
+/******************************************************************************
+* Set the remaining time text in a reminder window.
+* Called on every minute boundary.
+*/
+void MessageWin::setRemainingTextMinute()
+{
+ QString text;
+ int mins = (QDateTime::currentDateTime().secsTo(mDateTime.dateTime()) + 59) / 60;
+ if (mins < 60)
+ text = i18n("in 1 minute's time", "in %n minutes' time", (mins > 0 ? mins : 0));
+ else if (mins % 60 == 0)
+ text = i18n("in 1 hour's time", "in %n hours' time", mins/60);
+ else if (mins % 60 == 1)
+ text = i18n("in 1 hour 1 minute's time", "in %n hours 1 minute's time", mins/60);
+ else
+ text = i18n("in 1 hour %1 minutes' time", "in %n hours %1 minutes' time", mins/60).arg(mins%60);
+ mRemainingText->setText(text);
+}
+
+/******************************************************************************
+* Save settings to the session managed config file, for restoration
+* when the program is restored.
+*/
+void MessageWin::saveProperties(KConfig* config)
+{
+ if (mShown && !mErrorWindow)
+ {
+ config->writeEntry(QString::fromLatin1("EventID"), mEventID);
+ config->writeEntry(QString::fromLatin1("AlarmType"), mAlarmType);
+ config->writeEntry(QString::fromLatin1("Message"), mMessage);
+ config->writeEntry(QString::fromLatin1("Type"), mAction);
+ config->writeEntry(QString::fromLatin1("Font"), mFont);
+ config->writeEntry(QString::fromLatin1("BgColour"), mBgColour);
+ config->writeEntry(QString::fromLatin1("FgColour"), mFgColour);
+ config->writeEntry(QString::fromLatin1("ConfirmAck"), mConfirmAck);
+ if (mDateTime.isValid())
+ {
+ config->writeEntry(QString::fromLatin1("Time"), mDateTime.dateTime());
+ config->writeEntry(QString::fromLatin1("DateOnly"), mDateTime.isDateOnly());
+ }
+ if (mCloseTime.isValid())
+ config->writeEntry(QString::fromLatin1("Expiry"), mCloseTime);
+#ifndef WITHOUT_ARTS
+ if (mAudioRepeat && mSilenceButton && mSilenceButton->isEnabled())
+ {
+ // Only need to restart sound file playing if it's being repeated
+ config->writePathEntry(QString::fromLatin1("AudioFile"), mAudioFile);
+ config->writeEntry(QString::fromLatin1("Volume"), static_cast<int>(mVolume * 100));
+ }
+#endif
+ config->writeEntry(QString::fromLatin1("Speak"), mSpeak);
+ config->writeEntry(QString::fromLatin1("Height"), height());
+ config->writeEntry(QString::fromLatin1("DeferMins"), mDefaultDeferMinutes);
+ config->writeEntry(QString::fromLatin1("NoDefer"), mNoDefer);
+ config->writeEntry(QString::fromLatin1("NoPostAction"), mNoPostAction);
+ config->writeEntry(QString::fromLatin1("KMailSerial"), mKMailSerialNumber);
+ }
+ else
+ config->writeEntry(QString::fromLatin1("Invalid"), true);
+}
+
+/******************************************************************************
+* Read settings from the session managed config file.
+* This function is automatically called whenever the app is being restored.
+* Read in whatever was saved in saveProperties().
+*/
+void MessageWin::readProperties(KConfig* config)
+{
+ mInvalid = config->readBoolEntry(QString::fromLatin1("Invalid"), false);
+ mEventID = config->readEntry(QString::fromLatin1("EventID"));
+ mAlarmType = KAAlarm::Type(config->readNumEntry(QString::fromLatin1("AlarmType")));
+ mMessage = config->readEntry(QString::fromLatin1("Message"));
+ mAction = KAEvent::Action(config->readNumEntry(QString::fromLatin1("Type")));
+ mFont = config->readFontEntry(QString::fromLatin1("Font"));
+ mBgColour = config->readColorEntry(QString::fromLatin1("BgColour"));
+ mFgColour = config->readColorEntry(QString::fromLatin1("FgColour"));
+ mConfirmAck = config->readBoolEntry(QString::fromLatin1("ConfirmAck"));
+ QDateTime invalidDateTime;
+ QDateTime dt = config->readDateTimeEntry(QString::fromLatin1("Time"), &invalidDateTime);
+ bool dateOnly = config->readBoolEntry(QString::fromLatin1("DateOnly"));
+ mDateTime.set(dt, dateOnly);
+ mCloseTime = config->readDateTimeEntry(QString::fromLatin1("Expiry"), &invalidDateTime);
+#ifndef WITHOUT_ARTS
+ mAudioFile = config->readPathEntry(QString::fromLatin1("AudioFile"));
+ mVolume = static_cast<float>(config->readNumEntry(QString::fromLatin1("Volume"))) / 100;
+ mFadeVolume = -1;
+ mFadeSeconds = 0;
+ if (!mAudioFile.isEmpty())
+ mAudioRepeat = true;
+#endif
+ mSpeak = config->readBoolEntry(QString::fromLatin1("Speak"));
+ mRestoreHeight = config->readNumEntry(QString::fromLatin1("Height"));
+ mDefaultDeferMinutes = config->readNumEntry(QString::fromLatin1("DeferMins"));
+ mNoDefer = config->readBoolEntry(QString::fromLatin1("NoDefer"));
+ mNoPostAction = config->readBoolEntry(QString::fromLatin1("NoPostAction"));
+ mKMailSerialNumber = config->readUnsignedLongNumEntry(QString::fromLatin1("KMailSerial"));
+ mShowEdit = false;
+ kdDebug(5950) << "MessageWin::readProperties(" << mEventID << ")" << endl;
+ if (mAlarmType != KAAlarm::INVALID_ALARM)
+ {
+ // Recreate the event from the calendar file (if possible)
+ if (!mEventID.isEmpty())
+ {
+ const Event* kcalEvent = AlarmCalendar::activeCalendar()->event(mEventID);
+ if (!kcalEvent)
+ {
+ // It's not in the active calendar, so try the displaying calendar
+ AlarmCalendar* cal = AlarmCalendar::displayCalendar();
+ if (cal->isOpen())
+ kcalEvent = cal->event(KAEvent::uid(mEventID, KAEvent::DISPLAYING));
+ }
+ if (kcalEvent)
+ {
+ mEvent.set(*kcalEvent);
+ mEvent.setUid(KAEvent::ACTIVE); // in case it came from the display calendar
+ mShowEdit = true;
+ }
+ }
+ initView();
+ }
+}
+
+/******************************************************************************
+* Returns the existing message window (if any) which is displaying the event
+* with the specified ID.
+*/
+MessageWin* MessageWin::findEvent(const QString& eventID)
+{
+ for (QValueList<MessageWin*>::Iterator it = mWindowList.begin(); it != mWindowList.end(); ++it)
+ {
+ MessageWin* w = *it;
+ if (w->mEventID == eventID && !w->mErrorWindow)
+ return w;
+ }
+ return 0;
+}
+
+/******************************************************************************
+* Beep and play the audio file, as appropriate.
+*/
+void MessageWin::playAudio()
+{
+ if (mBeep)
+ {
+ // Beep using two methods, in case the sound card/speakers are switched off or not working
+ KNotifyClient::beep(); // beep through the sound card & speakers
+ QApplication::beep(); // beep through the internal speaker
+ }
+ if (!mAudioFile.isEmpty())
+ {
+ if (!mVolume && mFadeVolume <= 0)
+ return; // ensure zero volume doesn't play anything
+#ifdef WITHOUT_ARTS
+ QString play = mAudioFile;
+ QString file = QString::fromLatin1("file:");
+ if (mAudioFile.startsWith(file))
+ play = mAudioFile.mid(file.length());
+ KAudioPlayer::play(QFile::encodeName(play));
+#else
+ // An audio file is specified. Because loading it may take some time,
+ // call it on a timer to allow the window to display first.
+ QTimer::singleShot(0, this, SLOT(slotPlayAudio()));
+#endif
+ }
+ else if (mSpeak)
+ {
+ // The message is to be spoken. In case of error messges,
+ // call it on a timer to allow the window to display first.
+ QTimer::singleShot(0, this, SLOT(slotSpeak()));
+ }
+}
+
+/******************************************************************************
+* Speak the message.
+* Called asynchronously to avoid delaying the display of the message.
+*/
+void MessageWin::slotSpeak()
+{
+ DCOPClient* client = kapp->dcopClient();
+ if (!client->isApplicationRegistered("kttsd"))
+ {
+ // kttsd is not running, so start it
+ QString error;
+ if (kapp->startServiceByDesktopName("kttsd", QStringList(), &error))
+ {
+ kdDebug(5950) << "MessageWin::slotSpeak(): failed to start kttsd: " << error << endl;
+ if (!haveErrorMessage(ErrMsg_Speak))
+ {
+ KMessageBox::detailedError(0, i18n("Unable to speak message"), error);
+ clearErrorMessage(ErrMsg_Speak);
+ }
+ return;
+ }
+ }
+ QByteArray data;
+ QDataStream arg(data, IO_WriteOnly);
+ arg << mMessage << "";
+ if (!client->send("kttsd", "KSpeech", "sayMessage(QString,QString)", data))
+ {
+ kdDebug(5950) << "MessageWin::slotSpeak(): sayMessage() DCOP error" << endl;
+ if (!haveErrorMessage(ErrMsg_Speak))
+ {
+ KMessageBox::detailedError(0, i18n("Unable to speak message"), i18n("DCOP Call sayMessage failed"));
+ clearErrorMessage(ErrMsg_Speak);
+ }
+ }
+}
+
+/******************************************************************************
+* Play the audio file.
+* Called asynchronously to avoid delaying the display of the message.
+*/
+void MessageWin::slotPlayAudio()
+{
+#ifndef WITHOUT_ARTS
+ // First check that it exists, to avoid possible crashes if the filename is badly specified
+ MainWindow* mmw = MainWindow::mainMainWindow();
+ KURL url(mAudioFile);
+ if (!url.isValid() || !KIO::NetAccess::exists(url, true, mmw)
+ || !KIO::NetAccess::download(url, mLocalAudioFile, mmw))
+ {
+ kdError(5950) << "MessageWin::playAudio(): Open failure: " << mAudioFile << endl;
+ if (!haveErrorMessage(ErrMsg_AudioFile))
+ {
+ KMessageBox::error(this, i18n("Cannot open audio file:\n%1").arg(mAudioFile));
+ clearErrorMessage(ErrMsg_AudioFile);
+ }
+ return;
+ }
+ if (!mArtsDispatcher)
+ {
+ mFadeTimer = 0;
+ mPlayTimer = new QTimer(this);
+ connect(mPlayTimer, SIGNAL(timeout()), SLOT(checkAudioPlay()));
+ mArtsDispatcher = new KArtsDispatcher;
+ mPlayedOnce = false;
+ mAudioFileStart = QTime::currentTime();
+ initAudio(true);
+ if (!mPlayObject->object().isNull())
+ checkAudioPlay();
+ if (!mUsingKMix && mVolume >= 0)
+ {
+ // Output error message now that everything else has been done.
+ // (Outputting it earlier would delay things until it is acknowledged.)
+ kdWarning(5950) << "Unable to set master volume (KMix: " << mKMixError << ")\n";
+ if (!haveErrorMessage(ErrMsg_Volume))
+ {
+ KMessageBox::information(this, i18n("Unable to set master volume\n(Error accessing KMix:\n%1)").arg(mKMixError),
+ QString::null, QString::fromLatin1("KMixError"));
+ clearErrorMessage(ErrMsg_Volume);
+ }
+ }
+ }
+#endif
+}
+
+#ifndef WITHOUT_ARTS
+/******************************************************************************
+* Set up the audio file for playing.
+*/
+void MessageWin::initAudio(bool firstTime)
+{
+ KArtsServer aserver;
+ Arts::SoundServerV2 sserver = aserver.server();
+ KDE::PlayObjectFactory factory(sserver);
+ mPlayObject = factory.createPlayObject(mLocalAudioFile, true);
+ if (firstTime)
+ {
+ // Save the existing sound volume setting for restoration afterwards,
+ // and set the desired volume for the alarm.
+ mUsingKMix = false;
+ float volume = mVolume; // initial volume
+ if (volume >= 0)
+ {
+ // The volume has been specified
+ if (mFadeVolume >= 0)
+ volume = mFadeVolume; // fading, so adjust the initial volume
+
+ // Get the current master volume from KMix
+ int vol = getKMixVolume();
+ if (vol >= 0)
+ {
+ mOldVolume = vol; // success
+ mUsingKMix = true;
+ setKMixVolume(static_cast<int>(volume * 100));
+ }
+ }
+ if (!mUsingKMix)
+ {
+ /* Adjust within the current master volume, because either
+ * a) the volume is not specified, in which case we want to play
+ * at 100% of the current master volume setting, or
+ * b) KMix is not available to set the master volume.
+ */
+ mOldVolume = sserver.outVolume().scaleFactor(); // save volume for restoration afterwards
+ sserver.outVolume().scaleFactor(volume >= 0 ? volume : 1);
+ }
+ }
+ mSilenceButton->setEnabled(true);
+ mPlayed = false;
+ connect(mPlayObject, SIGNAL(playObjectCreated()), SLOT(checkAudioPlay()));
+ if (!mPlayObject->object().isNull())
+ checkAudioPlay();
+}
+#endif
+
+/******************************************************************************
+* Called when the audio file has loaded and is ready to play, or on a timer
+* when play is expected to have completed.
+* If it is ready to play, start playing it (for the first time or repeated).
+* If play has not yet completed, wait a bit longer.
+*/
+void MessageWin::checkAudioPlay()
+{
+#ifndef WITHOUT_ARTS
+ if (!mPlayObject)
+ return;
+ if (mPlayObject->state() == Arts::posIdle)
+ {
+ // The file has loaded and is ready to play, or play has completed
+ if (mPlayedOnce && !mAudioRepeat)
+ {
+ // Play has completed
+ stopPlay();
+ return;
+ }
+
+ // Start playing the file, either for the first time or again
+ kdDebug(5950) << "MessageWin::checkAudioPlay(): start\n";
+ if (!mPlayedOnce)
+ {
+ // Start playing the file for the first time
+ QTime now = QTime::currentTime();
+ mAudioFileLoadSecs = mAudioFileStart.secsTo(now);
+ if (mAudioFileLoadSecs < 0)
+ mAudioFileLoadSecs += 86400;
+ if (mVolume >= 0 && mFadeVolume >= 0 && mFadeSeconds > 0)
+ {
+ // Set up volume fade
+ mAudioFileStart = now;
+ mFadeTimer = new QTimer(this);
+ connect(mFadeTimer, SIGNAL(timeout()), SLOT(slotFade()));
+ mFadeTimer->start(1000); // adjust volume every second
+ }
+ mPlayedOnce = true;
+ }
+ if (mAudioFileLoadSecs < 3)
+ {
+ /* The aRts library takes several attempts before a PlayObject can
+ * be replayed, leaving a gap of perhaps 5 seconds between plays.
+ * So if loading the file takes a short time, it's better to reload
+ * the PlayObject rather than try to replay the same PlayObject.
+ */
+ if (mPlayed)
+ {
+ // Playing has completed. Start playing again.
+ delete mPlayObject;
+ initAudio(false);
+ if (mPlayObject->object().isNull())
+ return;
+ }
+ mPlayed = true;
+ mPlayObject->play();
+ }
+ else
+ {
+ // The file is slow to load, so attempt to replay the PlayObject
+ static Arts::poTime t0((long)0, (long)0, 0, std::string());
+ Arts::poTime current = mPlayObject->currentTime();
+ if (current.seconds || current.ms)
+ mPlayObject->seek(t0);
+ else
+ mPlayObject->play();
+ }
+ }
+
+ // The sound file is still playing
+ Arts::poTime overall = mPlayObject->overallTime();
+ Arts::poTime current = mPlayObject->currentTime();
+ int time = 1000*(overall.seconds - current.seconds) + overall.ms - current.ms;
+ if (time < 0)
+ time = 0;
+ kdDebug(5950) << "MessageWin::checkAudioPlay(): wait for " << (time+100) << "ms\n";
+ mPlayTimer->start(time + 100, true);
+#endif
+}
+
+/******************************************************************************
+* Called when play completes, the Silence button is clicked, or the window is
+* closed, to reset the sound volume and terminate audio access.
+*/
+void MessageWin::stopPlay()
+{
+#ifndef WITHOUT_ARTS
+ if (mArtsDispatcher)
+ {
+ // Restore the sound volume to what it was before the sound file
+ // was played, provided that nothing else has modified it since.
+ if (!mUsingKMix)
+ {
+ KArtsServer aserver;
+ Arts::StereoVolumeControl svc = aserver.server().outVolume();
+ float currentVolume = svc.scaleFactor();
+ float eventVolume = mVolume;
+ if (eventVolume < 0)
+ eventVolume = 1;
+ if (currentVolume == eventVolume)
+ svc.scaleFactor(mOldVolume);
+ }
+ else if (mVolume >= 0)
+ {
+ int eventVolume = static_cast<int>(mVolume * 100);
+ int currentVolume = getKMixVolume();
+ // Volume returned isn't always exactly equal to volume set
+ if (currentVolume < 0 || abs(currentVolume - eventVolume) < 5)
+ setKMixVolume(static_cast<int>(mOldVolume));
+ }
+ }
+ delete mPlayObject; mPlayObject = 0;
+ delete mArtsDispatcher; mArtsDispatcher = 0;
+ if (!mLocalAudioFile.isEmpty())
+ {
+ KIO::NetAccess::removeTempFile(mLocalAudioFile); // removes it only if it IS a temporary file
+ mLocalAudioFile = QString::null;
+ }
+ if (mSilenceButton)
+ mSilenceButton->setEnabled(false);
+#endif
+}
+
+/******************************************************************************
+* Called every second to fade the volume when the audio file starts playing.
+*/
+void MessageWin::slotFade()
+{
+#ifndef WITHOUT_ARTS
+ QTime now = QTime::currentTime();
+ int elapsed = mAudioFileStart.secsTo(now);
+ if (elapsed < 0)
+ elapsed += 86400; // it's the next day
+ float volume;
+ if (elapsed >= mFadeSeconds)
+ {
+ // The fade has finished. Set to normal volume.
+ volume = mVolume;
+ delete mFadeTimer;
+ mFadeTimer = 0;
+ if (!mVolume)
+ {
+ kdDebug(5950) << "MessageWin::slotFade(0)\n";
+ stopPlay();
+ return;
+ }
+ }
+ else
+ volume = mFadeVolume + ((mVolume - mFadeVolume) * elapsed) / mFadeSeconds;
+ kdDebug(5950) << "MessageWin::slotFade(" << volume << ")\n";
+ if (mArtsDispatcher)
+ {
+ if (mUsingKMix)
+ setKMixVolume(static_cast<int>(volume * 100));
+ else if (mArtsDispatcher)
+ {
+ KArtsServer aserver;
+ aserver.server().outVolume().scaleFactor(volume);
+ }
+ }
+#endif
+}
+
+#ifndef WITHOUT_ARTS
+/******************************************************************************
+* Get the master volume from KMix.
+* Reply < 0 if failure.
+*/
+int MessageWin::getKMixVolume()
+{
+ if (!KAlarm::runProgram(KMIX_APP_NAME, KMIX_DCOP_WINDOW, mKMixName, mKMixError)) // start KMix if it isn't already running
+ return -1;
+ QByteArray data, replyData;
+ QCString replyType;
+ QDataStream arg(data, IO_WriteOnly);
+ if (!kapp->dcopClient()->call(mKMixName, KMIX_DCOP_OBJECT, "masterVolume()", data, replyType, replyData)
+ || replyType != "int")
+ return -1;
+ int result;
+ QDataStream reply(replyData, IO_ReadOnly);
+ reply >> result;
+ return (result >= 0) ? result : 0;
+}
+
+/******************************************************************************
+* Set the master volume using KMix.
+*/
+void MessageWin::setKMixVolume(int percent)
+{
+ if (!mUsingKMix)
+ return;
+ if (!KAlarm::runProgram(KMIX_APP_NAME, KMIX_DCOP_WINDOW, mKMixName, mKMixError)) // start KMix if it isn't already running
+ return;
+ QByteArray data;
+ QDataStream arg(data, IO_WriteOnly);
+ arg << percent;
+ if (!kapp->dcopClient()->send(mKMixName, KMIX_DCOP_OBJECT, "setMasterVolume(int)", data))
+ kdError(5950) << "MessageWin::setKMixVolume(): kmix dcop call failed\n";
+}
+#endif
+
+/******************************************************************************
+* Raise the alarm window, re-output any required audio notification, and
+* reschedule the alarm in the calendar file.
+*/
+void MessageWin::repeat(const KAAlarm& alarm)
+{
+ if (mDeferDlg)
+ {
+ // Cancel any deferral dialogue so that the user notices something's going on,
+ // and also because the deferral time limit will have changed.
+ delete mDeferDlg;
+ mDeferDlg = 0;
+ }
+ const Event* kcalEvent = mEventID.isNull() ? 0 : AlarmCalendar::activeCalendar()->event(mEventID);
+ if (kcalEvent)
+ {
+ mAlarmType = alarm.type(); // store new alarm type for use if it is later deferred
+ if (!mDeferDlg || Preferences::modalMessages())
+ {
+ raise();
+ playAudio();
+ }
+ KAEvent event(*kcalEvent);
+ mDeferButton->setEnabled(true);
+ setDeferralLimit(event); // ensure that button is disabled when alarm can't be deferred any more
+ theApp()->alarmShowing(event, mAlarmType, mDateTime);
+ }
+}
+
+/******************************************************************************
+* Display the window.
+* If windows are being positioned away from the mouse cursor, it is initially
+* positioned at the top left to slightly reduce the number of times the
+* windows need to be moved in showEvent().
+*/
+void MessageWin::show()
+{
+ if (mCloseTime.isValid())
+ {
+ // Set a timer to auto-close the window
+ int delay = QDateTime::currentDateTime().secsTo(mCloseTime);
+ if (delay < 0)
+ delay = 0;
+ QTimer::singleShot(delay * 1000, this, SLOT(close()));
+ if (!delay)
+ return; // don't show the window if auto-closing is already due
+ }
+ if (Preferences::messageButtonDelay() == 0)
+ move(0, 0);
+ MainWindowBase::show();
+}
+
+/******************************************************************************
+* Returns the window's recommended size exclusive of its frame.
+* For message windows, the size if limited to fit inside the working area of
+* the desktop.
+*/
+QSize MessageWin::sizeHint() const
+{
+ if (mAction != KAEvent::MESSAGE)
+ return MainWindowBase::sizeHint();
+ if (!mWinModule)
+ mWinModule = new KWinModule(0, KWinModule::INFO_DESKTOP);
+ QSize frame = frameGeometry().size();
+ QSize contents = geometry().size();
+ QSize desktop = mWinModule->workArea().size();
+ QSize maxSize(desktop.width() - (frame.width() - contents.width()),
+ desktop.height() - (frame.height() - contents.height()));
+ return MainWindowBase::sizeHint().boundedTo(maxSize);
+}
+
+/******************************************************************************
+* Called when the window is shown.
+* The first time, output any required audio notification, and reschedule or
+* delete the event from the calendar file.
+*/
+void MessageWin::showEvent(QShowEvent* se)
+{
+ MainWindowBase::showEvent(se);
+ if (!mShown)
+ {
+ if (mErrorWindow)
+ enableButtons(); // don't bother repositioning error messages
+ else
+ {
+ /* Set the window size.
+ * Note that the frame thickness is not yet known when this
+ * method is called, so for large windows the size needs to be
+ * set again later.
+ */
+ QSize s = sizeHint(); // fit the window round the message
+ if (mAction == KAEvent::FILE && !mErrorMsgs.count())
+ KAlarm::readConfigWindowSize("FileMessage", s);
+ resize(s);
+
+ mButtonDelay = Preferences::messageButtonDelay() * 1000;
+ if (!mButtonDelay)
+ {
+ /* Try to ensure that the window can't accidentally be acknowledged
+ * by the user clicking the mouse just as it appears.
+ * To achieve this, move the window so that the OK button is as far away
+ * from the cursor as possible. If the buttons are still too close to the
+ * cursor, disable the buttons for a short time.
+ * N.B. This can't be done in show(), since the geometry of the window
+ * is not known until it is displayed. Unfortunately by moving the
+ * window in showEvent(), a flicker is unavoidable.
+ * See the Qt documentation on window geometry for more details.
+ */
+ // PROBLEM: The frame size is not known yet!
+
+ /* Find the usable area of the desktop or, if the desktop comprises
+ * multiple screens, the usable area of the current screen. (If the
+ * message is displayed on a screen other than that currently being
+ * worked with, it might not be noticed.)
+ */
+ QPoint cursor = QCursor::pos();
+ if (!mWinModule)
+ mWinModule = new KWinModule(0, KWinModule::INFO_DESKTOP);
+ QRect desk = mWinModule->workArea();
+ QDesktopWidget* dw = QApplication::desktop();
+ if (dw->numScreens() > 1)
+ desk &= dw->screenGeometry(dw->screenNumber(cursor));
+
+ QRect frame = frameGeometry();
+ QRect rect = geometry();
+ // Find the offsets from the outside of the frame to the edges of the OK button
+ QRect button(mOkButton->mapToParent(QPoint(0, 0)), mOkButton->mapToParent(mOkButton->rect().bottomRight()));
+ int buttonLeft = button.left() + rect.left() - frame.left();
+ int buttonRight = width() - button.right() + frame.right() - rect.right();
+ int buttonTop = button.top() + rect.top() - frame.top();
+ int buttonBottom = height() - button.bottom() + frame.bottom() - rect.bottom();
+
+ int centrex = (desk.width() + buttonLeft - buttonRight) / 2;
+ int centrey = (desk.height() + buttonTop - buttonBottom) / 2;
+ int x = (cursor.x() < centrex) ? desk.right() - frame.width() : desk.left();
+ int y = (cursor.y() < centrey) ? desk.bottom() - frame.height() : desk.top();
+
+ // Find the enclosing rectangle for the new button positions
+ // and check if the cursor is too near
+ QRect buttons = mOkButton->geometry().unite(mKAlarmButton->geometry());
+ buttons.moveBy(rect.left() + x - frame.left(), rect.top() + y - frame.top());
+ int minDistance = proximityMultiple * mOkButton->height();
+ if ((abs(cursor.x() - buttons.left()) < minDistance
+ || abs(cursor.x() - buttons.right()) < minDistance)
+ && (abs(cursor.y() - buttons.top()) < minDistance
+ || abs(cursor.y() - buttons.bottom()) < minDistance))
+ mButtonDelay = proximityButtonDelay; // too near - disable buttons initially
+
+ if (x != frame.left() || y != frame.top())
+ {
+ mPositioning = true;
+ move(x, y);
+ }
+ }
+ if (!mPositioning)
+ displayComplete(); // play audio, etc.
+ if (mAction == KAEvent::MESSAGE)
+ {
+ // Set the window size once the frame size is known
+ QTimer::singleShot(0, this, SLOT(setMaxSize()));
+ }
+ }
+ mShown = true;
+ }
+}
+
+/******************************************************************************
+* Called when the window has been moved.
+*/
+void MessageWin::moveEvent(QMoveEvent* e)
+{
+ MainWindowBase::moveEvent(e);
+ if (mPositioning)
+ {
+ // The window has just been initially positioned
+ mPositioning = false;
+ displayComplete(); // play audio, etc.
+ }
+}
+
+/******************************************************************************
+* Reset the iniital window size if it exceeds the working area of the desktop.
+*/
+void MessageWin::setMaxSize()
+{
+ QSize s = sizeHint();
+ if (width() > s.width() || height() > s.height())
+ resize(s);
+}
+
+/******************************************************************************
+* Called when the window has been displayed properly (in its correct position),
+* to play sounds and reschedule the event.
+*/
+void MessageWin::displayComplete()
+{
+ playAudio();
+ if (mRescheduleEvent)
+ theApp()->alarmShowing(mEvent, mAlarmType, mDateTime);
+
+ // Enable the window's buttons either now or after the configured delay
+ if (mButtonDelay > 0)
+ QTimer::singleShot(mButtonDelay, this, SLOT(enableButtons()));
+ else
+ enableButtons();
+}
+
+/******************************************************************************
+* Enable the window's buttons.
+*/
+void MessageWin::enableButtons()
+{
+ mOkButton->setEnabled(true);
+ mKAlarmButton->setEnabled(true);
+ if (mDeferButton && !mDisableDeferral)
+ mDeferButton->setEnabled(true);
+ if (mEditButton)
+ mEditButton->setEnabled(true);
+ if (mKMailButton)
+ mKMailButton->setEnabled(true);
+}
+
+/******************************************************************************
+* Called when the window's size has changed (before it is painted).
+*/
+void MessageWin::resizeEvent(QResizeEvent* re)
+{
+ if (mRestoreHeight)
+ {
+ // Restore the window height on session restoration
+ if (mRestoreHeight != re->size().height())
+ {
+ QSize size = re->size();
+ size.setHeight(mRestoreHeight);
+ resize(size);
+ }
+ else if (isVisible())
+ mRestoreHeight = 0;
+ }
+ else
+ {
+ if (mShown && mAction == KAEvent::FILE && !mErrorMsgs.count())
+ KAlarm::writeConfigWindowSize("FileMessage", re->size());
+ MainWindowBase::resizeEvent(re);
+ }
+}
+
+/******************************************************************************
+* Called when a close event is received.
+* Only quits the application if there is no system tray icon displayed.
+*/
+void MessageWin::closeEvent(QCloseEvent* ce)
+{
+ // Don't prompt or delete the alarm from the display calendar if the session is closing
+ if (!mErrorWindow && !theApp()->sessionClosingDown())
+ {
+ if (mConfirmAck && !mNoCloseConfirm)
+ {
+ // Ask for confirmation of acknowledgement. Use warningYesNo() because its default is No.
+ if (KMessageBox::warningYesNo(this, i18n("Do you really want to acknowledge this alarm?"),
+ i18n("Acknowledge Alarm"), i18n("&Acknowledge"), KStdGuiItem::cancel())
+ != KMessageBox::Yes)
+ {
+ ce->ignore();
+ return;
+ }
+ }
+ if (!mEventID.isNull())
+ {
+ // Delete from the display calendar
+ KAlarm::deleteDisplayEvent(KAEvent::uid(mEventID, KAEvent::DISPLAYING));
+ }
+ }
+ MainWindowBase::closeEvent(ce);
+}
+
+/******************************************************************************
+* Called when the KMail button is clicked.
+* Tells KMail to display the email message displayed in this message window.
+*/
+void MessageWin::slotShowKMailMessage()
+{
+ kdDebug(5950) << "MessageWin::slotShowKMailMessage()\n";
+ if (!mKMailSerialNumber)
+ return;
+ QString err = KAlarm::runKMail(false);
+ if (!err.isNull())
+ {
+ KMessageBox::sorry(this, err);
+ return;
+ }
+ QCString replyType;
+ QByteArray data, replyData;
+ QDataStream arg(data, IO_WriteOnly);
+ arg << (Q_UINT32)mKMailSerialNumber << QString::null;
+ if (kapp->dcopClient()->call("kmail", KMAIL_DCOP_OBJECT, "showMail(Q_UINT32,QString)", data, replyType, replyData)
+ && replyType == "bool")
+ {
+ bool result;
+ QDataStream replyStream(replyData, IO_ReadOnly);
+ replyStream >> result;
+ if (result)
+ return; // success
+ }
+ kdError(5950) << "MessageWin::slotShowKMailMessage(): kmail dcop call failed\n";
+ KMessageBox::sorry(this, i18n("Unable to locate this email in KMail"));
+}
+
+/******************************************************************************
+* Called when the Edit... button is clicked.
+* Displays the alarm edit dialog.
+*/
+void MessageWin::slotEdit()
+{
+ kdDebug(5950) << "MessageWin::slotEdit()" << endl;
+ EditAlarmDlg editDlg(false, i18n("Edit Alarm"), this, "editDlg", &mEvent);
+ if (editDlg.exec() == QDialog::Accepted)
+ {
+ KAEvent event;
+ editDlg.getEvent(event);
+
+ // Update the displayed lists and the calendar file
+ KAlarm::UpdateStatus status;
+ if (AlarmCalendar::activeCalendar()->event(mEventID))
+ {
+ // The old alarm hasn't expired yet, so replace it
+ status = KAlarm::modifyEvent(mEvent, event, 0, &editDlg);
+ Undo::saveEdit(mEvent, event);
+ }
+ else
+ {
+ // The old event has expired, so simply create a new one
+ status = KAlarm::addEvent(event, 0, &editDlg);
+ Undo::saveAdd(event);
+ }
+
+ if (status == KAlarm::UPDATE_KORG_ERR)
+ KAlarm::displayKOrgUpdateError(&editDlg, KAlarm::KORG_ERR_MODIFY, 1);
+ KAlarm::outputAlarmWarnings(&editDlg, &event);
+
+ // Close the alarm window
+ mNoCloseConfirm = true; // allow window to close without confirmation prompt
+ close();
+ }
+}
+
+/******************************************************************************
+* Set up to disable the defer button when the deferral limit is reached.
+*/
+void MessageWin::setDeferralLimit(const KAEvent& event)
+{
+ if (mDeferButton)
+ {
+ mDeferLimit = event.deferralLimit().dateTime();
+ MidnightTimer::connect(this, SLOT(checkDeferralLimit())); // check every day
+ mDisableDeferral = false;
+ checkDeferralLimit();
+ }
+}
+
+/******************************************************************************
+* Check whether the deferral limit has been reached.
+* If so, disable the Defer button.
+* N.B. Ideally, just a single QTimer::singleShot() call would be made to disable
+* the defer button at the corret time. But for a 32-bit integer, the
+* milliseconds parameter overflows in about 25 days, so instead a daily
+* check is done until the day when the deferral limit is reached, followed
+* by a non-overflowing QTimer::singleShot() call.
+*/
+void MessageWin::checkDeferralLimit()
+{
+ if (!mDeferButton || !mDeferLimit.isValid())
+ return;
+ int n = QDate::currentDate().daysTo(mDeferLimit.date());
+ if (n > 0)
+ return;
+ MidnightTimer::disconnect(this, SLOT(checkDeferralLimit()));
+ if (n == 0)
+ {
+ // The deferral limit will be reached today
+ n = QTime::currentTime().secsTo(mDeferLimit.time());
+ if (n > 0)
+ {
+ QTimer::singleShot(n * 1000, this, SLOT(checkDeferralLimit()));
+ return;
+ }
+ }
+ mDeferButton->setEnabled(false);
+ mDisableDeferral = true;
+}
+
+/******************************************************************************
+* Called when the Defer... button is clicked.
+* Displays the defer message dialog.
+*/
+void MessageWin::slotDefer()
+{
+ mDeferDlg = new DeferAlarmDlg(i18n("Defer Alarm"), QDateTime::currentDateTime().addSecs(60),
+ false, this, "deferDlg");
+ if (mDefaultDeferMinutes > 0)
+ mDeferDlg->setDeferMinutes(mDefaultDeferMinutes);
+ mDeferDlg->setLimit(mEventID);
+ if (!Preferences::modalMessages())
+ lower();
+ if (mDeferDlg->exec() == QDialog::Accepted)
+ {
+ DateTime dateTime = mDeferDlg->getDateTime();
+ int delayMins = mDeferDlg->deferMinutes();
+ const Event* kcalEvent = mEventID.isNull() ? 0 : AlarmCalendar::activeCalendar()->event(mEventID);
+ if (kcalEvent)
+ {
+ // The event still exists in the calendar file.
+ KAEvent event(*kcalEvent);
+ bool repeat = event.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true);
+ event.setDeferDefaultMinutes(delayMins);
+ KAlarm::updateEvent(event, 0, mDeferDlg, true, !repeat);
+ if (event.deferred())
+ mNoPostAction = true;
+ }
+ else
+ {
+ KAEvent event;
+ kcalEvent = AlarmCalendar::displayCalendar()->event(KAEvent::uid(mEventID, KAEvent::DISPLAYING));
+ if (kcalEvent)
+ {
+ event.reinstateFromDisplaying(KAEvent(*kcalEvent));
+ event.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true);
+ }
+ else
+ {
+ // The event doesn't exist any more !?!, so create a new one
+ event.set(dateTime.dateTime(), mMessage, mBgColour, mFgColour, mFont, mAction, mLateCancel, mFlags);
+ event.setAudioFile(mAudioFile, mVolume, mFadeVolume, mFadeSeconds);
+ event.setArchive();
+ event.setEventID(mEventID);
+ }
+ event.setDeferDefaultMinutes(delayMins);
+ // Add the event back into the calendar file, retaining its ID
+ // and not updating KOrganizer
+ KAlarm::addEvent(event, 0, mDeferDlg, true, false);
+ if (event.deferred())
+ mNoPostAction = true;
+ if (kcalEvent)
+ {
+ event.setUid(KAEvent::EXPIRED);
+ KAlarm::deleteEvent(event, false);
+ }
+ }
+ if (theApp()->wantRunInSystemTray())
+ {
+ // Alarms are to be displayed only if the system tray icon is running,
+ // so start it if necessary so that the deferred alarm will be shown.
+ theApp()->displayTrayIcon(true);
+ }
+ mNoCloseConfirm = true; // allow window to close without confirmation prompt
+ close();
+ }
+ else
+ raise();
+ delete mDeferDlg;
+ mDeferDlg = 0;
+}
+
+/******************************************************************************
+* Called when the KAlarm icon button in the message window is clicked.
+* Displays the main window, with the appropriate alarm selected.
+*/
+void MessageWin::displayMainWindow()
+{
+ KAlarm::displayMainWindowSelected(mEventID);
+}
+
+/******************************************************************************
+* Check whether the specified error message is already displayed for this
+* alarm, and note that it will now be displayed.
+* Reply = true if message is already displayed.
+*/
+bool MessageWin::haveErrorMessage(unsigned msg) const
+{
+ if (!mErrorMessages.contains(mEventID))
+ mErrorMessages.insert(mEventID, 0);
+ bool result = (mErrorMessages[mEventID] & msg);
+ mErrorMessages[mEventID] |= msg;
+ return result;
+}
+
+void MessageWin::clearErrorMessage(unsigned msg) const
+{
+ if (mErrorMessages.contains(mEventID))
+ {
+ if (mErrorMessages[mEventID] == msg)
+ mErrorMessages.remove(mEventID);
+ else
+ mErrorMessages[mEventID] &= ~msg;
+ }
+}
+
+
+/******************************************************************************
+* Check whether the message window should be modal, i.e. with title bar etc.
+* Normally this follows the Preferences setting, but if there is a full screen
+* window displayed, on X11 the message window has to bypass the window manager
+* in order to display on top of it (which has the side effect that it will have
+* no window decoration).
+*/
+bool wantModal()
+{
+ bool modal = Preferences::modalMessages();
+ if (modal)
+ {
+ KWinModule wm(0, KWinModule::INFO_DESKTOP);
+ KWin::WindowInfo wi = KWin::windowInfo(wm.activeWindow(), NET::WMState);
+ modal = !(wi.valid() && wi.hasState(NET::FullScreen));
+ }
+ return modal;
+}
+
+
+/*=============================================================================
+= Class MWMimeSourceFactory
+* Gets the mime type of a text file from not only its extension (as per
+* QMimeSourceFactory), but also from its contents. This allows the detection
+* of plain text files without file name extensions.
+=============================================================================*/
+MWMimeSourceFactory::MWMimeSourceFactory(const QString& absPath, KTextBrowser* view)
+ : QMimeSourceFactory(),
+ mMimeType("text/plain"),
+ mLast(0)
+{
+ view->setMimeSourceFactory(this);
+ QString type = KMimeType::findByPath(absPath)->name();
+ switch (KAlarm::fileType(type))
+ {
+ case KAlarm::TextPlain:
+ case KAlarm::TextFormatted:
+ mMimeType = type.latin1();
+ // fall through to 'TextApplication'
+ case KAlarm::TextApplication:
+ default:
+ // It's assumed to be a text file
+ mTextFile = absPath;
+ view->QTextBrowser::setSource(absPath);
+ break;
+
+ case KAlarm::Image:
+ // It's an image file
+ QString text = "<img source=\"";
+ text += absPath;
+ text += "\">";
+ view->setText(text);
+ break;
+ }
+ setFilePath(QFileInfo(absPath).dirPath(true));
+}
+
+MWMimeSourceFactory::~MWMimeSourceFactory()
+{
+ delete mLast;
+}
+
+const QMimeSource* MWMimeSourceFactory::data(const QString& abs_name) const
+{
+ if (abs_name == mTextFile)
+ {
+ QFileInfo fi(abs_name);
+ if (fi.isReadable())
+ {
+ QFile f(abs_name);
+ if (f.open(IO_ReadOnly) && f.size())
+ {
+ QByteArray ba(f.size());
+ f.readBlock(ba.data(), ba.size());
+ QStoredDrag* sr = new QStoredDrag(mMimeType);
+ sr->setEncodedData(ba);
+ delete mLast;
+ mLast = sr;
+ return sr;
+ }
+ }
+ }
+ return QMimeSourceFactory::data(abs_name);
+}