mirror of https://github.com/cutefishos/settings
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
276 lines
10 KiB
C++
276 lines
10 KiB
C++
// SPDX-FileCopyrightText: 2026 Reion Wong <reionwong@gmail.com>
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "base/event_loop.h"
|
|
#include "base/timer.h"
|
|
#include "pages/battery/battery.h"
|
|
#include "pages/battery/batteryhistory.h"
|
|
#include "pages/battery/historygraph.h"
|
|
#include "ui/label.h"
|
|
#include "ui/layout.h"
|
|
#include "ui/cell_layout.h"
|
|
#include "ui/segmented_control.h"
|
|
#include "ui/theme.h"
|
|
#include "cutefish/upower/battery_status.h"
|
|
#include "widgets/card.h"
|
|
#include "widgets/listrow.h"
|
|
#include "widgets/metrics.h"
|
|
#include "widgets/sections.h"
|
|
#include "window/options.h"
|
|
#include "window/page.h"
|
|
#include "window/pagelist.h"
|
|
#include "window/settingswindow.h"
|
|
|
|
using namespace cutefish;
|
|
using namespace cutefish::ui;
|
|
|
|
namespace settings {
|
|
|
|
namespace {
|
|
|
|
constexpr uint32_t kHistoryDurations[] = {86400, 7 * 86400};
|
|
|
|
// The section title with the range picker at its end: BatterySection.qml's
|
|
// RowLayout.
|
|
class HistoryTitle : public RowLayout
|
|
{
|
|
public:
|
|
HistoryTitle()
|
|
{
|
|
setSpacing(metrics::m);
|
|
setLayoutHints({.fillWidth = true});
|
|
m_title = addChild<SectionLabel>("History");
|
|
m_title->setName("battery:historytitle");
|
|
m_picker = addChild<SegmentedControl>(std::vector<std::string>{"24 hours", "7 days"});
|
|
// Compact, to sit in the title row.
|
|
m_picker->setContentHeight(28);
|
|
}
|
|
|
|
SegmentedControl* picker() const { return m_picker; }
|
|
|
|
protected:
|
|
void layout() override
|
|
{
|
|
// Two equal segments, each wide enough for the longer label. A whole
|
|
// width, as the right edge must meet the card's.
|
|
const double widest = std::max(m_picker->implicitItemWidth(0), m_picker->implicitItemWidth(1));
|
|
const double width = std::ceil(widest * 2 + metrics::l * 4 + m_picker->padding() * 2);
|
|
if (m_picker->layoutHints().preferredWidth != width)
|
|
m_picker->setLayoutHints({.preferredWidth = width});
|
|
RowLayout::layout();
|
|
}
|
|
|
|
private:
|
|
SectionLabel* m_title;
|
|
SegmentedControl* m_picker;
|
|
};
|
|
|
|
class BatteryPage : public Page
|
|
{
|
|
public:
|
|
explicit BatteryPage(SettingsWindow& window)
|
|
: Page(window, "Battery")
|
|
{
|
|
// History.
|
|
m_historySection = content()->addChild<ColumnLayout>();
|
|
m_historySection->setName("battery:history");
|
|
m_historySection->setSpacing(metrics::sectionLabelSpacing);
|
|
m_historySection->setLayoutHints({.fillWidth = true});
|
|
HistoryTitle* title = m_historySection->addChild<HistoryTitle>();
|
|
m_rangePicker = title->picker();
|
|
m_rangePicker->setName("battery:range");
|
|
m_rangePicker->onCurrentChanged = [this](int index) {
|
|
BatteryHistory::Query query = m_history.query();
|
|
query.duration = kHistoryDurations[index];
|
|
m_history.load(query);
|
|
};
|
|
Card* graphCard = m_historySection->addChild<Card>();
|
|
graphCard->setSpacing(0);
|
|
graphCard->setName("battery:graphcard");
|
|
m_graph = graphCard->addChild<HistoryGraph>();
|
|
m_graph->setName("battery:graph");
|
|
// `height: 240` on an item without an implicit size.
|
|
m_graph->setLayoutHints({.fillWidth = true, .fallbackHeight = 240});
|
|
m_graph->setValueAxis(100, 25, "%", 0);
|
|
m_graph->setDuration(m_plottedDuration);
|
|
|
|
// Health.
|
|
m_health = content()->addChild<Section>("Health");
|
|
m_health->setName("battery:health");
|
|
Card* health = m_health->addChild<Card>(Card::Style::List);
|
|
m_condition = health->addChild<InfoRow>("Condition");
|
|
m_capacity = health->addChild<InfoRow>("Maximum capacity");
|
|
m_lastCharged = health->addChild<InfoRow>();
|
|
m_lastCharged->setName("battery:lastcharged");
|
|
|
|
// Charging.
|
|
m_charging = content()->addChild<Section>("Charging");
|
|
m_charging->setName("battery:charging");
|
|
Card* charging = m_charging->addChild<Card>(Card::Style::List);
|
|
m_chargeLimit = charging->addChild<SwitchRow>();
|
|
m_chargeLimit->setName("battery:chargelimit");
|
|
m_chargeLimit->onToggled = [this](bool checked) { m_battery.setChargeLimitEnabled(checked); };
|
|
m_chargeLimitNote = m_charging->addChild<Footnote>();
|
|
m_chargeLimitNote->setName("battery:chargelimitnote");
|
|
|
|
Card* percentage = content()->addChild<Card>(Card::Style::List);
|
|
SwitchRow* showPercent = percentage->addChild<SwitchRow>("Show percentage in status bar");
|
|
showPercent->setChecked(m_battery.showPercent());
|
|
showPercent->onToggled = [this](bool checked) { m_battery.setPercentEnabled(checked); };
|
|
|
|
m_subscriptions.push_back(m_battery.capacityChanged.connect([this] { updateHealth(); }));
|
|
m_subscriptions.push_back(m_battery.lastChargedChanged.connect([this] { updateHealth(); }));
|
|
m_subscriptions.push_back(m_battery.chargeLimitChanged.connect([this] { updateChargeLimit(); }));
|
|
m_subscriptions.push_back(m_battery.udiChanged.connect([this] {
|
|
BatteryHistory::Query query = m_history.query();
|
|
query.device = m_battery.udi();
|
|
m_history.load(query);
|
|
}));
|
|
m_subscriptions.push_back(m_battery.availableChanged.connect([this] { updateRefreshTimer(); }));
|
|
m_subscriptions.push_back(window.activeChanged.connect([this] { updateRefreshTimer(); }));
|
|
m_subscriptions.push_back(m_history.samplesChanged.connect([this] { historyChanged(); }));
|
|
|
|
// Picks up new samples and moves the plot's right edge along with the
|
|
// clock, only while the page is in front.
|
|
m_refresh.onTimeout = [this] { m_history.refresh(); };
|
|
|
|
m_history.load({m_battery.udi(), kHistoryDurations[0], BatteryHistory::Type::Charge});
|
|
|
|
m_historySection->setVisible(false);
|
|
updateHealth();
|
|
updateChargeLimit();
|
|
updateRefreshTimer();
|
|
}
|
|
|
|
private:
|
|
void historyChanged()
|
|
{
|
|
// The range the plotted samples cover follows the picker only once
|
|
// they arrive, so the old samples are never drawn across the new range.
|
|
m_plottedDuration = m_history.query().duration;
|
|
std::vector<HistoryGraph::Point> points;
|
|
points.reserve(m_history.samples().size());
|
|
for (const auto& sample : m_history.samples())
|
|
points.push_back({double(sample.time), sample.value, sample.state});
|
|
m_graph->setPoints(std::move(points));
|
|
m_graph->setEnd(m_history.samples().empty() ? 0 : m_history.samples().back().time);
|
|
m_graph->setDuration(m_plottedDuration);
|
|
m_historySection->setVisible(m_history.samples().size() > 2);
|
|
}
|
|
|
|
void updateRefreshTimer()
|
|
{
|
|
const bool running = m_battery.isAvailable() && settingsWindow().isActive();
|
|
if (running == m_refresh.isActive())
|
|
return;
|
|
if (!running) {
|
|
m_refresh.stop();
|
|
return;
|
|
}
|
|
m_refresh.start(std::chrono::minutes(1));
|
|
// The history loads by itself at first, so only a return needs a refresh.
|
|
if (!m_history.samples().empty())
|
|
m_history.refresh();
|
|
}
|
|
|
|
void updateHealth()
|
|
{
|
|
const int capacity = m_battery.capacity();
|
|
m_health->setVisible(capacity != 0);
|
|
m_condition->setValue(capacity >= 90 ? "Excellent" : capacity >= 80 ? "Normal" : "Poor");
|
|
m_capacity->setValue(std::to_string(capacity) + "%");
|
|
const int lastCharged = m_battery.lastChargedPercent();
|
|
m_lastCharged->setText("Last charged to " + std::to_string(lastCharged) + "%");
|
|
m_lastCharged->setValue(m_battery.lastChargedTime());
|
|
m_lastCharged->setVisible(lastCharged != 0);
|
|
}
|
|
|
|
void updateChargeLimit()
|
|
{
|
|
m_charging->setVisible(m_battery.chargeLimitSupported());
|
|
m_chargeLimit->setText("Limit charging to " + std::to_string(m_battery.chargeLimitEnd()) + "%");
|
|
m_chargeLimit->setChecked(m_battery.chargeLimitEnabled());
|
|
updateChargeLimitNote();
|
|
}
|
|
|
|
void updateChargeLimitNote()
|
|
{
|
|
const bool held = m_battery.chargeLimitHeld();
|
|
m_chargeLimitNote->setText(
|
|
held ? "While plugged in, charging stops at " + std::to_string(m_battery.chargeLimitEnd())
|
|
+ "% and resumes below " + std::to_string(m_battery.chargeLimitStart())
|
|
+ "%, so the battery wears more slowly."
|
|
: "This computer's battery controller did not keep the limit, so the battery still "
|
|
"charges fully.");
|
|
m_chargeLimitNote->setColor(held ? std::nullopt : std::optional<Color>(accent::orange));
|
|
}
|
|
|
|
Battery m_battery;
|
|
BatteryHistory m_history;
|
|
base::Timer m_refresh;
|
|
double m_plottedDuration = kHistoryDurations[0];
|
|
std::vector<base::Subscription> m_subscriptions;
|
|
|
|
ColumnLayout* m_historySection;
|
|
SegmentedControl* m_rangePicker;
|
|
HistoryGraph* m_graph;
|
|
Section* m_health;
|
|
InfoRow* m_condition;
|
|
InfoRow* m_capacity;
|
|
InfoRow* m_lastCharged;
|
|
Section* m_charging;
|
|
SwitchRow* m_chargeLimit;
|
|
Footnote* m_chargeLimitNote;
|
|
};
|
|
|
|
// The page is there while cutefish-services reports a battery; until the
|
|
// service has answered, there is none either way.
|
|
class BatteryWatch : public AvailabilityWatch
|
|
{
|
|
public:
|
|
explicit BatteryWatch(AvailabilityCallback callback)
|
|
: m_callback(std::move(callback))
|
|
{
|
|
if (options().mockBattery) {
|
|
answerLater(true);
|
|
return;
|
|
}
|
|
m_status = upower::BatteryStatus::shared();
|
|
if (m_status->isReady()) {
|
|
answerLater(m_status->isAvailable());
|
|
return;
|
|
}
|
|
m_ready = m_status->readyChanged.connect([this] { m_callback(m_status->isAvailable()); });
|
|
}
|
|
|
|
private:
|
|
// Not from inside the watch's creation, which the window is still busy with.
|
|
void answerLater(bool available)
|
|
{
|
|
m_answer.setSingleShot(true);
|
|
m_answer.onTimeout = [this, available] { m_callback(available); };
|
|
m_answer.start(std::chrono::milliseconds(0));
|
|
}
|
|
|
|
AvailabilityCallback m_callback;
|
|
std::shared_ptr<upower::BatteryStatus> m_status;
|
|
base::Subscription m_ready;
|
|
base::Timer m_answer;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
static const PageRegistration registration(
|
|
"battery", [](SettingsWindow& window) -> std::unique_ptr<Page> { return std::make_unique<BatteryPage>(window); },
|
|
[](SettingsWindow&, AvailabilityCallback callback) -> std::unique_ptr<AvailabilityWatch> {
|
|
return std::make_unique<BatteryWatch>(std::move(callback));
|
|
});
|
|
|
|
} // namespace settings
|