Only update menu items if visible as we force a redraw when re-shown.
[dhcpcd-ui] / src / dhcpcd-qt / dhcpcd-qt.cpp
1 /*
2  * dhcpcd-qt
3  * Copyright 2014 Roy Marples <roy@marples.name>
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
15  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
18  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24  * SUCH DAMAGE.
25  */
26
27 #include <QCursor>
28 #include <QDebug>
29 #include <QList>
30 #include <QSocketNotifier>
31 #include <QtGui>
32
33 #include <cerrno>
34
35 #include "config.h"
36 #include "dhcpcd-qt.h"
37 #include "dhcpcd-about.h"
38 #include "dhcpcd-preferences.h"
39 #include "dhcpcd-wi.h"
40 #include "dhcpcd-ifmenu.h"
41 #include "dhcpcd-ssidmenu.h"
42
43 #ifdef NOTIFY
44 #include <knotification.h>
45 #endif
46
47 DhcpcdQt::DhcpcdQt()
48 {
49
50         createActions();
51         createTrayIcon();
52
53         onLine = carrier = false;
54         lastStatus = NULL;
55         aniTimer = new QTimer(this);
56         connect(aniTimer, SIGNAL(timeout()), this, SLOT(animate()));
57         notifier = NULL;
58         retryOpenTimer = NULL;
59
60         about = NULL;
61         preferences = NULL;
62
63         wis = new QList<DhcpcdWi *>();
64         ssidMenu = NULL;
65
66         qDebug("%s", "Connecting ...");
67         con = dhcpcd_new();
68         if (con == NULL) {
69                 qCritical("libdhcpcd: %s", strerror(errno));
70                 exit(EXIT_FAILURE);
71                 return;
72         }
73         dhcpcd_set_progname(con, "dhcpcd-qt");
74         dhcpcd_set_status_callback(con, dhcpcd_status_cb, this);
75         dhcpcd_set_if_callback(con, dhcpcd_if_cb, this);
76         dhcpcd_wpa_set_scan_callback(con, dhcpcd_wpa_scan_cb, this);
77         dhcpcd_wpa_set_status_callback(con, dhcpcd_wpa_status_cb, this);
78         tryOpen();
79 }
80
81 DhcpcdQt::~DhcpcdQt()
82 {
83
84         if (ssidMenu) {
85                 ssidMenu->setVisible(false);
86                 ssidMenu->deleteLater();
87         }
88
89         if (con != NULL) {
90                 dhcpcd_close(con);
91                 dhcpcd_free(con);
92         }
93
94         free(lastStatus);
95
96         for (auto &wi : *wis)
97                 wi->deleteLater();
98         delete wis;
99 }
100
101 DHCPCD_CONNECTION *DhcpcdQt::getConnection()
102 {
103
104         return con;
105 }
106
107 QList<DhcpcdWi *> *DhcpcdQt::getWis()
108 {
109
110         return wis;
111 }
112
113 void DhcpcdQt::animate()
114 {
115         const char *icon;
116
117         if (onLine) {
118                 if (aniCounter++ > 6) {
119                         aniTimer->stop();
120                         aniCounter = 0;
121                         return;
122                 }
123
124                 if (aniCounter % 2 == 0)
125                         icon = "network-idle";
126                 else
127                         icon = "network-transmit-receive";
128         } else {
129                 switch(aniCounter++) {
130                 case 0:
131                         icon = "network-transmit";
132                         break;
133                 case 1:
134                         icon = "network-receive";
135                         break;
136                 default:
137                         icon = "network-idle";
138                         aniCounter = 0;
139                 }
140         }
141
142         setIcon("status", icon);
143 }
144
145 void DhcpcdQt::updateOnline(bool showIf)
146 {
147         bool isOn, isCarrier;
148         char *msg;
149         DHCPCD_IF *ifs, *i;
150         QString msgs;
151
152         isOn = isCarrier = false;
153         ifs = dhcpcd_interfaces(con);
154         for (i = ifs; i; i = i->next) {
155                 if (strcmp(i->type, "link") == 0) {
156                         if (i->up)
157                                 isCarrier = true;
158                 } else {
159                         if (i->up)
160                                 isOn = true;
161                 }
162                 msg = dhcpcd_if_message(i, NULL);
163                 if (msg) {
164                         if (showIf)
165                                 qDebug() << msg;
166                         if (msgs.isEmpty())
167                                 msgs = QString::fromAscii(msg);
168                         else
169                                 msgs += '\n' + QString::fromAscii(msg);
170                         free(msg);
171                 } else if (showIf)
172                         qDebug() << i->ifname << i->reason;
173         }
174
175         if (onLine != isOn || carrier != isCarrier) {
176                 onLine = isOn;
177                 carrier = isCarrier;
178                 aniTimer->stop();
179                 aniCounter = 0;
180                 if (isOn) {
181                         animate();
182                         aniTimer->start(300);
183                 } else if (isCarrier) {
184                         animate();
185                         aniTimer->start(500);
186                 } else
187                         setIcon("status", "network-offline");
188         }
189
190         trayIcon->setToolTip(msgs);
191 }
192
193 void DhcpcdQt::statusCallback(const char *status)
194 {
195
196         qDebug("Status changed to %s", status);
197         if (strcmp(status, "down") == 0) {
198                 aniTimer->stop();
199                 aniCounter = 0;
200                 onLine = carrier = false;
201                 setIcon("status", "network-offline");
202                 trayIcon->setToolTip(tr("Not connected to dhcpcd"));
203                 /* Close down everything */
204                 if (notifier) {
205                         notifier->setEnabled(false);
206                         notifier->deleteLater();
207                         notifier = NULL;
208                 }
209                 if (ssidMenu) {
210                         ssidMenu->deleteLater();
211                         ssidMenu = NULL;
212                 }
213                 preferencesAction->setEnabled(false);
214                 if (preferences) {
215                         preferences->deleteLater();
216                         preferences = NULL;
217                 }
218         } else {
219                 bool refresh;
220
221                 if (lastStatus == NULL || strcmp(lastStatus, "down") == 0) {
222                         qDebug("Connected to dhcpcd-%s", dhcpcd_version(con));
223                         refresh = true;
224                 } else
225                         refresh = strcmp(lastStatus, "opened") ? false : true;
226                 updateOnline(refresh);
227         }
228
229         free(lastStatus);
230         lastStatus = strdup(status);
231
232         if (strcmp(status, "down") == 0) {
233                 if (retryOpenTimer == NULL) {
234                         retryOpenTimer = new QTimer(this);
235                         connect(retryOpenTimer, SIGNAL(timeout()),
236                             this, SLOT(tryOpen()));
237                         retryOpenTimer->start(DHCPCD_RETRYOPEN);
238                 }
239         }
240 }
241
242 void DhcpcdQt::dhcpcd_status_cb(_unused DHCPCD_CONNECTION *con,
243     const char *status, void *d)
244 {
245         DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;
246
247         dhcpcdQt->statusCallback(status);
248 }
249
250 void DhcpcdQt::ifCallback(DHCPCD_IF *i)
251 {
252         char *msg;
253         bool new_msg;
254
255         if (strcmp(i->reason, "RENEW") &&
256             strcmp(i->reason, "STOP") &&
257             strcmp(i->reason, "STOPPED"))
258         {
259                 msg = dhcpcd_if_message(i, &new_msg);
260                 if (msg) {
261                         qDebug("%s", msg);
262                         if (new_msg) {
263                                 QSystemTrayIcon::MessageIcon icon =
264                                     i->up ? QSystemTrayIcon::Information :
265                                     QSystemTrayIcon::Warning;
266                                 QString t = tr("Network Event");
267                                 QString m = msg;
268                                 notify(t, m, icon);
269                         }
270                         free(msg);
271                 }
272         }
273
274         updateOnline(false);
275
276         if (i->wireless) {
277                 for (auto &wi : *wis) {
278                         DHCPCD_WPA *wpa = wi->getWpa();
279                         if (dhcpcd_wpa_if(wpa) == i) {
280                                 DHCPCD_WI_SCAN *scans;
281
282                                 scans = dhcpcd_wi_scans(i);
283                                 processScans(wi, scans);
284                         }
285                 }
286         }
287 }
288
289 void DhcpcdQt::dhcpcd_if_cb(DHCPCD_IF *i, void *d)
290 {
291         DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;
292
293         dhcpcdQt->ifCallback(i);
294 }
295
296 DhcpcdWi *DhcpcdQt::findWi(DHCPCD_WPA *wpa)
297 {
298
299         for (auto &wi : *wis) {
300                 if (wi->getWpa() == wpa)
301                         return wi;
302         }
303         return NULL;
304 }
305
306 void DhcpcdQt::processScans(DhcpcdWi *wi, DHCPCD_WI_SCAN *scans)
307 {
308         DHCPCD_WI_SCAN *s1, *s2;
309
310         QString title = tr("New Access Point");
311         QString txt;
312         for (s1 = scans; s1; s1 = s1->next) {
313                 for (s2 = wi->getScans(); s2; s2 = s2->next) {
314                         if (strcmp(s1->ssid, s2->ssid) == 0)
315                                 break;
316                 }
317                 if (s2 == NULL) {
318                         if (!txt.isEmpty()) {
319                                 title = tr("New Access Points");
320                                 txt += '\n';
321                         }
322                         txt += s1->ssid;
323                 }
324         }
325         if (!txt.isEmpty() &&
326             (ssidMenu == NULL || !ssidMenu->isVisible()))
327                 notify(title, txt);
328
329         wi->setScans(scans);
330         if (ssidMenu && ssidMenu->isVisible())
331                 ssidMenu->popup(ssidMenuPos);
332 }
333
334 void DhcpcdQt::scanCallback(DHCPCD_WPA *wpa)
335 {
336         DHCPCD_WI_SCAN *scans;
337         int fd = dhcpcd_wpa_get_fd(wpa);
338         DhcpcdWi *wi;
339
340         wi = findWi(wpa);
341         if (fd == -1) {
342                 qCritical("No fd for WPA");
343                 if (wi) {
344                         wis->removeOne(wi);
345                         wi->deleteLater();
346                 }
347                 return;
348         }
349
350         DHCPCD_IF *i = dhcpcd_wpa_if(wpa);
351         if (i == NULL) {
352                 qCritical("No interface for WPA");
353                 if (wi) {
354                         wis->removeOne(wi);
355                         wi->deleteLater();
356                 }
357                 return;
358         }
359
360         qDebug("%s: Received scan results", i->ifname);
361         scans = dhcpcd_wi_scans(i);
362         if (wi == NULL) {
363                 wi = new DhcpcdWi(this, wpa);
364                 if (wi->open()) {
365                         wis->append(wi);
366                         wi->setScans(scans);
367                 } else
368                         wi->deleteLater();
369         } else
370                 processScans(wi, scans);
371
372 }
373
374 void DhcpcdQt::dhcpcd_wpa_scan_cb(DHCPCD_WPA *wpa, void *d)
375 {
376         DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;
377
378         dhcpcdQt->scanCallback(wpa);
379 }
380
381 void DhcpcdQt::wpaStatusCallback(DHCPCD_WPA *wpa, const char *status)
382 {
383         DHCPCD_IF *i;
384
385         i = dhcpcd_wpa_if(wpa);
386         qDebug("%s: WPA status %s", i->ifname, status);
387         if (strcmp(status, "down") == 0) {
388                 DhcpcdWi *wi = findWi(wpa);
389                 if (wi) {
390                         wis->removeOne(wi);
391                         wi->deleteLater();
392                 }
393         }
394 }
395
396 void DhcpcdQt::dhcpcd_wpa_status_cb(DHCPCD_WPA *wpa, const char *status,
397     void *d)
398 {
399         DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;
400
401         dhcpcdQt->wpaStatusCallback(wpa, status);
402 }
403
404 void DhcpcdQt::tryOpen() {
405         int fd = dhcpcd_open(con, true);
406         static int last_error;
407
408         if (fd == -1) {
409                 if (errno == EACCES || errno == EPERM) {
410                         if ((fd = dhcpcd_open(con, false)) != -1)
411                                 goto unprived;
412                 }
413                 if (errno != last_error) {
414                         last_error = errno;
415                         const char *errt = strerror(errno);
416                         qCritical("dhcpcd_open: %s", errt);
417                         trayIcon->setToolTip(
418                             tr("Error connecting to dhcpcd: %1").arg(errt));
419                 }
420                 if (retryOpenTimer == NULL) {
421                         retryOpenTimer = new QTimer(this);
422                         connect(retryOpenTimer, SIGNAL(timeout()),
423                             this, SLOT(tryOpen()));
424                         retryOpenTimer->start(DHCPCD_RETRYOPEN);
425                 }
426                 return;
427         }
428
429 unprived:
430         /* Start listening to WPA events */
431         dhcpcd_wpa_start(con);
432
433         if (retryOpenTimer) {
434                 retryOpenTimer->stop();
435                 retryOpenTimer->deleteLater();
436                 retryOpenTimer = NULL;
437         }
438
439         notifier = new QSocketNotifier(fd, QSocketNotifier::Read);
440         connect(notifier, SIGNAL(activated(int)), this, SLOT(dispatch()));
441
442         preferencesAction->setEnabled(dhcpcd_privileged(con));
443 }
444
445 void DhcpcdQt::dispatch()
446 {
447
448         dhcpcd_dispatch(con);
449 }
450
451 void DhcpcdQt::notify(QString &title, QString &msg,
452 #ifdef NOTIFY
453     QSystemTrayIcon::MessageIcon
454 #else
455     QSystemTrayIcon::MessageIcon icon
456 #endif
457     )
458 {
459
460 #ifdef NOTIFY
461         KNotification *n = new KNotification("event", this);
462         n->setTitle(title);
463         n->setText(msg);
464         n->sendEvent();
465 #else
466         //trayIcon->showMessage(title, msg, icon);
467 #endif
468 }
469
470 void DhcpcdQt::closeEvent(QCloseEvent *event)
471 {
472
473         if (trayIcon->isVisible()) {
474                 hide();
475                 event->ignore();
476         }
477 }
478
479 QIcon DhcpcdQt::getIcon(QString category, QString name)
480 {
481         QIcon icon;
482
483         if (QIcon::hasThemeIcon(name))
484                 icon = QIcon::fromTheme(name);
485         else
486                 icon = QIcon(ICONDIR "/hicolor/scalable/" + category + "/" + name + ".svg");
487
488         return icon;
489 }
490
491 void DhcpcdQt::setIcon(QString category, QString name)
492 {
493         QIcon icon = getIcon(category, name);
494
495         trayIcon->setIcon(icon);
496 }
497
498 QIcon DhcpcdQt::icon()
499 {
500
501         return getIcon("status", "network-transmit-receive");
502 }
503
504 void DhcpcdQt::menuDeleted(QMenu *menu)
505 {
506
507         if (ssidMenu == menu)
508                 ssidMenu = NULL;
509 }
510
511 void DhcpcdQt::createSsidMenu()
512 {
513
514         if (ssidMenu) {
515                 ssidMenu->deleteLater();
516                 ssidMenu = NULL;
517         }
518         if (wis->size() == 0)
519                 return;
520
521         ssidMenu = new QMenu(this);
522         if (wis->size() == 1)
523                 wis->first()->createMenu(ssidMenu);
524         else {
525                 for (auto &wi : *wis)
526                         ssidMenu->addMenu(wi->createIfMenu(ssidMenu));
527         }
528         ssidMenuPos = QCursor::pos();
529         ssidMenu->popup(ssidMenuPos);
530 }
531
532 void DhcpcdQt::iconActivated(QSystemTrayIcon::ActivationReason reason)
533 {
534
535         if (reason == QSystemTrayIcon::Trigger)
536                 createSsidMenu();
537 }
538
539 void DhcpcdQt::dialogClosed(QDialog *dialog)
540 {
541
542         if (dialog == about)
543                 about = NULL;
544         else if (dialog == preferences)
545                 preferences = NULL;
546 }
547
548 void DhcpcdQt::showPreferences()
549 {
550
551         if (preferences == NULL) {
552                 preferences = new DhcpcdPreferences(this);
553                 preferences->show();
554         } else
555                 preferences->activateWindow();
556 }
557
558 void DhcpcdQt::showAbout()
559 {
560
561         if (about == NULL) {
562                 about = new DhcpcdAbout(this);
563                 about->show();
564         } else
565                 about->activateWindow();
566 }
567
568 void DhcpcdQt::createActions()
569 {
570
571         preferencesAction = new QAction(tr("&Preferences"), this);
572         preferencesAction->setIcon(QIcon::fromTheme("preferences-system-network"));
573         preferencesAction->setEnabled(false);
574         connect(preferencesAction, SIGNAL(triggered()),
575             this, SLOT(showPreferences()));
576
577         aboutAction = new QAction(tr("&About"), this);
578         aboutAction->setIcon(QIcon::fromTheme("help-about"));
579         connect(aboutAction, SIGNAL(triggered()), this, SLOT(showAbout()));
580
581         quitAction = new QAction(tr("&Quit"), this);
582         quitAction->setIcon(QIcon::fromTheme("application-exit"));
583         connect(quitAction, SIGNAL(triggered()), qApp, SLOT(quit()));
584
585 }
586
587 void DhcpcdQt::createTrayIcon()
588 {
589
590         trayIconMenu = new QMenu(this);
591         trayIconMenu->addAction(preferencesAction);
592         trayIconMenu->addSeparator();
593         trayIconMenu->addAction(aboutAction);
594         trayIconMenu->addAction(quitAction);
595
596         trayIcon = new QSystemTrayIcon(this);
597         setIcon("status", "network-offline");
598         trayIcon->setContextMenu(trayIconMenu);
599
600         connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
601             this, SLOT(iconActivated(QSystemTrayIcon::ActivationReason)));
602
603         trayIcon->show();
604 }