2570a028ea
第二、三阶段功能: 数据增强(第一阶段): - 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
956 lines
40 KiB
QML
956 lines
40 KiB
QML
// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd.
|
|
//
|
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
|
|
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
|
|
|
|
AppletItem {
|
|
id: root
|
|
objectName: "graphics driver applet"
|
|
property int dockOrder: 21
|
|
property int dockSize: Panel.rootObject.dockItemMaxSize || 48
|
|
|
|
implicitWidth: dockSize
|
|
implicitHeight: dockSize
|
|
|
|
readonly property var applet: Applet
|
|
readonly property bool ready: applet ? applet.ready : false
|
|
readonly property string gpuSummary: applet ? (applet.gpuSummary || qsTr("No GPU detected")) : qsTr("No GPU detected")
|
|
readonly property string deviceInfo: applet ? (applet.deviceInfo || qsTr("Unknown")) : qsTr("Unknown")
|
|
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)
|
|
readonly property color tertiaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.5)
|
|
readonly property color cardBackground: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.06)
|
|
readonly property color cardBorder: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.1)
|
|
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
|
|
height: dockSize * 0.7
|
|
color: "transparent"
|
|
radius: width * 0.45
|
|
border.width: 1
|
|
border.color: root.iconColor
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: "GPU"
|
|
font.pixelSize: root.dockSize * 0.22
|
|
font.bold: true
|
|
color: root.iconColor
|
|
}
|
|
}
|
|
|
|
// 悬停提示
|
|
PanelToolTip {
|
|
id: toolTip
|
|
text: root.ready ? buildToolTipText() : qsTr("No GPU detected")
|
|
toolTipX: DockPanelPositioner.x
|
|
toolTipY: DockPanelPositioner.y
|
|
}
|
|
|
|
// 统计数据刷新定时器
|
|
Timer {
|
|
id: statsRefreshTimer
|
|
interval: 1000
|
|
repeat: true
|
|
onTriggered: {
|
|
if (root.applet) {
|
|
root.applet.refreshStats()
|
|
}
|
|
}
|
|
}
|
|
|
|
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: {
|
|
if (hovered && !driverPopup.popupVisible) {
|
|
toolTipShowTimer.start()
|
|
if (root.applet) {
|
|
root.applet.refreshStats()
|
|
}
|
|
statsRefreshTimer.start()
|
|
} else {
|
|
if (toolTipShowTimer.running) {
|
|
toolTipShowTimer.stop()
|
|
}
|
|
toolTip.close()
|
|
if (!driverPopup.popupVisible) {
|
|
statsRefreshTimer.stop()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 弹出窗口
|
|
PanelPopup {
|
|
id: driverPopup
|
|
width: 360
|
|
height: 420
|
|
popupX: DockPanelPositioner.x
|
|
popupY: DockPanelPositioner.y
|
|
|
|
onPopupVisibleChanged: {
|
|
if (popupVisible) {
|
|
toolTip.close()
|
|
statsRefreshTimer.start()
|
|
if (root.applet) {
|
|
root.applet.refreshStats()
|
|
}
|
|
} else {
|
|
statsRefreshTimer.stop()
|
|
}
|
|
}
|
|
|
|
Control {
|
|
id: popupContainer
|
|
anchors.fill: parent
|
|
padding: 16
|
|
|
|
contentItem: ColumnLayout {
|
|
spacing: 14
|
|
|
|
// 标题区域
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 10
|
|
|
|
Item { Layout.fillWidth: true }
|
|
|
|
Rectangle {
|
|
width: 28
|
|
height: 28
|
|
color: accentBlueLight
|
|
radius: 8
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: "GPU"
|
|
font.pixelSize: 12
|
|
font.bold: true
|
|
color: accentBlue
|
|
}
|
|
}
|
|
|
|
Text {
|
|
text: qsTr("Graphics Driver Manager")
|
|
font.pixelSize: 16
|
|
font.bold: true
|
|
color: root.primaryText
|
|
}
|
|
|
|
Item { Layout.fillWidth: true }
|
|
}
|
|
|
|
// 分隔线
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 1
|
|
color: root.cardBorder
|
|
}
|
|
|
|
// 当前驱动信息
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 8
|
|
visible: root.ready
|
|
|
|
Text {
|
|
text: qsTr("Current Driver:")
|
|
font.pixelSize: 12
|
|
color: root.secondaryText
|
|
}
|
|
|
|
Text {
|
|
text: root.currentDriver
|
|
font.pixelSize: 12
|
|
font.bold: true
|
|
color: root.primaryText
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
// GPU 模式标签
|
|
Rectangle {
|
|
visible: root.gpuMode.length > 0
|
|
width: modeText.implicitWidth + 16
|
|
height: 20
|
|
color: root.accentBlueLight
|
|
radius: 10
|
|
|
|
Text {
|
|
id: modeText
|
|
anchors.centerIn: parent
|
|
text: root.gpuMode
|
|
font.pixelSize: 11
|
|
font.bold: true
|
|
color: root.accentBlue
|
|
}
|
|
}
|
|
|
|
// 帮助图标
|
|
Item {
|
|
visible: root.gpuMode.length > 0
|
|
width: 16
|
|
height: 16
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: width / 2
|
|
color: helpHover.hovered ? root.accentBlue : root.tertiaryText
|
|
}
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: "?"
|
|
font.pixelSize: 10
|
|
font.bold: true
|
|
color: "white"
|
|
}
|
|
|
|
HoverHandler {
|
|
id: helpHover
|
|
}
|
|
|
|
ToolTip {
|
|
visible: helpHover.hovered
|
|
delay: 200
|
|
text: {
|
|
if (root.gpuMode === "PRIME")
|
|
return qsTr("PRIME mode: the integrated GPU handles display, the discrete GPU renders on demand. 3D apps use PRIME Render Offload to invoke the discrete GPU.")
|
|
if (root.gpuMode === "Bumblebee")
|
|
return qsTr("Bumblebee mode: a legacy dual-GPU solution, now deprecated.")
|
|
if (root.gpuMode === "Solo")
|
|
return qsTr("Solo mode: only one GPU detected.")
|
|
if (root.gpuMode === "Hybrid")
|
|
return qsTr("Hybrid mode: dual GPU detected, but no known switching solution found.")
|
|
return ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// GPU 列表区域
|
|
Flickable {
|
|
id: gpuListFlick
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
clip: true
|
|
contentWidth: width
|
|
contentHeight: gpuColumn.height
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
Column {
|
|
id: gpuColumn
|
|
width: gpuListFlick.width
|
|
spacing: 10
|
|
visible: root.ready
|
|
|
|
Repeater {
|
|
model: root.ready ? root.deviceInfo.split("\n").filter(function(line) { return line.trim().length > 0 }) : []
|
|
|
|
delegate: Rectangle {
|
|
id: gpuCard
|
|
width: gpuColumn.width
|
|
height: 120
|
|
color: root.cardBackground
|
|
radius: 10
|
|
border.width: 1
|
|
border.color: root.cardBorder
|
|
|
|
property string statsLine: root.applet && root.applet.gpuStats ? (root.applet.gpuStats[index] || "") : ""
|
|
property bool isPrimaryGpu: root.applet && root.applet.gpuPrimary ? (root.applet.gpuPrimary[index] === "true") : false
|
|
property string tempStr: {
|
|
var parts = statsLine.split("|")
|
|
var temp = parts[0]
|
|
return temp === "-1" || temp === "" ? "—" : temp
|
|
}
|
|
property string gpuUsageStr: {
|
|
var parts = statsLine.split("|")
|
|
var usage = parts[1]
|
|
return usage === "-1" || usage === "" ? "—" : usage
|
|
}
|
|
property string memUsageStr: {
|
|
var parts = statsLine.split("|")
|
|
var usage = parts[2]
|
|
return usage === "-1" || usage === "" ? "—" : usage
|
|
}
|
|
property string memUsedStr: {
|
|
var parts = statsLine.split("|")
|
|
var used = parts[3]
|
|
return used === "-1" || used === "" ? "—" : used
|
|
}
|
|
property string memTotalStr: {
|
|
var parts = statsLine.split("|")
|
|
var total = parts[4]
|
|
return total === "-1" || total === "" ? "—" : total
|
|
}
|
|
property real gpuUsagePercent: {
|
|
var parts = statsLine.split("|")
|
|
var usage = parts[1]
|
|
if (usage === "-1" || usage === "") return 0
|
|
return parseFloat(usage)
|
|
}
|
|
property color tempColor: {
|
|
var parts = statsLine.split("|")
|
|
var temp = parseInt(parts[0])
|
|
if (isNaN(temp) || temp === -1) return root.primaryText
|
|
if (temp >= 80) return Qt.rgba(220/255, 38/255, 38/255, 1)
|
|
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)
|
|
// 第一张卡片 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
|
|
}
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 10
|
|
spacing: 6
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 12
|
|
|
|
// GPU 图标
|
|
Rectangle {
|
|
width: 36
|
|
height: 36
|
|
color: root.accentBlueLight
|
|
radius: 8
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: parseGpuVendorShort(modelData)
|
|
font.pixelSize: 14
|
|
font.bold: true
|
|
color: root.accentBlue
|
|
}
|
|
}
|
|
|
|
// GPU 信息
|
|
ColumnLayout {
|
|
spacing: 4
|
|
Layout.fillWidth: true
|
|
|
|
RowLayout {
|
|
spacing: 6
|
|
Layout.fillWidth: true
|
|
|
|
Text {
|
|
text: parseGpuName(modelData)
|
|
font.pixelSize: 13
|
|
font.bold: true
|
|
color: root.primaryText
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
// 主/副显标识
|
|
Rectangle {
|
|
visible: gpuCard.isPrimaryGpu
|
|
width: badgeText.implicitWidth + 12
|
|
height: 16
|
|
color: Qt.rgba(22/255, 163/255, 74/255, 0.15)
|
|
radius: 8
|
|
|
|
Text {
|
|
id: badgeText
|
|
anchors.centerIn: parent
|
|
text: qsTr("Primary")
|
|
font.pixelSize: 10
|
|
font.bold: true
|
|
color: Qt.rgba(22/255, 163/255, 74/255, 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
text: parseDriverInfo(modelData)
|
|
font.pixelSize: 11
|
|
color: root.secondaryText
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
}
|
|
|
|
// 统计数据区域
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 6
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 12
|
|
|
|
// 温度
|
|
Text {
|
|
text: tempStr !== "—" ? tempStr + "°C" : "—"
|
|
font.pixelSize: 12
|
|
color: gpuCard.tempColor
|
|
font.bold: tempStr !== "—"
|
|
}
|
|
|
|
Item { Layout.fillWidth: true }
|
|
|
|
// 显存信息
|
|
Text {
|
|
text: (memUsedStr !== "—" && memTotalStr !== "—") ? memUsedStr + "/" + memTotalStr + " MB" : "—"
|
|
font.pixelSize: 11
|
|
color: root.secondaryText
|
|
}
|
|
}
|
|
|
|
// GPU 使用率进度条
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 8
|
|
|
|
Text {
|
|
text: "GPU"
|
|
font.pixelSize: 11
|
|
color: root.tertiaryText
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 4
|
|
color: root.cardBorder
|
|
radius: 2
|
|
|
|
Rectangle {
|
|
width: Math.min(gpuCard.gpuUsagePercent, 100) * parent.width / 100
|
|
height: parent.height
|
|
color: root.accentBlue
|
|
radius: 2
|
|
}
|
|
}
|
|
|
|
Text {
|
|
text: gpuUsageStr !== "-" ? gpuUsageStr + "%" : "-"
|
|
font.pixelSize: 11
|
|
color: root.secondaryText
|
|
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() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 未检测到 GPU 提示
|
|
Rectangle {
|
|
anchors.centerIn: parent
|
|
width: gpuListFlick.width - 32
|
|
height: 80
|
|
color: root.cardBackground
|
|
radius: 10
|
|
border.width: 1
|
|
border.color: root.cardBorder
|
|
visible: !root.ready
|
|
|
|
ColumnLayout {
|
|
anchors.centerIn: parent
|
|
spacing: 6
|
|
|
|
Text {
|
|
text: "⚠"
|
|
font.pixelSize: 24
|
|
}
|
|
|
|
Text {
|
|
text: qsTr("No GPU detected")
|
|
font.pixelSize: 13
|
|
color: root.secondaryText
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
text: qsTr("Refresh")
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 36
|
|
font.pixelSize: 13
|
|
|
|
background: Rectangle {
|
|
color: refreshBtn.hovered ? Qt.darker(root.accentBlue, 1.2) : root.accentBlue
|
|
radius: 8
|
|
}
|
|
|
|
contentItem: Text {
|
|
text: refreshBtn.text
|
|
color: "white"
|
|
font.pixelSize: refreshBtn.font.pixelSize
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
|
|
onClicked: {
|
|
if (root.applet) {
|
|
root.applet.refreshDeviceInfo()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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, driverPopup.width, driverPopup.height)
|
|
})
|
|
}
|
|
}
|
|
|
|
// 解析 GPU 名称的辅助函数
|
|
function parseGpuName(info) {
|
|
var parts = info.split("(")
|
|
if (parts.length > 0) {
|
|
return parts[0].trim()
|
|
}
|
|
return info
|
|
}
|
|
|
|
// 解析驱动信息的辅助函数
|
|
function parseDriverInfo(info) {
|
|
var startIndex = info.indexOf("(")
|
|
var endIndex = info.lastIndexOf(")")
|
|
if (startIndex !== -1 && endIndex !== -1) {
|
|
return info.substring(startIndex + 1, endIndex)
|
|
}
|
|
return info
|
|
}
|
|
|
|
// 解析驱动名称
|
|
function parseDriverName(info) {
|
|
var driver = parseDriverInfo(info)
|
|
var parts = driver.split(" ")
|
|
return parts[0] || qsTr("Unknown")
|
|
}
|
|
|
|
// 解析驱动版本号
|
|
function parseDriverVersion(info) {
|
|
var driver = parseDriverInfo(info)
|
|
var parts = driver.split(" ")
|
|
if (parts.length > 1) {
|
|
return parts[1]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// 识别 GPU 厂商简称
|
|
// 注意:用单词边界匹配,避免 "corporation" 中的 "ati" 等子串误匹配
|
|
function parseGpuVendorShort(info) {
|
|
var name = parseGpuName(info).toLowerCase()
|
|
if (/nvidia/.test(name)) return "NV"
|
|
if (/\bamd\b/.test(name) || /\bati\b/.test(name) || /advanced micro/.test(name)) return "AMD"
|
|
if (/intel/.test(name)) return "Intel"
|
|
if (/qualcomm/.test(name)) return "QC"
|
|
if (/microsoft/.test(name)) return "MS"
|
|
return "GPU"
|
|
}
|
|
|
|
// 构建 tooltip 文本
|
|
function buildToolTipText() {
|
|
if (!root.applet || !root.applet.gpuStats) {
|
|
return root.currentDriver || qsTr("No GPU detected")
|
|
}
|
|
|
|
var lines = root.deviceInfo.split("\n").filter(function(line) { return line.trim().length > 0 })
|
|
var result = []
|
|
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var vendor = parseGpuVendorShort(lines[i])
|
|
var statsLine = root.applet.gpuStats[i] || ""
|
|
var parts = statsLine.split("|")
|
|
|
|
var temp = parts[0]
|
|
var gpuUsage = parts[1]
|
|
|
|
var tempStr = (temp === "-1" || temp === "") ? "" : temp + "°C"
|
|
var gpuUsageStr = (gpuUsage === "-1" || gpuUsage === "") ? "" : gpuUsage + "%"
|
|
|
|
result.push(vendor + " " + tempStr + " " + gpuUsageStr)
|
|
}
|
|
|
|
return result.join(" | ")
|
|
}
|
|
|
|
// 点击处理
|
|
TapHandler {
|
|
acceptedButtons: Qt.LeftButton
|
|
gesturePolicy: TapHandler.ReleaseWithinBounds
|
|
|
|
onTapped: {
|
|
if (driverPopup.popupVisible) {
|
|
driverPopup.close()
|
|
} else {
|
|
Panel.requestClosePopup()
|
|
const point = root.mapToItem(null, root.width / 2, root.height / 2)
|
|
driverPopup.DockPanelPositioner.bounding = Qt.rect(point.x, point.y, driverPopup.width, driverPopup.height)
|
|
driverPopup.open()
|
|
}
|
|
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")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|