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