Files
JNetApplet/package/networkview.qml
T
Jokul 110032f3c9 refactor: 抽取NetCommon.qml消除跨组件重复代码
新建 package/components/NetCommon.qml 聚合公共颜色定义(主题感知色
+强调色)和工具函数(isPhysicalIf/formatSpeed/formatTotal/downloadValueColor)。

networkview.qml、NetworkPopup.qml、TrafficChartWindow.qml 删除各自
的重复定义改为引用 common.xxx,净删除91行重复代码。downloadValueColor
由属性绑定改为函数(调用方传速度值)。视觉行为零变化。
2026-07-24 11:00:02 +08:00

399 lines
17 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-FileCopyrightText: 2026 Jokul
//
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.15
import Qt.labs.platform as Platform
import org.deepin.ds 1.0
import org.deepin.ds.dock 1.0
import org.deepin.dtk 1.0
import "components"
AppletItem {
id: root
objectName: "network monitor applet"
property int dockOrder: 21
property int dockSize: Panel.rootObject.dockItemMaxSize || 48
// 任务栏方向:0=Top, 1=Right, 2=Bottom, 3=Left% 2 为 0 表示水平,1 表示竖向
// 设计原因:dde-shell 规范用法,org.deepin.ds.dock 已在 line 11 导入
property bool isVerticalDock: Panel.position % 2 === 1
// 水平任务栏:宽度稍宽容纳双行数值,高度匹配 dock 尺寸
// 竖向任务栏:宽度匹配 dock 尺寸(~40px),高度加大容纳竖排字符
// 2.4 倍:dockSize=40 时约 96px,可容纳最多 8 字符的速度字符串(如 "1023.99K"
// 增大字号后需更高以容纳竖排字符
implicitWidth: isVerticalDock ? dockSize : Math.round(dockSize * 1.35)
implicitHeight: isVerticalDock ? Math.round(dockSize * 2.4) : dockSize
readonly property var applet: Applet
readonly property bool ready: applet ? applet.ready : false
readonly property real downloadSpeed: applet ? applet.downloadSpeed : 0
readonly property real uploadSpeed: applet ? applet.uploadSpeed : 0
readonly property real totalDownload: applet ? applet.totalDownload : 0
readonly property real totalUpload: applet ? applet.totalUpload : 0
readonly property string activeInterface: applet ? (applet.activeInterface || "") : ""
// 活动接口的 IPv4 地址,由 C++ 后端 detectIpAddress 持续更新
readonly property string ipAddress: applet ? (applet.ipAddress || "") : ""
// 活动接口的 IPv6 全球地址,由 C++ 后端 detectIpAddress 持续更新
readonly property string ipv6Address: applet ? (applet.ipv6Address || "") : ""
readonly property var networkInterfaces: applet ? (applet.networkInterfaces || []) : []
readonly property var interfaceStats: applet ? (applet.interfaceStats || []) : []
// 公共颜色与格式化函数:集中定义于 components/NetCommon.qml(随 import "components" 引入),
// 与 NetworkPopup、TrafficChartWindow 共享同一份实现,消除跨组件重复定义
NetCommon { id: common }
// 排序后的接口列表:物理网卡在前,虚拟网卡在后,各自按名称排序
// 设计原因:保持稳定排序,活动接口不再移到最前,避免切换网卡时 chip 顺序跳动;
// 活动 chip 若被截断,由 chipFlickable 自动滚动露出完整样式;
// 物理网卡判断逻辑共用 common.isPhysicalIf(与 C++ isPhysicalInterface 一致)
readonly property var sortedInterfaces: {
if (!root.ready || root.networkInterfaces.length === 0) return []
var physical = []
var virtual = []
for (var i = 0; i < root.networkInterfaces.length; i++) {
var name = root.networkInterfaces[i]
if (common.isPhysicalIf(name)) physical.push(name)
else virtual.push(name)
}
physical.sort()
virtual.sort()
return physical.concat(virtual)
}
// 紧凑格式化速度(用于任务栏图标和 tooltip,不带单位后缀,节省空间)
// 设计原因:任务栏 ~48px 空间有限,"1.2M" 比 "1.2 MB/s" 节省约一半宽度
// 最小单位为 KB:B 级别也转换为 KB 显示(如 512 B/s -> "0.50K"),
// 避免纯数字无后缀时用户无法判断单位;所有级别保留 2 位小数
function formatSpeedShort(bytesPerSec) {
if (bytesPerSec < 1024 * 1024) {
return (bytesPerSec / 1024).toFixed(2) + "K"
} else if (bytesPerSec < 1024 * 1024 * 1024) {
return (bytesPerSec / (1024 * 1024)).toFixed(2) + "M"
} else {
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(2) + "G"
}
}
// 带单位的速度/总量格式化函数(formatSpeed / formatTotal)已移至 NetCommon
// 本文件未直接使用;弹窗与图表窗口经 common.formatSpeed / common.formatTotal 调用
// 颜色别名:转发 common 的公共颜色,保持下游 root.xxx 引用不变。
// 仅保留本文件实际使用的两个(primaryText 供 speedTextColor 回退、
// accentRed 传入各独立窗口);其余公共颜色本文件未用,不再重复定义。
// 颜色派生逻辑与主题适配的设计原因见 NetCommon.qml 文件头注释
readonly property color primaryText: common.primaryText
readonly property color accentRed: common.accentRed
// 深色模式检测:用 DTK 系统调色板的窗口背景亮度判定,而非 DockPalette。
// 设计原因:DockPalette.iconTextPalette 反映的是任务栏图标文字色,不随系统深色主题变化;
// DTK.palette.window 是系统主题的窗口背景色,深色模式下为深色(hslLightness < 0.5),
// 浅色模式下为浅色(hslLightness >= 0.5)。主题切换时 DTK.paletteChanged 自动触发重新求值。
// 检测结果通过属性注入各独立窗口(AboutWindow 等独立 Window 无法访问 dock 上下文的 DockPalette
readonly property bool isDarkMode: DTK.palette.window.hslLightness < 0.5
// 任务栏网速数值字体色:用户自定义色优先(持久化),未设置时跟随系统主题
// 设计原因:primaryText 派生自 DTK.palette.windowText,深浅色系统主题自动适配;
// 用户主动选色后覆盖,空串回退到 primaryText 保持自适应
readonly property color speedTextColor: (applet && applet.textColor.length > 0) ? applet.textColor : primaryText
// 任务栏图标区:双行紧凑数值,箭头与数值紧贴、左对齐
// 设计原因:箭头与数值作为一组固定在左侧,数值左对齐紧贴箭头;
// 不给每行固定高度,让 RowLayout 按内容自然高度排列,
// 整组垂直居中于 dock 区域,避免两行间出现过大间隙
Column {
visible: !root.isVerticalDock
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 4
spacing: 0
// 下载速度行
RowLayout {
spacing: 2
Text {
text: "↓"
font.pixelSize: root.dockSize * 0.20
color: root.speedTextColor
Layout.alignment: Qt.AlignVCenter
}
Text {
text: root.formatSpeedShort(root.downloadSpeed)
font.pixelSize: root.dockSize * 0.25
font.weight: Font.Medium
color: root.speedTextColor
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignLeft
}
}
// 上传速度行
RowLayout {
spacing: 2
Text {
text: "↑"
font.pixelSize: root.dockSize * 0.20
color: root.speedTextColor
Layout.alignment: Qt.AlignVCenter
}
Text {
text: root.formatSpeedShort(root.uploadSpeed)
font.pixelSize: root.dockSize * 0.25
font.weight: Font.Medium
color: root.speedTextColor
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignLeft
}
}
}
// 竖向任务栏布局:双列字符竖排,每列一个方向(下载|上传)
// 设计原因:竖向任务栏宽度仅 ~40px,水平布局的 "↓6.64K" 约 35px 勉强容纳但
// 数值长度变化时易裁切;逐字符竖排每列仅 ~10px,双列 ~25px,舒适容纳且视觉对称
RowLayout {
visible: root.isVerticalDock
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 4
spacing: 4
// 下载列:箭头在上,数值字符从上往下竖排
Column {
spacing: 0
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
Text {
text: "↓"
font.pixelSize: root.dockSize * 0.20
color: root.speedTextColor
anchors.horizontalCenter: parent.horizontalCenter
}
Repeater {
// 将 "6.64K" 拆为 ["6", ".", "6", "4", "K"] 逐字符渲染
// 属性绑定:downloadSpeed 变化时 formatSpeedShort 重算,split 自动刷新 model
model: root.formatSpeedShort(root.downloadSpeed).split('')
Text {
text: modelData
font.pixelSize: root.dockSize * 0.25
height: font.pixelSize
font.weight: Font.Medium
color: root.speedTextColor
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
// 上传列:结构与下载列对称,颜色用 uploadValueColor
Column {
spacing: 0
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
Text {
text: "↑"
font.pixelSize: root.dockSize * 0.20
color: root.speedTextColor
anchors.horizontalCenter: parent.horizontalCenter
}
Repeater {
model: root.formatSpeedShort(root.uploadSpeed).split('')
Text {
text: modelData
font.pixelSize: root.dockSize * 0.25
height: font.pixelSize
font.weight: Font.Medium
color: root.speedTextColor
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
// 悬停提示:网卡名 + IPv4 单行,IPv6 在第二行(仅有 IPv6 时追加)
// 设计原因:IPv6 地址较长,单行容纳不下;无 IPv6 时保持单行与原视觉一致
PanelToolTip {
id: toolTip
text: root.ready
? (root.activeInterface
+ " · IPv4" + (root.ipAddress || qsTr("无"))
+ (root.ipv6Address ? "\nIPv6" + root.ipv6Address : ""))
: qsTr("未检测到网络接口")
toolTipX: DockPanelPositioner.x
toolTipY: DockPanelPositioner.y
}
// 注意:不再需要 QML 侧的 statsRefreshTimer。
// C++ 后端 NetworkMonitorApplet::init() 已启动 1 秒间隔的 m_refreshTimer
// 持续调用 refresh()->readNetworkStats()->calculateSpeed() 更新速度属性。
// QML 通过属性绑定自动获取最新值,无需主动触发 refresh。
// 此前 QML 额外调用 applet.refresh() 会打破 1 秒定时间隔,
// 导致 calculateSpeed() 基于不固定间隔计算字节差,速度显示接近 0。
Timer {
id: toolTipShowTimer
interval: 50
onTriggered: {
const point = root.mapToItem(null, root.width / 2, root.height / 2)
toolTip.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, toolTip.width, toolTip.height)
toolTip.open()
}
}
HoverHandler {
onHoveredChanged: {
// 仅控制 tooltip 显示,不触发 refresh。
// C++ 后端 m_refreshTimer 持续每秒更新速度属性,QML 属性绑定自动反映。
if (hovered && !networkPopup.popupVisible) {
toolTipShowTimer.start()
} else {
if (toolTipShowTimer.running) {
toolTipShowTimer.stop()
}
toolTip.close()
}
}
}
// 弹出窗口:PanelPopup 壳在此处(dock 上下文类型不能作为独立组件根元素),
// 弹窗内容由 NetworkPopup.qml 提供,传入 applet,内部自行派生颜色和数据
PanelPopup {
id: networkPopup
width: 360
height: 320
popupX: DockPanelPositioner.x
popupY: DockPanelPositioner.y
onPopupVisibleChanged: {
if (popupVisible) {
toolTip.close()
// popup 打开时确保活动 chip 完整可见
Qt.callLater(function() { popupContent.scrollToActiveChip(false) })
}
}
NetworkPopup {
id: popupContent
applet: root.applet
anchors.fill: parent
}
Component.onCompleted: {
DockPanelPositioner.bounding = Qt.binding(function () {
const point = root.mapToItem(null, root.width / 2, root.height / 2)
return Qt.rect(point.x, point.y, networkPopup.width, networkPopup.height)
})
}
}
// 右键菜单:使用 Qt.labs.platform.Menu + MenuHelper
// 设计原因:dde-shell dock 环境下 AppletItem 嵌入 layer-shell 窗口,
// QtQuick.Controls.Menu.popup() 无法获取有效 QQuickWindow 来弹出菜单。
// 必须用 Qt.labs.platform.Menu(原生菜单)配合 org.deepin.ds.dock 的
// MenuHelper 单例管理菜单生命周期(确保同一时间只有一个菜单打开)。
Platform.Menu {
id: contextMenu
Platform.MenuItem {
text: qsTr("设置")
onTriggered: {
settingsWindow.show()
settingsWindow.raise()
settingsWindow.requestActivate()
}
}
// 流量波动图:屏幕居中独立窗口,展示活动接口最近 30 分钟网速趋势
Platform.MenuItem {
text: qsTr("流量波动图")
onTriggered: {
trafficChartWindow.show()
trafficChartWindow.raise()
trafficChartWindow.requestActivate()
}
}
Platform.MenuSeparator {}
Platform.MenuItem {
text: qsTr("关于")
onTriggered: {
aboutWindow.show()
aboutWindow.raise()
aboutWindow.requestActivate()
}
}
}
// 关于窗口:抽取为独立组件 package/components/AboutWindow.qml
// 依赖通过属性传入:accentColor = root.accentRedversion 取自 C++ 后端 applet.version
// isDarkMode 取自上方 DockPalette 检测结果
AboutWindow {
id: aboutWindow
accentColor: root.accentRed
version: root.applet ? root.applet.version : "1.0"
isDarkMode: root.isDarkMode
}
// 流量波动图窗口:屏幕居中独立窗口,展示活动接口最近 30 分钟网速趋势
// 依赖通过属性传入:accentColor = root.accentRedapplet = root.applet
// isDarkMode 取自上方 DockPalette 检测结果
TrafficChartWindow {
id: trafficChartWindow
accentColor: root.accentRed
applet: root.applet
isDarkMode: root.isDarkMode
}
// 设置窗口:独立顶层窗口,在桌面中间弹出
// 设计原因:Dialog 使用父窗口(dock layer-surface)的 overlay,仅覆盖任务栏区域,
// 无法在桌面中间显示。改用 Window 创建独立顶层窗口,可在桌面任意位置弹出。
SettingsWindow {
id: settingsWindow
applet: root.applet
networkInterfaces: root.networkInterfaces
activeInterface: root.activeInterface
accentColor: root.accentRed
isDarkMode: root.isDarkMode
}
// 点击处理
TapHandler {
acceptedButtons: Qt.LeftButton
gesturePolicy: TapHandler.ReleaseWithinBounds
onTapped: {
if (networkPopup.popupVisible) {
networkPopup.close()
} else {
Panel.requestClosePopup()
const point = root.mapToItem(null, root.width / 2, root.height / 2)
networkPopup.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, networkPopup.width, networkPopup.height)
networkPopup.open()
}
toolTip.close()
}
}
// 右键点击:弹出上下文菜单
TapHandler {
acceptedButtons: Qt.RightButton
gesturePolicy: TapHandler.ReleaseWithinBounds
onTapped: {
// 关闭可能打开的 popup,避免视觉冲突
if (networkPopup.popupVisible) networkPopup.close()
toolTip.close()
// 使用 MenuHelper 打开菜单(dde-shell dock 环境必需,
// 它会先关闭当前活动菜单再打开新菜单,保证同一时间只有一个菜单)
MenuHelper.openMenu(contextMenu)
}
}
}