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