feat: 添加GPU频率/功耗/风扇监控、温度告警、趋势图、右键菜单、进程列表、GPU切换
第二、三阶段功能: 数据增强(第一阶段): - GPU频率: NVIDIA nvidia-smi clocks.gr, Intel gt/gt0/rps_act_freq_mhz - 显存频率: NVIDIA nvidia-smi clocks.mem - 功耗: NVIDIA nvidia-smi power.draw, AMD hwmon power1_average - 风扇转速: NVIDIA nvidia-smi fan.speed, AMD hwmon fan1_input - 任务栏图标根据最高温度变色(≥80°C红/≥60°C橙) - readGpuStats 每次刷新前重置统计字段,避免残留旧值 交互提升(第二阶段): - 温度告警: GPU温度≥80°C时通过D-Bus发送桌面通知,30秒冷却 - 右键菜单: 使用Qt.labs.platform原生菜单+MenuHelper,支持刷新和GPU切换 - 迷你趋势图: Canvas绘制近60秒温度走势折线 - 英文翻译: 新增en.ts,23条全部翻译 高级功能(第三阶段): - NVIDIA进程列表: nvidia-smi --query-compute-apps查询,弹框中显示进程名和显存占用 - GPU切换: pkexec prime-select set <mode>,弹框按钮+右键菜单,仅PRIME模式显示 其他改进: - 自定义深色主题tooltip替代系统默认白色tooltip - 第一张卡片tooltip显示在下方避免被裁剪 - CMakeLists.txt lupdate添加C++源文件提取tr()翻译 - gpuStats格式扩展为9字段: temp|gpuUsage|memUsage|memUsed|memTotal|gpuClock|memClock|powerDraw|fanSpeed
This commit is contained in:
+338
-12
@@ -5,6 +5,7 @@
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import Qt.labs.platform 1.1 as LP
|
||||
import org.deepin.ds 1.0
|
||||
import org.deepin.ds.dock 1.0
|
||||
import org.deepin.dtk 1.0
|
||||
@@ -25,6 +26,24 @@ AppletItem {
|
||||
readonly property string currentDriver: applet ? (applet.currentDriver || qsTr("Unknown")) : qsTr("Unknown")
|
||||
readonly property string gpuMode: applet ? (applet.gpuMode || "") : ""
|
||||
|
||||
// 所有 GPU 中的最高温度,用于任务栏图标变色
|
||||
readonly property int maxTemp: {
|
||||
if (!applet || !applet.gpuStats) return -1
|
||||
var max = -1
|
||||
for (var i = 0; i < applet.gpuStats.length; i++) {
|
||||
var parts = applet.gpuStats[i].split("|")
|
||||
var temp = parseInt(parts[0])
|
||||
if (!isNaN(temp) && temp > max) max = temp
|
||||
}
|
||||
return max
|
||||
}
|
||||
// 图标颜色:温度 >=80 红色,>=60 橙色,否则正常
|
||||
readonly property color iconColor: {
|
||||
if (maxTemp >= 80) return Qt.rgba(220/255, 38/255, 38/255, 1)
|
||||
if (maxTemp >= 60) return Qt.rgba(245/255, 158/255, 11/255, 1)
|
||||
return root.primaryText
|
||||
}
|
||||
|
||||
property Palette basePalette: DockPalette.iconTextPalette
|
||||
readonly property color primaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.9)
|
||||
readonly property color secondaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.65)
|
||||
@@ -34,7 +53,7 @@ AppletItem {
|
||||
readonly property color accentBlue: Qt.rgba(20 / 255, 80 / 255, 160 / 255, 1)
|
||||
readonly property color accentBlueLight: Qt.rgba(20 / 255, 80 / 255, 160 / 255, 0.12)
|
||||
|
||||
// 图标区域
|
||||
// 图标区域(根据最高温度变色)
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: dockSize * 0.7
|
||||
@@ -42,14 +61,14 @@ AppletItem {
|
||||
color: "transparent"
|
||||
radius: width * 0.45
|
||||
border.width: 1
|
||||
border.color: root.secondaryText
|
||||
border.color: root.iconColor
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "GPU"
|
||||
font.pixelSize: root.dockSize * 0.22
|
||||
font.bold: true
|
||||
color: root.primaryText
|
||||
color: root.iconColor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,8 +291,8 @@ AppletItem {
|
||||
|
||||
delegate: Rectangle {
|
||||
id: gpuCard
|
||||
width: gpuColumn.width
|
||||
height: 98
|
||||
width: gpuColumn.width
|
||||
height: 120
|
||||
color: root.cardBackground
|
||||
radius: 10
|
||||
border.width: 1
|
||||
@@ -320,12 +339,65 @@ AppletItem {
|
||||
if (temp >= 60) return Qt.rgba(245/255, 158/255, 11/255, 1)
|
||||
return Qt.rgba(22/255, 163/255, 74/255, 1)
|
||||
}
|
||||
property string gpuClockStr: {
|
||||
var parts = statsLine.split("|")
|
||||
var v = parts[5]
|
||||
return v === "-1" || v === "" ? "-" : v
|
||||
}
|
||||
property string memClockStr: {
|
||||
var parts = statsLine.split("|")
|
||||
var v = parts[6]
|
||||
return v === "-1" || v === "" ? "-" : v
|
||||
}
|
||||
property string powerStr: {
|
||||
var parts = statsLine.split("|")
|
||||
var v = parts[7]
|
||||
return v === "-1.0" || v === "" ? "-" : v
|
||||
}
|
||||
property string fanStr: {
|
||||
var parts = statsLine.split("|")
|
||||
var v = parts[8]
|
||||
return v === "-1" || v === "" ? "-" : v
|
||||
}
|
||||
|
||||
ToolTip.text: qsTr("GPU: ") + parseGpuName(modelData) + "\n"
|
||||
+ qsTr("Driver: ") + parseDriverName(modelData) + "\n"
|
||||
+ qsTr("Version: ") + (parseDriverVersion(modelData) || qsTr("Unknown"))
|
||||
ToolTip.visible: cardHover.hovered
|
||||
ToolTip.delay: 300
|
||||
// 自定义深色主题悬浮提示(替代系统默认白色 tooltip)
|
||||
// 第一张卡片 tooltip 显示在下方,避免被弹框顶部裁剪
|
||||
Rectangle {
|
||||
visible: cardHover.hovered
|
||||
anchors.top: index === 0 ? parent.bottom : undefined
|
||||
anchors.topMargin: index === 0 ? 6 : 0
|
||||
anchors.bottom: index === 0 ? undefined : parent.top
|
||||
anchors.bottomMargin: index === 0 ? 0 : 6
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: tooltipContent.implicitWidth + 24
|
||||
height: tooltipContent.implicitHeight + 16
|
||||
color: Qt.rgba(0, 0, 0, 0.85)
|
||||
radius: 8
|
||||
z: 100
|
||||
|
||||
ColumnLayout {
|
||||
id: tooltipContent
|
||||
anchors.centerIn: parent
|
||||
spacing: 3
|
||||
|
||||
Text {
|
||||
text: qsTr("GPU: ") + parseGpuName(modelData)
|
||||
color: "white"
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: qsTr("Driver: ") + parseDriverName(modelData)
|
||||
color: Qt.rgba(1, 1, 1, 0.65)
|
||||
font.pixelSize: 10
|
||||
}
|
||||
Text {
|
||||
text: qsTr("Version: ") + (parseDriverVersion(modelData) || qsTr("Unknown"))
|
||||
color: Qt.rgba(1, 1, 1, 0.65)
|
||||
font.pixelSize: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: cardHover
|
||||
@@ -456,10 +528,96 @@ AppletItem {
|
||||
}
|
||||
|
||||
Text {
|
||||
text: gpuUsageStr !== "—" ? gpuUsageStr + "%" : "—"
|
||||
text: gpuUsageStr !== "-" ? gpuUsageStr + "%" : "-"
|
||||
font.pixelSize: 11
|
||||
color: root.secondaryText
|
||||
font.bold: gpuUsageStr !== "—"
|
||||
font.bold: gpuUsageStr !== "-"
|
||||
}
|
||||
}
|
||||
|
||||
// 频率/功耗/风扇信息行
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
// GPU 频率
|
||||
Text {
|
||||
text: gpuClockStr !== "-" ? gpuClockStr + " MHz" : "-"
|
||||
font.pixelSize: 10
|
||||
color: root.tertiaryText
|
||||
visible: gpuClockStr !== "-"
|
||||
}
|
||||
|
||||
// 功耗
|
||||
Text {
|
||||
text: powerStr !== "-" ? powerStr + " W" : ""
|
||||
font.pixelSize: 10
|
||||
color: root.tertiaryText
|
||||
visible: powerStr !== "-"
|
||||
}
|
||||
|
||||
// 风扇转速
|
||||
Text {
|
||||
text: fanStr !== "-" ? qsTr("Fan") + " " + fanStr + "%" : ""
|
||||
font.pixelSize: 10
|
||||
color: root.tertiaryText
|
||||
visible: fanStr !== "-"
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
// 显存频率
|
||||
Text {
|
||||
text: memClockStr !== "-" ? memClockStr + " MHz" : ""
|
||||
font.pixelSize: 10
|
||||
color: root.tertiaryText
|
||||
visible: memClockStr !== "-"
|
||||
}
|
||||
}
|
||||
|
||||
// 温度趋势迷你图
|
||||
Canvas {
|
||||
id: tempCanvas
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 24
|
||||
visible: {
|
||||
if (!root.applet || !root.applet.tempHistory) return false
|
||||
var h = root.applet.tempHistory[index]
|
||||
return h && h.length > 1
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.reset()
|
||||
var w = width
|
||||
var h = height
|
||||
if (!root.applet || !root.applet.tempHistory) return
|
||||
var history = root.applet.tempHistory[index]
|
||||
if (!history || history.length < 2) return
|
||||
|
||||
// 有效温度范围 20-100°C
|
||||
var minT = 20, maxT = 100
|
||||
var len = history.length
|
||||
ctx.strokeStyle = gpuCard.tempColor
|
||||
ctx.lineWidth = 1.5
|
||||
ctx.beginPath()
|
||||
for (var i = 0; i < len; i++) {
|
||||
var x = (i / (len - 1)) * w
|
||||
var t = history[i]
|
||||
if (t < 0) t = minT
|
||||
var y = h - ((t - minT) / (maxT - minT)) * h
|
||||
if (y < 0) y = 0
|
||||
if (y > h) y = h
|
||||
if (i === 0) ctx.moveTo(x, y)
|
||||
else ctx.lineTo(x, y)
|
||||
}
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// 温度历史变化时重绘
|
||||
Connections {
|
||||
target: root.applet
|
||||
function onTempHistoryChanged() { tempCanvas.requestPaint() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -497,6 +655,124 @@ AppletItem {
|
||||
}
|
||||
}
|
||||
|
||||
// NVIDIA 进程列表(仅有进程时显示)
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: processList.implicitHeight + 24
|
||||
color: root.cardBackground
|
||||
radius: 10
|
||||
border.width: 1
|
||||
border.color: root.cardBorder
|
||||
visible: root.applet && root.applet.gpuProcesses && root.applet.gpuProcesses.length > 0
|
||||
|
||||
ColumnLayout {
|
||||
id: processList
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: qsTr("GPU Processes")
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
color: root.secondaryText
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.applet ? (root.applet.gpuProcesses || []) : []
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: modelData.split("|")[1] || "?"
|
||||
font.pixelSize: 10
|
||||
color: root.primaryText
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: (modelData.split("|")[2] || "?") + " MB"
|
||||
font.pixelSize: 10
|
||||
color: root.tertiaryText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GPU 切换按钮(仅 PRIME 模式下显示)
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
visible: root.gpuMode === "PRIME"
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 30
|
||||
font.pixelSize: 11
|
||||
text: qsTr("NVIDIA")
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.hovered ? root.accentBlueLight : root.cardBackground
|
||||
radius: 6
|
||||
border.width: 1
|
||||
border.color: root.cardBorder
|
||||
}
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: root.accentBlue
|
||||
font.pixelSize: 11
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
onClicked: if (root.applet) root.applet.switchGpu("nvidia")
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 30
|
||||
font.pixelSize: 11
|
||||
text: qsTr("Intel")
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.hovered ? root.accentBlueLight : root.cardBackground
|
||||
radius: 6
|
||||
border.width: 1
|
||||
border.color: root.cardBorder
|
||||
}
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: root.accentBlue
|
||||
font.pixelSize: 11
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
onClicked: if (root.applet) root.applet.switchGpu("intel")
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 30
|
||||
font.pixelSize: 11
|
||||
text: qsTr("On-Demand")
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.hovered ? root.accentBlueLight : root.cardBackground
|
||||
radius: 6
|
||||
border.width: 1
|
||||
border.color: root.cardBorder
|
||||
}
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: root.accentBlue
|
||||
font.pixelSize: 11
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
onClicked: if (root.applet) root.applet.switchGpu("on-demand")
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新按钮
|
||||
Button {
|
||||
id: refreshBtn
|
||||
@@ -626,4 +902,54 @@ AppletItem {
|
||||
toolTip.close()
|
||||
}
|
||||
}
|
||||
|
||||
// 右键菜单(使用 Qt.labs.platform 原生菜单,通过 dock 的 MenuHelper 弹出)
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||
|
||||
onTapped: {
|
||||
contextMenuLoader.active = true
|
||||
if (contextMenuLoader.item) {
|
||||
MenuHelper.openMenu(contextMenuLoader.item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contextMenuLoader
|
||||
active: false
|
||||
sourceComponent: LP.Menu {
|
||||
LP.MenuItem {
|
||||
text: qsTr("Refresh")
|
||||
onTriggered: {
|
||||
if (root.applet) root.applet.refreshDeviceInfo()
|
||||
}
|
||||
}
|
||||
|
||||
LP.MenuItem {
|
||||
text: qsTr("Switch to NVIDIA")
|
||||
visible: root.gpuMode === "PRIME"
|
||||
onTriggered: {
|
||||
if (root.applet) root.applet.switchGpu("nvidia")
|
||||
}
|
||||
}
|
||||
|
||||
LP.MenuItem {
|
||||
text: qsTr("Switch to Intel")
|
||||
visible: root.gpuMode === "PRIME"
|
||||
onTriggered: {
|
||||
if (root.applet) root.applet.switchGpu("intel")
|
||||
}
|
||||
}
|
||||
|
||||
LP.MenuItem {
|
||||
text: qsTr("Switch to On-Demand")
|
||||
visible: root.gpuMode === "PRIME"
|
||||
onTriggered: {
|
||||
if (root.applet) root.applet.switchGpu("on-demand")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user