1 /*
2  *  traywindow.cpp  -  the KDE system tray applet
3  *  Program:  kalarm
4  *  SPDX-FileCopyrightText: 2002-2021 David Jarvie <djarvie@kde.org>
5  *
6  *  SPDX-License-Identifier: GPL-2.0-or-later
7  */
8 
9 #include "traywindow.h"
10 
11 #include "functions.h"
12 #include "kalarmapp.h"
13 #include "mainwindow.h"
14 #include "messagedisplay.h"
15 #include "newalarmaction.h"
16 #include "prefdlg.h"
17 #include "preferences.h"
18 #include "resourcescalendar.h"
19 #include "resources/datamodel.h"
20 #include "resources/eventmodel.h"
21 #include "lib/synchtimer.h"
22 #include "kalarm_debug.h"
23 
24 #include <KAlarmCal/AlarmText>
25 
26 #include <KLocalizedString>
27 #include <KStandardAction>
28 #include <KAboutData>
29 
30 #include <QList>
31 #include <QTimer>
32 #include <QLocale>
33 #include <QMenu>
34 
35 #include <stdlib.h>
36 #include <limits.h>
37 
38 using namespace KAlarmCal;
39 
40 struct TipItem
41 {
42     QDateTime  dateTime;
43     QString    text;
44 };
45 
46 
47 /*=============================================================================
48 = Class: TrayWindow
49 = The KDE system tray window.
50 =============================================================================*/
51 
TrayWindow(MainWindow * parent)52 TrayWindow::TrayWindow(MainWindow* parent)
53     : KStatusNotifierItem(parent)
54     , mAssocMainWindow(parent)
55     , mStatusUpdateTimer(new QTimer(this))
56 {
57     qCDebug(KALARM_LOG) << "TrayWindow:";
58     setToolTipIconByName(QStringLiteral("kalarm"));
59     setToolTipTitle(KAboutData::applicationData().displayName());
60     setIconByName(QStringLiteral("kalarm"));
61     setStatus(KStatusNotifierItem::Active);
62     // Set up the context menu
63     mActionEnabled = KAlarm::createAlarmEnableAction(this);
64     addAction(QStringLiteral("tAlarmsEnable"), mActionEnabled);
65     contextMenu()->addAction(mActionEnabled);
66     connect(theApp(), &KAlarmApp::alarmEnabledToggled, this, &TrayWindow::setEnabledStatus);
67     contextMenu()->addSeparator();
68 
69     mActionNew = new NewAlarmAction(false, i18nc("@action", "&New Alarm"), this);
70     addAction(QStringLiteral("tNew"), mActionNew);
71     contextMenu()->addAction(mActionNew);
72     connect(mActionNew, &NewAlarmAction::selected, this, &TrayWindow::slotNewAlarm);
73     connect(mActionNew, &NewAlarmAction::selectedTemplate, this, &TrayWindow::slotNewFromTemplate);
74     contextMenu()->addSeparator();
75 
76     QAction* a = KAlarm::createStopPlayAction(this);
77     addAction(QStringLiteral("tStopPlay"), a);
78     contextMenu()->addAction(a);
79     QObject::connect(theApp(), &KAlarmApp::audioPlaying, a, &QAction::setVisible);
80     QObject::connect(theApp(), &KAlarmApp::audioPlaying, this, &TrayWindow::updateStatus);
81 
82     a = KAlarm::createSpreadWindowsAction(this);
83     addAction(QStringLiteral("tSpread"), a);
84     contextMenu()->addAction(a);
85     contextMenu()->addSeparator();
86     contextMenu()->addAction(KStandardAction::preferences(this, &TrayWindow::slotPreferences, this));
87 
88     // Disable standard quit behaviour. We have to intercept the quit event
89     // (which triggers KStatusNotifierItem to quit unconditionally).
90     QAction* act = action(QStringLiteral("quit"));
91     if (act)
92     {
93         disconnect(act, &QAction::triggered, this, nullptr);
94         connect(act, &QAction::triggered, this, &TrayWindow::slotQuit);
95     }
96 
97     // Set icon to correspond with the alarms enabled menu status
98     setEnabledStatus(theApp()->alarmsEnabled());
99 
100     connect(ResourcesCalendar::instance(), &ResourcesCalendar::haveDisabledAlarmsChanged, this, &TrayWindow::slotHaveDisabledAlarms);
101     connect(this, &TrayWindow::activateRequested, this, &TrayWindow::slotActivateRequested);
102     connect(this, &TrayWindow::secondaryActivateRequested, this, &TrayWindow::slotSecondaryActivateRequested);
103     slotHaveDisabledAlarms(ResourcesCalendar::haveDisabledAlarms());
104 
105     // Hack: KSNI does not let us know when it is about to show the tooltip,
106     // so we need to update it whenever something change in it.
107 
108     // This timer ensures that updateToolTip() is not called several times in a row
109     mToolTipUpdateTimer = new QTimer(this);
110     mToolTipUpdateTimer->setInterval(0);
111     mToolTipUpdateTimer->setSingleShot(true);
112     connect(mToolTipUpdateTimer, &QTimer::timeout, this, &TrayWindow::updateToolTip);
113 
114     // Update every minute to show accurate deadlines
115     MinuteTimer::connect(mToolTipUpdateTimer, SLOT(start()));
116 
117     // Update when alarms are modified
118     AlarmListModel* all = DataModel::allAlarmListModel();
119     connect(all, &QAbstractItemModel::dataChanged,  mToolTipUpdateTimer, qOverload<>(&QTimer::start));
120     connect(all, &QAbstractItemModel::rowsInserted, mToolTipUpdateTimer, qOverload<>(&QTimer::start));
121     connect(all, &QAbstractItemModel::rowsMoved,    mToolTipUpdateTimer, qOverload<>(&QTimer::start));
122     connect(all, &QAbstractItemModel::rowsRemoved,  mToolTipUpdateTimer, qOverload<>(&QTimer::start));
123     connect(all, &QAbstractItemModel::modelReset,   mToolTipUpdateTimer, qOverload<>(&QTimer::start));
124 
125     // Set auto-hide status when next alarm or preferences change
126     mStatusUpdateTimer->setSingleShot(true);
127     connect(mStatusUpdateTimer, &QTimer::timeout, this, &TrayWindow::updateStatus);
128     connect(ResourcesCalendar::instance(), &ResourcesCalendar::earliestAlarmChanged, this, &TrayWindow::updateStatus);
129     Preferences::connect(&Preferences::autoHideSystemTrayChanged, this, &TrayWindow::updateStatus);
130     updateStatus();
131 
132     // Update when tooltip preferences are modified
133     Preferences::connect(&Preferences::tooltipPreferencesChanged, mToolTipUpdateTimer, qOverload<>(&QTimer::start));
134 }
135 
~TrayWindow()136 TrayWindow::~TrayWindow()
137 {
138     qCDebug(KALARM_LOG) << "~TrayWindow";
139     theApp()->removeWindow(this);
140     Q_EMIT deleted();
141 }
142 
143 /******************************************************************************
144 * Called when the "New Alarm" menu item is selected to edit a new alarm.
145 */
slotNewAlarm(EditAlarmDlg::Type type)146 void TrayWindow::slotNewAlarm(EditAlarmDlg::Type type)
147 {
148     KAlarm::editNewAlarm(type);
149 }
150 
151 /******************************************************************************
152 * Called when the "New Alarm" menu item is selected to edit a new alarm from a
153 * template.
154 */
slotNewFromTemplate(const KAEvent & event)155 void TrayWindow::slotNewFromTemplate(const KAEvent& event)
156 {
157     KAlarm::editNewAlarm(event);
158 }
159 
160 /******************************************************************************
161 * Called when the "Configure KAlarm" menu item is selected.
162 */
slotPreferences()163 void TrayWindow::slotPreferences()
164 {
165     KAlarmPrefDlg::display();
166 }
167 
168 /******************************************************************************
169 * Called when the Quit context menu item is selected.
170 * Note that KAlarmApp::doQuit()  must be called by the event loop, not directly
171 * from the menu item, since otherwise the tray icon will be deleted while still
172 * processing the menu, resulting in a crash.
173 * Ideally, the connect() call setting up this slot in the constructor would use
174 * Qt::QueuedConnection, but the slot is never called in that case.
175 */
slotQuit()176 void TrayWindow::slotQuit()
177 {
178     // Note: QTimer::singleShot(0, ...) never calls the slot.
179     QTimer::singleShot(1, this, &TrayWindow::slotQuitAfter);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
180 }
slotQuitAfter()181 void TrayWindow::slotQuitAfter()
182 {
183     theApp()->doQuit(static_cast<QWidget*>(parent()));
184 }
185 
186 /******************************************************************************
187 * Called when the Alarms Enabled action status has changed.
188 * Updates the alarms enabled menu item check state, and the icon pixmap.
189 */
setEnabledStatus(bool status)190 void TrayWindow::setEnabledStatus(bool status)
191 {
192     qCDebug(KALARM_LOG) << "TrayWindow::setEnabledStatus:" << status;
193     updateIcon();
194     updateStatus();
195     updateToolTip();
196 }
197 
198 /******************************************************************************
199 * Called when individual alarms are enabled or disabled.
200 * Set the enabled icon to show or hide a disabled indication.
201 */
slotHaveDisabledAlarms(bool haveDisabled)202 void TrayWindow::slotHaveDisabledAlarms(bool haveDisabled)
203 {
204     qCDebug(KALARM_LOG) << "TrayWindow::slotHaveDisabledAlarms:" << haveDisabled;
205     mHaveDisabledAlarms = haveDisabled;
206     updateIcon();
207     updateToolTip();
208 }
209 
210 /******************************************************************************
211 * Show the associated main window.
212 */
showAssocMainWindow()213 void TrayWindow::showAssocMainWindow()
214 {
215     if (mAssocMainWindow)
216     {
217         mAssocMainWindow->show();
218         mAssocMainWindow->raise();
219         mAssocMainWindow->activateWindow();
220     }
221 }
222 
223 /******************************************************************************
224 * A left click displays the KAlarm main window.
225 */
slotActivateRequested()226 void TrayWindow::slotActivateRequested()
227 {
228     // Left click: display/hide the first main window
229     if (mAssocMainWindow  &&  mAssocMainWindow->isVisible())
230     {
231         mAssocMainWindow->raise();
232         mAssocMainWindow->activateWindow();
233     }
234 }
235 
236 /******************************************************************************
237 * A middle button click displays the New Alarm window.
238 */
slotSecondaryActivateRequested()239 void TrayWindow::slotSecondaryActivateRequested()
240 {
241     if (mActionNew->isEnabled())
242         mActionNew->trigger();    // display a New Alarm dialog
243 }
244 
245 /******************************************************************************
246 * Adjust icon auto-hide status according to when the next alarm is due.
247 * The icon is always shown if audio is playing, to give access to the 'stop'
248 * menu option.
249 */
updateStatus()250 void TrayWindow::updateStatus()
251 {
252     mStatusUpdateTimer->stop();
253     int period =  Preferences::autoHideSystemTray();
254     // If the icon is always to be shown (AutoHideSystemTray = 0),
255     // or audio is playing, show the icon.
256     bool active = !period || MessageDisplay::isAudioPlaying();
257     if (!active)
258     {
259         // Show the icon only if the next active alarm complies
260         active = theApp()->alarmsEnabled();
261         if (active)
262         {
263             KADateTime dt;
264             const KAEvent& event = ResourcesCalendar::earliestAlarm(dt);
265             active = event.isValid();
266             if (active  &&  period > 0)
267             {
268                 qint64 delay = KADateTime::currentLocalDateTime().secsTo(dt);
269                 delay -= static_cast<qint64>(period) * 60;   // delay until icon to be shown
270                 active = (delay <= 0);
271                 if (!active)
272                 {
273                     // First alarm trigger is too far in future, so tray icon is to
274                     // be auto-hidden. Set timer for when it should be shown again.
275                     delay *= 1000;   // convert to msec
276                     int delay_int = static_cast<int>(delay);
277                     if (delay_int != delay)
278                         delay_int = INT_MAX;
279                     mStatusUpdateTimer->setInterval(delay_int);
280                     mStatusUpdateTimer->start();
281                 }
282             }
283         }
284     }
285     setStatus(active ? Active : Passive);
286 }
287 
288 /******************************************************************************
289 * Adjust tooltip according to the app state.
290 * The tooltip text shows alarms due in the next 24 hours. The limit of 24
291 * hours is because only times, not dates, are displayed.
292 */
updateToolTip()293 void TrayWindow::updateToolTip()
294 {
295     bool enabled = theApp()->alarmsEnabled();
296     QString subTitle;
297     if (enabled && Preferences::tooltipAlarmCount())
298         subTitle = tooltipAlarmText();
299 
300     if (!enabled)
301         subTitle = i18n("Disabled");
302     else if (mHaveDisabledAlarms)
303     {
304         if (!subTitle.isEmpty())
305             subTitle += QLatin1String("<br/>");
306         subTitle += i18nc("@info:tooltip Brief: some alarms are disabled", "(Some alarms disabled)");
307     }
308     setToolTipSubTitle(subTitle);
309 }
310 
311 /******************************************************************************
312 * Adjust icon according to the app state.
313 */
updateIcon()314 void TrayWindow::updateIcon()
315 {
316     setIconByName(!theApp()->alarmsEnabled() ? QStringLiteral("kalarm-disabled")
317                   : mHaveDisabledAlarms ? QStringLiteral("kalarm-partdisabled")
318                   : QStringLiteral("kalarm"));
319 }
320 
321 /******************************************************************************
322 * Return the tooltip text showing alarms due in the next 24 hours.
323 * The limit of 24 hours is because only times, not dates, are displayed.
324 */
tooltipAlarmText() const325 QString TrayWindow::tooltipAlarmText() const
326 {
327     KAEvent event;
328     const QString& prefix = Preferences::tooltipTimeToPrefix();
329     int maxCount = Preferences::tooltipAlarmCount();
330     const KADateTime now = KADateTime::currentLocalDateTime();
331     const KADateTime tomorrow = now.addDays(1);
332 
333     // Get today's and tomorrow's alarms, sorted in time order
334     int i, iend;
335     QList<TipItem> items;    //clazy:exclude=inefficient-qlist,inefficient-qlist-soft   QList is better than QVector for insertions
336     QVector<KAEvent> events = KAlarm::getSortedActiveEvents(const_cast<TrayWindow*>(this), &mAlarmsModel);
337     for (i = 0, iend = events.count();  i < iend;  ++i)
338     {
339         KAEvent* event = &events[i];
340         if (event->actionSubType() == KAEvent::MESSAGE)
341         {
342             TipItem item;
343             QDateTime dateTime = event->nextTrigger(KAEvent::DISPLAY_TRIGGER).effectiveKDateTime().toLocalZone().qDateTime();
344             if (dateTime > tomorrow.qDateTime())
345                 break;   // ignore alarms after tomorrow at the current clock time
346             item.dateTime = dateTime;
347 
348             // The alarm is due today, or early tomorrow
349             if (Preferences::showTooltipAlarmTime())
350             {
351                 item.text += QLocale().toString(item.dateTime.time(), QLocale::ShortFormat);
352                 item.text += QLatin1Char(' ');
353             }
354             if (Preferences::showTooltipTimeToAlarm())
355             {
356                 int mins = (now.qDateTime().secsTo(item.dateTime) + 59) / 60;
357                 if (mins < 0)
358                     mins = 0;
359                 char minutes[3] = "00";
360                 minutes[0] = static_cast<char>((mins%60) / 10 + '0');
361                 minutes[1] = static_cast<char>((mins%60) % 10 + '0');
362                 if (Preferences::showTooltipAlarmTime())
363                     item.text += i18nc("@info prefix + hours:minutes", "(%1%2:%3)", prefix, mins/60, QLatin1String(minutes));
364                 else
365                     item.text += i18nc("@info prefix + hours:minutes", "%1%2:%3", prefix, mins/60, QLatin1String(minutes));
366                 item.text += QLatin1Char(' ');
367             }
368             item.text += AlarmText::summary(*event);
369 
370             // Insert the item into the list in time-sorted order
371             int it = 0;
372             for (int itend = items.count();  it < itend;  ++it)
373             {
374                 if (item.dateTime <= items.at(it).dateTime)
375                     break;
376             }
377             items.insert(it, item);
378         }
379     }
380     qCDebug(KALARM_LOG) << "TrayWindow::tooltipAlarmText";
381     QString text;
382     int count = 0;
383     for (i = 0, iend = items.count();  i < iend;  ++i)
384     {
385         qCDebug(KALARM_LOG) << "TrayWindow::tooltipAlarmText: --" << (count+1) << ")" << items.at(i).text;
386         if (i > 0)
387             text += QLatin1String("<br />");
388         text += items.at(i).text;
389         if (++count == maxCount)
390             break;
391     }
392     return text;
393 }
394 
395 /******************************************************************************
396 * Called when the associated main window is closed.
397 */
removeWindow(MainWindow * win)398 void TrayWindow::removeWindow(MainWindow* win)
399 {
400     if (win == mAssocMainWindow)
401         mAssocMainWindow = nullptr;
402 }
403 
404 // vim: et sw=4:
405