diff --git a/src/messagewin.cpp b/src/messagewin.cpp index d58771a9..de2019e0 100644 --- a/src/messagewin.cpp +++ b/src/messagewin.cpp @@ -1,2416 +1,2425 @@ /* * messagewin.cpp - displays an alarm message * Program: kalarm * Copyright © 2001-2020 David Jarvie * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "messagewin.h" #include "messagewin_p.h" #include "config-kalarm.h" #include "alarmcalendar.h" #include "deferdlg.h" #include "editdlg.h" #include "functions.h" #include "kalarmapp.h" #include "mainwindow.h" #include "preferences.h" #include "resources/resources.h" #include "lib/autoqpointer.h" #include "lib/config.h" #include "lib/desktop.h" #include "lib/file.h" #include "lib/messagebox.h" #include "lib/pushbutton.h" #include "lib/shellprocess.h" #include "lib/synchtimer.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if KDEPIM_HAVE_X11 #include #include #include #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KCalendarCore; using namespace KAlarmCal; #if KDEPIM_HAVE_X11 enum FullScreenType { NoFullScreen = 0, FullScreen = 1, FullScreenActive = 2 }; static FullScreenType haveFullScreenWindow(int screen); static FullScreenType findFullScreenWindows(const QVector& screenRects, QVector& screenTypes); #endif #include "kmailinterface.h" static const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail"); static const QLatin1String KMAIL_DBUS_PATH("/KMail"); // The delay for enabling message window buttons if a zero delay is // configured, i.e. the windows are placed far from the cursor. static const int proximityButtonDelay = 1000; // (milliseconds) static const int proximityMultiple = 10; // multiple of button height distance from cursor for proximity // A text label widget which can be scrolled and copied with the mouse class MessageText : public KTextEdit { public: MessageText(QWidget* parent = nullptr) : KTextEdit(parent), mNewLine(false) { setReadOnly(true); setFrameStyle(NoFrame); setLineWrapMode(NoWrap); } int scrollBarHeight() const { return horizontalScrollBar()->height(); } int scrollBarWidth() const { return verticalScrollBar()->width(); } void setBackgroundColour(const QColor& c) { QPalette pal = viewport()->palette(); pal.setColor(viewport()->backgroundRole(), c); viewport()->setPalette(pal); } QSize sizeHint() const override { const QSizeF docsize = document()->size(); return QSize(static_cast(docsize.width() + 0.99) + verticalScrollBar()->width(), static_cast(docsize.height() + 0.99) + horizontalScrollBar()->height()); } bool newLine() const { return mNewLine; } void setNewLine(bool nl) { mNewLine = nl; } private: bool mNewLine; }; // Basic flags for the window static const Qt::WindowFlags WFLAGS = Qt::WindowStaysOnTopHint; static const Qt::WindowFlags WFLAGS2 = Qt::WindowContextHelpButtonHint; static const Qt::WidgetAttribute WidgetFlags = Qt::WA_DeleteOnClose; // Error message bit masks enum { ErrMsg_Speak = 0x01, ErrMsg_AudioFile = 0x02 }; QList MessageWin::mWindowList; QMap MessageWin::mErrorMessages; bool MessageWin::mRedisplayed = false; // There can only be one audio thread at a time: trying to play multiple // sound files simultaneously would result in a cacophony, and besides // that, Phonon currently crashes... QPointer MessageWin::mAudioThread; MessageWin* AudioThread::mAudioOwner = nullptr; /****************************************************************************** * Construct the message window for the specified alarm. * Other alarms in the supplied event may have been updated by the caller, so * the whole event needs to be stored for updating the calendar file when it is * displayed. */ MessageWin::MessageWin(const KAEvent* event, const KAAlarm& alarm, int flags) : MainWindowBase(nullptr, static_cast(WFLAGS | WFLAGS2 | ((flags & ALWAYS_HIDE) || getWorkAreaAndModal() ? Qt::WindowType(0) : Qt::X11BypassWindowManagerHint))) , mMessage(event->cleanText()) , mFont(event->font()) , mBgColour(event->bgColour()) , mFgColour(event->fgColour()) , mEventId(*event) , mAudioFile(event->audioFile()) , mVolume(event->soundVolume()) , mFadeVolume(event->fadeVolume()) , mFadeSeconds(qMin(event->fadeSeconds(), 86400)) , mDefaultDeferMinutes(event->deferDefaultMinutes()) , mAlarmType(alarm.type()) , mAction(event->actionSubType()) , mAkonadiItemId(event->akonadiItemId()) , mCommandError(event->commandError()) , mRestoreHeight(0) , mAudioRepeatPause(event->repeatSoundPause()) , mConfirmAck(event->confirmAck()) , mNoDefer(true) , mInvalid(false) , mEvent(*event) , mOriginalEvent(*event) , mResource(Resources::resourceForEvent(mEventId.eventId())) , mAlwaysHide(flags & ALWAYS_HIDE) , mNoPostAction(alarm.type() & KAAlarm::REMINDER_ALARM) , mBeep(event->beep()) , mSpeak(event->speak()) , mRescheduleEvent(!(flags & NO_RESCHEDULE)) { qCDebug(KALARM_LOG) << "MessageWin:" << (void*)this << "event" << mEventId; setAttribute(static_cast(WidgetFlags)); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("MessageWin")); // used by LikeBack if (alarm.type() & KAAlarm::REMINDER_ALARM) { if (event->reminderMinutes() < 0) { event->previousOccurrence(alarm.dateTime(false).effectiveKDateTime(), mDateTime, false); if (!mDateTime.isValid() && event->repeatAtLogin()) mDateTime = alarm.dateTime().addSecs(event->reminderMinutes() * 60); } else mDateTime = event->mainDateTime(true); } else mDateTime = alarm.dateTime(true); if (!(flags & (NO_INIT_VIEW | ALWAYS_HIDE))) { const bool readonly = AlarmCalendar::resources()->eventReadOnly(mEventId.eventId()); mShowEdit = !mEventId.isEmpty() && !readonly; mNoDefer = readonly || (flags & NO_DEFER) || alarm.repeatAtLogin(); initView(); } // Set to save settings automatically, but don't save window size. // File alarm window size is saved elsewhere. setAutoSaveSettings(QStringLiteral("MessageWin"), false); mWindowList.append(this); if (event->autoClose()) mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().qDateTime().addSecs(event->lateCancel() * 60); if (mAlwaysHide) { hide(); displayComplete(); // play audio, etc. } } /****************************************************************************** * Display an error message window. * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note * that the option is specific to 'event'. */ void MessageWin::showError(const KAEvent& event, const DateTime& alarmDateTime, const QStringList& errmsgs, const QString& dontShowAgain) { if (!dontShowAgain.isEmpty() && KAlarm::dontShowErrors(EventId(event), dontShowAgain)) return; // Don't pile up duplicate error messages for the same alarm for (int i = 0, end = mWindowList.count(); i < end; ++i) { const MessageWin* w = mWindowList[i]; if (w->mErrorWindow && w->mEventId == EventId(event) && w->mErrorMsgs == errmsgs && w->mDontShowAgain == dontShowAgain) return; } (new MessageWin(&event, alarmDateTime, errmsgs, dontShowAgain))->show(); } /****************************************************************************** * Construct the message window for a specified error message. * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note * that the option is specific to 'event'. */ MessageWin::MessageWin(const KAEvent* event, const DateTime& alarmDateTime, const QStringList& errmsgs, const QString& dontShowAgain) : MainWindowBase(nullptr, WFLAGS | WFLAGS2) , mMessage(event->cleanText()) , mDateTime(alarmDateTime) , mEventId(*event) , mAlarmType(KAAlarm::MAIN_ALARM) , mAction(event->actionSubType()) , mAkonadiItemId(-1) , mCommandError(KAEvent::CMD_NO_ERROR) , mErrorMsgs(errmsgs) , mDontShowAgain(dontShowAgain) , mRestoreHeight(0) , mConfirmAck(false) , mShowEdit(false) , mNoDefer(true) , mInvalid(false) , mEvent(*event) , mOriginalEvent(*event) , mErrorWindow(true) , mNoPostAction(true) { qCDebug(KALARM_LOG) << "MessageWin: errmsg"; setAttribute(static_cast(WidgetFlags)); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("ErrorWin")); // used by LikeBack getWorkAreaAndModal(); initView(); mWindowList.append(this); } /****************************************************************************** * Construct the message window for restoration by session management. * The window is initialised by readProperties(). */ MessageWin::MessageWin() : MainWindowBase(nullptr, WFLAGS) { qCDebug(KALARM_LOG) << "MessageWin:" << (void*)this << "restore"; setAttribute(WidgetFlags); setWindowModality(Qt::WindowModal); setObjectName(QStringLiteral("RestoredMsgWin")); // used by LikeBack getWorkAreaAndModal(); mWindowList.append(this); } /****************************************************************************** * Destructor. Perform any post-alarm actions before tidying up. */ MessageWin::~MessageWin() { qCDebug(KALARM_LOG) << "~MessageWin" << (void*)this << mEventId; if (AudioThread::mAudioOwner == this && !mAudioThread.isNull()) mAudioThread->quit(); mErrorMessages.remove(mEventId); mWindowList.removeAll(this); delete mTempFile; if (!mRecreating) { if (!mNoPostAction && !mEvent.postAction().isEmpty()) theApp()->alarmCompleted(mEvent); if (!instanceCount(true)) theApp()->quitIf(); // no visible windows remain - check whether to quit } } /****************************************************************************** * Construct the message window. */ void MessageWin::initView() { const bool reminder = (!mErrorWindow && (mAlarmType & KAAlarm::REMINDER_ALARM)); const int leading = fontMetrics().leading(); setCaption((mAlarmType & KAAlarm::REMINDER_ALARM) ? i18nc("@title:window", "Reminder") : i18nc("@title:window", "Message")); QWidget* topWidget = new QWidget(this); setCentralWidget(topWidget); QVBoxLayout* topLayout = new QVBoxLayout(topWidget); int dcm = style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->setContentsMargins(dcm, dcm, dcm, dcm); topLayout->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QPalette labelPalette = palette(); labelPalette.setColor(backgroundRole(), labelPalette.color(QPalette::Window)); // Show the alarm date/time, together with a reminder text where appropriate. // Alarm date/time: display time zone if not local time zone. mTimeLabel = new QLabel(topWidget); mTimeLabel->setText(dateTimeToDisplay()); mTimeLabel->setFrameStyle(QFrame::StyledPanel); mTimeLabel->setPalette(labelPalette); mTimeLabel->setAutoFillBackground(true); topLayout->addWidget(mTimeLabel, 0, Qt::AlignHCenter); mTimeLabel->setWhatsThis(i18nc("@info:whatsthis", "The scheduled date/time for the message (as opposed to the actual time of display).")); if (mDateTime.isValid()) { // Reminder if (reminder) { // Create a label "time\nReminder" by inserting the time at the // start of the translated string, allowing for possible HTML tags // enclosing "Reminder". QString s = i18nc("@info", "Reminder"); QRegExp re(QStringLiteral("^(<[^>]+>)*")); re.indexIn(s); s.insert(re.matchedLength(), mTimeLabel->text() + QLatin1String("
")); mTimeLabel->setText(s); mTimeLabel->setAlignment(Qt::AlignHCenter); } } else mTimeLabel->hide(); if (!mErrorWindow) { // It's a normal alarm message window switch (mAction) { case KAEvent::FILE: { // Display the file name KSqueezedTextLabel* label = new KSqueezedTextLabel(mMessage, topWidget); label->setFrameStyle(QFrame::StyledPanel); label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); label->setPalette(labelPalette); label->setAutoFillBackground(true); label->setWhatsThis(i18nc("@info:whatsthis", "The file whose contents are displayed below")); topLayout->addWidget(label, 0, Qt::AlignHCenter); // Display contents of file const QUrl url = QUrl::fromUserInput(mMessage, QString(), QUrl::AssumeLocalFile); auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 0, KIO::HideProgressInfo); const bool exists = statJob->exec(); const bool isDir = statJob->statResult().isDir(); bool opened = false; if (exists && !isDir) { auto job = KIO::storedGet(url); KJobWidgets::setWindow(job, MainWindow::mainMainWindow()); if (job->exec()) { opened = true; const QByteArray data = job->data(); QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl(url); if (mime.name() == QLatin1String("application/octet-stream")) mime = db.mimeTypeForData(mTempFile); const File::FileType fileType = File::fileType(mime); switch (fileType) { case File::Image: case File::TextFormatted: delete mTempFile; mTempFile = new QTemporaryFile; mTempFile->open(); mTempFile->write(data); break; default: break; } QTextBrowser* view = new QTextBrowser(topWidget); view->setFrameStyle(QFrame::NoFrame); view->setWordWrapMode(QTextOption::NoWrap); QPalette pal = view->viewport()->palette(); pal.setColor(view->viewport()->backgroundRole(), mBgColour); view->viewport()->setPalette(pal); view->setTextColor(mFgColour); view->setCurrentFont(mFont); switch (fileType) { case File::Image: view->setHtml(QLatin1String("
fileName() + QLatin1String("\">
")); mTempFile->close(); // keep the file available to be displayed break; case File::TextFormatted: view->QTextBrowser::setSource(QUrl::fromLocalFile(mTempFile->fileName())); //krazy:exclude=qclasses delete mTempFile; mTempFile = nullptr; break; default: view->setPlainText(QString::fromUtf8(data)); break; } view->setMinimumSize(view->sizeHint()); topLayout->addWidget(view); // Set the default size to 20 lines square. // Note that after the first file has been displayed, this size // is overridden by the user-set default stored in the config file. // So there is no need to calculate an accurate size. int h = 20*view->fontMetrics().lineSpacing() + 2*view->frameWidth(); view->resize(QSize(h, h).expandedTo(view->sizeHint())); view->setWhatsThis(i18nc("@info:whatsthis", "The contents of the file to be displayed")); } } if (!exists || isDir || !opened) { mErrorMsgs += isDir ? i18nc("@info", "File is a folder") : exists ? i18nc("@info", "Failed to open file") : i18nc("@info", "File not found"); } break; } case KAEvent::MESSAGE: { // Message label // Using MessageText instead of QLabel allows scrolling and mouse copying MessageText* text = new MessageText(topWidget); text->setAutoFillBackground(true); text->setBackgroundColour(mBgColour); text->setTextColor(mFgColour); text->setCurrentFont(mFont); text->insertPlainText(mMessage); const int lineSpacing = text->fontMetrics().lineSpacing(); const QSize s = text->sizeHint(); const int h = s.height(); text->setMaximumHeight(h + text->scrollBarHeight()); text->setMinimumHeight(qMin(h, lineSpacing*4)); text->setMaximumWidth(s.width() + text->scrollBarWidth()); text->setWhatsThis(i18nc("@info:whatsthis", "The alarm message")); const int vspace = lineSpacing/2; const int hspace = lineSpacing - style()->pixelMetric(QStyle::PM_DefaultChildMargin); topLayout->addSpacing(vspace); topLayout->addStretch(); // Don't include any horizontal margins if message is 2/3 screen width if (text->sizeHint().width() >= Desktop::workArea(mScreenNumber).width()*2/3) topLayout->addWidget(text, 1, Qt::AlignHCenter); else { QHBoxLayout* layout = new QHBoxLayout(); layout->addSpacing(hspace); layout->addWidget(text, 1, Qt::AlignHCenter); layout->addSpacing(hspace); topLayout->addLayout(layout); } if (!reminder) topLayout->addStretch(); break; } case KAEvent::COMMAND: { mCommandText = new MessageText(topWidget); mCommandText->setBackgroundColour(mBgColour); mCommandText->setTextColor(mFgColour); mCommandText->setCurrentFont(mFont); topLayout->addWidget(mCommandText); mCommandText->setWhatsThis(i18nc("@info:whatsthis", "The output of the alarm's command")); theApp()->execCommandAlarm(mEvent, mEvent.alarm(mAlarmType), this, SLOT(readProcessOutput(ShellProcess*))); break; } case KAEvent::EMAIL: default: break; } if (reminder && mEvent.reminderMinutes() > 0) { // Advance reminder: show remaining time until the actual alarm mRemainingText = new QLabel(topWidget); mRemainingText->setFrameStyle(QFrame::Box | QFrame::Raised); mRemainingText->setContentsMargins(leading, leading, leading, leading); mRemainingText->setPalette(labelPalette); mRemainingText->setAutoFillBackground(true); if (mDateTime.isDateOnly() || KADateTime::currentLocalDate().daysTo(mDateTime.date()) > 0) { setRemainingTextDay(); MidnightTimer::connect(this, SLOT(setRemainingTextDay())); // update every day } else { setRemainingTextMinute(); MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute } topLayout->addWidget(mRemainingText, 0, Qt::AlignHCenter); topLayout->addSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); topLayout->addStretch(); } } else { // It's an error message switch (mAction) { case KAEvent::EMAIL: { // Display the email addresses and subject. QFrame* frame = new QFrame(topWidget); frame->setFrameStyle(QFrame::Box | QFrame::Raised); frame->setWhatsThis(i18nc("@info:whatsthis", "The email to send")); topLayout->addWidget(frame, 0, Qt::AlignHCenter); QGridLayout* grid = new QGridLayout(frame); grid->setContentsMargins(dcm, dcm, dcm, dcm); grid->setSpacing(style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing)); QLabel* label = new QLabel(i18nc("@info Email addressee", "To:"), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 0, Qt::AlignLeft); label = new QLabel(mEvent.emailAddresses(QStringLiteral("\n")), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 0, 1, Qt::AlignLeft); label = new QLabel(i18nc("@info Email subject", "Subject:"), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 1, 0, Qt::AlignLeft); label = new QLabel(mEvent.emailSubject(), frame); label->setFixedSize(label->sizeHint()); grid->addWidget(label, 1, 1, Qt::AlignLeft); break; } case KAEvent::COMMAND: case KAEvent::FILE: case KAEvent::MESSAGE: default: // Just display the error message strings break; } } if (!mErrorMsgs.count()) { topWidget->setAutoFillBackground(true); QPalette palette = topWidget->palette(); palette.setColor(topWidget->backgroundRole(), mBgColour); topWidget->setPalette(palette); } else { setCaption(i18nc("@title:window", "Error")); QHBoxLayout* layout = new QHBoxLayout(); int m = 2 * dcm; layout->setContentsMargins(m, m, m, m); layout->addStretch(); topLayout->addLayout(layout); QLabel* label = new QLabel(topWidget); label->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-error")).pixmap(style()->pixelMetric(QStyle::PM_MessageBoxIconSize))); label->setFixedSize(label->sizeHint()); layout->addWidget(label, 0, Qt::AlignRight); QVBoxLayout* vlayout = new QVBoxLayout(); layout->addLayout(vlayout); for (QStringList::ConstIterator it = mErrorMsgs.constBegin(); it != mErrorMsgs.constEnd(); ++it) { label = new QLabel(*it, topWidget); label->setFixedSize(label->sizeHint()); vlayout->addWidget(label, 0, Qt::AlignLeft); } layout->addStretch(); if (!mDontShowAgain.isEmpty()) { mDontShowAgainCheck = new QCheckBox(i18nc("@option:check", "Do not display this error message again for this alarm"), topWidget); mDontShowAgainCheck->setFixedSize(mDontShowAgainCheck->sizeHint()); topLayout->addWidget(mDontShowAgainCheck, 0, Qt::AlignLeft); } } QGridLayout* grid = new QGridLayout(); grid->setColumnStretch(0, 1); // keep the buttons right-adjusted in the window topLayout->addLayout(grid); int gridIndex = 1; // Close button mOkButton = new PushButton(KStandardGuiItem::close(), topWidget); // Prevent accidental acknowledgement of the message if the user is typing when the window appears mOkButton->clearFocus(); mOkButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mOkButton->setFixedSize(mOkButton->sizeHint()); connect(mOkButton, &QAbstractButton::clicked, this, &MessageWin::slotOk); grid->addWidget(mOkButton, 0, gridIndex++, Qt::AlignHCenter); mOkButton->setWhatsThis(i18nc("@info:whatsthis", "Acknowledge the alarm")); if (mShowEdit) { // Edit button mEditButton = new PushButton(i18nc("@action:button", "&Edit..."), topWidget); mEditButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mEditButton->setFixedSize(mEditButton->sizeHint()); connect(mEditButton, &QAbstractButton::clicked, this, &MessageWin::slotEdit); grid->addWidget(mEditButton, 0, gridIndex++, Qt::AlignHCenter); mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the alarm.")); } // Defer button mDeferButton = new PushButton(i18nc("@action:button", "&Defer..."), topWidget); mDeferButton->setFocusPolicy(Qt::ClickFocus); // don't allow keyboard selection mDeferButton->setFixedSize(mDeferButton->sizeHint()); connect(mDeferButton, &QAbstractButton::clicked, this, &MessageWin::slotDefer); grid->addWidget(mDeferButton, 0, gridIndex++, Qt::AlignHCenter); mDeferButton->setWhatsThis(xi18nc("@info:whatsthis", "Defer the alarm until later." "You will be prompted to specify when the alarm should be redisplayed.")); if (mNoDefer) mDeferButton->hide(); else setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more if (!mAudioFile.isEmpty() && (mVolume || mFadeVolume > 0)) { // Silence button to stop sound repetition mSilenceButton = new PushButton(topWidget); mSilenceButton->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-stop"))); grid->addWidget(mSilenceButton, 0, gridIndex++, Qt::AlignHCenter); mSilenceButton->setToolTip(i18nc("@info:tooltip", "Stop sound")); mSilenceButton->setWhatsThis(i18nc("@info:whatsthis", "Stop playing the sound")); // To avoid getting in a mess, disable the button until sound playing has been set up mSilenceButton->setEnabled(false); } if (mAkonadiItemId >= 0) { // KMail button mKMailButton = new PushButton(topWidget); mKMailButton->setIcon(QIcon::fromTheme(QStringLiteral("internet-mail"))); connect(mKMailButton, &QAbstractButton::clicked, this, &MessageWin::slotShowKMailMessage); grid->addWidget(mKMailButton, 0, gridIndex++, Qt::AlignHCenter); mKMailButton->setToolTip(xi18nc("@info:tooltip Locate this email in KMail", "Locate in KMail")); mKMailButton->setWhatsThis(xi18nc("@info:whatsthis", "Locate and highlight this email in KMail")); } // KAlarm button mKAlarmButton = new PushButton(topWidget); mKAlarmButton->setIcon(QIcon::fromTheme(KAboutData::applicationData().componentName())); connect(mKAlarmButton, &QAbstractButton::clicked, this, &MessageWin::displayMainWindow); grid->addWidget(mKAlarmButton, 0, gridIndex++, Qt::AlignHCenter); mKAlarmButton->setToolTip(xi18nc("@info:tooltip", "Activate KAlarm")); mKAlarmButton->setWhatsThis(xi18nc("@info:whatsthis", "Activate KAlarm")); int butsize = mKAlarmButton->sizeHint().height(); if (mSilenceButton) butsize = qMax(butsize, mSilenceButton->sizeHint().height()); if (mKMailButton) butsize = qMax(butsize, mKMailButton->sizeHint().height()); mKAlarmButton->setFixedSize(butsize, butsize); if (mSilenceButton) mSilenceButton->setFixedSize(butsize, butsize); if (mKMailButton) mKMailButton->setFixedSize(butsize, butsize); // Disable all buttons initially, to prevent accidental clicking on if they happen to be // under the mouse just as the window appears. mOkButton->setEnabled(false); if (mDeferButton->isVisible()) mDeferButton->setEnabled(false); if (mEditButton) mEditButton->setEnabled(false); if (mKMailButton) mKMailButton->setEnabled(false); mKAlarmButton->setEnabled(false); topLayout->activate(); setMinimumSize(QSize(grid->sizeHint().width() + 2 * style()->pixelMetric(QStyle::PM_DefaultChildMargin), sizeHint().height())); const bool modal = !(windowFlags() & Qt::X11BypassWindowManagerHint); NET::States wstate = NET::Sticky | NET::KeepAbove; if (modal) wstate |= NET::Modal; WId winid = winId(); KWindowSystem::setState(winid, wstate); KWindowSystem::setOnAllDesktops(winid, true); mInitialised = true; // the window's widgets have been created } /****************************************************************************** * Return the number of message windows, optionally excluding always-hidden ones. */ int MessageWin::instanceCount(bool excludeAlwaysHidden) { int count = mWindowList.count(); if (excludeAlwaysHidden) { for (MessageWin* win : qAsConst(mWindowList)) { if (win->mAlwaysHide) --count; } } return count; } bool MessageWin::hasDefer() const { return mDeferButton && mDeferButton->isVisible(); } /****************************************************************************** * Show the Defer button when it was previously hidden. */ void MessageWin::showDefer() { if (mDeferButton) { mNoDefer = false; mDeferButton->show(); setDeferralLimit(mEvent); // ensure that button is disabled when alarm can't be deferred any more resize(sizeHint()); } } /****************************************************************************** * Convert a reminder window into a normal alarm window. */ void MessageWin::cancelReminder(const KAEvent& event, const KAAlarm& alarm) { if (!mInitialised) return; mDateTime = alarm.dateTime(true); mNoPostAction = false; mAlarmType = alarm.type(); if (event.autoClose()) mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().qDateTime().addSecs(event.lateCancel() * 60); setCaption(i18nc("@title:window", "Message")); mTimeLabel->setText(dateTimeToDisplay()); if (mRemainingText) mRemainingText->hide(); MidnightTimer::disconnect(this, SLOT(setRemainingTextDay())); MinuteTimer::disconnect(this, SLOT(setRemainingTextMinute())); setMinimumHeight(0); centralWidget()->layout()->activate(); setMinimumHeight(sizeHint().height()); resize(sizeHint()); } /****************************************************************************** * Show the alarm's trigger time. * This is assumed to have previously been hidden. */ void MessageWin::showDateTime(const KAEvent& event, const KAAlarm& alarm) { if (!mTimeLabel) return; mDateTime = (alarm.type() & KAAlarm::REMINDER_ALARM) ? event.mainDateTime(true) : alarm.dateTime(true); if (mDateTime.isValid()) { mTimeLabel->setText(dateTimeToDisplay()); mTimeLabel->show(); } } /****************************************************************************** * Get the trigger time to display. */ QString MessageWin::dateTimeToDisplay() { QString tm; if (mDateTime.isValid()) { if (mDateTime.isDateOnly()) tm = QLocale().toString(mDateTime.date(), QLocale::ShortFormat); else { bool showZone = false; if (mDateTime.timeType() == KADateTime::UTC || (mDateTime.timeType() == KADateTime::TimeZone && !mDateTime.isLocalZone())) { // Display time zone abbreviation if it's different from the local // zone. Note that the iCalendar time zone might represent the local // time zone in a slightly different way from the system time zone, // so the zone comparison above might not produce the desired result. const QString tz = mDateTime.kDateTime().toString(QStringLiteral("%Z")); KADateTime local = mDateTime.kDateTime(); local.setTimeSpec(KADateTime::Spec::LocalZone()); showZone = (local.toString(QStringLiteral("%Z")) != tz); } const QDateTime dt = mDateTime.qDateTime(); tm = QLocale().toString(dt, QLocale::ShortFormat); if (showZone) tm += QLatin1Char(' ') + mDateTime.timeZone().displayName(dt, QTimeZone::ShortName, QLocale()); } } return tm; } /****************************************************************************** * Set the remaining time text in a reminder window. * Called at the start of every day (at the user-defined start-of-day time). */ void MessageWin::setRemainingTextDay() { QString text; const int days = KADateTime::currentLocalDate().daysTo(mDateTime.date()); if (days <= 0 && !mDateTime.isDateOnly()) { // The alarm is due today, so start refreshing every minute MidnightTimer::disconnect(this, SLOT(setRemainingTextDay())); setRemainingTextMinute(); MinuteTimer::connect(this, SLOT(setRemainingTextMinute())); // update every minute } else { if (days <= 0) text = i18nc("@info", "Today"); else if (days % 7) text = i18ncp("@info", "Tomorrow", "in %1 days' time", days); else text = i18ncp("@info", "in 1 week's time", "in %1 weeks' time", days/7); } mRemainingText->setText(text); } /****************************************************************************** * Set the remaining time text in a reminder window. * Called on every minute boundary. */ void MessageWin::setRemainingTextMinute() { QString text; const int mins = (KADateTime::currentUtcDateTime().secsTo(mDateTime.effectiveKDateTime()) + 59) / 60; if (mins < 60) text = i18ncp("@info", "in 1 minute's time", "in %1 minutes' time", (mins > 0 ? mins : 0)); else if (mins % 60 == 0) text = i18ncp("@info", "in 1 hour's time", "in %1 hours' time", mins/60); else { QString hourText = i18ncp("@item:intext inserted into 'in ... %1 minute's time' below", "1 hour", "%1 hours", mins/60); text = i18ncp("@info '%2' is the previous message '1 hour'/'%1 hours'", "in %2 1 minute's time", "in %2 %1 minutes' time", mins%60, hourText); } mRemainingText->setText(text); } /****************************************************************************** * Called when output is available from the command which is providing the text * for this window. Add the output and resize the window to show it. */ void MessageWin::readProcessOutput(ShellProcess* proc) { const QByteArray data = proc->readAll(); if (!data.isEmpty()) { // Strip any trailing newline, to avoid showing trailing blank line // in message window. if (mCommandText->newLine()) mCommandText->append(QStringLiteral("\n")); const int nl = data.endsWith('\n') ? 1 : 0; mCommandText->setNewLine(nl); mCommandText->insertPlainText(QString::fromLocal8Bit(data.data(), data.length() - nl)); resize(sizeHint()); } } /****************************************************************************** * Save settings to the session managed config file, for restoration * when the program is restored. */ void MessageWin::saveProperties(KConfigGroup& config) { if (mShown && !mErrorWindow && !mAlwaysHide) { config.writeEntry("EventID", mEventId.eventId()); config.writeEntry("CollectionID", mResource.id()); config.writeEntry("AlarmType", static_cast(mAlarmType)); if (mAlarmType == KAAlarm::INVALID_ALARM) qCCritical(KALARM_LOG) << "MessageWin::saveProperties: Invalid alarm: id=" << mEventId << ", alarm count=" << mEvent.alarmCount(); config.writeEntry("Message", mMessage); config.writeEntry("Type", static_cast(mAction)); config.writeEntry("Font", mFont); config.writeEntry("BgColour", mBgColour); config.writeEntry("FgColour", mFgColour); config.writeEntry("ConfirmAck", mConfirmAck); if (mDateTime.isValid()) { -//TODO: Write KADateTime when it becomes possible config.writeEntry("Time", mDateTime.effectiveDateTime()); config.writeEntry("DateOnly", mDateTime.isDateOnly()); QByteArray zone; if (mDateTime.isUtc()) zone = "UTC"; + else if (mDateTime.isOffsetFromUtc()) + { + int offset = mDateTime.utcOffset(); + if (offset >= 0) + zone = '+' + QByteArray::number(offset); + else + zone = QByteArray::number(offset); + } else if (mDateTime.timeType() == KADateTime::TimeZone) { const QTimeZone tz = mDateTime.timeZone(); if (tz.isValid()) zone = tz.id(); } config.writeEntry("TimeZone", zone); } if (mCloseTime.isValid()) config.writeEntry("Expiry", mCloseTime); if (mAudioRepeatPause >= 0 && mSilenceButton && mSilenceButton->isEnabled()) { // Only need to restart sound file playing if it's being repeated config.writePathEntry("AudioFile", mAudioFile); config.writeEntry("Volume", static_cast(mVolume * 100)); config.writeEntry("AudioPause", mAudioRepeatPause); } config.writeEntry("Speak", mSpeak); config.writeEntry("Height", height()); config.writeEntry("DeferMins", mDefaultDeferMinutes); config.writeEntry("NoDefer", mNoDefer); config.writeEntry("NoPostAction", mNoPostAction); config.writeEntry("AkonadiItemId", mAkonadiItemId); config.writeEntry("CmdErr", static_cast(mCommandError)); config.writeEntry("DontShowAgain", mDontShowAgain); } else config.writeEntry("Invalid", true); } /****************************************************************************** * Read settings from the session managed config file. * This function is automatically called whenever the app is being restored. * Read in whatever was saved in saveProperties(). */ void MessageWin::readProperties(const KConfigGroup& config) { mInvalid = config.readEntry("Invalid", false); const QString eventId = config.readEntry("EventID"); const ResourceId collectionId = config.readEntry("CollectionID", ResourceId(-1)); mAlarmType = static_cast(config.readEntry("AlarmType", 0)); if (mAlarmType == KAAlarm::INVALID_ALARM) { mInvalid = true; qCCritical(KALARM_LOG) << "MessageWin::readProperties: Invalid alarm: id=" << eventId; } mMessage = config.readEntry("Message"); mAction = static_cast(config.readEntry("Type", 0)); mFont = config.readEntry("Font", QFont()); mBgColour = config.readEntry("BgColour", QColor(Qt::white)); mFgColour = config.readEntry("FgColour", QColor(Qt::black)); mConfirmAck = config.readEntry("ConfirmAck", false); QDateTime invalidDateTime; QDateTime dt = config.readEntry("Time", invalidDateTime); const QByteArray zoneId = config.readEntry("TimeZone").toLatin1(); KADateTime::Spec timeSpec; if (zoneId.isEmpty()) timeSpec = KADateTime::LocalZone; else if (zoneId == "UTC") timeSpec = KADateTime::UTC; + else if (zoneId.startsWith('+') || zoneId.startsWith('-')) + timeSpec.setType(KADateTime::OffsetFromUTC, zoneId.toInt()); else timeSpec = QTimeZone(zoneId); mDateTime = KADateTime(dt.date(), dt.time(), timeSpec); const bool dateOnly = config.readEntry("DateOnly", false); if (dateOnly) mDateTime.setDateOnly(true); mCloseTime = config.readEntry("Expiry", invalidDateTime); mCloseTime.setTimeSpec(Qt::UTC); mAudioFile = config.readPathEntry("AudioFile", QString()); mVolume = static_cast(config.readEntry("Volume", 0)) / 100; mFadeVolume = -1; mFadeSeconds = 0; if (!mAudioFile.isEmpty()) // audio file URL was only saved if it repeats mAudioRepeatPause = config.readEntry("AudioPause", 0); mBeep = false; // don't beep after restart (similar to not playing non-repeated sound file) mSpeak = config.readEntry("Speak", false); mRestoreHeight = config.readEntry("Height", 0); mDefaultDeferMinutes = config.readEntry("DeferMins", 0); mNoDefer = config.readEntry("NoDefer", false); mNoPostAction = config.readEntry("NoPostAction", true); mAkonadiItemId = config.readEntry("AkonadiItemId", QVariant(QVariant::LongLong)).toLongLong(); mCommandError = KAEvent::CmdErrType(config.readEntry("CmdErr", static_cast(KAEvent::CMD_NO_ERROR))); mDontShowAgain = config.readEntry("DontShowAgain", QString()); mShowEdit = false; // Temporarily initialise mResource and mEventId - they will be set by redisplayAlarm() mResource = Resources::resource(collectionId); mEventId = EventId(collectionId, eventId); qCDebug(KALARM_LOG) << "MessageWin::readProperties:" << eventId; if (mAlarmType != KAAlarm::INVALID_ALARM) { // Recreate the event from the calendar file (if possible) if (eventId.isEmpty()) initView(); else { // Close any other window for this alarm which has already been restored by redisplayAlarms() if (!Resources::allCreated()) { connect(Resources::instance(), &Resources::resourcesCreated, this, &MessageWin::showRestoredAlarm); return; } redisplayAlarm(); } } } /****************************************************************************** * Fetch the restored alarm from the calendar and redisplay it in this window. */ void MessageWin::showRestoredAlarm() { qCDebug(KALARM_LOG) << "MessageWin::showRestoredAlarm:" << mEventId; redisplayAlarm(); show(); } /****************************************************************************** * Fetch the restored alarm from the calendar and redisplay it in this window. */ void MessageWin::redisplayAlarm() { mResource = Resources::resourceForEvent(mEventId.eventId()); mEventId.setResourceId(mResource.id()); qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarm:" << mEventId; // Delete any already existing window for the same event MessageWin* duplicate = findEvent(mEventId, this); if (duplicate) qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarm: Deleting duplicate window:" << mEventId; delete duplicate; KAEvent* event = AlarmCalendar::resources()->event(mEventId); if (event) { mEvent = *event; mShowEdit = true; } else { // It's not in the active calendar, so try the displaying or archive calendars retrieveEvent(mEvent, mResource, mShowEdit, mNoDefer); mNoDefer = !mNoDefer; } initView(); } /****************************************************************************** * Redisplay alarms which were being shown when the program last exited. * Normally, these alarms will have been displayed by session restoration, but * if the program crashed or was killed, we can redisplay them here so that * they won't be lost. */ void MessageWin::redisplayAlarms() { if (mRedisplayed) return; qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms"; mRedisplayed = true; AlarmCalendar* cal = AlarmCalendar::displayCalendar(); if (cal && cal->isOpen()) { KAEvent event; Resource resource; const Event::List events = cal->kcalEvents(); for (int i = 0, end = events.count(); i < end; ++i) { bool showDefer, showEdit; reinstateFromDisplaying(events[i], event, resource, showEdit, showDefer); const EventId eventId(event); if (findEvent(eventId)) qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms: Message window already exists:" << eventId; else { // This event should be displayed, but currently isn't being const KAAlarm alarm = event.convertDisplayingAlarm(); if (alarm.type() == KAAlarm::INVALID_ALARM) { qCCritical(KALARM_LOG) << "MessageWin::redisplayAlarms: Invalid alarm: id=" << eventId; continue; } qCDebug(KALARM_LOG) << "MessageWin::redisplayAlarms:" << eventId; const bool login = alarm.repeatAtLogin(); const int flags = NO_RESCHEDULE | (login ? NO_DEFER : 0) | NO_INIT_VIEW; MessageWin* win = new MessageWin(&event, alarm, flags); win->mResource = resource; const bool rw = resource.isWritable(event.category()); win->mShowEdit = rw ? showEdit : false; win->mNoDefer = (rw && !login) ? !showDefer : true; win->initView(); win->show(); } } } } /****************************************************************************** * Retrieves the event with the current ID from the displaying calendar file, * or if not found there, from the archive calendar. */ bool MessageWin::retrieveEvent(KAEvent& event, Resource& resource, bool& showEdit, bool& showDefer) { const Event::Ptr kcalEvent = AlarmCalendar::displayCalendar()->kcalEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING)); if (!reinstateFromDisplaying(kcalEvent, event, resource, showEdit, showDefer)) { // The event isn't in the displaying calendar. // Try to retrieve it from the archive calendar. KAEvent* ev = nullptr; Resource archiveRes = Resources::getStandard(CalEvent::ARCHIVED); if (archiveRes.isValid()) ev = AlarmCalendar::resources()->event(EventId(archiveRes.id(), CalEvent::uid(mEventId.eventId(), CalEvent::ARCHIVED))); if (!ev) return false; event = *ev; event.setArchive(); // ensure that it gets re-archived if it's saved event.setCategory(CalEvent::ACTIVE); if (mEventId.eventId() != event.id()) qCCritical(KALARM_LOG) << "MessageWin::retrieveEvent: Wrong event ID"; event.setEventId(mEventId.eventId()); resource = Resource(); showEdit = true; showDefer = true; qCDebug(KALARM_LOG) << "MessageWin::retrieveEvent:" << event.id() << ": success"; } return true; } /****************************************************************************** * Retrieves the displayed event from the calendar file, or if not found there, * from the displaying calendar. */ bool MessageWin::reinstateFromDisplaying(const Event::Ptr& kcalEvent, KAEvent& event, Resource& resource, bool& showEdit, bool& showDefer) { if (!kcalEvent) return false; ResourceId resourceId; event.reinstateFromDisplaying(kcalEvent, resourceId, showEdit, showDefer); event.setCollectionId(resourceId); resource = Resources::resource(resourceId); qCDebug(KALARM_LOG) << "MessageWin::reinstateFromDisplaying:" << EventId(event) << ": success"; return true; } /****************************************************************************** * Called when an alarm is currently being displayed, to store a copy of the * alarm in the displaying calendar, and to reschedule it for its next repetition. * If no repetitions remain, cancel it. */ void MessageWin::alarmShowing(KAEvent& event) { qCDebug(KALARM_LOG) << "MessageWin::alarmShowing:" << event.id() << "," << KAAlarm::debugType(mAlarmType); const KAAlarm alarm = event.alarm(mAlarmType); if (!alarm.isValid()) { qCCritical(KALARM_LOG) << "MessageWin::alarmShowing: Alarm type not found:" << event.id() << ":" << mAlarmType; return; } if (!mAlwaysHide) { // Copy the alarm to the displaying calendar in case of a crash, etc. KAEvent dispEvent; const ResourceId id = Resources::resourceForEvent(event.id()).id(); dispEvent.setDisplaying(event, mAlarmType, id, mDateTime.effectiveKDateTime(), mShowEdit, !mNoDefer); AlarmCalendar* cal = AlarmCalendar::displayCalendarOpen(); if (cal) { cal->deleteDisplayEvent(dispEvent.id()); // in case it already exists cal->addEvent(dispEvent); cal->save(); } } theApp()->rescheduleAlarm(event, alarm); } /****************************************************************************** * Spread alarm windows over the screen so that they are all visible, or pile * them on top of each other again. * Reply = true if windows are now scattered, false if piled up. */ bool MessageWin::spread(bool scatter) { if (instanceCount(true) <= 1) // ignore always-hidden windows return false; const QRect desk = Desktop::workArea(); // get the usable area of the desktop if (scatter == isSpread(desk.topLeft())) return scatter; if (scatter) { // Usually there won't be many windows, so a crude // scattering algorithm should suffice. int x = desk.left(); int y = desk.top(); int ynext = y; for (int errmsgs = 0; errmsgs < 2; ++errmsgs) { // Display alarm messages first, then error messages, since most // error messages tend to be the same height. for (int i = 0, end = mWindowList.count(); i < end; ++i) { MessageWin* w = mWindowList[i]; if ((!errmsgs && w->mErrorWindow) || (errmsgs && !w->mErrorWindow)) continue; const QSize sz = w->frameGeometry().size(); if (x + sz.width() > desk.right()) { x = desk.left(); y = ynext; } int ytmp = y; if (y + sz.height() > desk.bottom()) { ytmp = desk.bottom() - sz.height(); if (ytmp < desk.top()) ytmp = desk.top(); } w->move(x, ytmp); x += sz.width(); if (ytmp + sz.height() > ynext) ynext = ytmp + sz.height(); } } } else { // Move all windows to the top left corner for (int i = 0, end = mWindowList.count(); i < end; ++i) mWindowList[i]->move(desk.topLeft()); } return scatter; } /****************************************************************************** * Check whether message windows are all piled up, or are spread out. * Reply = true if windows are currently spread, false if piled up. */ bool MessageWin::isSpread(const QPoint& topLeft) { for (int i = 0, end = mWindowList.count(); i < end; ++i) { if (mWindowList[i]->pos() != topLeft) return true; } return false; } /****************************************************************************** * Returns the existing message window (if any) which is displaying the event * with the specified ID. */ MessageWin* MessageWin::findEvent(const EventId& eventId, MessageWin* exclude) { if (!eventId.isEmpty()) { for (int i = 0, end = mWindowList.count(); i < end; ++i) { MessageWin* w = mWindowList[i]; if (w != exclude && w->mEventId == eventId && !w->mErrorWindow) return w; } } return nullptr; } /****************************************************************************** * Beep and play the audio file, as appropriate. */ void MessageWin::playAudio() { if (mBeep) { // Beep using two methods, in case the sound card/speakers are switched off or not working QApplication::beep(); // beep through the internal speaker KNotification::beep(); // beep through the sound card & speakers } if (!mAudioFile.isEmpty()) { if (!mVolume && mFadeVolume <= 0) return; // ensure zero volume doesn't play anything startAudio(); // play the audio file } else if (mSpeak) { // The message is to be spoken. In case of error messges, // call it on a timer to allow the window to display first. QTimer::singleShot(0, this, &MessageWin::slotSpeak); } } /****************************************************************************** * Speak the message. * Called asynchronously to avoid delaying the display of the message. */ void MessageWin::slotSpeak() { KPIMTextEdit::TextToSpeech *tts = KPIMTextEdit::TextToSpeech::self(); if (!tts->isReady()) { KAMessageBox::detailedError(MainWindow::mainMainWindow(), i18nc("@info", "Unable to speak message"), i18nc("@info", "Text-to-speech subsystem is not available")); clearErrorMessage(ErrMsg_Speak); return; } tts->say(mMessage); } /****************************************************************************** * Called when another window's audio thread has been destructed. * Start playing this window's audio file. Because initialising the sound system * and loading the file may take some time, it is called in a separate thread to * allow the window to display first. */ void MessageWin::startAudio() { if (mAudioThread) { // An audio file is already playing for another message // window, so wait until it has finished. connect(mAudioThread.data(), &QObject::destroyed, this, &MessageWin::audioTerminating); } else { qCDebug(KALARM_LOG) << "MessageWin::startAudio:" << QThread::currentThread(); mAudioThread = new AudioThread(this, mAudioFile, mVolume, mFadeVolume, mFadeSeconds, mAudioRepeatPause); connect(mAudioThread.data(), &AudioThread::readyToPlay, this, &MessageWin::playReady); connect(mAudioThread.data(), &QThread::finished, this, &MessageWin::playFinished); if (mSilenceButton) connect(mSilenceButton, &QAbstractButton::clicked, mAudioThread.data(), &QThread::quit); // Notify after creating mAudioThread, so that isAudioPlaying() will // return the correct value. theApp()->notifyAudioPlaying(true); mAudioThread->start(); } } /****************************************************************************** * Return whether audio playback is currently active. */ bool MessageWin::isAudioPlaying() { return mAudioThread; } /****************************************************************************** * Stop audio playback. */ void MessageWin::stopAudio(bool wait) { qCDebug(KALARM_LOG) << "MessageWin::stopAudio"; if (mAudioThread) mAudioThread->stop(wait); } /****************************************************************************** * Called when another window's audio thread is being destructed. * Wait until the destructor has finished. */ void MessageWin::audioTerminating() { QTimer::singleShot(0, this, &MessageWin::startAudio); } /****************************************************************************** * Called when the audio file is ready to start playing. */ void MessageWin::playReady() { if (mSilenceButton) mSilenceButton->setEnabled(true); } /****************************************************************************** * Called when the audio file thread finishes. */ void MessageWin::playFinished() { if (mSilenceButton) mSilenceButton->setEnabled(false); if (mAudioThread) // mAudioThread can actually be null here! { const QString errmsg = mAudioThread->error(); if (!errmsg.isEmpty() && !haveErrorMessage(ErrMsg_AudioFile)) { KAMessageBox::error(this, errmsg); clearErrorMessage(ErrMsg_AudioFile); } } delete mAudioThread.data(); if (mAlwaysHide) close(); } /****************************************************************************** * Constructor for audio thread. */ AudioThread::AudioThread(MessageWin* parent, const QString& audioFile, float volume, float fadeVolume, int fadeSeconds, int repeatPause) : QThread(parent), mFile(audioFile), mVolume(volume), mFadeVolume(fadeVolume), mFadeSeconds(fadeSeconds), mRepeatPause(repeatPause), mAudioObject(nullptr) { if (mAudioOwner) qCCritical(KALARM_LOG) << "MessageWin::AudioThread: mAudioOwner already set"; mAudioOwner = parent; } /****************************************************************************** * Destructor for audio thread. Waits for thread completion and tidies up. * Note that this destructor is executed in the parent thread. */ AudioThread::~AudioThread() { qCDebug(KALARM_LOG) << "~MessageWin::AudioThread"; stop(true); // stop playing and tidy up (timeout 3 seconds) delete mAudioObject; mAudioObject = nullptr; if (mAudioOwner == parent()) mAudioOwner = nullptr; // Notify after deleting mAudioThread, so that isAudioPlaying() will // return the correct value. QTimer::singleShot(0, theApp(), &KAlarmApp::notifyAudioStopped); } /****************************************************************************** * Quits the thread and waits for thread completion and tidies up. */ void AudioThread::stop(bool waiT) { qCDebug(KALARM_LOG) << "MessageWin::AudioThread::stop"; quit(); // stop playing and tidy up wait(3000); // wait for run() to exit (timeout 3 seconds) if (!isFinished()) { // Something has gone wrong - forcibly kill the thread terminate(); if (waiT) wait(); } } /****************************************************************************** * Kick off the thread to play the audio file. */ void AudioThread::run() { mMutex.lock(); if (mAudioObject) { mMutex.unlock(); return; } qCDebug(KALARM_LOG) << "MessageWin::AudioThread::run:" << QThread::currentThread() << mFile; const QString audioFile = mFile; const QUrl url = QUrl::fromUserInput(mFile); mFile = url.isLocalFile() ? url.toLocalFile() : url.toString(); Phonon::MediaSource source(url); if (source.type() == Phonon::MediaSource::Invalid) { mError = xi18nc("@info", "Cannot open audio file: %1", audioFile); mMutex.unlock(); qCCritical(KALARM_LOG) << "MessageWin::AudioThread::run: Open failure:" << audioFile; return; } mAudioObject = new Phonon::MediaObject(); mAudioObject->setCurrentSource(source); mAudioObject->setTransitionTime(100); // workaround to prevent clipping of end of files in Xine backend Phonon::AudioOutput* output = new Phonon::AudioOutput(Phonon::NotificationCategory, mAudioObject); mPath = Phonon::createPath(mAudioObject, output); if (mVolume >= 0 || mFadeVolume >= 0) { const float vol = (mVolume >= 0) ? mVolume : output->volume(); const float maxvol = qMax(vol, mFadeVolume); output->setVolume(maxvol); if (mFadeVolume >= 0 && mFadeSeconds > 0) { Phonon::VolumeFaderEffect* fader = new Phonon::VolumeFaderEffect(mAudioObject); fader->setVolume(mFadeVolume / maxvol); fader->fadeTo(mVolume / maxvol, mFadeSeconds * 1000); mPath.insertEffect(fader); } } connect(mAudioObject, &Phonon::MediaObject::stateChanged, this, &AudioThread::playStateChanged, Qt::DirectConnection); connect(mAudioObject, &Phonon::MediaObject::finished, this, &AudioThread::checkAudioPlay, Qt::DirectConnection); mPlayedOnce = false; mPausing = false; mMutex.unlock(); Q_EMIT readyToPlay(); checkAudioPlay(); // Start an event loop. // The function will exit once exit() or quit() is called. // First, ensure that the thread object is deleted once it has completed. connect(this, &QThread::finished, this, &QObject::deleteLater); exec(); stopPlay(); } /****************************************************************************** * Called when the audio file has loaded and is ready to play, or when play * has completed. * If it is ready to play, start playing it (for the first time or repeated). * If play has not yet completed, wait a bit longer. */ void AudioThread::checkAudioPlay() { mMutex.lock(); if (!mAudioObject) { mMutex.unlock(); return; } if (mPausing) mPausing = false; else { // The file has loaded and is ready to play, or play has completed if (mPlayedOnce) { if (mRepeatPause < 0) { // Play has completed mMutex.unlock(); stopPlay(); return; } if (mRepeatPause > 0) { // Pause before playing the file again mPausing = true; QTimer::singleShot(mRepeatPause * 1000, this, &AudioThread::checkAudioPlay); mMutex.unlock(); return; } } mPlayedOnce = true; } // Start playing the file, either for the first time or again qCDebug(KALARM_LOG) << "MessageWin::AudioThread::checkAudioPlay: start"; mAudioObject->play(); mMutex.unlock(); } /****************************************************************************** * Called when the playback object changes state. * If an error has occurred, quit and return the error to the caller. */ void AudioThread::playStateChanged(Phonon::State newState) { if (newState == Phonon::ErrorState) { QMutexLocker locker(&mMutex); const QString err = mAudioObject->errorString(); if (!err.isEmpty()) { qCCritical(KALARM_LOG) << "MessageWin::AudioThread::playStateChanged: Play failure:" << mFile << ":" << err; mError = xi18nc("@info", "Error playing audio file: %1%2", mFile, err); exit(1); } } } /****************************************************************************** * Called when play completes, the Silence button is clicked, or the window is * closed, to terminate audio access. */ void AudioThread::stopPlay() { mMutex.lock(); if (mAudioObject) { mAudioObject->stop(); const QList effects = mPath.effects(); for (int i = 0; i < effects.count(); ++i) { mPath.removeEffect(effects[i]); delete effects[i]; } delete mAudioObject; mAudioObject = nullptr; } mMutex.unlock(); quit(); // exit the event loop, if it's still running } QString AudioThread::error() const { QMutexLocker locker(&mMutex); return mError; } /****************************************************************************** * Raise the alarm window, re-output any required audio notification, and * reschedule the alarm in the calendar file. */ void MessageWin::repeat(const KAAlarm& alarm) { if (!mInitialised) return; if (mDeferDlg) { // Cancel any deferral dialog so that the user notices something's going on, // and also because the deferral time limit will have changed. delete mDeferDlg; mDeferDlg = nullptr; } KAEvent* event = mEventId.isEmpty() ? nullptr : AlarmCalendar::resources()->event(mEventId); if (event) { mAlarmType = alarm.type(); // store new alarm type for use if it is later deferred if (mAlwaysHide) playAudio(); else { if (!mDeferDlg || Preferences::modalMessages()) { raise(); playAudio(); } if (mDeferButton->isVisible()) { mDeferButton->setEnabled(true); setDeferralLimit(*event); // ensure that button is disabled when alarm can't be deferred any more } } alarmShowing(*event); } } /****************************************************************************** * Display the window. * If windows are being positioned away from the mouse cursor, it is initially * positioned at the top left to slightly reduce the number of times the * windows need to be moved in showEvent(). */ void MessageWin::show() { if (mCloseTime.isValid()) { // Set a timer to auto-close the window int delay = QDateTime::currentDateTimeUtc().secsTo(mCloseTime); if (delay < 0) delay = 0; QTimer::singleShot(delay * 1000, this, &QWidget::close); if (!delay) return; // don't show the window if auto-closing is already due } if (Preferences::messageButtonDelay() == 0) move(0, 0); MainWindowBase::show(); } /****************************************************************************** * Returns the window's recommended size exclusive of its frame. */ QSize MessageWin::sizeHint() const { QSize desired; switch (mAction) { case KAEvent::MESSAGE: desired = MainWindowBase::sizeHint(); break; case KAEvent::COMMAND: if (mShown) { // For command output, expand the window to accommodate the text const QSize texthint = mCommandText->sizeHint(); int w = texthint.width() + 2 * style()->pixelMetric(QStyle::PM_DefaultChildMargin); if (w < width()) w = width(); const int ypadding = height() - mCommandText->height(); desired = QSize(w, texthint.height() + ypadding); break; } // fall through to default Q_FALLTHROUGH(); default: return MainWindowBase::sizeHint(); } // Limit the size to fit inside the working area of the desktop const QSize desktop = Desktop::workArea(mScreenNumber).size(); const QSize frameThickness = frameGeometry().size() - geometry().size(); // title bar & window frame return desired.boundedTo(desktop - frameThickness); } /****************************************************************************** * Called when the window is shown. * The first time, output any required audio notification, and reschedule or * delete the event from the calendar file. */ void MessageWin::showEvent(QShowEvent* se) { MainWindowBase::showEvent(se); if (mShown || !mInitialised) return; if (mErrorWindow || mAlarmType == KAAlarm::INVALID_ALARM) { // Don't bother repositioning error messages, // and invalid alarms should be deleted anyway. enableButtons(); } else { /* Set the window size. * Note that the frame thickness is not yet known when this * method is called, so for large windows the size needs to be * set again later. */ bool execComplete = true; QSize s = sizeHint(); // fit the window round the message if (mAction == KAEvent::FILE && !mErrorMsgs.count()) Config::readWindowSize("FileMessage", s); resize(s); const QRect desk = Desktop::workArea(mScreenNumber); const QRect frame = frameGeometry(); mButtonDelay = Preferences::messageButtonDelay() * 1000; if (mButtonDelay) { // Position the window in the middle of the screen, and // delay enabling the buttons. mPositioning = true; move((desk.width() - frame.width())/2, (desk.height() - frame.height())/2); execComplete = false; } else { /* Try to ensure that the window can't accidentally be acknowledged * by the user clicking the mouse just as it appears. * To achieve this, move the window so that the OK button is as far away * from the cursor as possible. If the buttons are still too close to the * cursor, disable the buttons for a short time. * N.B. This can't be done in show(), since the geometry of the window * is not known until it is displayed. Unfortunately by moving the * window in showEvent(), a flicker is unavoidable. * See the Qt documentation on window geometry for more details. */ // PROBLEM: The frame size is not known yet! const QPoint cursor = QCursor::pos(); const QRect rect = geometry(); // Find the offsets from the outside of the frame to the edges of the OK button const QRect button(mOkButton->mapToParent(QPoint(0, 0)), mOkButton->mapToParent(mOkButton->rect().bottomRight())); const int buttonLeft = button.left() + rect.left() - frame.left(); const int buttonRight = width() - button.right() + frame.right() - rect.right(); const int buttonTop = button.top() + rect.top() - frame.top(); const int buttonBottom = height() - button.bottom() + frame.bottom() - rect.bottom(); const int centrex = (desk.width() + buttonLeft - buttonRight) / 2; const int centrey = (desk.height() + buttonTop - buttonBottom) / 2; const int x = (cursor.x() < centrex) ? desk.right() - frame.width() : desk.left(); const int y = (cursor.y() < centrey) ? desk.bottom() - frame.height() : desk.top(); // Find the enclosing rectangle for the new button positions // and check if the cursor is too near QRect buttons = mOkButton->geometry().united(mKAlarmButton->geometry()); buttons.translate(rect.left() + x - frame.left(), rect.top() + y - frame.top()); const int minDistance = proximityMultiple * mOkButton->height(); if ((abs(cursor.x() - buttons.left()) < minDistance || abs(cursor.x() - buttons.right()) < minDistance) && (abs(cursor.y() - buttons.top()) < minDistance || abs(cursor.y() - buttons.bottom()) < minDistance)) mButtonDelay = proximityButtonDelay; // too near - disable buttons initially if (x != frame.left() || y != frame.top()) { mPositioning = true; move(x, y); execComplete = false; } } if (execComplete) displayComplete(); // play audio, etc. } // Set the window size etc. once the frame size is known QTimer::singleShot(0, this, &MessageWin::frameDrawn); mShown = true; } /****************************************************************************** * Called when the window has been moved. */ void MessageWin::moveEvent(QMoveEvent* e) { MainWindowBase::moveEvent(e); theApp()->setSpreadWindowsState(isSpread(Desktop::workArea(mScreenNumber).topLeft())); if (mPositioning) { // The window has just been initially positioned mPositioning = false; displayComplete(); // play audio, etc. } } /****************************************************************************** * Called after (hopefully) the window frame size is known. * Reset the initial window size if it exceeds the working area of the desktop. * Set the 'spread windows' menu item status. */ void MessageWin::frameDrawn() { if (!mErrorWindow && mAction == KAEvent::MESSAGE) { const QSize s = sizeHint(); if (width() > s.width() || height() > s.height()) resize(s); } theApp()->setSpreadWindowsState(isSpread(Desktop::workArea(mScreenNumber).topLeft())); } /****************************************************************************** * Called when the window has been displayed properly (in its correct position), * to play sounds and reschedule the event. */ void MessageWin::displayComplete() { delete mTempFile; mTempFile = nullptr; playAudio(); if (mRescheduleEvent) alarmShowing(mEvent); if (!mAlwaysHide) { // Enable the window's buttons either now or after the configured delay if (mButtonDelay > 0) QTimer::singleShot(mButtonDelay, this, &MessageWin::enableButtons); else enableButtons(); } } /****************************************************************************** * Enable the window's buttons. */ void MessageWin::enableButtons() { mOkButton->setEnabled(true); mKAlarmButton->setEnabled(true); if (mDeferButton->isVisible() && !mDisableDeferral) mDeferButton->setEnabled(true); if (mEditButton) mEditButton->setEnabled(true); if (mKMailButton) mKMailButton->setEnabled(true); } /****************************************************************************** * Called when the window's size has changed (before it is painted). */ void MessageWin::resizeEvent(QResizeEvent* re) { if (mRestoreHeight) { // Restore the window height on session restoration if (mRestoreHeight != re->size().height()) { QSize size = re->size(); size.setHeight(mRestoreHeight); resize(size); } else if (isVisible()) mRestoreHeight = 0; } else { if (mShown && mAction == KAEvent::FILE && !mErrorMsgs.count()) Config::writeWindowSize("FileMessage", re->size()); MainWindowBase::resizeEvent(re); } } /****************************************************************************** * Called when a close event is received. * Only quits the application if there is no system tray icon displayed. */ void MessageWin::closeEvent(QCloseEvent* ce) { // Don't prompt or delete the alarm from the display calendar if the session is closing if (!mErrorWindow && !qApp->isSavingSession()) { if (mConfirmAck && !mNoCloseConfirm) { // Ask for confirmation of acknowledgement. Use warningYesNo() because its default is No. if (KAMessageBox::warningYesNo(this, i18nc("@info", "Do you really want to acknowledge this alarm?"), i18nc("@action:button", "Acknowledge Alarm"), KGuiItem(i18nc("@action:button", "Acknowledge")), KStandardGuiItem::cancel()) != KMessageBox::Yes) { ce->ignore(); return; } } if (!mEventId.isEmpty()) { // Delete from the display calendar KAlarm::deleteDisplayEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING)); } } MainWindowBase::closeEvent(ce); } /****************************************************************************** * Called when the OK button is clicked. */ void MessageWin::slotOk() { if (mDontShowAgainCheck && mDontShowAgainCheck->isChecked()) KAlarm::setDontShowErrors(mEventId, mDontShowAgain); close(); } /****************************************************************************** * Called when the KMail button is clicked. * Tells KMail to display the email message displayed in this message window. */ void MessageWin::slotShowKMailMessage() { qCDebug(KALARM_LOG) << "MessageWin::slotShowKMailMessage"; if (mAkonadiItemId < 0) return; const QString err = KAlarm::runKMail(); if (!err.isNull()) { KAMessageBox::sorry(this, err); return; } org::kde::kmail::kmail kmail(KMAIL_DBUS_SERVICE, KMAIL_DBUS_PATH, QDBusConnection::sessionBus()); // Display the message contents QDBusReply reply = kmail.showMail(mAkonadiItemId); bool failed1 = true; bool failed2 = true; if (!reply.isValid()) qCCritical(KALARM_LOG) << "kmail 'showMail' D-Bus call failed:" << reply.error().message(); else if (reply.value()) failed1 = false; // Select the mail folder containing the message Akonadi::ItemFetchJob* job = new Akonadi::ItemFetchJob(Akonadi::Item(mAkonadiItemId)); job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent); Akonadi::Item::List items; if (job->exec()) items = job->items(); if (items.isEmpty() || !items.at(0).isValid()) qCWarning(KALARM_LOG) << "MessageWin::slotShowKMailMessage: No parent found for item" << mAkonadiItemId; else { const Akonadi::Item& it = items.at(0); const Akonadi::Collection::Id colId = it.parentCollection().id(); reply = kmail.selectFolder(QString::number(colId)); if (!reply.isValid()) qCCritical(KALARM_LOG) << "kmail 'selectFolder' D-Bus call failed:" << reply.error().message(); else if (reply.value()) failed2 = false; } if (failed1 || failed2) KAMessageBox::sorry(this, xi18nc("@info", "Unable to locate this email in KMail")); } /****************************************************************************** * Called when the Edit... button is clicked. * Displays the alarm edit dialog. * * NOTE: The alarm edit dialog is made a child of the main window, not this * window, so that if this window closes before the dialog (e.g. on * auto-close), KAlarm doesn't crash. The dialog is set non-modal so that * the main window is unaffected, but modal mode is simulated so that * this window is inactive while the dialog is open. */ void MessageWin::slotEdit() { qCDebug(KALARM_LOG) << "MessageWin::slotEdit"; MainWindow* mainWin = MainWindow::mainMainWindow(); mEditDlg = EditAlarmDlg::create(false, &mOriginalEvent, false, mainWin, EditAlarmDlg::RES_IGNORE); mEditDlg->setAttribute(Qt::WA_NativeWindow, true); KWindowSystem::setMainWindow(mEditDlg->windowHandle(), winId()); KWindowSystem::setOnAllDesktops(mEditDlg->winId(), false); setButtonsReadOnly(true); connect(mEditDlg, &QDialog::accepted, this, &MessageWin::editCloseOk); connect(mEditDlg, &QDialog::rejected, this, &MessageWin::editCloseCancel); connect(mEditDlg, &QObject::destroyed, this, &MessageWin::editCloseCancel); connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, this, &MessageWin::activeWindowChanged); mainWin->editAlarm(mEditDlg, mOriginalEvent); } /****************************************************************************** * Called when OK is clicked in the alarm edit dialog invoked by the Edit button. * Closes the window. */ void MessageWin::editCloseOk() { mEditDlg = nullptr; mNoCloseConfirm = true; // allow window to close without confirmation prompt close(); } /****************************************************************************** * Called when Cancel is clicked in the alarm edit dialog invoked by the Edit * button, or when the dialog is deleted. */ void MessageWin::editCloseCancel() { mEditDlg = nullptr; setButtonsReadOnly(false); } /****************************************************************************** * Called when the active window has changed. If this window has become the * active window and there is an alarm edit dialog, simulate a modal dialog by * making the alarm edit dialog the active window instead. */ void MessageWin::activeWindowChanged(WId win) { if (mEditDlg && win == winId()) KWindowSystem::activateWindow(mEditDlg->winId()); } /****************************************************************************** * Set or clear the read-only state of the dialog buttons. */ void MessageWin::setButtonsReadOnly(bool ro) { mOkButton->setReadOnly(ro, true); mDeferButton->setReadOnly(ro, true); mEditButton->setReadOnly(ro, true); if (mSilenceButton) mSilenceButton->setReadOnly(ro, true); if (mKMailButton) mKMailButton->setReadOnly(ro, true); mKAlarmButton->setReadOnly(ro, true); } /****************************************************************************** * Set up to disable the defer button when the deferral limit is reached. */ void MessageWin::setDeferralLimit(const KAEvent& event) { mDeferLimit = event.deferralLimit().effectiveKDateTime().toUtc().qDateTime(); MidnightTimer::connect(this, SLOT(checkDeferralLimit())); // check every day mDisableDeferral = false; checkDeferralLimit(); } /****************************************************************************** * Check whether the deferral limit has been reached. * If so, disable the Defer button. * N.B. Ideally, just a single QTimer::singleShot() call would be made to disable * the defer button at the corret time. But for a 32-bit integer, the * milliseconds parameter overflows in about 25 days, so instead a daily * check is done until the day when the deferral limit is reached, followed * by a non-overflowing QTimer::singleShot() call. */ void MessageWin::checkDeferralLimit() { if (!mDeferButton->isEnabled() || !mDeferLimit.isValid()) return; int n = KADateTime::currentLocalDate().daysTo(KADateTime(mDeferLimit, KADateTime::LocalZone).date()); if (n > 0) return; MidnightTimer::disconnect(this, SLOT(checkDeferralLimit())); if (n == 0) { // The deferral limit will be reached today n = QDateTime::currentDateTimeUtc().secsTo(mDeferLimit); if (n > 0) { QTimer::singleShot(n * 1000, this, &MessageWin::checkDeferralLimit); return; } } mDeferButton->setEnabled(false); mDisableDeferral = true; } /****************************************************************************** * Called when the Defer... button is clicked. * Displays the defer message dialog. */ void MessageWin::slotDefer() { mDeferDlg = new DeferAlarmDlg(KADateTime::currentDateTime(Preferences::timeSpec()).addSecs(60), mDateTime.isDateOnly(), false, this); if (windowFlags() & Qt::X11BypassWindowManagerHint) mDeferDlg->setWindowFlags(mDeferDlg->windowFlags() | Qt::X11BypassWindowManagerHint); mDeferDlg->setObjectName(QStringLiteral("DeferDlg")); // used by LikeBack mDeferDlg->setDeferMinutes(mDefaultDeferMinutes > 0 ? mDefaultDeferMinutes : Preferences::defaultDeferTime()); mDeferDlg->setLimit(mEvent); if (!Preferences::modalMessages()) lower(); if (mDeferDlg->exec() == QDialog::Accepted) { const DateTime dateTime = mDeferDlg->getDateTime(); const int delayMins = mDeferDlg->deferMinutes(); // Fetch the up-to-date alarm from the calendar. Note that it could have // changed since it was displayed. const KAEvent* event = mEventId.isEmpty() ? nullptr : AlarmCalendar::resources()->event(mEventId); if (event) { // The event still exists in the active calendar qCDebug(KALARM_LOG) << "MessageWin::slotDefer: Deferring event" << mEventId; KAEvent newev(*event); newev.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true); newev.setDeferDefaultMinutes(delayMins); KAlarm::updateEvent(newev, mDeferDlg, true); if (newev.deferred()) mNoPostAction = true; } else { // Try to retrieve the event from the displaying or archive calendars Resource resource; KAEvent event; bool showEdit, showDefer; if (!retrieveEvent(event, resource, showEdit, showDefer)) { // The event doesn't exist any more !?!, so recurrence data, // flags, and more, have been lost. KAMessageBox::error(this, xi18nc("@info", "Cannot defer alarm:Alarm not found.")); raise(); delete mDeferDlg; mDeferDlg = nullptr; mDeferButton->setEnabled(false); mEditButton->setEnabled(false); return; } qCDebug(KALARM_LOG) << "MessageWin::slotDefer: Deferring retrieved event" << mEventId; event.defer(dateTime, (mAlarmType & KAAlarm::REMINDER_ALARM), true); event.setDeferDefaultMinutes(delayMins); event.setCommandError(mCommandError); // Add the event back into the calendar file, retaining its ID // and not updating KOrganizer. KAlarm::addEvent(event, &resource, mDeferDlg, KAlarm::USE_EVENT_ID); if (event.deferred()) mNoPostAction = true; // Finally delete it from the archived calendar now that it has // been reactivated. event.setCategory(CalEvent::ARCHIVED); KAlarm::deleteEvent(event, false); } if (theApp()->wantShowInSystemTray()) { // Alarms are to be displayed only if the system tray icon is running, // so start it if necessary so that the deferred alarm will be shown. theApp()->displayTrayIcon(true); } mNoCloseConfirm = true; // allow window to close without confirmation prompt close(); } else raise(); delete mDeferDlg; mDeferDlg = nullptr; } /****************************************************************************** * Called when the KAlarm icon button in the message window is clicked. * Displays the main window, with the appropriate alarm selected. */ void MessageWin::displayMainWindow() { KAlarm::displayMainWindowSelected(mEventId.eventId()); } /****************************************************************************** * Check whether the specified error message is already displayed for this * alarm, and note that it will now be displayed. * Reply = true if message is already displayed. */ bool MessageWin::haveErrorMessage(unsigned msg) const { if (!mErrorMessages.contains(mEventId)) mErrorMessages.insert(mEventId, 0); const bool result = (mErrorMessages[mEventId] & msg); mErrorMessages[mEventId] |= msg; return result; } void MessageWin::clearErrorMessage(unsigned msg) const { if (mErrorMessages.contains(mEventId)) { if (mErrorMessages[mEventId] == msg) mErrorMessages.remove(mEventId); else mErrorMessages[mEventId] &= ~msg; } } /****************************************************************************** * Check whether the message window should be modal, i.e. with title bar etc. * Normally this follows the Preferences setting, but if there is a full screen * window displayed, on X11 the message window has to bypass the window manager * in order to display on top of it (which has the side effect that it will have * no window decoration). * * Also find the usable area of the desktop (excluding panel etc.), on the * appropriate screen if there are multiple screens. */ bool MessageWin::getWorkAreaAndModal() { mScreenNumber = -1; const bool modal = Preferences::modalMessages(); #if KDEPIM_HAVE_X11 const QList screens = QGuiApplication::screens(); const int numScreens = screens.count(); if (numScreens > 1) { // There are multiple screens. // Check for any full screen windows, even if they are not the active // window, and try not to show the alarm message their screens. mScreenNumber = QApplication::desktop()->screenNumber(MainWindow::mainMainWindow()); // default = KAlarm's screen if (QGuiApplication::primaryScreen()->virtualSiblings().size() > 1) { // The screens form a single virtual desktop. // Xinerama, for example, uses this scheme. QVector screenTypes(numScreens); QVector screenRects(numScreens); for (int s = 0; s < numScreens; ++s) screenRects[s] = screens[s]->geometry(); const FullScreenType full = findFullScreenWindows(screenRects, screenTypes); if (full == NoFullScreen || screenTypes[mScreenNumber] == NoFullScreen) return modal; for (int s = 0; s < numScreens; ++s) { if (screenTypes[s] == NoFullScreen) { // There is no full screen window on this screen mScreenNumber = s; return modal; } } // All screens contain a full screen window: use one without // an active full screen window. for (int s = 0; s < numScreens; ++s) { if (screenTypes[s] == FullScreen) { mScreenNumber = s; return modal; } } } else { // The screens are completely separate from each other. int inactiveScreen = -1; FullScreenType full = haveFullScreenWindow(mScreenNumber); //qCDebug(KALARM_LOG)<<"full="<& screenRects, QVector& screenTypes) { FullScreenType result = NoFullScreen; screenTypes.fill(NoFullScreen); xcb_connection_t* connection = QX11Info::connection(); const NETRootInfo rootInfo(connection, NET::ClientList | NET::ActiveWindow, NET::Properties2()); const xcb_window_t rootWindow = rootInfo.rootWindow(); const xcb_window_t activeWindow = rootInfo.activeWindow(); const xcb_window_t* windows = rootInfo.clientList(); const int windowCount = rootInfo.clientListCount(); //qCDebug(KALARM_LOG)<<"Virtual desktops: Window count="< * Based on KOrganizer's ResourceView class and KAddressBook's ResourceSelection class, * Copyright (C) 2003,2004 Cornelius Schumacher * Copyright (C) 2003-2004 Reinhold Kainhofer * Copyright (c) 2004 Tobias Koenig * * 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 "resourceselector.h" #include "alarmcalendar.h" #include "kalarmapp.h" #include "preferences.h" #include "resources/akonadidatamodel.h" #include "resources/akonadiresourcecreator.h" #include "resources/akonadiresourcemigrator.h" #include "resources/datamodel.h" #include "resources/resources.h" #include "resources/resourcemodel.h" #include "lib/autoqpointer.h" #include "lib/messagebox.h" #include "lib/packedlayout.h" #include "kalarm_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace KCalendarCore; ResourceSelector::ResourceSelector(QWidget* parent) : QFrame(parent) { QBoxLayout* topLayout = new QVBoxLayout(this); QLabel* label = new QLabel(i18nc("@title:group", "Calendars"), this); topLayout->addWidget(label, 0, Qt::AlignHCenter); mAlarmType = new QComboBox(this); mAlarmType->addItem(i18nc("@item:inlistbox", "Active Alarms")); mAlarmType->addItem(i18nc("@item:inlistbox", "Archived Alarms")); mAlarmType->addItem(i18nc("@item:inlistbox", "Alarm Templates")); mAlarmType->setFixedHeight(mAlarmType->sizeHint().height()); mAlarmType->setWhatsThis(i18nc("@info:whatsthis", "Choose which type of data to show alarm calendars for")); topLayout->addWidget(mAlarmType); // No spacing between combo box and listview. ResourceFilterCheckListModel* model = DataModel::createResourceFilterCheckListModel(this); mListView = new ResourceView(model, this); connect(mListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ResourceSelector::selectionChanged); mListView->setContextMenuPolicy(Qt::CustomContextMenu); connect(mListView, &ResourceView::customContextMenuRequested, this, &ResourceSelector::contextMenuRequested); mListView->setWhatsThis(i18nc("@info:whatsthis", "List of available calendars of the selected type. The checked state shows whether a calendar " "is enabled (checked) or disabled (unchecked). The default calendar is shown in bold.")); topLayout->addWidget(mListView, 1); PackedLayout* blayout = new PackedLayout(Qt::AlignHCenter); blayout->setContentsMargins(0, 0, 0, 0); topLayout->addLayout(blayout); mAddButton = new QPushButton(i18nc("@action:button", "Add..."), this); mEditButton = new QPushButton(i18nc("@action:button", "Edit..."), this); mDeleteButton = new QPushButton(i18nc("@action:button", "Remove"), this); blayout->addWidget(mAddButton); blayout->addWidget(mEditButton); blayout->addWidget(mDeleteButton); mEditButton->setWhatsThis(i18nc("@info:whatsthis", "Edit the highlighted calendar")); mDeleteButton->setWhatsThis(xi18nc("@info:whatsthis", "Remove the highlighted calendar from the list." "The calendar itself is left intact, and may subsequently be reinstated in the list if desired.")); mEditButton->setDisabled(true); mDeleteButton->setDisabled(true); connect(mAddButton, &QPushButton::clicked, this, &ResourceSelector::addResource); connect(mEditButton, &QPushButton::clicked, this, &ResourceSelector::editResource); connect(mDeleteButton, &QPushButton::clicked, this, &ResourceSelector::removeResource); - connect(Resources::instance(), &Resources::resourceRemoved, - this, &ResourceSelector::selectionChanged); - connect(mAlarmType, static_cast(&QComboBox::activated), this, &ResourceSelector::alarmTypeSelected); QTimer::singleShot(0, this, SLOT(alarmTypeSelected())); Preferences::connect(SIGNAL(archivedKeepDaysChanged(int)), this, SLOT(archiveDaysChanged(int))); } /****************************************************************************** * Called when an alarm type has been selected. * Filter the resource list to show resources of the selected alarm type, and * add appropriate whatsThis texts to the list and to the Add button. */ void ResourceSelector::alarmTypeSelected() { QString addTip; switch (mAlarmType->currentIndex()) { case 0: mCurrentAlarmType = CalEvent::ACTIVE; addTip = i18nc("@info:tooltip", "Add a new active alarm calendar"); break; case 1: mCurrentAlarmType = CalEvent::ARCHIVED; addTip = i18nc("@info:tooltip", "Add a new archived alarm calendar"); break; case 2: mCurrentAlarmType = CalEvent::TEMPLATE; addTip = i18nc("@info:tooltip", "Add a new alarm template calendar"); break; } // WORKAROUND: Switch scroll bars off to avoid crash (see explanation // in reinstateAlarmTypeScrollBars() description). mListView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); mListView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); mListView->resourceModel()->setEventTypeFilter(mCurrentAlarmType); mAddButton->setWhatsThis(addTip); mAddButton->setToolTip(addTip); // WORKAROUND: Switch scroll bars back on after allowing geometry to update ... QTimer::singleShot(0, this, &ResourceSelector::reinstateAlarmTypeScrollBars); selectionChanged(); // enable/disable buttons } /****************************************************************************** * WORKAROUND for crash due to presumed Qt bug. * Switch scroll bars off. This is to avoid a crash which can very occasionally * happen when changing from a list of calendars which requires vertical scroll * bars, to a list whose text is very slightly wider but which doesn't require * scroll bars at all. (The suspicion is that the width is such that it would * require horizontal scroll bars if the vertical scroll bars were still * present.) Presumably due to a Qt bug, this can result in a recursive call to * ResourceView::viewportEvent() with a Resize event. * * The crash only occurs if the ResourceSelector happens to have exactly (within * one pixel) the "right" width to create the crash. */ void ResourceSelector::reinstateAlarmTypeScrollBars() { mListView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); mListView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); } /****************************************************************************** * Prompt the user for a new resource to add to the list. */ void ResourceSelector::addResource() { ResourceCreator* creator = DataModel::createResourceCreator(mCurrentAlarmType, this); connect(creator, &ResourceCreator::resourceAdded, this, &ResourceSelector::slotResourceAdded); creator->createResource(); } /****************************************************************************** * Called when a resource is added to the calendar data model, after being * created by addResource(). */ void ResourceSelector::slotResourceAdded(Resource& resource, CalEvent::Type alarmType) { const CalEvent::Types types = resource.alarmTypes(); resource.setEnabled(types); if (!(types & alarmType)) { // The user has selected alarm types for the resource which don't // include the currently displayed type. // Show a resource list which includes a selected type. int index = -1; if (types & CalEvent::ACTIVE) index = 0; else if (types & CalEvent::ARCHIVED) index = 1; else if (types & CalEvent::TEMPLATE) index = 2; if (index >= 0) { mAlarmType->setCurrentIndex(index); alarmTypeSelected(); } } } /****************************************************************************** * Edit the currently selected resource. */ void ResourceSelector::editResource() { currentResource().editResource(this); } /****************************************************************************** * Update the backend storage format for the currently selected resource in the * displayed list. */ void ResourceSelector::updateResource() { Resource resource = currentResource(); if (!resource.isValid()) return; DataModel::updateCalendarToCurrentFormat(resource, true, this); } /****************************************************************************** * Remove the currently selected resource from the displayed list. */ void ResourceSelector::removeResource() { Resource resource = currentResource(); if (!resource.isValid()) return; qCDebug(KALARM_LOG) << "ResourceSelector::removeResource:" << resource.displayName(); const QString name = resource.configName(); // Check if it's the standard or only resource for at least one type. const CalEvent::Types allTypes = resource.alarmTypes(); const CalEvent::Types standardTypes = Resources::standardTypes(resource, true); const CalEvent::Type currentType = currentResourceType(); const CalEvent::Type stdType = (standardTypes & CalEvent::ACTIVE) ? CalEvent::ACTIVE : (standardTypes & CalEvent::ARCHIVED) ? CalEvent::ARCHIVED : CalEvent::EMPTY; if (stdType == CalEvent::ACTIVE) { KAMessageBox::sorry(this, i18nc("@info", "You cannot remove your default active alarm calendar.")); return; } if (stdType == CalEvent::ARCHIVED && Preferences::archivedKeepDays()) { // Only allow the archived alarms standard resource to be removed if // we're not saving archived alarms. KAMessageBox::sorry(this, i18nc("@info", "You cannot remove your default archived alarm calendar " "while expired alarms are configured to be kept.")); return; } QString text; if (standardTypes) { // It's a standard resource for at least one alarm type if (allTypes != currentType) { // It also contains alarm types other than the currently displayed type const QString stdTypes = ResourceDataModelBase::typeListForDisplay(standardTypes); QString otherTypes; const CalEvent::Types nonStandardTypes(allTypes & ~standardTypes); if (nonStandardTypes != currentType) otherTypes = xi18nc("@info", "It also contains:%1", ResourceDataModelBase::typeListForDisplay(nonStandardTypes)); text = xi18nc("@info", "%1 is the default calendar for:%2%3" "Do you really want to remove it from all calendar lists?", name, stdTypes, otherTypes); } else text = xi18nc("@info", "Do you really want to remove your default calendar (%1) from the list?", name); } else if (allTypes != currentType) text = xi18nc("@info", "%1 contains:%2Do you really want to remove it from all calendar lists?", name, ResourceDataModelBase::typeListForDisplay(allTypes)); else text = xi18nc("@info", "Do you really want to remove the calendar %1 from the list?", name); if (KAMessageBox::warningContinueCancel(this, text, QString(), KStandardGuiItem::remove()) == KMessageBox::Cancel) return; resource.removeResource(); } /****************************************************************************** * Called when the current selection changes, to enable/disable the * Delete and Edit buttons accordingly. */ void ResourceSelector::selectionChanged() { bool state = mListView->selectionModel()->selectedRows().count(); mDeleteButton->setEnabled(state); mEditButton->setEnabled(state); } /****************************************************************************** * Initialise the button and context menu actions. */ void ResourceSelector::initActions(KActionCollection* actions) { mActionReload = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("@action Reload calendar", "Re&load"), this); actions->addAction(QStringLiteral("resReload"), mActionReload); connect(mActionReload, &QAction::triggered, this, &ResourceSelector::reloadResource); mActionShowDetails = new QAction(QIcon::fromTheme(QStringLiteral("help-about")), i18nc("@action", "Show &Details"), this); actions->addAction(QStringLiteral("resDetails"), mActionShowDetails); connect(mActionShowDetails, &QAction::triggered, this, &ResourceSelector::showInfo); mActionSetColour = new QAction(QIcon::fromTheme(QStringLiteral("color-picker")), i18nc("@action", "Set &Color..."), this); actions->addAction(QStringLiteral("resSetColour"), mActionSetColour); connect(mActionSetColour, &QAction::triggered, this, &ResourceSelector::setColour); mActionClearColour = new QAction(i18nc("@action", "Clear C&olor"), this); actions->addAction(QStringLiteral("resClearColour"), mActionClearColour); connect(mActionClearColour, &QAction::triggered, this, &ResourceSelector::clearColour); mActionEdit = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18nc("@action", "&Edit..."), this); actions->addAction(QStringLiteral("resEdit"), mActionEdit); connect(mActionEdit, &QAction::triggered, this, &ResourceSelector::editResource); mActionUpdate = new QAction(i18nc("@action", "&Update Calendar Format"), this); actions->addAction(QStringLiteral("resUpdate"), mActionUpdate); connect(mActionUpdate, &QAction::triggered, this, &ResourceSelector::updateResource); mActionRemove = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action", "&Remove"), this); actions->addAction(QStringLiteral("resRemove"), mActionRemove); connect(mActionRemove, &QAction::triggered, this, &ResourceSelector::removeResource); mActionSetDefault = new KToggleAction(this); actions->addAction(QStringLiteral("resDefault"), mActionSetDefault); connect(mActionSetDefault, &KToggleAction::triggered, this, &ResourceSelector::setStandard); QAction* action = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18nc("@action", "&Add..."), this); actions->addAction(QStringLiteral("resAdd"), action); connect(action, &QAction::triggered, this, &ResourceSelector::addResource); mActionImport = new QAction(i18nc("@action", "Im&port..."), this); actions->addAction(QStringLiteral("resImport"), mActionImport); connect(mActionImport, &QAction::triggered, this, &ResourceSelector::importCalendar); mActionExport = new QAction(i18nc("@action", "E&xport..."), this); actions->addAction(QStringLiteral("resExport"), mActionExport); connect(mActionExport, &QAction::triggered, this, &ResourceSelector::exportCalendar); } void ResourceSelector::setContextMenu(QMenu* menu) { mContextMenu = menu; } /****************************************************************************** * Display the context menu for the selected calendar. */ void ResourceSelector::contextMenuRequested(const QPoint& viewportPos) { if (!mContextMenu) return; bool active = false; bool writable = false; bool updatable = false; Resource resource; if (mListView->selectionModel()->hasSelection()) { const QModelIndex index = mListView->indexAt(viewportPos); if (index.isValid()) resource = mListView->resourceModel()->resource(index); else mListView->clearSelection(); } CalEvent::Type type = currentResourceType(); bool haveCalendar = resource.isValid(); if (haveCalendar) { active = resource.isEnabled(type); const int rw = resource.writableStatus(type); writable = (rw > 0); const KACalendar::Compat compatibility = resource.compatibility(); if (!rw && (compatibility & ~KACalendar::Converted) && !(compatibility & ~(KACalendar::Convertible | KACalendar::Converted))) updatable = true; // the calendar format is convertible to the current KAlarm format if (!(resource.alarmTypes() & type)) type = CalEvent::EMPTY; } mActionReload->setEnabled(active); mActionShowDetails->setEnabled(haveCalendar); mActionSetColour->setEnabled(haveCalendar); mActionClearColour->setEnabled(haveCalendar); mActionClearColour->setVisible(resource.backgroundColour().isValid()); mActionEdit->setEnabled(haveCalendar); mActionUpdate->setEnabled(updatable); mActionRemove->setEnabled(haveCalendar); mActionImport->setEnabled(active && writable); mActionExport->setEnabled(active); QString text; switch (type) { case CalEvent::ACTIVE: text = i18nc("@action", "Use as &Default for Active Alarms"); break; case CalEvent::ARCHIVED: text = i18nc("@action", "Use as &Default for Archived Alarms"); break; case CalEvent::TEMPLATE: text = i18nc("@action", "Use as &Default for Alarm Templates"); break; default: break; } mActionSetDefault->setText(text); bool standard = Resources::isStandard(resource, type); mActionSetDefault->setChecked(active && writable && standard); mActionSetDefault->setEnabled(active && writable); mContextMenu->popup(mListView->viewport()->mapToGlobal(viewportPos)); } /****************************************************************************** * Called from the context menu to reload the selected resource. */ void ResourceSelector::reloadResource() { Resource resource = currentResource(); if (resource.isValid()) DataModel::reload(resource); } /****************************************************************************** * Called from the context menu to save the selected resource. */ void ResourceSelector::saveResource() { // Save resource is not applicable to Akonadi } /****************************************************************************** * Called when the length of time archived alarms are to be stored changes. * If expired alarms are now to be stored, this also sets any single archived * alarm resource to be the default. */ void ResourceSelector::archiveDaysChanged(int days) { if (days) { const Resource resource = Resources::getStandard(CalEvent::ARCHIVED); if (resource.isValid()) theApp()->purgeNewArchivedDefault(resource); } } /****************************************************************************** * Called from the context menu to set the selected resource as the default * for its alarm type. The resource is automatically made active. */ void ResourceSelector::setStandard() { Resource resource = currentResource(); if (resource.isValid()) { CalEvent::Type alarmType = currentResourceType(); bool standard = mActionSetDefault->isChecked(); if (standard) resource.setEnabled(alarmType, true); Resources::setStandard(resource, alarmType, standard); if (alarmType == CalEvent::ARCHIVED) theApp()->purgeNewArchivedDefault(resource); } } /****************************************************************************** * Called from the context menu to merge alarms from an external calendar into * the selected resource (if any). */ void ResourceSelector::importCalendar() { Resource resource = currentResource(); AlarmCalendar::resources()->importAlarms(this, &resource); } /****************************************************************************** * Called from the context menu to copy the selected resource's alarms to an * external calendar. */ void ResourceSelector::exportCalendar() { const Resource resource = currentResource(); if (resource.isValid()) AlarmCalendar::exportAlarms(AlarmCalendar::resources()->events(resource), this); } /****************************************************************************** * Called from the context menu to set a colour for the selected resource. */ void ResourceSelector::setColour() { Resource resource = currentResource(); if (resource.isValid()) { QColor colour = resource.backgroundColour(); if (!colour.isValid()) colour = QApplication::palette().color(QPalette::Base); colour = QColorDialog::getColor(colour, this); if (colour.isValid()) resource.setBackgroundColour(colour); } } /****************************************************************************** * Called from the context menu to clear the display colour for the selected * resource. */ void ResourceSelector::clearColour() { Resource resource = currentResource(); if (resource.isValid()) resource.setBackgroundColour(QColor()); } /****************************************************************************** * Called from the context menu to display information for the selected resource. */ void ResourceSelector::showInfo() { const Resource resource = currentResource(); if (resource.isValid()) { const QString name = resource.displayName(); const QString id = resource.configName(); // resource name const QString calType = resource.storageTypeString(true); const CalEvent::Type alarmType = currentResourceType(); const QString storage = resource.storageTypeString(false); const QString location = resource.displayLocation(); const CalEvent::Types altypes = resource.alarmTypes(); QStringList alarmTypes; if (altypes & CalEvent::ACTIVE) alarmTypes << i18nc("@info", "Active alarms"); if (altypes & CalEvent::ARCHIVED) alarmTypes << i18nc("@info", "Archived alarms"); if (altypes & CalEvent::TEMPLATE) alarmTypes << i18nc("@info", "Alarm templates"); const QString alarmTypeString = alarmTypes.join(i18nc("@info List separator", ", ")); QString perms = ResourceDataModelBase::readOnlyTooltip(resource); if (perms.isEmpty()) perms = i18nc("@info", "Read-write"); const QString enabled = resource.isEnabled(alarmType) ? i18nc("@info", "Enabled") : i18nc("@info", "Disabled"); const QString std = Resources::isStandard(resource, alarmType) ? i18nc("@info Parameter in 'Default calendar: Yes/No'", "Yes") : i18nc("@info Parameter in 'Default calendar: Yes/No'", "No"); const QString text = xi18nc("@info", "%1" "ID: %2" "Calendar type: %3" "Contents: %4" "%5: %6" "Permissions: %7" "Status: %8" "Default calendar: %9", name, id, calType, alarmTypeString, storage, location, perms, enabled, std); // Display the resource information. Because the user requested // the information, don't raise a KNotify event. KAMessageBox::information(this, text, QString(), QString(), KMessageBox::Options()); } } /****************************************************************************** * Return the currently selected resource in the list. */ Resource ResourceSelector::currentResource() const { return mListView->resource(mListView->selectionModel()->currentIndex()); } /****************************************************************************** * Return the currently selected resource type. */ CalEvent::Type ResourceSelector::currentResourceType() const { switch (mAlarmType->currentIndex()) { case 0: return CalEvent::ACTIVE; case 1: return CalEvent::ARCHIVED; case 2: return CalEvent::TEMPLATE; default: return CalEvent::EMPTY; } } void ResourceSelector::resizeEvent(QResizeEvent* re) { Q_EMIT resized(re->oldSize(), re->size()); } // vim: et sw=4: