Commit 97e843d3 authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇
Browse files

[Free Space Notifer] Use critical notification instead of tray icon and monitor Root, too

This refactors the free space notifier module to use a critical (i.e. persistent and always on top) notification for warning of low disk space.
The, albeit blinking, tray icon is easy to miss, especially when you're running a full screen terminal, which I typically do while compiling stuff.
It now also monitors the Root folder, if it's on a separate partition from the user's home. Furthermore, Filelight is offered to explore the drive (if installed).

The overall warning logic remains pretty much the same:

* Once the drive goes below the configured threshold a warning notification is shown, it stays on screen until dismissed by the user or
  when free space is above warning threshold again.
* The notification is emitted again when free space drops below half the previous threshold, for added sense of urgency should the drive
  be rapidly filled up
* The notification is also emitted again if free space remains below the threshold for more than one hour

(It will only emit again when it was closed, obviously, so you won't end up with a tonne of popups after a few hours ;)

I don't think this needs to be separately configurable for Home and Root, since the default threshold is like 200 MiB (it's not a configured
percentage), so the absolute free space it warns about will be the same, even if your Home is giant compared to Root.

BUG: 340582
FIXED-IN: 5.20.0

Differential Revision: https://phabricator.kde.org/D29770
parent 8eb25ec5
......@@ -4,6 +4,8 @@ set(kded_freespacenotifier_SRCS freespacenotifier.cpp module.cpp)
ki18n_wrap_ui(kded_freespacenotifier_SRCS freespacenotifier_prefs_base.ui)
qt5_add_dbus_interface(kded_freespacenotifier_SRCS ${KDED_DBUS_INTERFACE} kded_interface)
kconfig_add_kcfg_files(kded_freespacenotifier_SRCS settings.kcfgc)
add_library(freespacenotifier MODULE ${kded_freespacenotifier_SRCS})
......@@ -14,8 +16,9 @@ target_link_libraries(freespacenotifier
KF5::DBusAddons
KF5::I18n
KF5::KIOCore
KF5::KIOWidgets
KF5::KIOGui
KF5::Notifications
KF5::Service
)
install(TARGETS freespacenotifier DESTINATION ${KDE_INSTALL_PLUGINDIR}/kf5/kded )
......
......@@ -2,6 +2,7 @@
Copyright (c) 2006 Lukas Tinkl <ltinkl@suse.cz>
Copyright (c) 2008 Lubos Lunak <l.lunak@suse.cz>
Copyright (c) 2009 Ivo Anjo <knuckles@gmail.com>
Copyright (c) 2020 Kai Uwe Broulik <kde@broulik.de>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
......@@ -19,47 +20,33 @@
#include "freespacenotifier.h"
#include <QDir>
#include <QMenu>
#include <QDBusInterface>
#include <QDebug>
#include <KLocalizedString>
#include <KRun>
#include <KConfigDialog>
#include <KStatusNotifierItem>
#include <KNotification>
#include <KNotificationJobUiDelegate>
#include <KService>
#include <KIO/ApplicationLauncherJob>
#include <KIO/FileSystemFreeSpaceJob>
#include <KIO/OpenUrlJob>
#include <chrono>
#include "settings.h"
#include "ui_freespacenotifier_prefs_base.h"
FreeSpaceNotifier::FreeSpaceNotifier(QObject *parent)
FreeSpaceNotifier::FreeSpaceNotifier(const QString &path, const KLocalizedString &notificationText, QObject *parent)
: QObject(parent)
, m_lastAvailTimer(nullptr)
, m_notification(nullptr)
, m_sni(nullptr)
, m_lastAvail(-1)
, m_path(path)
, m_notificationText(notificationText)
{
// If we are running, notifications are enabled
FreeSpaceNotifierSettings::setEnableNotification(true);
connect(&timer, &QTimer::timeout, this, &FreeSpaceNotifier::checkFreeDiskSpace);
timer.start(1000 * 60 /* 1 minute */);
connect(&m_timer, &QTimer::timeout, this, &FreeSpaceNotifier::checkFreeDiskSpace);
m_timer.start(std::chrono::minutes(1));
}
FreeSpaceNotifier::~FreeSpaceNotifier()
{
// The notification is automatically destroyed when it goes away, so we only need to do this if
// it is still being shown
if (m_notification) {
m_notification->close();
}
if (m_sni) {
m_sni->deleteLater();
}
}
void FreeSpaceNotifier::checkFreeDiskSpace()
......@@ -67,146 +54,109 @@ void FreeSpaceNotifier::checkFreeDiskSpace()
if (!FreeSpaceNotifierSettings::enableNotification()) {
// do nothing if notifying is disabled;
// also stop the timer that probably got us here in the first place
timer.stop();
m_timer.stop();
return;
}
auto *job = KIO::fileSystemFreeSpace(QUrl::fromLocalFile(QDir::homePath()));
auto *job = KIO::fileSystemFreeSpace(QUrl::fromLocalFile(m_path));
connect(job, &KIO::FileSystemFreeSpaceJob::result, this, [this](KIO::Job* job, KIO::filesize_t size, KIO::filesize_t available) {
if (job->error()) {
return;
}
int limit = FreeSpaceNotifierSettings::minimumSpace(); // MiB
qint64 avail = available / (1024 * 1024); // to MiB
bool warn = false;
const int limit = FreeSpaceNotifierSettings::minimumSpace(); // MiB
const qint64 avail = available / (1024 * 1024); // to MiB
if (avail < limit) {
// avail disk space dropped under a limit
if (m_lastAvail < 0 || avail < m_lastAvail / 2) { // always warn the first time or when available dropped to a half of previous one, warn again
m_lastAvail = avail;
warn = true;
} else if (avail > m_lastAvail) { // the user freed some space
m_lastAvail = avail; // so warn if it goes low again
if (m_sni) {
// keep the SNI active, but don't blink
m_sni->setStatus(KStatusNotifierItem::Active);
m_sni->setToolTip(QStringLiteral("drive-harddisk"), i18n("Low Disk Space"), i18n("Remaining space in your Home folder: %1 MiB", QLocale::system().toString(avail)));
}
if (avail >= limit) {
if (m_notification) {
m_notification->close();
}
// do not change lastAvail otherwise, to handle free space slowly going down
return;
}
if (warn) {
int availpct = int(100 * available / size);
if (!m_sni) {
m_sni = new KStatusNotifierItem(QStringLiteral("freespacenotifier"));
m_sni->setIconByName(QStringLiteral("drive-harddisk"));
m_sni->setOverlayIconByName(QStringLiteral("dialog-warning"));
m_sni->setTitle(i18n("Low Disk Space"));
m_sni->setCategory(KStatusNotifierItem::Hardware);
const int availPercent = int(100 * available / size);
const QString text = m_notificationText.subs(avail).subs(availPercent).toString();
QMenu *sniMenu = new QMenu();
QAction *action = new QAction(i18nc("Opens a file manager like dolphin", "Open File Manager..."), nullptr);
connect(action, &QAction::triggered, this, &FreeSpaceNotifier::openFileManager);
sniMenu->addAction(action);
// Make sure the notification text is always up to date whenever we checked free space
if (m_notification) {
m_notification->setText(text);
}
action = new QAction(i18nc("Allows the user to configure the warning notification being shown", "Configure Warning..."), nullptr);
connect(action, &QAction::triggered, this, &FreeSpaceNotifier::showConfiguration);
sniMenu->addAction(action);
// User freed some space, warn if it goes low again
if (m_lastAvail > -1 && avail > m_lastAvail) {
m_lastAvail = avail;
return;
}
action = new QAction(i18nc("Allows the user to hide this notifier item", "Hide"), nullptr);
connect(action, &QAction::triggered, this, &FreeSpaceNotifier::hideSni);
sniMenu->addAction(action);
// Always warn the first time or when available space dropped to half of the previous time
const bool warn = (m_lastAvail < 0 || avail < m_lastAvail / 2);
if (!warn) {
return;
}
m_sni->setContextMenu(sniMenu);
m_sni->setStandardActionsEnabled(false);
}
m_lastAvail = avail;
m_sni->setStatus(KStatusNotifierItem::NeedsAttention);
m_sni->setToolTip(QStringLiteral("drive-harddisk"), i18n("Low Disk Space"), i18n("Remaining space in your Home folder: %1 MiB", QLocale::system().toString(avail)));
if (!m_notification) {
m_notification = new KNotification(QStringLiteral("freespacenotif"));
m_notification->setComponentName(QStringLiteral("freespacenotifier"));
m_notification->setText(text);
m_notification = new KNotification(QStringLiteral("freespacenotif"));
QStringList actions = {i18n("Configure Warning...")};
m_notification->setText(i18nc("Warns the user that the system is running low on space on his home folder, indicating the percentage and absolute MiB size remaining",
"Your Home folder is running out of disk space, you have %1 MiB remaining (%2%)", QLocale::system().toString(avail), availpct));
auto filelight = filelightService();
if (filelight) {
actions.prepend(i18n("Open in Filelight"));
} else {
// Do we really want the user opening Root in a file manager?
actions.prepend(i18n("Open in File Manager"));
}
connect(m_notification, &KNotification::closed, this, &FreeSpaceNotifier::cleanupNotification);
m_notification->setActions(actions);
m_notification->setComponentName(QStringLiteral("freespacenotifier"));
m_notification->sendEvent();
}
} else {
// free space is above limit again, remove the SNI
if (m_sni) {
m_sni->deleteLater();
m_sni = nullptr;
}
}
});
}
connect(m_notification, QOverload<uint>::of(&KNotification::activated), this, [this](uint actionId) {
if (actionId == 1) {
exploreDrive();
// TODO once we have "configure" action support in KNotification, wire it up instead of a button
} else if (actionId == 2) {
emit configureRequested();
}
});
void FreeSpaceNotifier::hideSni()
{
if (m_sni) {
m_sni->setStatus(KStatusNotifierItem::Passive);
QAction *action = qobject_cast<QAction*>(sender());
if (action) {
action->setDisabled(true);
connect(m_notification, &KNotification::closed, this, &FreeSpaceNotifier::onNotificationClosed);
m_notification->sendEvent();
}
}
});
}
void FreeSpaceNotifier::openFileManager()
KService::Ptr FreeSpaceNotifier::filelightService() const
{
cleanupNotification();
new KRun(QUrl::fromLocalFile(QDir::homePath()), nullptr);
if (m_sni) {
m_sni->setStatus(KStatusNotifierItem::Active);
}
return KService::serviceByDesktopName(QStringLiteral("org.kde.filelight"));
}
void FreeSpaceNotifier::showConfiguration()
void FreeSpaceNotifier::exploreDrive()
{
cleanupNotification();
if (KConfigDialog::showDialog(QStringLiteral("settings"))) {
auto service = filelightService();
if (!service) {
auto *job = new KIO::OpenUrlJob({QUrl::fromLocalFile(m_path)});
job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled));
job->start();
return;
}
KConfigDialog *dialog = new KConfigDialog(nullptr, QStringLiteral("settings"), FreeSpaceNotifierSettings::self());
QWidget *generalSettingsDlg = new QWidget();
Ui::freespacenotifier_prefs_base preferences;
preferences.setupUi(generalSettingsDlg);
dialog->addPage(generalSettingsDlg,
i18nc("The settings dialog main page name, as in 'general settings'", "General"),
QStringLiteral("system-run"));
connect(dialog, &KConfigDialog::finished, this, &FreeSpaceNotifier::configDialogClosed);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->show();
if (m_sni) {
m_sni->setStatus(KStatusNotifierItem::Active);
}
auto *job = new KIO::ApplicationLauncherJob(service);
job->setUrls({QUrl::fromLocalFile(m_path)});
job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled));
job->start();
}
void FreeSpaceNotifier::cleanupNotification()
void FreeSpaceNotifier::onNotificationClosed()
{
if (m_notification) {
m_notification->close();
}
m_notification = nullptr;
// warn again if constantly below limit for too long
if (!m_lastAvailTimer) {
m_lastAvailTimer = new QTimer(this);
connect(m_lastAvailTimer, &QTimer::timeout, this, &FreeSpaceNotifier::resetLastAvailable);
}
m_lastAvailTimer->start(1000 * 60 * 60 /* 1 hour*/);
m_lastAvailTimer->start(std::chrono::hours(1));
}
void FreeSpaceNotifier::resetLastAvailable()
......@@ -215,45 +165,3 @@ void FreeSpaceNotifier::resetLastAvailable()
m_lastAvailTimer->deleteLater();
m_lastAvailTimer = nullptr;
}
void FreeSpaceNotifier::configDialogClosed()
{
if (!FreeSpaceNotifierSettings::enableNotification()) {
disableFSNotifier();
}
}
/* The idea here is to disable ourselves by telling kded to stop autostarting us, and
* to kill the current running instance.
*/
void FreeSpaceNotifier::disableFSNotifier()
{
QDBusInterface iface(QStringLiteral("org.kde.kded5"),
QStringLiteral("/kded"),
QStringLiteral("org.kde.kded5") );
if (dbusError(iface)) {
return;
}
// Disable current module autoload
iface.call(QStringLiteral("setModuleAutoloading"), QStringLiteral("freespacenotifier"), false);
if (dbusError(iface)) {
return;
}
// Unload current module
iface.call(QStringLiteral("unloadModule"), QStringLiteral("freespacenotifier"));
if (dbusError(iface)) {
return;
}
}
bool FreeSpaceNotifier::dbusError(QDBusInterface &iface)
{
const QDBusError err = iface.lastError();
if (err.isValid()) {
qCritical() << "Failed to perform operation on kded [" << err.name() << "]:" << err.message();
return true;
}
return false;
}
......@@ -2,6 +2,7 @@
Copyright (c) 2006 Lukas Tinkl <ltinkl@suse.cz>
Copyright (c) 2008 Lubos Lunak <l.lunak@suse.cz>
Copyright (c) 2009 Ivo Anjo <knuckles@gmail.com>
Copyright (c) 2020 Kai Uwe Broulik <kde@broulik.de>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
......@@ -21,37 +22,41 @@
#define _FREESPACENOTIFIER_H_
#include <QTimer>
#include <QPointer>
#include <KLocalizedString>
#include <KService>
class KNotification;
class KStatusNotifierItem;
class QDBusInterface;
class FreeSpaceNotifier : public QObject
{
Q_OBJECT
public:
explicit FreeSpaceNotifier(QObject *parent = nullptr);
explicit FreeSpaceNotifier(const QString &path, const KLocalizedString &notificationText, QObject *parent = nullptr);
~FreeSpaceNotifier() override;
private Q_SLOTS:
signals:
void configureRequested();
private:
void checkFreeDiskSpace();
void resetLastAvailable();
void openFileManager();
void showConfiguration();
void cleanupNotification();
void configDialogClosed();
void hideSni();
private:
QTimer timer;
QTimer *m_lastAvailTimer;
KNotification *m_notification;
KStatusNotifierItem *m_sni;
qint64 m_lastAvail; // used to suppress repeated warnings when available space hasn't changed
void disableFSNotifier();
bool dbusError(QDBusInterface &iface);
KService::Ptr filelightService() const;
void exploreDrive();
void onNotificationClosed();
QString m_path;
KLocalizedString m_notificationText;
QTimer m_timer;
QTimer *m_lastAvailTimer = nullptr;
QPointer<KNotification> m_notification;
qint64 m_lastAvail = -1; // used to suppress repeated warnings when available space hasn't changed
};
#endif
......@@ -416,3 +416,4 @@ Comment[zh_CN]=您现在的磁盘空间过低
Comment[zh_TW]=您的磁碟空間快用完了
Contexts=warningnot
Action=Popup
Urgency=Critical
......@@ -19,13 +19,75 @@
#include "module.h"
#include <KConfigDialog>
#include <KMountPoint>
#include <KPluginFactory>
#include <QDir>
#include "kded_interface.h"
#include "ui_freespacenotifier_prefs_base.h"
#include "settings.h"
K_PLUGIN_CLASS_WITH_JSON(FreeSpaceNotifierModule, "freespacenotifier.json")
FreeSpaceNotifierModule::FreeSpaceNotifierModule(QObject* parent, const QList<QVariant>&)
: KDEDModule(parent)
{
// If the module is loaded, notifications are enabled
FreeSpaceNotifierSettings::setEnableNotification(true);
const QString rootPath = QStringLiteral("/");
const QString homePath = QDir::homePath();
auto *homeNotifier = new FreeSpaceNotifier(homePath,
ki18n("Your Home folder is running out of disk space, you have %1 MiB remaining (%2%)."),
this);
connect(homeNotifier, &FreeSpaceNotifier::configureRequested, this, &FreeSpaceNotifierModule::showConfiguration);
// If Home is on a separate partition from Root, warn for it, too.
auto homeMountPoint = KMountPoint::currentMountPoints().findByPath(homePath);
if (!homeMountPoint || homeMountPoint->mountPoint() != rootPath) {
auto *rootNotifier = new FreeSpaceNotifier(rootPath,
ki18n("Your Root partition is running out of disk space, you have %1 MiB remaining (%2%)."),
this);
connect(rootNotifier, &FreeSpaceNotifier::configureRequested, this, &FreeSpaceNotifierModule::showConfiguration);
}
}
void FreeSpaceNotifierModule::showConfiguration()
{
if (KConfigDialog::showDialog(QStringLiteral("settings"))) {
return;
}
KConfigDialog *dialog = new KConfigDialog(nullptr, QStringLiteral("settings"), FreeSpaceNotifierSettings::self());
QWidget *generalSettingsDlg = new QWidget();
Ui::freespacenotifier_prefs_base preferences;
preferences.setupUi(generalSettingsDlg);
dialog->addPage(generalSettingsDlg,
i18nc("The settings dialog main page name, as in 'general settings'", "General"),
QStringLiteral("system-run"));
connect(dialog, &KConfigDialog::finished, this, [this] {
if (!FreeSpaceNotifierSettings::enableNotification()) {
// The idea here is to disable ourselves by telling kded to stop autostarting us, and
// to kill the current running instance.
org::kde::kded5 kded(QStringLiteral("org.kde.kded5"),
QStringLiteral("/kded"),
QDBusConnection::sessionBus());
kded.setModuleAutoloading(QStringLiteral("freespacenotifier"), false);
kded.unloadModule(QStringLiteral("freespacenotifier"));
}
});
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->show();
}
#include "module.moc"
......@@ -31,8 +31,10 @@ class FreeSpaceNotifierModule
Q_OBJECT
public:
FreeSpaceNotifierModule(QObject* parent, const QList<QVariant>&);
private:
FreeSpaceNotifier notifier;
void showConfiguration();
};
#endif
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment