Warning, file /pim/kalarm/src/messagedisplayhelper.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 /* 0002 * messagedisplayhelper.cpp - helper class to display an alarm or error message 0003 * Program: kalarm 0004 * SPDX-FileCopyrightText: 2001-2023 David Jarvie <djarvie@kde.org> 0005 * 0006 * SPDX-License-Identifier: GPL-2.0-or-later 0007 */ 0008 0009 #include "messagedisplayhelper.h" 0010 #include "messagedisplayhelper_p.h" 0011 #include "messagedisplay.h" 0012 0013 #include "displaycalendar.h" 0014 #include "functions.h" 0015 #include "kalarmapp.h" 0016 #include "mainwindow.h" 0017 #include "resourcescalendar.h" 0018 #include "resources/resources.h" 0019 #include "lib/messagebox.h" 0020 #include "lib/pushbutton.h" 0021 #include "lib/synchtimer.h" 0022 #include "screensaver.h" // DBUS-generated 0023 #include "kalarm_debug.h" 0024 0025 #ifdef HAVE_TEXT_TO_SPEECH_SUPPORT 0026 #include <TextEditTextToSpeech/TextToSpeech> 0027 #endif 0028 0029 #include <KLocalizedString> 0030 #include <KConfig> 0031 #include <KIO/StatJob> 0032 #include <KIO/StoredTransferJob> 0033 #include <KJobWidgets> 0034 #include <KNotification> 0035 #include <phonon/MediaObject> 0036 #include <phonon/AudioOutput> 0037 #include <phonon/VolumeFaderEffect> 0038 0039 #include <QByteArray> 0040 #include <QLocale> 0041 #include <QMimeDatabase> 0042 #include <QRegularExpression> 0043 #include <QTemporaryFile> 0044 #include <QTextBrowser> 0045 #include <QThread> 0046 #include <QTimeZone> 0047 #include <QTimer> 0048 #include <QUrl> 0049 0050 using namespace KAlarmCal; 0051 0052 namespace 0053 { 0054 const char FDO_SCREENSAVER_SERVICE[] = "org.freedesktop.ScreenSaver"; 0055 const char FDO_SCREENSAVER_PATH[] = "/org/freedesktop/ScreenSaver"; 0056 } 0057 0058 // Error message bit masks 0059 enum 0060 { 0061 ErrMsg_Speak = 0x01, 0062 ErrMsg_AudioFile = 0x02 0063 }; 0064 0065 QList<MessageDisplayHelper*> MessageDisplayHelper::mInstanceList; 0066 QHash<EventId, unsigned> MessageDisplayHelper::mErrorMessages; 0067 // There can only be one audio thread at a time: trying to play multiple 0068 // sound files simultaneously would result in a cacophony, and besides 0069 // that, Phonon currently crashes... 0070 QPointer<QThread> MessageDisplayHelper::mAudioThread; 0071 QPointer<AudioPlayer> MessageDisplayHelper::mAudioPlayer; 0072 MessageDisplayHelper* MessageDisplayHelper::mAudioOwner = nullptr; 0073 0074 /****************************************************************************** 0075 * Construct the message display handler for the specified alarm. 0076 * Other alarms in the supplied event may have been updated by the caller, so 0077 * the whole event needs to be stored for updating the calendar file when it is 0078 * displayed. 0079 */ 0080 MessageDisplayHelper::MessageDisplayHelper(MessageDisplay* parent, const KAEvent& event, const KAAlarm& alarm, int flags) 0081 : mParent(parent) 0082 , mMessage(event.cleanText()) 0083 , mFont(event.font()) 0084 , mBgColour(event.bgColour()) 0085 , mFgColour(event.fgColour()) 0086 , mEventId(event) 0087 , mAudioFile(event.audioFile()) 0088 , mVolume(event.soundVolume()) 0089 , mFadeVolume(event.fadeVolume()) 0090 , mFadeSeconds(qMin(event.fadeSeconds(), 86400)) 0091 , mDefaultDeferMinutes(event.deferDefaultMinutes()) 0092 , mAlarmType(alarm.type()) 0093 , mAction(event.actionSubType()) 0094 , mEmailId(event.emailId()) 0095 , mCommandError(event.commandError()) 0096 , mAudioRepeatPause(event.repeatSoundPause()) 0097 , mConfirmAck(event.confirmAck()) 0098 , mNoDefer(true) 0099 , mInvalid(false) 0100 , mEvent(event) 0101 , mOriginalEvent(event) 0102 , mResource(Resources::resourceForEvent(mEventId.eventId())) 0103 , mAlwaysHide(flags & MessageDisplay::AlwaysHide) 0104 , mNoPostAction(alarm.type() & KAAlarm::Type::Reminder) 0105 , mBeep(event.beep()) 0106 , mSpeak(event.speak()) 0107 , mNoRecordCmdError(flags & MessageDisplay::NoRecordCmdError) 0108 , mRescheduleEvent(!(flags & MessageDisplay::NoReschedule)) 0109 { 0110 qCDebug(KALARM_LOG) << "MessageDisplayHelper():" << mEventId; 0111 if (alarm.type() & KAAlarm::Type::Reminder) 0112 { 0113 if (event.reminderMinutes() < 0) 0114 { 0115 event.previousOccurrence(alarm.dateTime(false).effectiveKDateTime(), mDateTime, false); 0116 if (!mDateTime.isValid() && event.repeatAtLogin()) 0117 mDateTime = alarm.dateTime().addSecs(event.reminderMinutes() * 60); 0118 } 0119 else 0120 mDateTime = event.mainDateTime(true); 0121 } 0122 else 0123 mDateTime = alarm.dateTime(true); 0124 if (!(flags & (MessageDisplay::NoInitView | MessageDisplay::AlwaysHide))) 0125 { 0126 const bool readonly = KAlarm::eventReadOnly(mEventId.eventId()); 0127 mShowEdit = !mEventId.isEmpty() && !readonly; 0128 mNoDefer = readonly || (flags & MessageDisplay::NoDefer) || alarm.repeatAtLogin(); 0129 } 0130 0131 mInstanceList.append(this); 0132 if (event.autoClose()) 0133 mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().qDateTime().addSecs(event.lateCancel() * 60); 0134 } 0135 0136 /****************************************************************************** 0137 * Construct the message display handler for a specified error message. 0138 * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note 0139 * that the option is specific to 'event'. 0140 */ 0141 MessageDisplayHelper::MessageDisplayHelper(MessageDisplay* parent, const KAEvent& event, const DateTime& alarmDateTime, 0142 const QStringList& errmsgs, const QString& dontShowAgain) 0143 : mParent(parent) 0144 , mMessage(event.cleanText()) 0145 , mDateTime(alarmDateTime) 0146 , mEventId(event) 0147 , mAlarmType(KAAlarm::Type::Main) 0148 , mAction(event.actionSubType()) 0149 , mEmailId(-1) 0150 , mCommandError(KAEvent::CmdErr::None) 0151 , mErrorMsgs(errmsgs) 0152 , mDontShowAgain(dontShowAgain) 0153 , mConfirmAck(false) 0154 , mShowEdit(false) 0155 , mNoDefer(true) 0156 , mInvalid(false) 0157 , mEvent(event) 0158 , mOriginalEvent(event) 0159 , mErrorWindow(true) 0160 , mNoPostAction(true) 0161 { 0162 qCDebug(KALARM_LOG) << "MessageDisplayHelper(errmsg)"; 0163 mInstanceList.append(this); 0164 } 0165 0166 /****************************************************************************** 0167 * Construct the message display handler for restoration by session management. 0168 * The handler is initialised by readProperties(). 0169 */ 0170 MessageDisplayHelper::MessageDisplayHelper(MessageDisplay* parent) 0171 : mParent(parent) 0172 { 0173 qCDebug(KALARM_LOG) << "MessageDisplayHelper(): restore"; 0174 mInstanceList.append(this); 0175 } 0176 0177 /****************************************************************************** 0178 * Destructor. Perform any post-alarm actions before tidying up. 0179 */ 0180 MessageDisplayHelper::~MessageDisplayHelper() 0181 { 0182 qCDebug(KALARM_LOG) << "~MessageDisplayHelper()" << mEventId; 0183 if (mAudioOwner == this && !mAudioPlayer.isNull()) 0184 mAudioPlayer->stop(); // mAudioPlayer will delete once stopped 0185 if (mAudioOwner == this) 0186 mAudioOwner = nullptr; 0187 // If the audio thread is destroyed while still running, it will crash. 0188 // So remove this instance as its parent to prevent its deletion as a child. 0189 if (mAudioThread.data()) 0190 mAudioThread->setParent(nullptr); 0191 mErrorMessages.remove(mEventId); 0192 mInstanceList.removeAll(this); 0193 delete mTempFile; 0194 if (!mNoPostAction && !mEvent.postAction().isEmpty()) 0195 theApp()->alarmCompleted(mEvent); 0196 } 0197 0198 /****************************************************************************** 0199 * Obtain the texts to show in the displayed alarm. 0200 */ 0201 void MessageDisplayHelper::initTexts() 0202 { 0203 const bool reminder = (!mErrorWindow && (mAlarmType & KAAlarm::Type::Reminder)); 0204 mTexts.title = (mAlarmType & KAAlarm::Type::Reminder) ? i18nc("@title:window", "Reminder") 0205 : i18nc("@title:window", "Message"); 0206 0207 // Show the alarm date/time, together with a reminder text where appropriate. 0208 if (mDateTime.isValid()) 0209 { 0210 // Alarm date/time: display time zone if not local time zone. 0211 mTexts.time = mTexts.timeFull = dateTimeToDisplay(); 0212 if (reminder) 0213 { 0214 // Reminder. 0215 // Create a label "time\nReminder" by inserting the time at the 0216 // start of the translated string, allowing for possible HTML tags 0217 // enclosing "Reminder". 0218 QString s = i18nc("@info", "Reminder"); 0219 static const QRegularExpression re(QStringLiteral("^(<[^>]+>)*")); // search for HTML tag "<...>" 0220 const QRegularExpressionMatch match = re.match(s); 0221 // Prefix the time, plus a newline, to "Reminder", inside any HTML tags. 0222 s.insert(match.capturedEnd(0), mTexts.time + QLatin1String("<br/>")); 0223 mTexts.timeFull = s; 0224 } 0225 } 0226 0227 if (!mErrorWindow) 0228 { 0229 // It's a normal alarm message display 0230 switch (mAction) 0231 { 0232 case KAEvent::SubAction::File: 0233 { 0234 // Display the file name 0235 mTexts.fileName = mMessage; 0236 0237 // Display contents of file 0238 const QUrl url = QUrl::fromUserInput(mMessage, QString(), QUrl::AssumeLocalFile); 0239 0240 auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, KIO::StatBasic, KIO::HideProgressInfo); 0241 const bool exists = statJob->exec(); 0242 const bool isDir = statJob->statResult().isDir(); 0243 0244 bool opened = false; 0245 if (exists && !isDir) 0246 { 0247 auto job = KIO::storedGet(url); 0248 KJobWidgets::setWindow(job, MainWindow::mainMainWindow()); 0249 if (job->exec()) 0250 { 0251 opened = true; 0252 const QByteArray data = job->data(); 0253 0254 QMimeDatabase db; 0255 QMimeType mime = db.mimeTypeForUrl(url); 0256 if (mime.name() == QLatin1String("application/octet-stream")) 0257 mime = db.mimeTypeForData(mTempFile); 0258 mTexts.fileType = File::fileType(mime); 0259 switch (mTexts.fileType) 0260 { 0261 case File::Type::Image: 0262 case File::Type::TextFormatted: 0263 delete mTempFile; 0264 mTempFile = new QTemporaryFile; 0265 mTempFile->open(); 0266 mTempFile->write(data); 0267 break; 0268 default: 0269 break; 0270 } 0271 0272 switch (mTexts.fileType) 0273 { 0274 case File::Type::Image: 0275 mTexts.message = QLatin1String(R"(<div align="center"><img src=")") + mTempFile->fileName() + QLatin1String(R"("></div>)"); 0276 mTempFile->close(); // keep the file available to be displayed 0277 break; 0278 case File::Type::TextFormatted: 0279 { 0280 QTextBrowser browser; 0281 browser.setSource(QUrl::fromLocalFile(mTempFile->fileName())); 0282 mTexts.message = browser.toHtml(); 0283 delete mTempFile; 0284 mTempFile = nullptr; 0285 break; 0286 } 0287 default: 0288 mTexts.message = QString::fromUtf8(data); 0289 break; 0290 } 0291 } 0292 } 0293 0294 if (!exists || isDir || !opened) 0295 { 0296 mErrorMsgs += isDir ? i18nc("@info", "File is a folder") : exists ? i18nc("@info", "Failed to open file") : i18nc("@info", "File not found"); 0297 } 0298 break; 0299 } 0300 case KAEvent::SubAction::Message: 0301 mTexts.message = mMessage; 0302 break; 0303 0304 case KAEvent::SubAction::Command: 0305 theApp()->execCommandAlarm(mEvent, mEvent.alarm(mAlarmType), mNoRecordCmdError, 0306 this, SLOT(readProcessOutput(ShellProcess*)), "commandCompleted"); 0307 break; 0308 0309 case KAEvent::SubAction::Email: 0310 default: 0311 break; 0312 } 0313 0314 if (reminder && mEvent.reminderMinutes() > 0) 0315 { 0316 // Advance reminder: show remaining time until the actual alarm 0317 if (mDateTime.isDateOnly() || KADateTime::currentLocalDate().daysTo(mDateTime.date()) > 0) 0318 { 0319 setRemainingTextDay(false); 0320 MidnightTimer::connect(this, SLOT(slotSetRemainingTextDay())); // update every day 0321 } 0322 else 0323 { 0324 setRemainingTextMinute(false); 0325 MinuteTimer::connect(this, SLOT(slotSetRemainingTextMinute())); // update every minute 0326 } 0327 } 0328 } 0329 else 0330 { 0331 // It's an error message 0332 switch (mAction) 0333 { 0334 case KAEvent::SubAction::Email: 0335 { 0336 // Display the email addresses and subject. 0337 mTexts.errorEmail[0] = i18nc("@info Email addressee", "To:"); 0338 mTexts.errorEmail[1] = mEvent.emailAddresses(QStringLiteral("\n")); 0339 mTexts.errorEmail[2] = i18nc("@info Email subject", "Subject:"); 0340 mTexts.errorEmail[3] = mEvent.emailSubject(); 0341 break; 0342 } 0343 case KAEvent::SubAction::Command: 0344 case KAEvent::SubAction::File: 0345 case KAEvent::SubAction::Message: 0346 default: 0347 // Just display the error message strings 0348 break; 0349 } 0350 } 0351 0352 if (!mErrorMsgs.isEmpty()) 0353 mTexts.title = i18nc("@title:window", "Error"); 0354 0355 mInitialised = true; // the alarm's texts have been created 0356 } 0357 0358 /****************************************************************************** 0359 * Return the number of message displays, optionally excluding always-hidden ones. 0360 */ 0361 int MessageDisplayHelper::instanceCount(bool excludeAlwaysHidden) 0362 { 0363 int count = mInstanceList.count(); 0364 if (excludeAlwaysHidden) 0365 { 0366 for (const MessageDisplayHelper* h : std::as_const(mInstanceList)) 0367 { 0368 if (h->mAlwaysHide) 0369 --count; 0370 } 0371 } 0372 return count; 0373 } 0374 0375 /****************************************************************************** 0376 * Check whether to display an error message. 0377 * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note 0378 * that the option is specific to 'event'. 0379 */ 0380 bool MessageDisplayHelper::shouldShowError(const KAEvent& event, const QStringList& errmsgs, const QString& dontShowAgain) 0381 { 0382 if (!dontShowAgain.isEmpty() 0383 && KAlarm::dontShowErrors(EventId(event), dontShowAgain)) 0384 return false; 0385 0386 // Don't pile up duplicate error messages for the same alarm 0387 EventId eid(event); 0388 for (const MessageDisplayHelper* h : std::as_const(mInstanceList)) 0389 { 0390 if (h->mErrorWindow && h->mEventId == eid 0391 && h->mErrorMsgs == errmsgs && h->mDontShowAgain == dontShowAgain) 0392 return false; 0393 } 0394 return true; 0395 } 0396 0397 /****************************************************************************** 0398 * Convert a reminder display into a normal alarm display. 0399 */ 0400 bool MessageDisplayHelper::cancelReminder(const KAEvent& event, const KAAlarm& alarm) 0401 { 0402 if (!mInitialised) 0403 return false; 0404 mDateTime = alarm.dateTime(true); 0405 mNoPostAction = false; 0406 mAlarmType = alarm.type(); 0407 if (event.autoClose()) 0408 mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().qDateTime().addSecs(event.lateCancel() * 60); 0409 mTexts.title = i18nc("@title:window", "Message"); 0410 mTexts.time = mTexts.timeFull = dateTimeToDisplay(); 0411 mTexts.remainingTime.clear(); 0412 MidnightTimer::disconnect(this, SLOT(slotSetRemainingTextDay())); 0413 MinuteTimer::disconnect(this, SLOT(slotSetRemainingTextMinute())); 0414 Q_EMIT textsChanged(DisplayTexts::Title | DisplayTexts::Time | DisplayTexts::TimeFull | DisplayTexts::RemainingTime); 0415 return true; 0416 } 0417 0418 /****************************************************************************** 0419 * Update the alarm's trigger time. No textsChanged() signal is emitted. 0420 */ 0421 bool MessageDisplayHelper::updateDateTime(const KAEvent& event, const KAAlarm& alarm) 0422 { 0423 mDateTime = (alarm.type() & KAAlarm::Type::Reminder) ? event.mainDateTime(true) : alarm.dateTime(true); 0424 if (!mDateTime.isValid()) 0425 return false; 0426 mTexts.time = mTexts.timeFull = dateTimeToDisplay(); 0427 return true; 0428 } 0429 0430 /****************************************************************************** 0431 * Get the trigger time to display. 0432 */ 0433 QString MessageDisplayHelper::dateTimeToDisplay() const 0434 { 0435 QString tm; 0436 if (mDateTime.isValid()) 0437 { 0438 QLocale locale; 0439 if (mDateTime.isDateOnly()) 0440 tm = locale.toString(mDateTime.date(), QLocale::ShortFormat); 0441 else 0442 { 0443 bool showZone = false; 0444 if (mDateTime.timeType() == KADateTime::UTC 0445 || (mDateTime.timeType() == KADateTime::TimeZone && !mDateTime.isLocalZone())) 0446 { 0447 // Display time zone abbreviation if it's different from the local 0448 // zone. Note that the iCalendar time zone might represent the local 0449 // time zone in a slightly different way from the system time zone, 0450 // so the zone comparison above might not produce the desired result. 0451 const QString tz = mDateTime.kDateTime().toString(QStringLiteral("%Z")); 0452 KADateTime local = mDateTime.kDateTime(); 0453 local.setTimeSpec(KADateTime::Spec::LocalZone()); 0454 showZone = (local.toString(QStringLiteral("%Z")) != tz); 0455 } 0456 const QDateTime dt = mDateTime.qDateTime(); 0457 tm = locale.toString(dt, QLocale::ShortFormat); 0458 if (showZone) 0459 tm += QLatin1Char(' ') + mDateTime.timeZone().displayName(dt, QTimeZone::ShortName, locale); 0460 } 0461 } 0462 return tm; 0463 } 0464 0465 /****************************************************************************** 0466 * Set the remaining time text in a reminder display. 0467 * Called at the start of every day (at the user-defined start-of-day time). 0468 */ 0469 void MessageDisplayHelper::setRemainingTextDay(bool notify) 0470 { 0471 const int days = KADateTime::currentLocalDate().daysTo(mDateTime.date()); 0472 if (days <= 0 && !mDateTime.isDateOnly()) 0473 { 0474 // The alarm is due today, so start refreshing every minute 0475 MidnightTimer::disconnect(this, SLOT(slotSetRemainingTextDay())); 0476 setRemainingTextMinute(notify); 0477 MinuteTimer::connect(this, SLOT(slotSetRemainingTextMinute())); // update every minute 0478 } 0479 else 0480 { 0481 if (days <= 0) 0482 mTexts.remainingTime = i18nc("@info", "Today"); 0483 else if (days % 7) 0484 mTexts.remainingTime = i18ncp("@info", "Tomorrow", "in %1 days' time", days); 0485 else 0486 mTexts.remainingTime = i18ncp("@info", "in 1 week's time", "in %1 weeks' time", days/7); 0487 if (notify) 0488 Q_EMIT textsChanged(DisplayTexts::RemainingTime); 0489 } 0490 } 0491 0492 /****************************************************************************** 0493 * Set the remaining time text in a reminder display. 0494 * Called on every minute boundary. 0495 */ 0496 void MessageDisplayHelper::setRemainingTextMinute(bool notify) 0497 { 0498 const int mins = (KADateTime::currentUtcDateTime().secsTo(mDateTime.effectiveKDateTime()) + 59) / 60; 0499 if (mins < 60) 0500 mTexts.remainingTime = i18ncp("@info", "in 1 minute's time", "in %1 minutes' time", (mins > 0 ? mins : 0)); 0501 else if (mins % 60 == 0) 0502 mTexts.remainingTime = i18ncp("@info", "in 1 hour's time", "in %1 hours' time", mins/60); 0503 else 0504 { 0505 QString hourText = i18ncp("@item:intext inserted into 'in ... %1 minute's time' below", "1 hour", "%1 hours", mins/60); 0506 mTexts.remainingTime = 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); 0507 } 0508 if (notify) 0509 Q_EMIT textsChanged(DisplayTexts::RemainingTime); 0510 } 0511 0512 /****************************************************************************** 0513 * Called when output is available from the command which is providing the text 0514 * for this display. Add the output. 0515 */ 0516 void MessageDisplayHelper::readProcessOutput(ShellProcess* proc) 0517 { 0518 const QByteArray data = proc->readAll(); 0519 if (!data.isEmpty()) 0520 { 0521 mCommandOutput.clear(); 0522 0523 // Strip any trailing newline, to avoid showing trailing blank line 0524 // in message display. 0525 QString newText; 0526 if (mTexts.newLine) 0527 newText = QStringLiteral("\n"); 0528 mTexts.newLine = data.endsWith('\n'); 0529 newText += QString::fromLocal8Bit(data.data(), data.length() - (mTexts.newLine ? 1 : 0)); 0530 mTexts.message += newText; 0531 Q_EMIT textsChanged(DisplayTexts::MessageAppend, newText); 0532 } 0533 } 0534 0535 /****************************************************************************** 0536 * Called when the command which is providing the text for this display has 0537 * completed. Check whether the command succeeded, even partially. 0538 */ 0539 void MessageDisplayHelper::commandCompleted(ShellProcess::Status status) 0540 { 0541 bool failed; 0542 switch (status) 0543 { 0544 case ShellProcess::Status::Success: 0545 case ShellProcess::Status::Died: 0546 failed = false; 0547 break; 0548 0549 case ShellProcess::Status::Unauthorised: 0550 case ShellProcess::Status::NotFound: 0551 case ShellProcess::Status::StartFail: 0552 case ShellProcess::Status::Inactive: 0553 default: 0554 failed = true; 0555 break; 0556 } 0557 Q_EMIT commandExited(!failed); 0558 } 0559 0560 /****************************************************************************** 0561 * Save settings to the session managed config file, for restoration 0562 * when the program is restored. 0563 */ 0564 bool MessageDisplayHelper::saveProperties(KConfigGroup& config) 0565 { 0566 if (!mErrorWindow && !mAlwaysHide) 0567 { 0568 config.writeEntry("EventID", mEventId.eventId()); 0569 config.writeEntry("CollectionID", mResource.id()); 0570 config.writeEntry("AlarmType", static_cast<int>(mAlarmType)); 0571 if (mAlarmType == KAAlarm::Type::Invalid) 0572 qCCritical(KALARM_LOG) << "MessageDisplayHelper::saveProperties: Invalid alarm: id=" << mEventId << ", alarm count=" << mEvent.alarmCount(); 0573 config.writeEntry("Message", mMessage); 0574 config.writeEntry("Type", static_cast<int>(mAction)); 0575 config.writeEntry("Font", mFont); 0576 config.writeEntry("BgColour", mBgColour); 0577 config.writeEntry("FgColour", mFgColour); 0578 config.writeEntry("ConfirmAck", mConfirmAck); 0579 if (mDateTime.isValid()) 0580 { 0581 config.writeEntry("Time", mDateTime.effectiveDateTime()); 0582 config.writeEntry("DateOnly", mDateTime.isDateOnly()); 0583 QByteArray zone; 0584 if (mDateTime.isUtc()) 0585 zone = "UTC"; 0586 else if (mDateTime.isOffsetFromUtc()) 0587 { 0588 int offset = mDateTime.utcOffset(); 0589 if (offset >= 0) 0590 zone = '+' + QByteArray::number(offset); 0591 else 0592 zone = QByteArray::number(offset); 0593 } 0594 else if (mDateTime.timeType() == KADateTime::TimeZone) 0595 { 0596 const QTimeZone tz = mDateTime.timeZone(); 0597 if (tz.isValid()) 0598 zone = tz.id(); 0599 } 0600 config.writeEntry("TimeZone", zone); 0601 } 0602 if (mCloseTime.isValid()) 0603 config.writeEntry("Expiry", mCloseTime); 0604 if (mAudioRepeatPause >= 0 && mSilenceButton && mSilenceButton->isEnabled()) 0605 { 0606 // Only need to restart sound file playing if it's being repeated 0607 config.writePathEntry("AudioFile", mAudioFile); 0608 config.writeEntry("Volume", static_cast<int>(mVolume * 100)); 0609 config.writeEntry("AudioPause", mAudioRepeatPause); 0610 } 0611 config.writeEntry("Speak", mSpeak); 0612 config.writeEntry("DeferMins", mDefaultDeferMinutes); 0613 config.writeEntry("NoDefer", mNoDefer); 0614 config.writeEntry("NoPostAction", mNoPostAction); 0615 config.writeEntry("EmailId", mEmailId); 0616 config.writeEntry("CmdErr", static_cast<int>(mCommandError)); 0617 config.writeEntry("DontShowAgain", mDontShowAgain); 0618 return true; 0619 } 0620 else 0621 { 0622 config.writeEntry("Invalid", true); 0623 return false; 0624 } 0625 } 0626 0627 /****************************************************************************** 0628 * Read settings from the session managed config file. 0629 * This function is automatically called whenever the app is being restored. 0630 * Read in whatever was saved in saveProperties(). 0631 * Reply = true if the parent display needs to initialise its display. 0632 */ 0633 bool MessageDisplayHelper::readProperties(const KConfigGroup& config) 0634 { 0635 return readPropertyValues(config) 0636 && processPropertyValues(); 0637 } 0638 0639 /****************************************************************************** 0640 * Read settings from the session managed config file. 0641 * This function is automatically called whenever the app is being restored. 0642 * Read in whatever was saved in saveProperties(). 0643 * Reply = true if the parent display needs to initialise its display. 0644 */ 0645 bool MessageDisplayHelper::readPropertyValues(const KConfigGroup& config) 0646 { 0647 const QString eventId = config.readEntry("EventID"); 0648 const ResourceId resourceId = config.readEntry("CollectionID", ResourceId(-1)); 0649 mInvalid = config.readEntry("Invalid", false); 0650 mAlarmType = static_cast<KAAlarm::Type>(config.readEntry("AlarmType", 0)); 0651 if (mAlarmType == KAAlarm::Type::Invalid) 0652 { 0653 mInvalid = true; 0654 qCCritical(KALARM_LOG) << "MessageDisplayHelper::readProperties: Invalid alarm: id=" << eventId; 0655 } 0656 mMessage = config.readEntry("Message"); 0657 mAction = static_cast<KAEvent::SubAction>(config.readEntry("Type", 0)); 0658 mFont = config.readEntry("Font", QFont()); 0659 mBgColour = config.readEntry("BgColour", QColor(Qt::white)); 0660 mFgColour = config.readEntry("FgColour", QColor(Qt::black)); 0661 mConfirmAck = config.readEntry("ConfirmAck", false); 0662 QDateTime invalidDateTime; 0663 QDateTime dt = config.readEntry("Time", invalidDateTime); 0664 const QByteArray zoneId = config.readEntry("TimeZone").toLatin1(); 0665 KADateTime::Spec timeSpec; 0666 if (zoneId.isEmpty()) 0667 timeSpec = KADateTime::LocalZone; 0668 else if (zoneId == "UTC") 0669 timeSpec = KADateTime::UTC; 0670 else if (zoneId.startsWith('+') || zoneId.startsWith('-')) 0671 timeSpec.setType(KADateTime::OffsetFromUTC, zoneId.toInt()); 0672 else 0673 timeSpec = QTimeZone(zoneId); 0674 mDateTime = KADateTime(dt.date(), dt.time(), timeSpec); 0675 const bool dateOnly = config.readEntry("DateOnly", false); 0676 if (dateOnly) 0677 mDateTime.setDateOnly(true); 0678 mCloseTime = config.readEntry("Expiry", invalidDateTime); 0679 mCloseTime.setTimeZone(QTimeZone::utc()); 0680 mAudioFile = config.readPathEntry("AudioFile", QString()); 0681 mVolume = static_cast<float>(config.readEntry("Volume", 0)) / 100; 0682 mFadeVolume = -1; 0683 mFadeSeconds = 0; 0684 if (!mAudioFile.isEmpty()) // audio file URL was only saved if it repeats 0685 mAudioRepeatPause = config.readEntry("AudioPause", 0); 0686 mBeep = false; // don't beep after restart (similar to not playing non-repeated sound file) 0687 mSpeak = config.readEntry("Speak", false); 0688 mDefaultDeferMinutes = config.readEntry("DeferMins", 0); 0689 mNoDefer = config.readEntry("NoDefer", false); 0690 mNoPostAction = config.readEntry("NoPostAction", true); 0691 mEmailId = config.readEntry("EmailId", KAEvent::EmailId(0)); 0692 mCommandError = KAEvent::CmdErr(config.readEntry("CmdErr", static_cast<int>(KAEvent::CmdErr::None))); 0693 mDontShowAgain = config.readEntry("DontShowAgain", QString()); 0694 mShowEdit = false; 0695 // Temporarily initialise mResource and mEventId - they will be set by redisplayAlarm() 0696 mResource = Resources::resource(resourceId); 0697 mEventId = EventId(resourceId, eventId); 0698 if (mAlarmType == KAAlarm::Type::Invalid) 0699 return false; 0700 qCDebug(KALARM_LOG) << "MessageDisplayHelper::readProperties:" << eventId; 0701 return true; 0702 } 0703 0704 /****************************************************************************** 0705 * Recreate the event from the calendar file (if possible). 0706 */ 0707 bool MessageDisplayHelper::processPropertyValues() 0708 { 0709 if (!mEventId.eventId().isEmpty()) 0710 { 0711 // Close any other display for this alarm which has already been restored by redisplayAlarms() 0712 if (!Resources::allCreated()) 0713 { 0714 connect(Resources::instance(), &Resources::resourcesCreated, 0715 this, &MessageDisplayHelper::showRestoredAlarm); 0716 return false; 0717 } 0718 redisplayAlarm(); 0719 } 0720 return true; 0721 } 0722 0723 /****************************************************************************** 0724 * Fetch the restored alarm from the calendar and redisplay it in this display. 0725 */ 0726 void MessageDisplayHelper::showRestoredAlarm() 0727 { 0728 qCDebug(KALARM_LOG) << "MessageDisplayHelper::showRestoredAlarm:" << mEventId; 0729 redisplayAlarm(); 0730 mParent->setUpDisplay(); 0731 mParent->showDisplay(); 0732 } 0733 0734 /****************************************************************************** 0735 * Fetch the restored alarm from the calendar and redisplay it in this display. 0736 */ 0737 void MessageDisplayHelper::redisplayAlarm() 0738 { 0739 mResource = Resources::resourceForEvent(mEventId.eventId()); 0740 mEventId.setResourceId(mResource.id()); 0741 qCDebug(KALARM_LOG) << "MessageDisplayHelper::redisplayAlarm:" << mEventId; 0742 // Delete any already existing display for the same event 0743 MessageDisplay* duplicate = findEvent(mEventId, mParent); 0744 if (duplicate) 0745 { 0746 qCDebug(KALARM_LOG) << "MessageDisplayHelper::redisplayAlarm: Deleting duplicate display:" << mEventId; 0747 delete duplicate; 0748 } 0749 0750 const KAEvent event = ResourcesCalendar::event(mEventId); 0751 if (event.isValid()) 0752 { 0753 mEvent = event; 0754 mShowEdit = true; 0755 } 0756 else 0757 { 0758 // It's not in the active calendar, so try the displaying or archive calendars 0759 mParent->retrieveEvent(mEventId, mEvent, mResource, mShowEdit, mNoDefer); 0760 mNoDefer = !mNoDefer; 0761 } 0762 } 0763 0764 /****************************************************************************** 0765 * Called when an alarm is currently being displayed, to store a copy of the 0766 * alarm in the displaying calendar, and to reschedule it for its next repetition. 0767 * If no repetitions remain, cancel it. 0768 */ 0769 bool MessageDisplayHelper::alarmShowing(KAEvent& event) 0770 { 0771 qCDebug(KALARM_LOG) << "MessageDisplayHelper::alarmShowing:" << event.id() << "," << KAAlarm::debugType(mAlarmType); 0772 const KAAlarm alarm = event.alarm(mAlarmType); 0773 if (!alarm.isValid()) 0774 { 0775 qCCritical(KALARM_LOG) << "MessageDisplayHelper::alarmShowing: Alarm type not found:" << event.id() << ":" << mAlarmType; 0776 return false; 0777 } 0778 if (!mAlwaysHide) 0779 { 0780 // Copy the alarm to the displaying calendar in case of a crash, etc. 0781 KAEvent dispEvent; 0782 const ResourceId id = Resources::resourceForEvent(event.id()).id(); 0783 dispEvent.setDisplaying(event, mAlarmType, id, 0784 mDateTime.effectiveKDateTime(), mShowEdit, !mNoDefer); 0785 if (DisplayCalendar::open()) 0786 { 0787 DisplayCalendar::deleteEvent(dispEvent.id()); // in case it already exists 0788 DisplayCalendar::addEvent(dispEvent); 0789 DisplayCalendar::save(); 0790 } 0791 } 0792 theApp()->rescheduleAlarm(event, alarm); 0793 return true; 0794 } 0795 0796 /****************************************************************************** 0797 * Returns the existing message display (if any) which is showing the event with 0798 * the specified ID. 0799 */ 0800 MessageDisplay* MessageDisplayHelper::findEvent(const EventId& eventId, MessageDisplay* exclude) 0801 { 0802 if (!eventId.isEmpty()) 0803 { 0804 for (const MessageDisplayHelper* h : std::as_const(mInstanceList)) 0805 { 0806 if (h->mParent != exclude && h->mEventId == eventId && !h->mErrorWindow) 0807 return h->mParent; 0808 } 0809 } 0810 return nullptr; 0811 } 0812 0813 /****************************************************************************** 0814 * Beep and play the audio file, as appropriate. 0815 */ 0816 void MessageDisplayHelper::playAudio() 0817 { 0818 if (mBeep) 0819 { 0820 // Beep using two methods, in case the sound card/speakers are switched off or not working 0821 QApplication::beep(); // beep through the internal speaker 0822 KNotification::beep(); // beep through the sound card & speakers 0823 } 0824 if (!mAudioFile.isEmpty()) 0825 { 0826 if (!mVolume && mFadeVolume <= 0) 0827 return; // ensure zero volume doesn't play anything 0828 startAudio(); // play the audio file 0829 } 0830 else if (mSpeak) 0831 { 0832 // The message is to be spoken. In case of error messages, 0833 // call it on a timer to allow the display to be shown first. 0834 QTimer::singleShot(0, this, &MessageDisplayHelper::slotSpeak); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 0835 } 0836 } 0837 0838 /****************************************************************************** 0839 * Speak the message. 0840 * Called asynchronously to avoid delaying the display of the message. 0841 */ 0842 void MessageDisplayHelper::slotSpeak() 0843 { 0844 #ifdef HAVE_TEXT_TO_SPEECH_SUPPORT 0845 TextEditTextToSpeech::TextToSpeech* tts = TextEditTextToSpeech::TextToSpeech::self(); 0846 if (!tts->isReady()) 0847 { 0848 KAMessageBox::detailedError(MainWindow::mainMainWindow(), i18nc("@info", "Unable to speak message"), i18nc("@info", "Text-to-speech subsystem is not available")); 0849 clearErrorMessage(ErrMsg_Speak); 0850 return; 0851 } 0852 0853 tts->say(mMessage); 0854 #endif 0855 } 0856 0857 /****************************************************************************** 0858 * Called when another display's audio thread has been destructed. 0859 * Start playing this display's audio file. Because initialising the sound system 0860 * and loading the file may take some time, it is called in a separate thread to 0861 * allow the display to show first. 0862 */ 0863 void MessageDisplayHelper::startAudio() 0864 { 0865 if (mAudioThread) 0866 { 0867 // An audio file is already playing for another message display, so 0868 // wait until it has finished. 0869 connect(mAudioThread.data(), &QObject::destroyed, this, &MessageDisplayHelper::audioTerminating); 0870 } 0871 else 0872 { 0873 qCDebug(KALARM_LOG) << "MessageDisplayHelper::startAudio:" << QThread::currentThread(); 0874 mAudioOwner = this; 0875 0876 // Create a thread for the audio player to run in, and create the audio 0877 // player as a worker to run in the thread created inside QThread. 0878 QThread* audioThread = new QThread(this); 0879 mAudioThread = audioThread; // set the QPointer 0880 AudioPlayer* audioPlayer = new AudioPlayer(mAudioFile, mVolume, mFadeVolume, mFadeSeconds, mAudioRepeatPause); 0881 mAudioPlayer = audioPlayer; // set the QPointer 0882 audioPlayer->moveToThread(audioThread); 0883 connect(audioThread, &QThread::started, audioPlayer, &AudioPlayer::execute); 0884 connect(audioPlayer, &AudioPlayer::destroyed, audioThread, &QThread::quit); 0885 connect(audioThread, &QThread::finished, audioThread, &QThread::deleteLater); 0886 connect(audioThread, &QThread::destroyed, audioThread, []() { qCDebug(KALARM_LOG) << "MessageDisplayHelper: Audio thread deleted"; }); 0887 connect(audioThread, &QThread::destroyed, this, [this]() { if (mAudioOwner == parent()) mAudioOwner = nullptr; }); 0888 0889 // Set up connections not in the thread-worker relationship. 0890 connect(audioPlayer, &AudioPlayer::readyToPlay, this, &MessageDisplayHelper::playReady); 0891 connect(audioThread, &QThread::finished, this, &MessageDisplayHelper::playFinished); 0892 if (mSilenceButton) 0893 connect(mSilenceButton, &QAbstractButton::clicked, this, &MessageDisplayHelper::stopAudioPlay); 0894 0895 // Notify after creating mAudioPlayer, so that isAudioPlaying() will 0896 // return the correct value. 0897 theApp()->notifyAudioPlaying(true); 0898 audioThread->start(); 0899 } 0900 } 0901 0902 /****************************************************************************** 0903 * Return whether audio playback is currently active. 0904 */ 0905 bool MessageDisplayHelper::isAudioPlaying() 0906 { 0907 return mAudioPlayer; 0908 } 0909 0910 /****************************************************************************** 0911 * Stop audio playback. 0912 */ 0913 void MessageDisplayHelper::stopAudio() 0914 { 0915 qCDebug(KALARM_LOG) << "MessageDisplayHelper::stopAudio"; 0916 if (mAudioPlayer) 0917 mAudioPlayer->stop(); 0918 } 0919 0920 /****************************************************************************** 0921 * Ensure that the screen wakes from sleep, in case the window manager doesn't 0922 * do this when the window is displayed. 0923 */ 0924 void MessageDisplayHelper::wakeScreen() 0925 { 0926 qCDebug(KALARM_LOG) << "MessageDisplayHelper::wakeScreen"; 0927 // Note that this freedesktop D-Bus call to wake the screen may not work on 0928 // all systems. It is known to work on X11. 0929 QDBusConnection conn = QDBusConnection::sessionBus(); 0930 if (conn.interface()->isServiceRegistered(QString::fromLatin1(FDO_SCREENSAVER_SERVICE))) 0931 { 0932 OrgFreedesktopScreenSaverInterface ssiface( 0933 QString::fromLatin1(FDO_SCREENSAVER_SERVICE), 0934 QString::fromLatin1(FDO_SCREENSAVER_PATH), 0935 conn); 0936 ssiface.SimulateUserActivity(); 0937 } 0938 } 0939 0940 /****************************************************************************** 0941 * Called when the audio file is ready to start playing. 0942 */ 0943 void MessageDisplayHelper::playReady() 0944 { 0945 if (mSilenceButton) 0946 mSilenceButton->setEnabled(true); 0947 } 0948 0949 /****************************************************************************** 0950 * Called when another display's audio thread is being destructed. 0951 * Wait until the destructor has finished. 0952 */ 0953 void MessageDisplayHelper::audioTerminating() 0954 { 0955 QTimer::singleShot(0, this, &MessageDisplayHelper::startAudio); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 0956 } 0957 0958 /****************************************************************************** 0959 * Called when the audio file thread finishes. 0960 */ 0961 void MessageDisplayHelper::playFinished() 0962 { 0963 if (mSilenceButton) 0964 mSilenceButton->setEnabled(false); 0965 if (mAudioPlayer) // mAudioPlayer can actually be null here! 0966 { 0967 const QString errmsg = mAudioPlayer->error(); 0968 if (!errmsg.isEmpty() && !haveErrorMessage(ErrMsg_AudioFile)) 0969 { 0970 KAMessageBox::error(mParent->displayParent(), errmsg); 0971 clearErrorMessage(ErrMsg_AudioFile); 0972 } 0973 } 0974 delete mAudioThread.data(); 0975 if (mAlwaysHide) 0976 mParent->closeDisplay(); 0977 } 0978 0979 /****************************************************************************** 0980 * Constructor for audio player. 0981 */ 0982 AudioPlayer::AudioPlayer(const QString& audioFile, float volume, float fadeVolume, int fadeSeconds, int repeatPause) 0983 : QObject() 0984 , mFile(audioFile) 0985 , mVolume(volume) 0986 , mFadeVolume(fadeVolume) 0987 , mFadeSeconds(fadeSeconds) 0988 , mRepeatPause(repeatPause) 0989 { 0990 } 0991 0992 /****************************************************************************** 0993 * Destructor for audio player. 0994 * Note that this destructor may be executed in the parent thread. 0995 */ 0996 AudioPlayer::~AudioPlayer() 0997 { 0998 qCDebug(KALARM_LOG) << "MessageDisplayHelper::~AudioPlayer"; 0999 mMutex.lock(); 1000 mPath.disconnect(); 1001 if (mFader) 1002 { 1003 mPath.removeEffect(mFader); 1004 delete mFader; 1005 mFader = nullptr; 1006 } 1007 delete mAudioObject; 1008 mAudioObject = nullptr; 1009 mMutex.unlock(); 1010 // Notify after deleting mAudioPlayer, so that isAudioPlaying() will 1011 // return the correct value. 1012 QTimer::singleShot(0, theApp(), &KAlarmApp::notifyAudioStopped); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 1013 } 1014 1015 /****************************************************************************** 1016 * Kick off playing the audio file. 1017 */ 1018 void AudioPlayer::execute() 1019 { 1020 mMutex.lock(); 1021 if (mAudioObject) 1022 { 1023 mMutex.unlock(); 1024 return; 1025 } 1026 qCDebug(KALARM_LOG) << "MessageDisplayHelper::AudioPlayer::execute:" << QThread::currentThread() << mFile; 1027 const QString audioFile = mFile; 1028 const QUrl url = QUrl::fromUserInput(mFile); 1029 mFile = url.isLocalFile() ? url.toLocalFile() : url.toString(); 1030 Phonon::MediaSource source(url); 1031 if (source.type() == Phonon::MediaSource::Invalid) 1032 { 1033 mError = xi18nc("@info", "Cannot open audio file: <filename>%1</filename>", audioFile); 1034 mMutex.unlock(); 1035 qCCritical(KALARM_LOG) << "MessageDisplayHelper::AudioPlayer::execute: Open failure:" << audioFile; 1036 return; 1037 } 1038 mAudioObject = new Phonon::MediaObject(this); 1039 mAudioObject->setCurrentSource(source); 1040 mAudioObject->setTransitionTime(100); // workaround to prevent clipping of end of files in Xine backend 1041 mAudioOutput = new Phonon::AudioOutput(Phonon::NotificationCategory, this); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 1042 mPath = Phonon::createPath(mAudioObject, mAudioOutput); 1043 if (mVolume >= 0 || mFadeVolume >= 0) 1044 { 1045 if (mVolume >= 0) 1046 mAudioOutput->setVolume(mVolume); 1047 if (mFadeVolume >= 0 && mFadeSeconds > 0) 1048 { 1049 mFader = new Phonon::VolumeFaderEffect(this); 1050 if (!mFader->isValid()) 1051 { 1052 // The current Phonon backend doesn't support fading. 1053 qCWarning(KALARM_LOG) << "MessageDisplayHelper::AudioPlayer: Current Phonon backend does not support fading"; 1054 delete mFader; 1055 mFader = nullptr; 1056 } 1057 else 1058 { 1059 if (mVolume < 0) 1060 mVolume = mAudioOutput->volume(); 1061 mPath.insertEffect(mFader); 1062 } 1063 } 1064 } 1065 connect(mAudioObject, &Phonon::MediaObject::stateChanged, this, &AudioPlayer::playStateChanged, Qt::DirectConnection); 1066 connect(mAudioObject, &Phonon::MediaObject::finished, this, &AudioPlayer::checkAudioPlay, Qt::DirectConnection); 1067 mPlayedOnce = false; 1068 mPausing = false; 1069 mMutex.unlock(); 1070 Q_EMIT readyToPlay(); 1071 checkAudioPlay(); 1072 } 1073 1074 /****************************************************************************** 1075 * Called when the audio file has loaded and is ready to play, or when play 1076 * has completed. 1077 * If it is ready to play, start playing it (for the first time or repeated). 1078 * If play has not yet completed, wait a bit longer. 1079 */ 1080 void AudioPlayer::checkAudioPlay() 1081 { 1082 mMutex.lock(); 1083 if (!mAudioObject) 1084 { 1085 mMutex.unlock(); 1086 return; 1087 } 1088 if (mPausing) 1089 mPausing = false; 1090 else 1091 { 1092 // The file has loaded and is ready to play, or play has completed 1093 if (mPlayedOnce) 1094 { 1095 if (mRepeatPause < 0) 1096 { 1097 // Play has completed 1098 mMutex.unlock(); 1099 stop(); 1100 return; 1101 } 1102 if (mRepeatPause > 0) 1103 { 1104 // Pause before playing the file again 1105 mPausing = true; 1106 QTimer::singleShot(mRepeatPause * 1000, this, &AudioPlayer::checkAudioPlay); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 1107 mMutex.unlock(); 1108 return; 1109 } 1110 } 1111 mPlayedOnce = true; 1112 } 1113 1114 // Start playing the file, either for the first time or again 1115 qCDebug(KALARM_LOG) << "MessageDisplayHelper::AudioPlayer::checkAudioPlay: start"; 1116 if (mVolume >= 0) 1117 mAudioOutput->setVolume(mVolume); 1118 if (mFader) 1119 { 1120 mFader->setVolume(mFadeVolume); 1121 mFader->fadeTo(mVolume, mFadeSeconds * 1000); 1122 } 1123 mAudioObject->play(); 1124 mMutex.unlock(); 1125 } 1126 1127 /****************************************************************************** 1128 * Called when the playback object changes state. 1129 * If an error has occurred, quit and return the error to the caller. 1130 */ 1131 void AudioPlayer::playStateChanged(Phonon::State newState) 1132 { 1133 qCDebug(KALARM_LOG) << "MessageDisplayHelper::AudioPlayer::playStateChanged:" << newState; 1134 switch (newState) 1135 { 1136 case Phonon::StoppedState: 1137 mMutex.lock(); 1138 if (mStopping) 1139 { 1140 mPath.disconnect(); 1141 if (mFader) 1142 { 1143 // Delete fader from the audio thread, in case it is still active. 1144 // because its timers can't be stopped from another thread. 1145 mPath.removeEffect(mFader); 1146 delete mFader; 1147 mFader = nullptr; 1148 } 1149 deleteLater(); 1150 } 1151 mMutex.unlock(); 1152 break; 1153 1154 case Phonon::ErrorState: 1155 { 1156 QMutexLocker locker(&mMutex); 1157 const QString err = mAudioObject->errorString(); 1158 if (!err.isEmpty()) 1159 { 1160 qCCritical(KALARM_LOG) << "MessageDisplayHelper::AudioPlayer::playStateChanged: Play failure:" << mFile << ":" << err; 1161 mError = xi18nc("@info", "<para>Error playing audio file: <filename>%1</filename></para><para>%2</para>", mFile, err); 1162 exit(1); 1163 } 1164 break; 1165 } 1166 1167 default: 1168 break; 1169 } 1170 } 1171 1172 /****************************************************************************** 1173 * Called when play completes, the Silence button is clicked, or the display is 1174 * closed, to terminate audio access. 1175 */ 1176 void AudioPlayer::stop() 1177 { 1178 qCDebug(KALARM_LOG) << "MessageDisplayHelper::AudioPlayer::stop"; 1179 mMutex.lock(); 1180 mStopping = true; 1181 if (mAudioObject) 1182 { 1183 if (mAudioObject->state() != Phonon::StoppedState) 1184 { 1185 mAudioObject->stop(); 1186 mMutex.unlock(); 1187 return; 1188 } 1189 } 1190 mMutex.unlock(); 1191 deleteLater(); 1192 } 1193 1194 QString AudioPlayer::error() const 1195 { 1196 QMutexLocker locker(&mMutex); 1197 return mError; 1198 } 1199 1200 /****************************************************************************** 1201 * Display the alarm. 1202 * Reply = true if the alarm should be shown, false if not. 1203 */ 1204 bool MessageDisplayHelper::activateAutoClose() 1205 { 1206 if (mCloseTime.isValid()) 1207 { 1208 // Set a timer to auto-close the display. 1209 int delay = QDateTime::currentDateTimeUtc().secsTo(mCloseTime); 1210 if (delay < 0) 1211 delay = 0; 1212 QTimer::singleShot(delay * 1000, this, &MessageDisplayHelper::autoCloseNow); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 1213 if (!delay) 1214 return false; // don't show the alarm if auto-closing is already due 1215 } 1216 return true; 1217 } 1218 1219 /****************************************************************************** 1220 * Called when the display has been shown properly (in its correct position), 1221 * to play sounds and reschedule the event. 1222 */ 1223 void MessageDisplayHelper::displayComplete(bool audio) 1224 { 1225 delete mTempFile; 1226 mTempFile = nullptr; 1227 if (audio) 1228 playAudio(); 1229 if (mRescheduleEvent) 1230 alarmShowing(mEvent); 1231 } 1232 1233 /****************************************************************************** 1234 * To be called when a close event is received. 1235 * Only quits the application if there is no system tray icon displayed. 1236 */ 1237 bool MessageDisplayHelper::closeEvent() 1238 { 1239 // Don't prompt or delete the alarm from the display calendar if the session is closing 1240 if (!mErrorWindow && !qApp->isSavingSession()) 1241 { 1242 if (mConfirmAck && !mParent->confirmAcknowledgement()) 1243 return false; 1244 1245 if (!mEventId.isEmpty()) 1246 { 1247 // Delete from the display calendar 1248 KAlarm::deleteDisplayEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING)); 1249 } 1250 } 1251 return true; 1252 } 1253 1254 /****************************************************************************** 1255 * Create an alarm edit dialog. 1256 * 1257 * NOTE: The alarm edit dialog is made a child of the main window, not of 1258 * displayParent(), so that if displayParent() closes before the dialog 1259 * (e.g. on auto-close), KAlarm doesn't crash. The dialog is set non-modal 1260 * so that the main window is unaffected, but modal mode is simulated so 1261 * that displayParent() is inactive while the dialog is open. 1262 */ 1263 EditAlarmDlg* MessageDisplayHelper::createEdit() 1264 { 1265 qCDebug(KALARM_LOG) << "MessageDisplayHelper::createEdit"; 1266 mEditDlg = EditAlarmDlg::create(false, mOriginalEvent, false, MainWindow::mainMainWindow(), EditAlarmDlg::RES_IGNORE); 1267 if (mEditDlg) 1268 { 1269 mEditDlg->setAttribute(Qt::WA_NativeWindow, true); 1270 connect(mEditDlg, &QDialog::accepted, this, &MessageDisplayHelper::editCloseOk); 1271 connect(mEditDlg, &QDialog::rejected, this, &MessageDisplayHelper::editCloseCancel); 1272 connect(mEditDlg, &QObject::destroyed, this, &MessageDisplayHelper::editCloseCancel); 1273 } 1274 return mEditDlg; 1275 } 1276 1277 /****************************************************************************** 1278 * Execute the alarm edit dialog. 1279 */ 1280 void MessageDisplayHelper::executeEdit() 1281 { 1282 MainWindow::mainMainWindow()->editAlarm(mEditDlg, mOriginalEvent); 1283 } 1284 1285 /****************************************************************************** 1286 * Called when OK is clicked in the alarm edit dialog invoked by the Edit button. 1287 * Closes the display. 1288 */ 1289 void MessageDisplayHelper::editCloseOk() 1290 { 1291 mEditDlg = nullptr; 1292 mNoCloseConfirm = true; // allow window to close without confirmation prompt 1293 mParent->closeDisplay(); 1294 } 1295 1296 /****************************************************************************** 1297 * Called when Cancel is clicked in the alarm edit dialog invoked by the Edit 1298 * button, or when the dialog is deleted. 1299 */ 1300 void MessageDisplayHelper::editCloseCancel() 1301 { 1302 mEditDlg = nullptr; 1303 mParent->editDlgCancelled(); 1304 } 1305 1306 /****************************************************************************** 1307 * Set up to disable the defer button when the deferral limit is reached. 1308 */ 1309 void MessageDisplayHelper::setDeferralLimit(const KAEvent& event) 1310 { 1311 mDeferLimit = event.deferralLimit().effectiveKDateTime().toUtc().qDateTime(); 1312 MidnightTimer::connect(this, SLOT(checkDeferralLimit())); // check every day 1313 mDisableDeferral = false; 1314 checkDeferralLimit(); 1315 } 1316 1317 /****************************************************************************** 1318 * Check whether the deferral limit has been reached. 1319 * If so, disable the Defer button. 1320 * N.B. Ideally, just a single QTimer::singleShot() call would be made to disable 1321 * the defer button at the correct time. But for a 32-bit integer, the 1322 * milliseconds parameter overflows in about 25 days, so instead a daily 1323 * check is done until the day when the deferral limit is reached, followed 1324 * by a non-overflowing QTimer::singleShot() call. 1325 */ 1326 void MessageDisplayHelper::checkDeferralLimit() 1327 { 1328 if (!mParent->isDeferButtonEnabled() || !mDeferLimit.isValid()) 1329 return; 1330 int n = KADateTime::currentLocalDate().daysTo(KADateTime(mDeferLimit, KADateTime::LocalZone).date()); 1331 if (n > 0) 1332 return; 1333 MidnightTimer::disconnect(this, SLOT(checkDeferralLimit())); 1334 if (n == 0) 1335 { 1336 // The deferral limit will be reached today 1337 n = QDateTime::currentDateTimeUtc().secsTo(mDeferLimit); 1338 if (n > 0) 1339 { 1340 QTimer::singleShot(n * 1000, this, &MessageDisplayHelper::checkDeferralLimit); //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) 1341 return; 1342 } 1343 } 1344 mParent->enableDeferButton(false); 1345 mDisableDeferral = true; 1346 } 1347 1348 /****************************************************************************** 1349 * Check whether the specified error message is already displayed for this 1350 * alarm, and note that it will now be displayed. 1351 * Reply = true if message is already displayed. 1352 */ 1353 bool MessageDisplayHelper::haveErrorMessage(unsigned msg) const 1354 { 1355 if (!mErrorMessages.contains(mEventId)) 1356 mErrorMessages.insert(mEventId, 0); 1357 unsigned& message = mErrorMessages[mEventId]; 1358 const bool result = (message & msg); 1359 message |= msg; 1360 return result; 1361 } 1362 1363 void MessageDisplayHelper::clearErrorMessage(unsigned msg) const 1364 { 1365 if (mErrorMessages.contains(mEventId)) 1366 { 1367 unsigned& message = mErrorMessages[mEventId]; 1368 if (message == msg) 1369 mErrorMessages.remove(mEventId); 1370 else 1371 message &= ~msg; 1372 } 1373 } 1374 1375 #include "moc_messagedisplayhelper_p.cpp" 1376 #include "moc_messagedisplayhelper.cpp" 1377 1378 // vim: et sw=4: