Warning, file /pim/kalarm/src/traywindow.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002  *  traywindow.cpp  -  the KDE system tray applet
0003  *  Program:  kalarm
0004  *  SPDX-FileCopyrightText: 2002-2023 David Jarvie <djarvie@kde.org>
0005  *
0006  *  SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include "traywindow.h"
0010 
0011 #include "functions.h"
0012 #include "kalarmapp.h"
0013 #include "mainwindow.h"
0014 #include "messagedisplay.h"
0015 #include "newalarmaction.h"
0016 #include "prefdlg.h"
0017 #include "preferences.h"
0018 #include "resourcescalendar.h"
0019 #include "resources/datamodel.h"
0020 #include "resources/eventmodel.h"
0021 #include "lib/synchtimer.h"
0022 #include "kalarmcalendar/alarmtext.h"
0023 #include "kalarm_debug.h"
0024 
0025 #include <KLocalizedString>
0026 #include <KStandardAction>
0027 #include <KAboutData>
0028 
0029 #include <QList>
0030 #include <QTimer>
0031 #include <QLocale>
0032 #include <QMenu>
0033 
0034 #include <stdlib.h>
0035 #include <limits.h>
0036 
0037 using namespace KAlarmCal;
0038 
0039 struct TipItem
0040 {
0041     QDateTime  dateTime;
0042     QString    text;
0043 };
0044 
0045 
0046 /*=============================================================================
0047 = Class: TrayWindow
0048 = The KDE system tray window.
0049 =============================================================================*/
0050 
0051 TrayWindow::TrayWindow(MainWindow* parent)
0052     : KStatusNotifierItem(parent)
0053     , mAssocMainWindow(parent)
0054     , mStatusUpdateTimer(new QTimer(this))
0055 {
0056     qCDebug(KALARM_LOG) << "TrayWindow:";
0057     setToolTipIconByName(QStringLiteral("kalarm"));
0058     setToolTipTitle(KAboutData::applicationData().displayName());
0059     setIconByName(QStringLiteral("kalarm"));
0060     setStatus(KStatusNotifierItem::Active);
0061     // Set up the context menu
0062     mActionEnabled = KAlarm::createAlarmEnableAction(this);
0063     addAction(QStringLiteral("tAlarmsEnable"), mActionEnabled);
0064     contextMenu()->addAction(mActionEnabled);
0065     connect(theApp(), &KAlarmApp::alarmEnabledToggled, this, &TrayWindow::setEnabledStatus);
0066     contextMenu()->addSeparator();
0067 
0068     mActionNew = new NewAlarmAction(false, i18nc("@action", "New Alarm"), this);
0069     addAction(QStringLiteral("tNew"), mActionNew);
0070     contextMenu()->addAction(mActionNew);
0071     connect(mActionNew, &NewAlarmAction::selected, this, &TrayWindow::slotNewAlarm);
0072     connect(mActionNew, &NewAlarmAction::selectedTemplate, this, &TrayWindow::slotNewFromTemplate);
0073     contextMenu()->addSeparator();
0074 
0075     QAction* a = KAlarm::createStopPlayAction(this);
0076     addAction(QStringLiteral("tStopPlay"), a);
0077     contextMenu()->addAction(a);
0078     QObject::connect(theApp(), &KAlarmApp::audioPlaying, a, &QAction::setVisible);
0079     QObject::connect(theApp(), &KAlarmApp::audioPlaying, this, &TrayWindow::updateStatus);
0080 
0081     a = KAlarm::createSpreadWindowsAction(this);
0082     addAction(QStringLiteral("tSpread"), a);
0083     contextMenu()->addAction(a);
0084     contextMenu()->addSeparator();
0085     contextMenu()->addAction(KStandardAction::preferences(this, &TrayWindow::slotPreferences, this));
0086 
0087     // Disable standard quit behaviour. We have to intercept the quit event
0088     // (which triggers KStatusNotifierItem to quit unconditionally).
0089     QAction* act = action(QStringLiteral("quit"));
0090     if (act)
0091     {
0092         disconnect(act, &QAction::triggered, this, nullptr);
0093         connect(act, &QAction::triggered, this, &TrayWindow::slotQuit);
0094     }
0095 
0096     // Set icon to correspond with the alarms enabled menu status
0097     setEnabledStatus(theApp()->alarmsEnabled());
0098 
0099     connect(ResourcesCalendar::instance(), &ResourcesCalendar::haveDisabledAlarmsChanged, this, &TrayWindow::slotHaveDisabledAlarms);
0100     connect(this, &TrayWindow::activateRequested, this, &TrayWindow::slotActivateRequested);
0101     connect(this, &TrayWindow::secondaryActivateRequested, this, &TrayWindow::slotSecondaryActivateRequested);
0102     slotHaveDisabledAlarms(ResourcesCalendar::haveDisabledAlarms());
0103 
0104     // Hack: KSNI does not let us know when it is about to show the tooltip,
0105     // so we need to update it whenever something change in it.
0106 
0107     // This timer ensures that updateToolTip() is not called several times in a row
0108     mToolTipUpdateTimer = new QTimer(this);
0109     mToolTipUpdateTimer->setInterval(0);
0110     mToolTipUpdateTimer->setSingleShot(true);
0111     connect(mToolTipUpdateTimer, &QTimer::timeout, this, &TrayWindow::updateToolTip);
0112 
0113     // Update every minute to show accurate deadlines
0114     MinuteTimer::connect(mToolTipUpdateTimer, SLOT(start()));
0115 
0116     // Update when alarms are modified
0117     AlarmListModel* all = DataModel::allAlarmListModel();
0118     connect(all, &QAbstractItemModel::dataChanged,  mToolTipUpdateTimer, qOverload<>(&QTimer::start));
0119     connect(all, &QAbstractItemModel::rowsInserted, mToolTipUpdateTimer, qOverload<>(&QTimer::start));
0120     connect(all, &QAbstractItemModel::rowsMoved,    mToolTipUpdateTimer, qOverload<>(&QTimer::start));
0121     connect(all, &QAbstractItemModel::rowsRemoved,  mToolTipUpdateTimer, qOverload<>(&QTimer::start));
0122     connect(all, &QAbstractItemModel::modelReset,   mToolTipUpdateTimer, qOverload<>(&QTimer::start));
0123 
0124     // Set auto-hide status when next alarm or preferences change
0125     mStatusUpdateTimer->setSingleShot(true);
0126     connect(mStatusUpdateTimer, &QTimer::timeout, this, &TrayWindow::updateStatus);
0127     connect(ResourcesCalendar::instance(), &ResourcesCalendar::earliestAlarmChanged, this, &TrayWindow::updateStatus);
0128     Preferences::connect(&Preferences::autoHideSystemTrayChanged, this, &TrayWindow::updateStatus);
0129     updateStatus();
0130 
0131     // Update when tooltip preferences are modified
0132     Preferences::connect(&Preferences::tooltipPreferencesChanged, mToolTipUpdateTimer, qOverload<>(&QTimer::start));
0133 }
0134 
0135 TrayWindow::~TrayWindow()
0136 {
0137     qCDebug(KALARM_LOG) << "~TrayWindow";
0138     theApp()->removeWindow(this);
0139     Q_EMIT deleted();
0140 }
0141 
0142 /******************************************************************************
0143 * Called when the "New Alarm" menu item is selected to edit a new alarm.
0144 */
0145 void TrayWindow::slotNewAlarm(EditAlarmDlg::Type type)
0146 {
0147     KAlarm::editNewAlarm(type);
0148 }
0149 
0150 /******************************************************************************
0151 * Called when the "New Alarm" menu item is selected to edit a new alarm from a
0152 * template.
0153 */
0154 void TrayWindow::slotNewFromTemplate(const KAEvent& event)
0155 {
0156     KAlarm::editNewAlarm(event);
0157 }
0158 
0159 /******************************************************************************
0160 * Called when the "Configure KAlarm" menu item is selected.
0161 */
0162 void TrayWindow::slotPreferences()
0163 {
0164     KAlarmPrefDlg::display();
0165 }
0166 
0167 /******************************************************************************
0168 * Called when the Quit context menu item is selected.
0169 * Note that KAlarmApp::doQuit()  must be called by the event loop, not directly
0170 * from the menu item, since otherwise the tray icon will be deleted while still
0171 * processing the menu, resulting in a crash.
0172 * Ideally, the connect() call setting up this slot in the constructor would use
0173 * Qt::QueuedConnection, but the slot is never called in that case.
0174 */
0175 void TrayWindow::slotQuit()
0176 {
0177     // Note: QTimer::singleShot(0, ...) never calls the slot.
0178     QTimer::singleShot(1, this, &TrayWindow::slotQuitAfter);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
0179 }
0180 void TrayWindow::slotQuitAfter()
0181 {
0182     theApp()->doQuit(static_cast<QWidget*>(parent()));
0183 }
0184 
0185 /******************************************************************************
0186 * Called when the Alarms Enabled action status has changed.
0187 * Updates the alarms enabled menu item check state, and the icon pixmap.
0188 */
0189 void TrayWindow::setEnabledStatus(bool status)
0190 {
0191     qCDebug(KALARM_LOG) << "TrayWindow::setEnabledStatus:" << status;
0192     updateIcon();
0193     updateStatus();
0194     updateToolTip();
0195 }
0196 
0197 /******************************************************************************
0198 * Called when individual alarms are enabled or disabled.
0199 * Set the enabled icon to show or hide a disabled indication.
0200 */
0201 void TrayWindow::slotHaveDisabledAlarms(bool haveDisabled)
0202 {
0203     qCDebug(KALARM_LOG) << "TrayWindow::slotHaveDisabledAlarms:" << haveDisabled;
0204     mHaveDisabledAlarms = haveDisabled;
0205     updateIcon();
0206     updateToolTip();
0207 }
0208 
0209 /******************************************************************************
0210 * Show the associated main window.
0211 */
0212 void TrayWindow::showAssocMainWindow()
0213 {
0214     if (mAssocMainWindow)
0215     {
0216         mAssocMainWindow->show();
0217         mAssocMainWindow->raise();
0218         mAssocMainWindow->activateWindow();
0219     }
0220 }
0221 
0222 /******************************************************************************
0223 * A left click displays the KAlarm main window.
0224 */
0225 void TrayWindow::slotActivateRequested()
0226 {
0227     // Left click: display/hide the first main window
0228     if (mAssocMainWindow  &&  mAssocMainWindow->isVisible())
0229     {
0230         mAssocMainWindow->raise();
0231         mAssocMainWindow->activateWindow();
0232     }
0233 }
0234 
0235 /******************************************************************************
0236 * A middle button click displays the New Alarm window.
0237 */
0238 void TrayWindow::slotSecondaryActivateRequested()
0239 {
0240     if (mActionNew->isEnabled())
0241         mActionNew->trigger();    // display a New Alarm dialog
0242 }
0243 
0244 /******************************************************************************
0245 * Adjust icon auto-hide status according to when the next alarm is due.
0246 * The icon is always shown if audio is playing, to give access to the 'stop'
0247 * menu option.
0248 */
0249 void TrayWindow::updateStatus()
0250 {
0251     mStatusUpdateTimer->stop();
0252     int period =  Preferences::autoHideSystemTray();
0253     // If the icon is always to be shown (AutoHideSystemTray = 0),
0254     // or audio is playing, show the icon.
0255     bool active = !period || MessageDisplay::isAudioPlaying();
0256     if (!active)
0257     {
0258         // Show the icon only if the next active alarm complies
0259         active = theApp()->alarmsEnabled();
0260         if (active)
0261         {
0262             KADateTime dt;
0263             const KAEvent& event = ResourcesCalendar::earliestAlarm(dt);
0264             active = event.isValid();
0265             if (active  &&  period > 0)
0266             {
0267                 qint64 delay = KADateTime::currentLocalDateTime().secsTo(dt);
0268                 delay -= static_cast<qint64>(period) * 60;   // delay until icon to be shown
0269                 active = (delay <= 0);
0270                 if (!active)
0271                 {
0272                     // First alarm trigger is too far in future, so tray icon is to
0273                     // be auto-hidden. Set timer for when it should be shown again.
0274                     delay *= 1000;   // convert to msec
0275                     int delay_int = static_cast<int>(delay);
0276                     if (delay_int != delay)
0277                         delay_int = INT_MAX;
0278                     mStatusUpdateTimer->setInterval(delay_int);
0279                     mStatusUpdateTimer->start();
0280                 }
0281             }
0282         }
0283     }
0284     setStatus(active ? Active : Passive);
0285 }
0286 
0287 /******************************************************************************
0288 * Adjust tooltip according to the app state.
0289 * The tooltip text shows alarms due in the next 24 hours. The limit of 24
0290 * hours is because only times, not dates, are displayed.
0291 */
0292 void TrayWindow::updateToolTip()
0293 {
0294     bool enabled = theApp()->alarmsEnabled();
0295     QString subTitle;
0296     if (enabled && Preferences::tooltipAlarmCount())
0297         subTitle = tooltipAlarmText();
0298 
0299     if (!enabled)
0300         subTitle = i18n("Disabled");
0301     else if (mHaveDisabledAlarms)
0302     {
0303         if (!subTitle.isEmpty())
0304             subTitle += QLatin1String("<br/>");
0305         subTitle += i18nc("@info:tooltip Brief: some alarms are disabled", "(Some alarms disabled)");
0306     }
0307     setToolTipSubTitle(subTitle);
0308 }
0309 
0310 /******************************************************************************
0311 * Adjust icon according to the app state.
0312 */
0313 void TrayWindow::updateIcon()
0314 {
0315     setIconByName(!theApp()->alarmsEnabled() ? QStringLiteral("kalarm-disabled")
0316                   : mHaveDisabledAlarms ? QStringLiteral("kalarm-partdisabled")
0317                   : QStringLiteral("kalarm"));
0318 }
0319 
0320 /******************************************************************************
0321 * Return the tooltip text showing alarms due in the next 24 hours.
0322 * The limit of 24 hours is because only times, not dates, are displayed.
0323 */
0324 QString TrayWindow::tooltipAlarmText() const
0325 {
0326     const QString& prefix = Preferences::tooltipTimeToPrefix();
0327     int maxCount = Preferences::tooltipAlarmCount();
0328     const KADateTime now = KADateTime::currentLocalDateTime();
0329     const KADateTime tomorrow = now.addDays(1);
0330 
0331     // Get today's and tomorrow's alarms, sorted in time order
0332     int i, iend;
0333     QList<TipItem> items;
0334     QList<KAEvent> events = KAlarm::getSortedActiveEvents(const_cast<TrayWindow*>(this), &mAlarmsModel);
0335     for (i = 0, iend = events.count();  i < iend;  ++i)
0336     {
0337         KAEvent* event = &events[i];
0338         if (event->actionSubType() == KAEvent::SubAction::Message)
0339         {
0340             TipItem item;
0341             QDateTime dateTime = event->nextTrigger(KAEvent::Trigger::Display).effectiveKDateTime().toLocalZone().qDateTime();
0342             if (dateTime > tomorrow.qDateTime())
0343                 break;   // ignore alarms after tomorrow at the current clock time
0344             item.dateTime = dateTime;
0345 
0346             // The alarm is due today, or early tomorrow
0347             if (Preferences::showTooltipAlarmTime())
0348             {
0349                 item.text += QLocale().toString(item.dateTime.time(), QLocale::ShortFormat);
0350                 item.text += QLatin1Char(' ');
0351             }
0352             if (Preferences::showTooltipTimeToAlarm())
0353             {
0354                 int mins = (now.qDateTime().secsTo(item.dateTime) + 59) / 60;
0355                 if (mins < 0)
0356                     mins = 0;
0357                 char minutes[3] = "00";
0358                 minutes[0] = static_cast<char>((mins%60) / 10 + '0');
0359                 minutes[1] = static_cast<char>((mins%60) % 10 + '0');
0360                 if (Preferences::showTooltipAlarmTime())
0361                     item.text += i18nc("@info prefix + hours:minutes", "(%1%2:%3)", prefix, mins/60, QLatin1String(minutes));
0362                 else
0363                     item.text += i18nc("@info prefix + hours:minutes", "%1%2:%3", prefix, mins/60, QLatin1String(minutes));
0364                 item.text += QLatin1Char(' ');
0365             }
0366             item.text += AlarmText::summary(*event);
0367 
0368             // Insert the item into the list in time-sorted order
0369             int it = 0;
0370             for (int itend = items.count();  it < itend;  ++it)
0371             {
0372                 if (item.dateTime <= items.at(it).dateTime)
0373                     break;
0374             }
0375             items.insert(it, item);
0376         }
0377     }
0378     qCDebug(KALARM_LOG) << "TrayWindow::tooltipAlarmText";
0379     QString text;
0380     int count = 0;
0381     for (i = 0, iend = items.count();  i < iend;  ++i)
0382     {
0383         qCDebug(KALARM_LOG) << "TrayWindow::tooltipAlarmText: --" << (count+1) << ")" << items.at(i).text;
0384         if (i > 0)
0385             text += QLatin1String("<br />");
0386         text += items.at(i).text;
0387         if (++count == maxCount)
0388             break;
0389     }
0390     return text;
0391 }
0392 
0393 /******************************************************************************
0394 * Called when the associated main window is closed.
0395 */
0396 void TrayWindow::removeWindow(MainWindow* win)
0397 {
0398     if (win == mAssocMainWindow)
0399         mAssocMainWindow = nullptr;
0400 }
0401 
0402 #include "moc_traywindow.cpp"
0403 
0404 // vim: et sw=4: