1 /*
2 SPDX-FileCopyrightText: 2005-2007 Joris Guisson <joris.guisson@gmail.com>
3 SPDX-FileCopyrightText: 2005-2007 Ivan Vasic <ivasic@gmail.com>
4 SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "trayicon.h"
8
9 #include <algorithm>
10
11 #include <QIcon>
12 #include <QLocale>
13 #include <QPainter>
14 #include <QToolTip>
15
16 #include <KActionCollection>
17 #include <KLocalizedString>
18 #include <KNotification>
19
20 #include "core.h"
21 #include "gui.h"
22 #include <interfaces/torrentactivityinterface.h>
23 #include <net/socketmonitor.h>
24 #include <settings.h>
25 #include <torrent/queuemanager.h>
26 #include <util/functions.h>
27 #include <util/log.h>
28
29 using namespace bt;
30
31 namespace kt
32 {
TrayIcon(Core * core,GUI * parent)33 TrayIcon::TrayIcon(Core *core, GUI *parent)
34 : QObject(parent)
35 , core(core)
36 , mwnd(parent)
37 , previousDownloadHeight(0)
38 , previousUploadHeight(0)
39 , max_upload_rate(nullptr)
40 , max_download_rate(nullptr)
41 , status_notifier_item(nullptr)
42 , queue_suspended(false)
43 , menu(nullptr)
44 {
45 connect(core, &Core::openedSilently, this, &TrayIcon::torrentSilentlyOpened);
46 connect(core, &Core::finished, this, &TrayIcon::finished);
47 connect(core, &Core::torrentStoppedByError, this, &TrayIcon::torrentStoppedByError);
48 connect(core, &Core::maxShareRatioReached, this, &TrayIcon::maxShareRatioReached);
49 connect(core, &Core::maxSeedTimeReached, this, &TrayIcon::maxSeedTimeReached);
50 connect(core, &Core::corruptedData, this, &TrayIcon::corruptedData);
51 connect(core, &Core::queuingNotPossible, this, &TrayIcon::queuingNotPossible);
52 connect(core, &Core::canNotStart, this, &TrayIcon::canNotStart);
53 connect(core, &Core::lowDiskSpace, this, &TrayIcon::lowDiskSpace);
54 connect(core, &Core::canNotLoadSilently, this, &TrayIcon::cannotLoadTorrentSilently);
55 connect(core, &Core::dhtNotEnabled, this, &TrayIcon::dhtNotEnabled);
56 connect(core->getQueueManager(), &QueueManager::suspendStateChanged, this, &TrayIcon::suspendStateChanged);
57
58 suspendStateChanged(core->getQueueManager()->getSuspendedState());
59 }
60
~TrayIcon()61 TrayIcon::~TrayIcon()
62 {
63 }
64
hide()65 void TrayIcon::hide()
66 {
67 if (!status_notifier_item)
68 return;
69
70 delete status_notifier_item;
71 status_notifier_item = nullptr;
72 menu = nullptr;
73 max_download_rate = max_upload_rate = nullptr;
74 }
75
show()76 void TrayIcon::show()
77 {
78 if (status_notifier_item) {
79 suspendStateChanged(core->getQueueManager()->getSuspendedState());
80 return;
81 }
82
83 status_notifier_item = new KStatusNotifierItem(mwnd);
84 connect(status_notifier_item, &KStatusNotifierItem::secondaryActivateRequested, this, &TrayIcon::secondaryActivate);
85
86 menu = status_notifier_item->contextMenu();
87
88 max_upload_rate = new SetMaxRate(core, SetMaxRate::UPLOAD, menu);
89 max_upload_rate->setTitle(i18n("Set max upload speed"));
90 max_download_rate = new SetMaxRate(core, SetMaxRate::DOWNLOAD, menu);
91 max_download_rate->setTitle(i18n("Set max download speed"));
92 menu->addMenu(max_download_rate);
93 menu->addMenu(max_upload_rate);
94 menu->addSeparator();
95
96 KActionCollection *ac = mwnd->getTorrentActivity()->part()->actionCollection();
97 menu->addAction(ac->action(QStringLiteral("start_all")));
98 menu->addAction(ac->action(QStringLiteral("stop_all")));
99 menu->addAction(ac->action(QStringLiteral("queue_suspend")));
100 menu->addSeparator();
101
102 ac = mwnd->actionCollection();
103 menu->addAction(ac->action(QStringLiteral("paste_url")));
104 menu->addAction(ac->action(QString::fromUtf8(KStandardAction::name(KStandardAction::Open))));
105 menu->addSeparator();
106 menu->addAction(ac->action(QString::fromUtf8(KStandardAction::name(KStandardAction::Preferences))));
107 menu->addSeparator();
108
109 status_notifier_item->setIconByName(QStringLiteral("ktorrent"));
110 status_notifier_item->setCategory(KStatusNotifierItem::ApplicationStatus);
111 status_notifier_item->setStatus(KStatusNotifierItem::Passive);
112 status_notifier_item->setStandardActionsEnabled(true);
113 status_notifier_item->setContextMenu(menu);
114
115 queue_suspended = core->getQueueManager()->getSuspendedState();
116 if (queue_suspended)
117 status_notifier_item->setOverlayIconByName(QStringLiteral("kt-pause"));
118 }
119
updateStats(const CurrentStats & stats)120 void TrayIcon::updateStats(const CurrentStats &stats)
121 {
122 if (!status_notifier_item)
123 return;
124
125 status_notifier_item->setStatus(core->getQueueManager()->getNumRunning(QueueManager::DOWNLOADS) > 0 ? KStatusNotifierItem::Active
126 : KStatusNotifierItem::Passive);
127 QString tip = i18n(
128 "Download speed: <b>%1</b><br/>"
129 "Upload speed: <b>%2</b><br/>"
130 "Received: <b>%3</b><br/>"
131 "Transmitted: <b>%4</b>",
132 BytesPerSecToString((double)stats.download_speed),
133 BytesPerSecToString((double)stats.upload_speed),
134 BytesToString(stats.bytes_downloaded),
135 BytesToString(stats.bytes_uploaded));
136
137 status_notifier_item->setToolTip(QStringLiteral("ktorrent"), i18n("Status"), tip);
138 }
139
showPassivePopup(const QString & msg,const QString & title)140 void TrayIcon::showPassivePopup(const QString &msg, const QString &title)
141 {
142 if (status_notifier_item)
143 status_notifier_item->showMessage(title, msg, QStringLiteral("ktorrent"));
144 }
145
cannotLoadTorrentSilently(const QString & msg)146 void TrayIcon::cannotLoadTorrentSilently(const QString &msg)
147 {
148 if (!Settings::showPopups())
149 return;
150
151 KNotification::event(QStringLiteral("CannotLoadSilently"), msg, QPixmap(), mwnd);
152 }
153
dhtNotEnabled(const QString & msg)154 void TrayIcon::dhtNotEnabled(const QString &msg)
155 {
156 if (!Settings::showPopups())
157 return;
158
159 KNotification::event(QStringLiteral("DHTNotEnabled"), msg, QPixmap(), mwnd);
160 }
161
torrentSilentlyOpened(bt::TorrentInterface * tc)162 void TrayIcon::torrentSilentlyOpened(bt::TorrentInterface *tc)
163 {
164 if (!Settings::showPopups())
165 return;
166
167 QString msg = i18n("<b>%1</b> was silently opened.", tc->getDisplayName());
168 KNotification::event(QStringLiteral("TorrentSilentlyOpened"), msg, QPixmap(), mwnd);
169 }
170
finished(bt::TorrentInterface * tc)171 void TrayIcon::finished(bt::TorrentInterface *tc)
172 {
173 if (!Settings::showPopups())
174 return;
175
176 const TorrentStats &s = tc->getStats();
177 double speed_up = (double)s.bytes_uploaded;
178 double speed_down = (double)(s.bytes_downloaded - s.imported_bytes);
179
180 QString msg = i18n(
181 "<b>%1</b> has completed downloading."
182 "<br>Average speed: %2 DL / %3 UL.",
183 tc->getDisplayName(),
184 BytesPerSecToString(speed_down / tc->getRunningTimeDL()),
185 BytesPerSecToString(speed_up / tc->getRunningTimeUL()));
186
187 KNotification::event(QStringLiteral("TorrentFinished"), msg, QPixmap(), mwnd);
188 }
189
maxShareRatioReached(bt::TorrentInterface * tc)190 void TrayIcon::maxShareRatioReached(bt::TorrentInterface *tc)
191 {
192 if (!Settings::showPopups())
193 return;
194
195 const TorrentStats &s = tc->getStats();
196 double speed_up = (double)s.bytes_uploaded;
197
198 QString msg = i18n(
199 "<b>%1</b> has reached its maximum share ratio of %2 and has been stopped."
200 "<br>Uploaded %3 at an average speed of %4.",
201 tc->getDisplayName(),
202 QLocale().toString(s.max_share_ratio, 'f', 2),
203 BytesToString(s.bytes_uploaded),
204 BytesPerSecToString(speed_up / tc->getRunningTimeUL()));
205
206 KNotification::event(QStringLiteral("MaxShareRatioReached"), msg, QPixmap(), mwnd);
207 }
208
maxSeedTimeReached(bt::TorrentInterface * tc)209 void TrayIcon::maxSeedTimeReached(bt::TorrentInterface *tc)
210 {
211 if (!Settings::showPopups())
212 return;
213
214 const TorrentStats &s = tc->getStats();
215 double speed_up = (double)s.bytes_uploaded;
216
217 QString msg = i18n(
218 "<b>%1</b> has reached its maximum seed time of %2 hours and has been stopped."
219 "<br>Uploaded %3 at an average speed of %4.",
220 tc->getDisplayName(),
221 QLocale().toString(s.max_seed_time, 'f', 2),
222 BytesToString(s.bytes_uploaded),
223 BytesPerSecToString(speed_up / tc->getRunningTimeUL()));
224
225 KNotification::event(QStringLiteral("MaxSeedTimeReached"), msg, QPixmap(), mwnd);
226 }
227
torrentStoppedByError(bt::TorrentInterface * tc,QString msg)228 void TrayIcon::torrentStoppedByError(bt::TorrentInterface *tc, QString msg)
229 {
230 if (!Settings::showPopups())
231 return;
232
233 QString err_msg = i18n("<b>%1</b> has been stopped with the following error: <br>%2", tc->getDisplayName(), msg);
234
235 KNotification::event(QStringLiteral("TorrentStoppedByError"), err_msg, QPixmap(), mwnd);
236 }
237
corruptedData(bt::TorrentInterface * tc)238 void TrayIcon::corruptedData(bt::TorrentInterface *tc)
239 {
240 if (!Settings::showPopups())
241 return;
242
243 QString err_msg = i18n(
244 "Corrupted data has been found in the torrent <b>%1</b>"
245 "<br>It would be a good idea to do a data integrity check on the torrent.",
246 tc->getDisplayName());
247
248 KNotification::event(QStringLiteral("CorruptedData"), err_msg, QPixmap(), mwnd);
249 }
250
queuingNotPossible(bt::TorrentInterface * tc)251 void TrayIcon::queuingNotPossible(bt::TorrentInterface *tc)
252 {
253 if (!Settings::showPopups())
254 return;
255
256 const TorrentStats &s = tc->getStats();
257
258 QString msg;
259
260 if (tc->overMaxRatio())
261 msg = i18n(
262 "<b>%1</b> has reached its maximum share ratio of %2 and cannot be enqueued. "
263 "<br>Remove the limit manually if you want to continue seeding.",
264 tc->getDisplayName(),
265 QLocale().toString(s.max_share_ratio, 'f', 2));
266 else
267 msg = i18n(
268 "<b>%1</b> has reached its maximum seed time of %2 hours and cannot be enqueued. "
269 "<br>Remove the limit manually if you want to continue seeding.",
270 tc->getDisplayName(),
271 QLocale().toString(s.max_seed_time, 'f', 2));
272
273 KNotification::event(QStringLiteral("QueueNotPossible"), msg, QPixmap(), mwnd);
274 }
275
canNotStart(bt::TorrentInterface * tc,bt::TorrentStartResponse reason)276 void TrayIcon::canNotStart(bt::TorrentInterface *tc, bt::TorrentStartResponse reason)
277 {
278 if (!Settings::showPopups())
279 return;
280
281 QString msg = i18n("Cannot start <b>%1</b>: <br>", tc->getDisplayName());
282 switch (reason) {
283 case bt::QM_LIMITS_REACHED:
284 if (tc->getStats().bytes_left_to_download == 0) {
285 // is a seeder
286 msg += i18np("Cannot seed more than 1 torrent. <br>", "Cannot seed more than %1 torrents. <br>", Settings::maxSeeds());
287 } else {
288 msg += i18np("Cannot download more than 1 torrent. <br>", "Cannot download more than %1 torrents. <br>", Settings::maxDownloads());
289 }
290 msg += i18n("Go to Settings -> Configure KTorrent, if you want to change the limits.");
291 KNotification::event(QStringLiteral("CannotStart"), msg, QPixmap(), mwnd);
292 break;
293 case bt::NOT_ENOUGH_DISKSPACE:
294 msg += i18n("There is not enough diskspace available.");
295 KNotification::event(QStringLiteral("CannotStart"), msg, QPixmap(), mwnd);
296 break;
297 default:
298 break;
299 }
300 }
301
lowDiskSpace(bt::TorrentInterface * tc,bool stopped)302 void TrayIcon::lowDiskSpace(bt::TorrentInterface *tc, bool stopped)
303 {
304 if (!Settings::showPopups())
305 return;
306
307 QString msg = i18n("Your disk is running out of space.<br /><b>%1</b> is being downloaded to '%2'.", tc->getDisplayName(), tc->getDataDir());
308
309 if (stopped)
310 msg.prepend(i18n("Torrent has been stopped.<br />"));
311
312 KNotification::event(QStringLiteral("LowDiskSpace"), msg);
313 }
314
updateMaxRateMenus()315 void TrayIcon::updateMaxRateMenus()
316 {
317 if (max_download_rate && max_upload_rate) {
318 max_upload_rate->update();
319 max_download_rate->update();
320 }
321 }
322
SetMaxRate(Core * core,Type t,QWidget * parent)323 SetMaxRate::SetMaxRate(Core *core, Type t, QWidget *parent)
324 : QMenu(parent)
325 {
326 setIcon(t == UPLOAD ? QIcon::fromTheme(QStringLiteral("kt-set-max-upload-speed")) : QIcon::fromTheme(QStringLiteral("kt-set-max-download-speed")));
327 m_core = core;
328 type = t;
329 makeMenu();
330 connect(this, &SetMaxRate::triggered, this, &SetMaxRate::onTriggered);
331 connect(this, &SetMaxRate::aboutToShow, this, &SetMaxRate::update);
332 }
333
~SetMaxRate()334 SetMaxRate::~SetMaxRate()
335 {
336 }
337
makeMenu()338 void SetMaxRate::makeMenu()
339 {
340 int rate = (type == UPLOAD) ? net::SocketMonitor::getUploadCap() / 1024 : net::SocketMonitor::getDownloadCap() / 1024;
341 int maxBandwidth = (rate > 0) ? rate : (type == UPLOAD) ? 0 : 20;
342 int delta = 0;
343 int maxBandwidthRounded;
344
345 if (type == UPLOAD)
346 setTitle(i18n("Upload speed limit in KiB/s"));
347 else
348 setTitle(i18n("Download speed limit in KiB/s"));
349
350 unlimited = addAction(i18n("Unlimited"));
351 unlimited->setCheckable(true);
352 unlimited->setChecked(rate == 0);
353
354 if ((maxBandwidth % 5) >= 3)
355 maxBandwidthRounded = maxBandwidth + 5 - (maxBandwidth % 5);
356 else
357 maxBandwidthRounded = maxBandwidth - (maxBandwidth % 5);
358
359 QList<int> values;
360 for (int i = 0; i < 15; i++) {
361 if (delta == 0)
362 values.append(maxBandwidth);
363 else {
364 if ((maxBandwidth % 5) != 0) {
365 values.append(maxBandwidthRounded - delta);
366 values.append(maxBandwidthRounded + delta);
367 } else {
368 values.append(maxBandwidth - delta);
369 values.append(maxBandwidth + delta);
370 }
371 }
372
373 delta += (delta >= 50) ? 50 : (delta >= 20) ? 10 : 5;
374 }
375
376 std::sort(values.begin(), values.end());
377 for (int v : qAsConst(values)) {
378 if (v >= 1) {
379 QAction *act = addAction(QString::number(v));
380 act->setCheckable(true);
381 act->setChecked(rate == v);
382 }
383 }
384 }
385
update()386 void SetMaxRate::update()
387 {
388 clear();
389 makeMenu();
390 }
391
onTriggered(QAction * act)392 void SetMaxRate::onTriggered(QAction *act)
393 {
394 int rate;
395 if (act == unlimited)
396 rate = 0;
397 else
398 rate = act->text().remove(QLatin1Char('&')).toInt(); // remove ampersands
399
400 if (type == UPLOAD) {
401 Settings::setMaxUploadRate(rate);
402 net::SocketMonitor::setUploadCap(Settings::maxUploadRate() * 1024);
403 } else {
404 Settings::setMaxDownloadRate(rate);
405 net::SocketMonitor::setDownloadCap(Settings::maxDownloadRate() * 1024);
406 }
407 Settings::self()->save();
408 }
409
suspendStateChanged(bool suspended)410 void TrayIcon::suspendStateChanged(bool suspended)
411 {
412 queue_suspended = suspended;
413 if (status_notifier_item)
414 status_notifier_item->setOverlayIconByName(suspended ? QStringLiteral("kt-pause") : QString());
415 }
416
secondaryActivate(const QPoint & pos)417 void TrayIcon::secondaryActivate(const QPoint &pos)
418 {
419 Q_UNUSED(pos);
420 core->setSuspendedState(!core->getSuspendedState());
421 }
422
423 }
424