22900737b0
- 新增 TextColorPicker.qml 颜色选择器组件:预设 8 色 + 跟随系统选项, 2px 深色边框高亮选中态,白色圆角卡片与设置窗口其他区视觉一致 - C++ 后端新增 textColor Q_PROPERTY:QString 存储,空串=跟随系统主题, 非空用 QColor 校验,写入 ~/.config/jnetapplet/settings.ini 持久化 - networkview.qml 新增 speedTextColor 属性回退逻辑:applet.textColor 优先,空串时回退到 DockPalette 派生的 primaryText(深浅色自适应) - 任务栏 4 处数值 Text(横向/竖向 × 下载/上传)color 绑定改为 speedTextColor;设置窗口网络接口区下方新增字体颜色选择区
465 lines
15 KiB
C++
465 lines
15 KiB
C++
// SPDX-FileCopyrightText: 2024 MyCompany
|
||
//
|
||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||
|
||
#include "networkmonitorapplet.h"
|
||
#include <pluginfactory.h>
|
||
|
||
#include <QFile>
|
||
#include <QTextStream>
|
||
#include <QDebug>
|
||
#include <QDir>
|
||
#include <QRegularExpression>
|
||
#include <QNetworkInterface>
|
||
#include <QColor>
|
||
#include <QSettings>
|
||
#include <QStandardPaths>
|
||
|
||
DS_BEGIN_NAMESPACE
|
||
|
||
NetworkMonitorApplet::NetworkMonitorApplet(QObject *parent)
|
||
: DApplet(parent)
|
||
, m_refreshTimer(nullptr)
|
||
, m_lastRxBytes(0)
|
||
, m_lastTxBytes(0)
|
||
, m_downloadSpeed(0)
|
||
, m_uploadSpeed(0)
|
||
, m_totalDownload(0)
|
||
, m_totalUpload(0)
|
||
, m_ready(false)
|
||
, m_firstUpdate(true)
|
||
, m_textColor()
|
||
{
|
||
// 从独立配置文件读取持久化的字体颜色
|
||
// 设计原因:使用独立配置文件避免污染 dde-shell 的共享配置,
|
||
// 空串表示"跟随系统",由 QML 层回退到 primaryText 实现
|
||
const QString configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)
|
||
+ QStringLiteral("/jnetapplet/settings.ini");
|
||
QSettings settings(configPath, QSettings::IniFormat);
|
||
if (settings.contains(QStringLiteral("textColor"))) {
|
||
const QString saved = settings.value(QStringLiteral("textColor")).toString();
|
||
// 空串合法(跟随系统),非空则校验是否为合法颜色值
|
||
if (saved.isEmpty() || QColor::isValidColorName(saved)) {
|
||
m_textColor = saved;
|
||
}
|
||
// 若校验失败则保持空串默认值(跟随系统),不抛出也不记录
|
||
}
|
||
}
|
||
|
||
NetworkMonitorApplet::~NetworkMonitorApplet()
|
||
{
|
||
}
|
||
|
||
bool NetworkMonitorApplet::load()
|
||
{
|
||
return DApplet::load();
|
||
}
|
||
|
||
bool NetworkMonitorApplet::init()
|
||
{
|
||
detectInterfaces();
|
||
|
||
// 启动定时刷新
|
||
m_refreshTimer = new QTimer(this);
|
||
connect(m_refreshTimer, &QTimer::timeout, this, &NetworkMonitorApplet::refresh);
|
||
m_refreshTimer->start(1000); // 每秒刷新一次
|
||
|
||
// 初始读取
|
||
readNetworkStats();
|
||
|
||
return DApplet::init();
|
||
}
|
||
|
||
double NetworkMonitorApplet::downloadSpeed() const
|
||
{
|
||
return static_cast<double>(m_downloadSpeed);
|
||
}
|
||
|
||
double NetworkMonitorApplet::uploadSpeed() const
|
||
{
|
||
return static_cast<double>(m_uploadSpeed);
|
||
}
|
||
|
||
double NetworkMonitorApplet::totalDownload() const
|
||
{
|
||
return static_cast<double>(m_totalDownload);
|
||
}
|
||
|
||
double NetworkMonitorApplet::totalUpload() const
|
||
{
|
||
return static_cast<double>(m_totalUpload);
|
||
}
|
||
|
||
QStringList NetworkMonitorApplet::networkInterfaces() const
|
||
{
|
||
return m_interfaceList;
|
||
}
|
||
|
||
QStringList NetworkMonitorApplet::interfaceStats() const
|
||
{
|
||
QStringList stats;
|
||
for (const QString &name : m_interfaceList) {
|
||
if (m_interfaces.contains(name)) {
|
||
const NetworkInterface &iface = m_interfaces[name];
|
||
stats << QString("%1|%2|%3|%4|%5")
|
||
.arg(iface.name)
|
||
.arg(iface.rxBytes)
|
||
.arg(iface.txBytes)
|
||
.arg(iface.rxPackets)
|
||
.arg(iface.txPackets);
|
||
}
|
||
}
|
||
return stats;
|
||
}
|
||
|
||
bool NetworkMonitorApplet::ready() const
|
||
{
|
||
return m_ready;
|
||
}
|
||
|
||
QString NetworkMonitorApplet::activeInterface() const
|
||
{
|
||
return m_activeInterface;
|
||
}
|
||
|
||
// 返回活动接口的 IPv4 地址,供 QML 显示
|
||
QString NetworkMonitorApplet::ipAddress() const
|
||
{
|
||
return m_ipAddress;
|
||
}
|
||
|
||
// 返回活动接口的 IPv6 全球地址,供 QML 显示
|
||
QString NetworkMonitorApplet::ipv6Address() const
|
||
{
|
||
return m_ipv6Address;
|
||
}
|
||
|
||
// 返回插件版本号,从 dde-shell 插件元数据(metadata.json)读取
|
||
// 版本唯一源为 CMakeLists.txt 的 project(VERSION),经 configure_file 写入 metadata.json,
|
||
// dde-shell 加载时存入 DPluginMetaData,此处通过继承自 DApplet 的 pluginMetaData() 读取
|
||
QString NetworkMonitorApplet::version() const
|
||
{
|
||
// DPluginMetaData::value() 已在 "Plugin" 层级内部查找(源码:frame/pluginmetadata.cpp)
|
||
// metadata.json 的 { "Plugin": { "Version": "x.y.z" } } 被 DPluginMetaData 加载后,
|
||
// value("Version") 直接返回版本号,无需再 .value("Plugin").toMap() 展开
|
||
return pluginMetaData().value("Version").toString();
|
||
}
|
||
|
||
// 返回任务栏网速字体颜色,空串表示跟随系统主题
|
||
QString NetworkMonitorApplet::textColor() const
|
||
{
|
||
return m_textColor;
|
||
}
|
||
|
||
// 设置任务栏网速字体颜色,空串表示跟随系统主题
|
||
// 设计原因:
|
||
// - 空串语义:空串作为"跟随系统"的语义值,QML 层用 textColor ? textColor : primaryText 做回退,
|
||
// 避免后端硬编码系统色
|
||
// - 校验逻辑:非空颜色值通过 QColor::isValidColor 校验,无效值直接忽略,不存储不发射信号
|
||
// - 独立配置文件:使用 ~/.config/jnetapplet/settings.ini 而非 dde-shell 共享配置,
|
||
// 避免污染其他组件的配置空间
|
||
void NetworkMonitorApplet::setTextColor(const QString &color)
|
||
{
|
||
// 空串视为合法(表示跟随系统),不校验
|
||
if (!color.isEmpty() && !QColor::isValidColorName(color)) {
|
||
return;
|
||
}
|
||
|
||
if (m_textColor == color) {
|
||
return;
|
||
}
|
||
|
||
m_textColor = color;
|
||
|
||
// 持久化到独立配置文件
|
||
const QString configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)
|
||
+ QStringLiteral("/jnetapplet/settings.ini");
|
||
QSettings settings(configPath, QSettings::IniFormat);
|
||
settings.setValue(QStringLiteral("textColor"), m_textColor);
|
||
settings.sync();
|
||
|
||
emit textColorChanged();
|
||
}
|
||
|
||
// 返回当前活动接口的下行速度历史,转换为 QVariantList of QPointF 供 QML 使用
|
||
// 设计原因:QPointF 仅携带 x/y 两个值,上传与下载分别对应两个列表
|
||
QVariantList NetworkMonitorApplet::speedHistoryDownload() const
|
||
{
|
||
QVariantList list;
|
||
if (m_activeInterface.isEmpty() || !m_speedHistory.contains(m_activeInterface)) {
|
||
return list;
|
||
}
|
||
const QVector<SpeedSample> &samples = m_speedHistory[m_activeInterface];
|
||
list.reserve(samples.size());
|
||
for (const SpeedSample &s : samples) {
|
||
list.append(QPointF(s.timestamp, s.downloadSpeed));
|
||
}
|
||
return list;
|
||
}
|
||
|
||
// 返回当前活动接口的上行速度历史,结构同 speedHistoryDownload
|
||
QVariantList NetworkMonitorApplet::speedHistoryUpload() const
|
||
{
|
||
QVariantList list;
|
||
if (m_activeInterface.isEmpty() || !m_speedHistory.contains(m_activeInterface)) {
|
||
return list;
|
||
}
|
||
const QVector<SpeedSample> &samples = m_speedHistory[m_activeInterface];
|
||
list.reserve(samples.size());
|
||
for (const SpeedSample &s : samples) {
|
||
list.append(QPointF(s.timestamp, s.uploadSpeed));
|
||
}
|
||
return list;
|
||
}
|
||
|
||
void NetworkMonitorApplet::refresh()
|
||
{
|
||
readNetworkStats();
|
||
}
|
||
|
||
void NetworkMonitorApplet::setActiveInterface(const QString &interface)
|
||
{
|
||
if (m_activeInterface != interface) {
|
||
m_activeInterface = interface;
|
||
m_firstUpdate = true;
|
||
emit activeInterfaceChanged();
|
||
// 接口切换后通知 QML 趋势图切换显示新接口的历史数据
|
||
emit speedHistoryChanged();
|
||
// 接口切换后立即检测新接口的 IP 地址
|
||
detectIpAddress();
|
||
}
|
||
}
|
||
|
||
void NetworkMonitorApplet::readNetworkStats()
|
||
{
|
||
QFile file("/proc/net/dev");
|
||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||
qDebug() << "Failed to open /proc/net/dev";
|
||
return;
|
||
}
|
||
|
||
QTextStream stream(&file);
|
||
QStringList lines = stream.readAll().split('\n', Qt::SkipEmptyParts);
|
||
file.close();
|
||
|
||
QMap<QString, NetworkInterface> newInterfaces;
|
||
|
||
// 跳过前两行标题
|
||
for (int i = 2; i < lines.size(); ++i) {
|
||
QString line = lines[i].trimmed();
|
||
if (line.isEmpty()) continue;
|
||
|
||
// 格式: "interface: rx_bytes rx_packets rx_errors rx_dropped ... tx_bytes tx_packets tx_errors tx_dropped ..."
|
||
QRegularExpression re("^([^:]+):\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+\\d+\\s+\\d+\\s+\\d+\\s+\\d+\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)");
|
||
QRegularExpressionMatch match = re.match(line);
|
||
|
||
if (match.hasMatch()) {
|
||
NetworkInterface iface;
|
||
iface.name = match.captured(1);
|
||
iface.rxBytes = match.captured(2).toLongLong();
|
||
iface.rxPackets = match.captured(3).toLongLong();
|
||
iface.rxErrors = match.captured(4).toLongLong();
|
||
iface.rxDropped = match.captured(5).toLongLong();
|
||
iface.txBytes = match.captured(6).toLongLong();
|
||
iface.txPackets = match.captured(7).toLongLong();
|
||
iface.txErrors = match.captured(8).toLongLong();
|
||
iface.txDropped = match.captured(9).toLongLong();
|
||
|
||
// 过滤掉回环接口和虚拟接口
|
||
if (iface.name != "lo" && !iface.name.startsWith("veth") &&
|
||
!iface.name.startsWith("docker") && !iface.name.startsWith("br-")) {
|
||
newInterfaces[iface.name] = iface;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检测接口变化
|
||
if (newInterfaces.keys() != m_interfaces.keys()) {
|
||
m_interfaceList = newInterfaces.keys();
|
||
m_interfaces = newInterfaces;
|
||
|
||
// 清理已消失接口的历史缓冲,避免内存泄漏
|
||
// 设计原因:USB 网卡、VPN 隧道等接口可能随时消失,其历史数据不再需要
|
||
const QStringList oldIfaces = m_speedHistory.keys();
|
||
for (const QString &name : oldIfaces) {
|
||
if (!newInterfaces.contains(name)) {
|
||
m_speedHistory.remove(name);
|
||
}
|
||
}
|
||
|
||
// 自动选择活动接口
|
||
if (m_activeInterface.isEmpty() && !m_interfaceList.isEmpty()) {
|
||
// 优先选择物理网卡(wlp/wlan/enp/eth)中有流量的接口,
|
||
// 避免选中 Meta/tun0 等虚拟代理接口作为默认显示
|
||
for (const QString &name : m_interfaceList) {
|
||
if (!isPhysicalInterface(name)) continue;
|
||
const NetworkInterface &iface = m_interfaces[name];
|
||
if (iface.rxBytes > 0 || iface.txBytes > 0) {
|
||
m_activeInterface = name;
|
||
break;
|
||
}
|
||
}
|
||
// 其次选择任意有流量的接口(物理网卡都无流量时的兜底)
|
||
if (m_activeInterface.isEmpty()) {
|
||
for (const QString &name : m_interfaceList) {
|
||
const NetworkInterface &iface = m_interfaces[name];
|
||
if (iface.rxBytes > 0 || iface.txBytes > 0) {
|
||
m_activeInterface = name;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
// 最后选第一个(无任何流量时)
|
||
if (m_activeInterface.isEmpty()) {
|
||
m_activeInterface = m_interfaceList.first();
|
||
}
|
||
emit activeInterfaceChanged();
|
||
}
|
||
|
||
m_ready = !m_interfaceList.isEmpty();
|
||
emit interfacesChanged();
|
||
emit readyChanged();
|
||
} else {
|
||
m_interfaces = newInterfaces;
|
||
}
|
||
|
||
calculateSpeed();
|
||
emit statsChanged();
|
||
// 每次刷新都检测 IP,以应对 DHCP 续约等 IP 变更场景
|
||
detectIpAddress();
|
||
}
|
||
|
||
void NetworkMonitorApplet::calculateSpeed()
|
||
{
|
||
qint64 currentRxBytes = getActiveRxBytes();
|
||
qint64 currentTxBytes = getActiveTxBytes();
|
||
|
||
if (m_firstUpdate) {
|
||
m_firstUpdate = false;
|
||
m_lastRxBytes = currentRxBytes;
|
||
m_lastTxBytes = currentTxBytes;
|
||
return;
|
||
}
|
||
|
||
// 计算速度(字节/秒)
|
||
m_downloadSpeed = currentRxBytes - m_lastRxBytes;
|
||
m_uploadSpeed = currentTxBytes - m_lastTxBytes;
|
||
|
||
// 更新总量
|
||
m_totalDownload = currentRxBytes;
|
||
m_totalUpload = currentTxBytes;
|
||
|
||
m_lastRxBytes = currentRxBytes;
|
||
m_lastTxBytes = currentTxBytes;
|
||
|
||
// 追加采样点到当前活动接口的环形缓冲
|
||
// 设计原因:applet 启动即持续采集,用户随时打开趋势图都能看到完整 5 分钟数据
|
||
if (!m_activeInterface.isEmpty()) {
|
||
SpeedSample sample;
|
||
sample.timestamp = QDateTime::currentSecsSinceEpoch();
|
||
sample.downloadSpeed = static_cast<double>(m_downloadSpeed);
|
||
sample.uploadSpeed = static_cast<double>(m_uploadSpeed);
|
||
m_speedHistory[m_activeInterface].append(sample);
|
||
|
||
// 滑动窗口裁剪:超过 300 点时丢弃最旧的
|
||
// removeFirst 是 O(n),但 300 点每秒一次开销可忽略,无需复杂环形索引
|
||
QVector<SpeedSample> &samples = m_speedHistory[m_activeInterface];
|
||
while (samples.size() > MAX_HISTORY_SAMPLES) {
|
||
samples.removeFirst();
|
||
}
|
||
|
||
emit speedHistoryChanged();
|
||
}
|
||
|
||
emit speedChanged();
|
||
emit totalChanged();
|
||
}
|
||
|
||
void NetworkMonitorApplet::detectInterfaces()
|
||
{
|
||
QDir sysClass("/sys/class/net");
|
||
if (!sysClass.exists()) return;
|
||
|
||
QStringList entries = sysClass.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||
m_interfaceList.clear();
|
||
|
||
for (const QString &entry : entries) {
|
||
// 过滤回环接口
|
||
if (entry != "lo") {
|
||
m_interfaceList << entry;
|
||
}
|
||
}
|
||
|
||
m_ready = !m_interfaceList.isEmpty();
|
||
emit interfacesChanged();
|
||
emit readyChanged();
|
||
}
|
||
|
||
qint64 NetworkMonitorApplet::getActiveRxBytes() const
|
||
{
|
||
if (m_activeInterface.isEmpty() || !m_interfaces.contains(m_activeInterface)) {
|
||
return 0;
|
||
}
|
||
return m_interfaces[m_activeInterface].rxBytes;
|
||
}
|
||
|
||
qint64 NetworkMonitorApplet::getActiveTxBytes() const
|
||
{
|
||
if (m_activeInterface.isEmpty() || !m_interfaces.contains(m_activeInterface)) {
|
||
return 0;
|
||
}
|
||
return m_interfaces[m_activeInterface].txBytes;
|
||
}
|
||
|
||
// 检测活动接口的 IPv4 与 IPv6 地址
|
||
// IPv4:跳过 loopback,取第一个
|
||
// IPv6:跳过 loopback 和 link-local (fe80::),取第一个全球地址
|
||
// 任一地址变化时分别 emit 对应信号通知 QML 更新
|
||
// 设计原因:DHCP 续约、网络切换、IPv6 SLAAC 等场景下地址可能变化,需持续检测
|
||
void NetworkMonitorApplet::detectIpAddress()
|
||
{
|
||
QString newIp;
|
||
QString newIpv6;
|
||
if (!m_activeInterface.isEmpty()) {
|
||
QNetworkInterface iface = QNetworkInterface::interfaceFromName(m_activeInterface);
|
||
for (const QNetworkAddressEntry &entry : iface.addressEntries()) {
|
||
// IPv4:跳过 loopback,取第一个
|
||
if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol
|
||
&& !entry.ip().isLoopback()
|
||
&& newIp.isEmpty()) {
|
||
newIp = entry.ip().toString();
|
||
}
|
||
// IPv6:跳过 loopback 和 link-local (fe80::),取第一个全球地址
|
||
if (entry.ip().protocol() == QAbstractSocket::IPv6Protocol
|
||
&& !entry.ip().isLoopback()
|
||
&& !entry.ip().isLinkLocal()
|
||
&& newIpv6.isEmpty()) {
|
||
newIpv6 = entry.ip().toString();
|
||
}
|
||
}
|
||
}
|
||
if (m_ipAddress != newIp) {
|
||
m_ipAddress = newIp;
|
||
emit ipAddressChanged();
|
||
}
|
||
if (m_ipv6Address != newIpv6) {
|
||
m_ipv6Address = newIpv6;
|
||
emit ipv6AddressChanged();
|
||
}
|
||
}
|
||
|
||
// 判断是否为物理网卡
|
||
// 物理网卡前缀:无线 wlp/wlan/wifi,有线 enp/eth
|
||
// 虚拟接口(Meta/tun/tap/docker/veth/br-)返回 false,
|
||
// 用于自动选择时优先真实网卡而非代理 TUN 接口
|
||
bool NetworkMonitorApplet::isPhysicalInterface(const QString &name) const
|
||
{
|
||
return name.startsWith("wlp") || name.startsWith("wlan")
|
||
|| name.startsWith("enp") || name.startsWith("eth");
|
||
}
|
||
|
||
D_APPLET_CLASS(NetworkMonitorApplet)
|
||
|
||
DS_END_NAMESPACE
|
||
|
||
#include "networkmonitorapplet.moc" |