diff --git a/PowerDevilSettings.kcfg b/PowerDevilSettings.kcfg --- a/PowerDevilSettings.kcfg +++ b/PowerDevilSettings.kcfg @@ -31,5 +31,8 @@ 2 + + 10 + diff --git a/daemon/powerdevilcore.h b/daemon/powerdevilcore.h --- a/daemon/powerdevilcore.h +++ b/daemon/powerdevilcore.h @@ -63,7 +63,10 @@ void emitNotification(const QString &evid, const QString &message = QString(), const QString &iconname = QString()); void emitRichNotification(const QString &evid, const QString &title, const QString &message = QString()); - bool emitBatteryChargePercentNotification(int currentPercent, int previousPercent); + + void emitNotification(const QString &eventId, const QString &title, const QString &message, const QString &iconName); + + bool emitBatteryChargePercentNotification(int currentPercent, int previousPercent, const QString &udi = QString()); BackendInterface *backend(); @@ -106,16 +109,17 @@ friend class Action; BackendInterface *m_backend; - QStringList m_loadedBatteriesUdi; QDBusServiceWatcher *m_notificationsWatcher; + bool m_notificationsReady = false; KSharedConfigPtr m_profilesConfig; QString m_currentProfile; - QHash< QString, int > m_batteriesPercent; - QHash< QString, bool > m_batteriesCharged; + QHash m_batteriesPercent; + QHash m_peripheralBatteriesPercent; + QHash m_batteriesCharged; QTimer *m_criticalBatteryTimer; QPointer m_criticalBatteryNotification; diff --git a/daemon/powerdevilcore.cpp b/daemon/powerdevilcore.cpp --- a/daemon/powerdevilcore.cpp +++ b/daemon/powerdevilcore.cpp @@ -239,7 +239,7 @@ profileId = activity; } else { // It doesn't, let's load the current state's profile - if (m_loadedBatteriesUdi.isEmpty()) { + if (m_batteriesPercent.isEmpty()) { qCDebug(POWERDEVIL) << "No batteries found, loading AC"; profileId = "AC"; } else if (activityConfig.readEntry("mode", "None") == "ActLike") { @@ -377,83 +377,75 @@ } } -void Core::onDeviceAdded(const QString& udi) +void Core::onDeviceAdded(const QString &udi) { - if (m_loadedBatteriesUdi.contains(udi)) { + if (m_batteriesPercent.contains(udi) || m_peripheralBatteriesPercent.contains(udi)) { // We already know about this device return; } using namespace Solid; Device device(udi); - Battery *b = qobject_cast(device.asDeviceInterface(DeviceInterface::Battery)); + Battery *b = qobject_cast(device.asDeviceInterface(DeviceInterface::Battery)); if (!b) { - // Not interesting to us return; } - if (b->type() != Solid::Battery::PrimaryBattery && b->type() != Solid::Battery::UpsBattery) { - // Not interesting to us - return; - } - - if (!b->isPowerSupply()) { - // TODO: At some later point it would be really nice to handle those batteries too - // eg, show "your mouse is running low", but in the mean time, we don't care about those - return; - } - - if (!connect(b, SIGNAL(chargePercentChanged(int,QString)), - this, SLOT(onBatteryChargePercentChanged(int,QString))) || - !connect(b, SIGNAL(chargeStateChanged(int,QString)), - this, SLOT(onBatteryChargeStateChanged(int,QString)))) { + if (!connect(b, &Battery::chargePercentChanged, this, &Core::onBatteryChargePercentChanged) || + !connect(b, &Battery::chargeStateChanged, this, &Core::onBatteryChargeStateChanged)) { emitNotification("powerdevilerror", i18n("Could not connect to battery interface.\n" "Please check your system configuration")); } - qCDebug(POWERDEVIL) << "A new battery was detected"; + qCDebug(POWERDEVIL) << "Battery with UDI" << udi << "was detected"; - m_batteriesPercent[udi] = b->chargePercent(); - m_batteriesCharged[udi] = (b->chargeState() == Solid::Battery::FullyCharged); - m_loadedBatteriesUdi.append(udi); + if (b->isPowerSupply()) { + m_batteriesPercent[udi] = b->chargePercent(); + m_batteriesCharged[udi] = (b->chargeState() == Solid::Battery::FullyCharged); + } else { // non-power supply batteries are treated separately + m_peripheralBatteriesPercent[udi] = b->chargePercent(); - const int chargePercent = currentChargePercent(); + // notify the user about the empty mouse/keyboard when plugging it in; don't when + // notifications aren't ready yet so we avoid showing them ontop of ksplash; + // also we'll notify about all devices when notifications are ready anyway + if (m_notificationsReady) { + emitBatteryChargePercentNotification(b->chargePercent(), 1000 /* so current is always lower than previous */, udi); + } + } // If a new battery has been added, let's clear some pending suspend actions if the new global batteries percentage is // higher than the battery critical level. (See bug 329537) - if (m_criticalBatteryTimer->isActive() && chargePercent > PowerDevilSettings::batteryCriticalLevel()) { + if (m_criticalBatteryTimer->isActive() && currentChargePercent() > PowerDevilSettings::batteryCriticalLevel()) { m_criticalBatteryTimer->stop(); if (m_criticalBatteryNotification) { m_criticalBatteryNotification->close(); } emitRichNotification("pluggedin", i18n("Extra Battery Added"), - i18n("All pending suspend actions have been canceled.")); + i18n("All pending suspend actions have been canceled.")); // FIXME This wording is too technical } } -void Core::onDeviceRemoved(const QString& udi) +void Core::onDeviceRemoved(const QString &udi) { - if (!m_loadedBatteriesUdi.contains(udi)) { + if (!m_batteriesPercent.contains(udi) && !m_peripheralBatteriesPercent.contains(udi)) { // We don't know about this device return; } using namespace Solid; Device device(udi); - Battery *b = qobject_cast(device.asDeviceInterface(DeviceInterface::Battery)); + Battery *b = qobject_cast(device.asDeviceInterface(DeviceInterface::Battery)); - disconnect(b, SIGNAL(chargePercentChanged(int,QString)), - this, SLOT(onBatteryChargePercentChanged(int,QString))); - disconnect(b, SIGNAL(chargeStateChanged(int,QString)), - this, SLOT(onBatteryChargeStateChanged(int,QString))); + disconnect(b, &Battery::chargePercentChanged, this, &Core::onBatteryChargePercentChanged); + disconnect(b, &Battery::chargeStateChanged, this, &Core::onBatteryChargeStateChanged); - qCDebug(POWERDEVIL) << "An existing battery has been removed"; + qCDebug(POWERDEVIL) << "Battery with UDI" << udi << "has been removed"; m_batteriesPercent.remove(udi); + m_peripheralBatteriesPercent.remove(udi); m_batteriesCharged.remove(udi); - m_loadedBatteriesUdi.removeOne(udi); } void Core::emitNotification(const QString &evid, const QString &message, const QString &iconname) @@ -467,14 +459,74 @@ } } +void Core::emitNotification(const QString &eventId, const QString &title, const QString &message, const QString &iconName) +{ + KNotification::event(eventId, title, message, iconName, 0, KNotification::CloseOnTimeout, "powerdevil"); +} + void Core::emitRichNotification(const QString &evid, const QString &title, const QString &message) { KNotification::event(evid, title, message, QPixmap(), 0, KNotification::CloseOnTimeout, "powerdevil"); } -bool Core::emitBatteryChargePercentNotification(int currentPercent, int previousPercent) +bool Core::emitBatteryChargePercentNotification(int currentPercent, int previousPercent, const QString &udi) { + using namespace Solid; + Device device(udi); + Battery *b = qobject_cast(device.asDeviceInterface(DeviceInterface::Battery)); + + if (b && !b->isPowerSupply()) { + // if you leave the device out of reach or it has not been initialized yet + // it won't be "there" and report 0%, don't show anything in this case + if (!b->isPresent()) { + return false; + } + + if (currentPercent <= PowerDevilSettings::peripheralBatteryLowLevel() && + previousPercent > PowerDevilSettings::peripheralBatteryLowLevel()) { + + QString name = device.product(); + if (!device.vendor().isEmpty()) { + name = i18nc("%1 is vendor name, %2 is product name", "%1 %2", device.vendor(), device.product()); + } + + QString title; + QString msg; + QString icon; + + switch(b->type()) { + case Battery::MouseBattery: + title = i18n("Mouse Battery Low (%1% Remaining)", currentPercent); + msg = i18nc("Placeholder is device name", + "The battery in your mouse (\"%1\") is low, and the device may turn itself off at any time. " + "Please replace or charge the battery as soon as possible.", name); + icon = QStringLiteral("input-mouse"); + break; + case Battery::KeyboardBattery: + title = i18n("Keyboard Battery Low (%1% Remaining)", currentPercent); + msg = i18nc("Placeholder is device name", + "The battery in your keyboard (\"%1\") is low, and the device may turn itself off at any time. " + "Please replace or charge the battery as soon as possible.", name); + icon = QStringLiteral("input-keyboard"); + break; + default: + title = i18nc("The battery in an external device", "Device Battery Low (%1% Remaining)", currentPercent); + msg = i18nc("Placeholder is device name", + "The battery in a connected device (\"%1\") is low, and the device may turn itself off at any time. " + "Please replace or charge the battery as soon as possible.", name); + icon = QStringLiteral("battery-caution"); + break; + } + + emitNotification("lowperipheralbattery", title, msg, icon); + + return true; + } + + return false; + } + if (m_backend->acAdapterState() == BackendInterface::Plugged) { return false; } @@ -565,23 +617,37 @@ void Core::onBatteryChargePercentChanged(int percent, const QString &udi) { + if (m_peripheralBatteriesPercent.contains(udi)) { + const int previousPercent = m_peripheralBatteriesPercent.value(udi); + m_peripheralBatteriesPercent[udi] = percent; + + if (percent < previousPercent) { + emitBatteryChargePercentNotification(percent, previousPercent, udi); + } + return; + } + // Compute the previous and current global percentage const int previousPercent = currentChargePercent(); const int currentPercent = previousPercent - (m_batteriesPercent[udi] - percent); // Update the battery percentage m_batteriesPercent[udi] = percent; if (currentPercent < previousPercent) { - if (emitBatteryChargePercentNotification(currentPercent, previousPercent)) { + if (emitBatteryChargePercentNotification(currentPercent, previousPercent, udi)) { // Only refresh status if a notification has actually been emitted loadProfile(); } } } void Core::onBatteryChargeStateChanged(int state, const QString &udi) { + if (!m_batteriesCharged.contains(udi)) { + return; + } + bool previousCharged = true; for (auto i = m_batteriesCharged.constBegin(); i != m_batteriesCharged.constEnd(); ++i) { if (!i.value()) { @@ -712,19 +778,31 @@ { Q_UNUSED(service); - static bool notificationsReady = false; - if (notificationsReady) { + if (m_notificationsReady) { return; } + bool needsRefresh = false; + // show warning about low batteries right on session startup, force it to show // by making sure the "old" percentage (that magic number) is always higher than the current one if (emitBatteryChargePercentNotification(currentChargePercent(), 1000)) { - // need to refresh status to prevent the notification from showing again when charge percentage changes + needsRefresh = true; + } + + // now also emit notifications for all peripheral batteries + for (auto it = m_peripheralBatteriesPercent.constBegin(), end = m_peripheralBatteriesPercent.constEnd(); it != end; ++it) { + if (emitBatteryChargePercentNotification(it.value() /*currentPercent*/, 1000, it.key() /*udi*/)) { + needsRefresh = true; + } + } + + // need to refresh status to prevent the notification from showing again when charge percentage changes + if (needsRefresh) { refreshStatus(); } - notificationsReady = true; + m_notificationsReady = true; if (m_notificationsWatcher) { delete m_notificationsWatcher; diff --git a/kcmodule/global/GeneralPage.cpp b/kcmodule/global/GeneralPage.cpp --- a/kcmodule/global/GeneralPage.cpp +++ b/kcmodule/global/GeneralPage.cpp @@ -89,13 +89,15 @@ void GeneralPage::fillUi() { - bool hasBattery = false; + bool hasPowerSupplyBattery = false; + bool hasPeripheralBattery = false; - Q_FOREACH(const Solid::Device &device, Solid::Device::listFromType(Solid::DeviceInterface::Battery, QString())) { + Q_FOREACH (const Solid::Device &device, Solid::Device::listFromType(Solid::DeviceInterface::Battery, QString())) { const Solid::Battery *b = qobject_cast (device.asDeviceInterface(Solid::DeviceInterface::Battery)); - if(b->type() == Solid::Battery::PrimaryBattery || b->type() == Solid::Battery::UpsBattery) { - hasBattery = true; - break; + if (b->isPowerSupply()) { + hasPowerSupplyBattery = true; + } else { + hasPeripheralBattery = true; } } @@ -118,26 +120,34 @@ connect(lowSpin, SIGNAL(valueChanged(int)), SLOT(changed())); connect(criticalSpin, SIGNAL(valueChanged(int)), SLOT(changed())); + connect(lowPeripheralSpin, SIGNAL(valueChanged(int)), SLOT(changed())); connect(BatteryCriticalCombo, SIGNAL(currentIndexChanged(int)), SLOT(changed())); - // Disable stuff, eventually - if (!hasBattery) { - batteryLevelsLabel->hide(); - + if (!hasPowerSupplyBattery) { BatteryCriticalLabel->hide(); BatteryCriticalCombo->hide(); lowLabel->hide(); lowSpin->hide(); criticalLabel->hide(); criticalSpin->hide(); } + + if (!hasPeripheralBattery) { + lowPeripheralLabel->hide(); + lowPeripheralSpin->hide(); + } + + if (!hasPeripheralBattery && !hasPeripheralBattery) { + batteryLevelsLabel->hide(); + } } void GeneralPage::load() { lowSpin->setValue(PowerDevilSettings::batteryLowLevel()); criticalSpin->setValue(PowerDevilSettings::batteryCriticalLevel()); + lowPeripheralSpin->setValue(PowerDevilSettings::peripheralBatteryLowLevel()); BatteryCriticalCombo->setCurrentIndex(BatteryCriticalCombo->findData(PowerDevilSettings::batteryCriticalAction())); } @@ -151,6 +161,7 @@ { PowerDevilSettings::setBatteryLowLevel(lowSpin->value()); PowerDevilSettings::setBatteryCriticalLevel(criticalSpin->value()); + PowerDevilSettings::setPeripheralBatteryLowLevel(lowPeripheralSpin->value()); PowerDevilSettings::setBatteryCriticalAction(BatteryCriticalCombo->itemData(BatteryCriticalCombo->currentIndex()).toInt()); diff --git a/kcmodule/global/generalPage.ui b/kcmodule/global/generalPage.ui --- a/kcmodule/global/generalPage.ui +++ b/kcmodule/global/generalPage.ui @@ -113,7 +113,7 @@ - + Qt::Vertical @@ -126,7 +126,7 @@ - + @@ -145,6 +145,23 @@ + + + + Low level for peripheral devices: + + + + + + + % + + + 100 + + + diff --git a/powerdevil.notifyrc b/powerdevil.notifyrc --- a/powerdevil.notifyrc +++ b/powerdevil.notifyrc @@ -650,6 +650,14 @@ Action= IconName=battery-100 +[Event/lowperipheralbattery] +Name=Peripheral Battery Low +Comment=The battery in a connected device, such as mouse or keyboard, is low +Contexts=warningnot +Sound=Oxygen-Sys-Warning.ogg +Action=Sound|Popup +IconName=battery-caution + [Event/pluggedin] Name=AC adaptor plugged in Name[ar]=وُصِلَ محوِّل الكهرباء