You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nm-tray-packaging/src/tray.cpp

398 lines
15 KiB

/*COPYRIGHT_HEADER
This file is a part of nm-tray.
Copyright (c)
2015~now Palo Kisa <palo.kisa@gmail.com>
nm-tray is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
COPYRIGHT_HEADER*/
#include "tray.h"
#include <QSystemTrayIcon>
#include <QMenu>
#include <QMessageBox>
#include <QApplication>
#include <QPersistentModelIndex>
#include <NetworkManagerQt/Manager>
#include <NetworkManagerQt/WirelessDevice>
#include "icons.h"
#include "nmmodel.h"
#include "nmproxy.h"
#include "log.h"
#include "dbus/org.freedesktop.Notifications.h"
#include "nmlist.h"
#include "connectioninfo.h"
#include "windowmenu.h"
// config keys
static const QString ENABLE_NOTIFICATIONS = QStringLiteral("enableNotifications");
class TrayPrivate
{
public:
TrayPrivate();
void updateState(QModelIndex const & index, bool removing);
void primaryConnectionUpdate();
void setShown(QPersistentModelIndex const & index);
void updateIcon();
void refreshIcon();
void openCloseDialog(QDialog * dialog);
void notify(QModelIndex const & index, bool removing);
public:
QSystemTrayIcon mTrayIcon;
QMenu mContextMenu;
QTimer mStateTimer;
QAction * mActEnableNetwork;
QAction * mActEnableWifi;
QAction * mActConnInfo;
QAction * mActDebugInfo;
NmModel mNmModel;
NmProxy mActiveConnections;
QPersistentModelIndex mPrimaryConnection;
QPersistentModelIndex mShownConnection;
QList<QPersistentModelIndex> mConnectionsToNotify; //!< just "created" connections to which notification wasn't sent yet
icons::Icon mIconCurrent;
icons::Icon mIcon2Show;
QTimer mIconTimer;
QScopedPointer<QDialog> mConnDialog;
QScopedPointer<QDialog> mInfoDialog;
org::freedesktop::Notifications mNotification;
// configuration
bool mEnableNotifications; //!< should info about connection establishment etc. be send by org.freedesktop.Notifications
};
TrayPrivate::TrayPrivate()
: mNotification{QStringLiteral("org.freedesktop.Notifications"), QStringLiteral("/org/freedesktop/Notifications"), QDBusConnection::sessionBus()}
{
mActiveConnections.setNmModel(&mNmModel, NmModel::ActiveConnectionType);
}
void TrayPrivate::updateState(QModelIndex const & index, bool removing)
{
notify(index, removing);
const auto state = static_cast<NetworkManager::ActiveConnection::State>(mActiveConnections.data(index, NmModel::ActiveConnectionStateRole).toInt());
const bool is_primary = mPrimaryConnection == index;
//qCDebug(NM_TRAY) << __FUNCTION__ << index << removing << mActiveConnections.data(index, NmModel::NameRole) << mActiveConnections.data(index, NmModel::ConnectionUuidRole).toString() << is_primary << mActiveConnections.data(index, NmModel::ConnectionTypeRole).toInt() << state;
if (removing || NetworkManager::ActiveConnection::Deactivated == state || NetworkManager::ActiveConnection::Deactivating == state)
{
if (is_primary)
{
mPrimaryConnection = QModelIndex{};
setShown(mPrimaryConnection);
} else if (mShownConnection == index)
{
setShown(mPrimaryConnection);
}
} else
{
if (is_primary || NetworkManager::ActiveConnection::Activating == state)
{
setShown(index);
} else if (mShownConnection == index)
{
setShown(mPrimaryConnection);
}
}
//TODO: optimize this text assembly (to not do it every time)?
if (mPrimaryConnection.isValid())
{
mTrayIcon.setToolTip(Tray::tr("<pre>Connection <strong>%1</strong>(%2) active</pre>")
.arg(mPrimaryConnection.data(NmModel::NameRole).toString())
.arg(mPrimaryConnection.data(NmModel::ActiveConnectionTypeStringRole).toString())
);
} else
{
mTrayIcon.setToolTip(Tray::tr("<pre>No active connection</pre>"));
}
}
void TrayPrivate::primaryConnectionUpdate()
{
NetworkManager::ActiveConnection::Ptr prim_conn = NetworkManager::primaryConnection();
if (!prim_conn || !prim_conn->isValid())
{
mPrimaryConnection = QModelIndex{};
setShown(mPrimaryConnection);
return;
}
//qCDebug(NM_TRAY) << __FUNCTION__ << prim_conn->uuid();
QModelIndexList l = mActiveConnections.match(mActiveConnections.index(0, 0, QModelIndex{}), NmModel::ActiveConnectionUuidRole, prim_conn->uuid(), -1, Qt::MatchExactly);
//qCDebug(NM_TRAY) << __FUNCTION__ << l.size();
//nothing to do if the connection not populated in model yet
if (0 >= l.size())
return;
Q_ASSERT(1 == l.size());
mPrimaryConnection = l.first();
updateState(mPrimaryConnection, false);
}
void TrayPrivate::setShown(QPersistentModelIndex const & index)
{
mShownConnection = index;
mIcon2Show = mShownConnection.isValid()
? static_cast<icons::Icon>(mActiveConnections.data(mShownConnection, NmModel::IconTypeRole).toInt()) : icons::NETWORK_OFFLINE;
//postpone setting the icon (for case we change the icon in till our event is finished)
mIconTimer.start();
}
void TrayPrivate::updateIcon()
{
if (mIconCurrent == mIcon2Show)
return;
mIconCurrent = mIcon2Show;
refreshIcon();
}
void TrayPrivate::refreshIcon()
{
//Note: the icons::getIcon chooses the right icon from list of possible candidates
// -> we need to refresh the icon in case of icon theme change
mTrayIcon.setIcon(icons::getIcon(mIconCurrent));
}
void TrayPrivate::openCloseDialog(QDialog * dialog)
{
if (dialog->isHidden() || dialog->isMinimized())
{
dialog->showNormal();
dialog->activateWindow();
dialog->raise();
} else
dialog->close();
}
void TrayPrivate::notify(QModelIndex const & index, bool removing)
{
if (!mEnableNotifications)
{
return;
}
QString summary, body;
if (removing)
{
mConnectionsToNotify.removeOne(index);
summary = Tray::tr("Connection lost");
body = Tray::tr("We have just lost the connection to %1 '%2'.");
} else
{
const int notif_i = mConnectionsToNotify.indexOf(index);
// do nothing if not just added or the connection is not activated yet
if (-1 == notif_i
|| NetworkManager::ActiveConnection::Activated != static_cast<NetworkManager::ActiveConnection::State>(mActiveConnections.data(index, NmModel::ActiveConnectionStateRole).toInt())
)
{
return;
}
mConnectionsToNotify.removeAt(notif_i); // fire the notification only once
summary = Tray::tr("Connection established");
body = Tray::tr("We have just established the connection to %1 '%2'.");
}
// TODO: do somehow check the result?
mNotification.Notify(Tray::tr("NetworkManager(nm-tray)")
, 0
, icons::getIcon(static_cast<icons::Icon>(mActiveConnections.data(index, NmModel::IconTypeRole).toInt())).name()
, summary
, body.arg(mActiveConnections.data(index, NmModel::ConnectionTypeStringRole).toString()).arg(mActiveConnections.data(index, NmModel::NameRole).toString())
, {}
, {}
, -1);
}
Tray::Tray(QObject *parent/* = nullptr*/)
: QObject{parent}
, d{new TrayPrivate}
{
d->mEnableNotifications = QSettings{}.value(ENABLE_NOTIFICATIONS, true).toBool();
connect(&d->mTrayIcon, &QSystemTrayIcon::activated, this, &Tray::onActivated);
//postpone the update in case of signals flood
connect(&d->mStateTimer, &QTimer::timeout, this, &Tray::setActionsStates);
d->mStateTimer.setSingleShot(true);
d->mStateTimer.setInterval(200);
d->mIconCurrent = static_cast<icons::Icon>(-1);
d->setShown(QModelIndex{});
d->refreshIcon(); //force setting the icon instantly
//postpone updating of the icon
connect(&d->mIconTimer, &QTimer::timeout, [this] { d->updateIcon(); });
d->mIconTimer.setSingleShot(true);
d->mIconTimer.setInterval(0);
d->mActEnableNetwork = d->mContextMenu.addAction(Tray::tr("Enable Networking"));
d->mActEnableWifi = d->mContextMenu.addAction(Tray::tr("Enable Wi-fi"));
d->mContextMenu.addSeparator();
QAction * enable_notifications = d->mContextMenu.addAction(Tray::tr("Enable notifications"));
d->mContextMenu.addSeparator();
d->mActConnInfo = d->mContextMenu.addAction(QIcon::fromTheme(QStringLiteral("dialog-information")), Tray::tr("Connection information"));
d->mActDebugInfo = d->mContextMenu.addAction(QIcon::fromTheme(QStringLiteral("dialog-information")), Tray::tr("Debug information"));
connect(d->mContextMenu.addAction(QIcon::fromTheme(QStringLiteral("document-edit")), Tray::tr("Edit connections...")), &QAction::triggered
, this, &Tray::onEditConnectionsTriggered);
d->mContextMenu.addSeparator();
connect(d->mContextMenu.addAction(QIcon::fromTheme(QStringLiteral("help-about")), Tray::tr("About")), &QAction::triggered
, this, &Tray::onAboutTriggered);
connect(d->mContextMenu.addAction(QIcon::fromTheme(QStringLiteral("application-exit")), Tray::tr("Quit")), &QAction::triggered
, this, &Tray::onQuitTriggered);
//for listening on the QEvent::ThemeChange (is delivered only to QWidget objects)
d->mContextMenu.installEventFilter(this);
d->mActEnableNetwork->setCheckable(true);
d->mActEnableWifi->setCheckable(true);
enable_notifications->setCheckable(true);
enable_notifications->setChecked(d->mEnableNotifications);
connect(d->mActEnableNetwork, &QAction::triggered, [this] (bool checked) { NetworkManager::setNetworkingEnabled(checked); });
connect(d->mActEnableWifi, &QAction::triggered, [this] (bool checked) { NetworkManager::setWirelessEnabled(checked); });
connect(enable_notifications, &QAction::triggered, [this] (bool checked) { d->mEnableNotifications = checked; QSettings{}.setValue(ENABLE_NOTIFICATIONS, checked); });
connect(d->mActConnInfo, &QAction::triggered, [this] (bool ) {
if (d->mInfoDialog.isNull())
{
d->mInfoDialog.reset(new ConnectionInfo{&d->mNmModel});
connect(d->mInfoDialog.data(), &QDialog::finished, [this] {
d->mInfoDialog.reset(nullptr);
});
}
d->openCloseDialog(d->mInfoDialog.data());
});
connect(d->mActDebugInfo, &QAction::triggered, [this] (bool ) {
if (d->mConnDialog.isNull())
{
d->mConnDialog.reset(new NmList{Tray::tr("nm-tray info"), &d->mNmModel});
connect(d->mConnDialog.data(), &QDialog::finished, [this] {
d->mConnDialog.reset(nullptr);
});
}
d->openCloseDialog(d->mConnDialog.data());
});
// Note: Force all the updates as the NetworkManager::Notifier signals aren't
// emitted at application startup.
d->primaryConnectionUpdate();
setActionsStates();
connect(NetworkManager::notifier(), &NetworkManager::Notifier::networkingEnabledChanged, &d->mStateTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
connect(NetworkManager::notifier(), &NetworkManager::Notifier::wirelessEnabledChanged, &d->mStateTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
connect(NetworkManager::notifier(), &NetworkManager::Notifier::wirelessHardwareEnabledChanged, &d->mStateTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
connect(NetworkManager::notifier(), &NetworkManager::Notifier::primaryConnectionChanged, this, &Tray::onPrimaryConnectionChanged);
connect(&d->mActiveConnections, &QAbstractItemModel::rowsInserted, [this] (QModelIndex const & parent, int first, int last) {
for (int i = first; i <= last; ++i)
{
const QModelIndex index = d->mActiveConnections.index(i, 0, parent);
//qCDebug(NM_TRAY) << "rowsInserted" << index;
if (d->mEnableNotifications)
{
d->mConnectionsToNotify.append(index);
}
d->updateState(index, false);
}
});
connect(&d->mActiveConnections, &QAbstractItemModel::rowsAboutToBeRemoved, [this] (QModelIndex const & parent, int first, int last) {
//qCDebug(NM_TRAY) << "rowsAboutToBeRemoved";
for (int i = first; i <= last; ++i)
d->updateState(d->mActiveConnections.index(i, 0, parent), true);
});
connect(&d->mActiveConnections, &QAbstractItemModel::dataChanged, [this] (const QModelIndex & topLeft, const QModelIndex & bottomRight, const QVector<int> & /*roles*/) {
//qCDebug(NM_TRAY) << "dataChanged";
for (auto const & i : QItemSelection{topLeft, bottomRight}.indexes())
d->updateState(i, false);
});
d->mTrayIcon.setContextMenu(&d->mContextMenu);
QTimer::singleShot(0, [this] { d->mTrayIcon.show(); });
}
Tray::~Tray()
{
}
bool Tray::eventFilter(QObject * object, QEvent * event)
{
Q_ASSERT(&d->mContextMenu == object);
if (QEvent::ThemeChange == event->type())
d->refreshIcon();
return false;
}
void Tray::onEditConnectionsTriggered()
{
// Note: let this object dangle, if the process isn't finished until our application is closed
QProcess * editor = new QProcess;
editor->setProcessChannelMode(QProcess::ForwardedChannels);
// TODO: allow the command to be configurable!?
qCInfo(NM_TRAY) << "starting the nm-connection-editor";
editor->start(QStringLiteral("nm-connection-editor"));
editor->closeWriteChannel();
connect(editor, static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), [editor] {
qCInfo(NM_TRAY) << "the nm-connection-editor finished";
editor->deleteLater();
});
}
void Tray::onAboutTriggered()
{
QMessageBox::about(nullptr, Tray::tr("%1 about").arg(QStringLiteral("nm-tray"))
, Tray::tr("<strong><a href=\"https://github.com/palinek/nm-tray\">nm-tray</a></strong> is a simple Qt based"
" frontend for <a href=\"https://wiki.gnome.org/Projects/NetworkManager\">NetworkManager</a>.<br/><br/>"
"Version: %1").arg(NM_TRAY_VERSION));
}
void Tray::onQuitTriggered()
{
QApplication::instance()->quit();
}
void Tray::onActivated()
{
QMenu * menu = new WindowMenu(&d->mNmModel);
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->popup(QCursor::pos());
}
void Tray::setActionsStates()
{
const bool net_enabled = NetworkManager::isNetworkingEnabled();
d->mActEnableNetwork->setChecked(net_enabled);
d->mActEnableWifi->setChecked(NetworkManager::isWirelessEnabled());
d->mActEnableWifi->setEnabled(NetworkManager::isNetworkingEnabled() && NetworkManager::isWirelessHardwareEnabled());
d->mActConnInfo->setEnabled(net_enabled);
}
void Tray::onPrimaryConnectionChanged(QString const & /*uni*/)
{
d->primaryConnectionUpdate();
}