64408bf842
- GPU切换按钮:核显厂商自动识别(AMD/Intel),当前模式高亮选中 - 切换确认覆盖层:15秒倒计时圆环,确认/取消按钮,超时自动取消 - 文字对比度提升:primaryText 0.95, secondaryText 0.8, tertiaryText 0.65 - 图表刻度文字 0.25->0.5,网格线 0.06->0.1 - 删除卡片内频率/功耗/风扇行,驱动名版本号两端对齐 - 卡片高度78,进度条宽度180,弹窗宽度480 - 翻译更新:切换确认相关8条新增
1364 lines
55 KiB
QML
1364 lines
55 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 QtQuick.Window 2.15
|
||
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 || "") : ""
|
||
|
||
// 当前 PRIME 模式(nvidia/on-demand/intel)
|
||
readonly property string currentPrimeMode: {
|
||
if (root.gpuMode !== "PRIME" || !root.applet || !root.applet.gpuPrimary) return ""
|
||
var lines = root.deviceInfo.split("\n").filter(function(l) { return l.trim().length > 0 })
|
||
for (var i = 0; i < lines.length; i++) {
|
||
if (/nvidia/i.test(lines[i]) && root.applet.gpuPrimary[i] === "true") {
|
||
return "nvidia"
|
||
}
|
||
}
|
||
return "on-demand"
|
||
}
|
||
|
||
// GPU 切换确认
|
||
property string pendingSwitchMode: ""
|
||
property int switchCountdown: 15
|
||
|
||
// 切换模式描述
|
||
function switchModeDesc(mode) {
|
||
if (mode === "nvidia") return qsTr("Switch to NVIDIA discrete GPU for maximum performance. Higher power consumption.")
|
||
if (mode === "intel") return qsTr("Switch to integrated GPU for power saving. NVIDIA GPU will be disabled.")
|
||
if (mode === "on-demand") return qsTr("Hybrid mode: integrated GPU for display, NVIDIA for on-demand rendering.")
|
||
return ""
|
||
}
|
||
|
||
function switchModeLabel(mode) {
|
||
if (mode === "nvidia") return "NVIDIA"
|
||
if (mode === "intel") return getIgpuVendorName()
|
||
if (mode === "on-demand") return qsTr("On-Demand")
|
||
return mode
|
||
}
|
||
|
||
function requestSwitch(mode) {
|
||
root.pendingSwitchMode = mode
|
||
root.switchCountdown = 15
|
||
switchConfirmTimer.start()
|
||
}
|
||
|
||
function confirmSwitch() {
|
||
if (root.pendingSwitchMode && root.applet) {
|
||
root.applet.switchGpu(root.pendingSwitchMode)
|
||
}
|
||
cancelSwitch()
|
||
}
|
||
|
||
function cancelSwitch() {
|
||
root.pendingSwitchMode = ""
|
||
switchConfirmTimer.stop()
|
||
}
|
||
|
||
// 所有 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.95)
|
||
readonly property color secondaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.8)
|
||
readonly property color tertiaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.65)
|
||
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: switchConfirmTimer
|
||
interval: 1000
|
||
repeat: true
|
||
onTriggered: {
|
||
root.switchCountdown--
|
||
if (root.switchCountdown <= 0) {
|
||
root.cancelSwitch()
|
||
}
|
||
}
|
||
}
|
||
|
||
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: 480
|
||
height: 440
|
||
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 {
|
||
width: 28
|
||
height: 28
|
||
radius: 6
|
||
color: refreshIconArea.containsMouse ? root.accentBlueLight : "transparent"
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: "⟳"
|
||
font.pixelSize: 18
|
||
color: refreshIconArea.containsMouse ? root.accentBlue : root.secondaryText
|
||
}
|
||
|
||
MouseArea {
|
||
id: refreshIconArea
|
||
anchors.fill: parent
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: {
|
||
if (root.applet) {
|
||
root.applet.refreshDeviceInfo()
|
||
}
|
||
}
|
||
}
|
||
|
||
// 旋转动画
|
||
RotationAnimator on rotation {
|
||
id: refreshSpin
|
||
running: false
|
||
from: 0
|
||
to: 360
|
||
duration: 500
|
||
}
|
||
|
||
Connections {
|
||
target: refreshIconArea
|
||
function onClicked() { refreshSpin.start() }
|
||
}
|
||
}
|
||
}
|
||
|
||
// 分隔线
|
||
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: 78
|
||
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
|
||
}
|
||
|
||
HoverHandler {
|
||
id: cardHover
|
||
}
|
||
|
||
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
|
||
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
spacing: 4
|
||
|
||
Text {
|
||
text: parseDriverName(modelData)
|
||
font.pixelSize: 11
|
||
color: root.secondaryText
|
||
}
|
||
|
||
Item { Layout.fillWidth: true }
|
||
|
||
Text {
|
||
text: parseDriverVersion(modelData) || qsTr("Unknown")
|
||
font.pixelSize: 11
|
||
color: root.tertiaryText
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 统计数据区域
|
||
ColumnLayout {
|
||
Layout.fillWidth: true
|
||
spacing: 6
|
||
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
spacing: 12
|
||
|
||
// 显存信息
|
||
Text {
|
||
text: (memUsedStr !== "-" && memTotalStr !== "-") ? memUsedStr + "/" + memTotalStr + " MB" : "-"
|
||
font.pixelSize: 11
|
||
color: root.secondaryText
|
||
}
|
||
|
||
Item { Layout.fillWidth: true }
|
||
|
||
// GPU 使用率进度条
|
||
RowLayout {
|
||
spacing: 8
|
||
|
||
Text {
|
||
text: "GPU"
|
||
font.pixelSize: 11
|
||
color: root.tertiaryText
|
||
}
|
||
|
||
Rectangle {
|
||
Layout.preferredWidth: 180
|
||
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 !== "-"
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 未检测到 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
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 合并温度趋势图
|
||
Rectangle {
|
||
Layout.fillWidth: true
|
||
Layout.preferredHeight: 90
|
||
color: root.cardBackground
|
||
radius: 10
|
||
border.width: 1
|
||
border.color: root.cardBorder
|
||
visible: {
|
||
if (!root.applet || !root.applet.tempHistory) return false
|
||
var th = root.applet.tempHistory
|
||
for (var i = 0; i < th.length; i++) {
|
||
if (th[i] && th[i].length > 1) return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
ColumnLayout {
|
||
anchors.fill: parent
|
||
anchors.margins: 10
|
||
spacing: 6
|
||
|
||
// 图例
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
spacing: 12
|
||
|
||
Repeater {
|
||
model: root.ready ? root.deviceInfo.split("\n").filter(function(line) { return line.trim().length > 0 }) : []
|
||
|
||
RowLayout {
|
||
spacing: 4
|
||
|
||
Rectangle {
|
||
width: 10
|
||
height: 10
|
||
radius: 2
|
||
color: {
|
||
var colors = [
|
||
Qt.rgba(22/255, 163/255, 74/255, 1),
|
||
Qt.rgba(20/255, 80/255, 160/255, 1),
|
||
Qt.rgba(245/255, 158/255, 11/255, 1)
|
||
]
|
||
return colors[index] || colors[0]
|
||
}
|
||
}
|
||
|
||
Text {
|
||
text: parseGpuVendorShort(modelData)
|
||
font.pixelSize: 10
|
||
color: root.secondaryText
|
||
}
|
||
}
|
||
}
|
||
|
||
Item { Layout.fillWidth: true }
|
||
|
||
Text {
|
||
text: qsTr("Temperature Trend")
|
||
font.pixelSize: 10
|
||
color: root.tertiaryText
|
||
}
|
||
}
|
||
|
||
// 趋势图 Canvas
|
||
Canvas {
|
||
id: mergedTempCanvas
|
||
Layout.fillWidth: true
|
||
Layout.fillHeight: true
|
||
|
||
onPaint: {
|
||
var ctx = getContext("2d")
|
||
ctx.reset()
|
||
var w = width
|
||
var h = height
|
||
if (!root.applet || !root.applet.tempHistory) return
|
||
|
||
var th = root.applet.tempHistory
|
||
var minT = 20, maxT = 100
|
||
var pad = 4
|
||
var colors = [
|
||
Qt.rgba(22/255, 163/255, 74/255, 1),
|
||
Qt.rgba(20/255, 80/255, 160/255, 1),
|
||
Qt.rgba(245/255, 158/255, 11/255, 1)
|
||
]
|
||
|
||
// 1. 绘制网格线和刻度
|
||
ctx.font = "9px sans-serif"
|
||
ctx.fillStyle = Qt.rgba(1, 1, 1, 0.5)
|
||
var temps = [40, 60, 80]
|
||
for (var ti = 0; ti < temps.length; ti++) {
|
||
var t = temps[ti]
|
||
var y = h - ((t - minT) / (maxT - minT)) * (h - pad * 2) - pad
|
||
ctx.strokeStyle = Qt.rgba(1, 1, 1, 0.1)
|
||
ctx.lineWidth = 1
|
||
ctx.setLineDash(t === 80 ? [3, 3] : [])
|
||
ctx.beginPath()
|
||
ctx.moveTo(24, y)
|
||
ctx.lineTo(w, y)
|
||
ctx.stroke()
|
||
ctx.setLineDash([])
|
||
ctx.fillText(t + "°C", 2, y + 3)
|
||
}
|
||
|
||
// 2. 绘制每个 GPU 的温度线
|
||
for (var g = 0; g < th.length; g++) {
|
||
var history = th[g]
|
||
if (!history || history.length < 2) continue
|
||
var len = history.length
|
||
var color = colors[g] || colors[0]
|
||
|
||
// 填充区域
|
||
ctx.beginPath()
|
||
for (var i = 0; i < len; i++) {
|
||
var x = 24 + (i / (len - 1)) * (w - 24)
|
||
var temp = history[i]
|
||
if (temp < 0) temp = minT
|
||
var y2 = h - ((temp - minT) / (maxT - minT)) * (h - pad * 2) - pad
|
||
if (i === 0) ctx.moveTo(x, y2)
|
||
else ctx.lineTo(x, y2)
|
||
}
|
||
ctx.lineTo(24 + (w - 24), h)
|
||
ctx.lineTo(24, h)
|
||
ctx.closePath()
|
||
var grad = ctx.createLinearGradient(0, 0, 0, h)
|
||
grad.addColorStop(0, Qt.rgba(color.r, color.g, color.b, 0.15))
|
||
grad.addColorStop(1, Qt.rgba(color.r, color.g, color.b, 0.01))
|
||
ctx.fillStyle = grad
|
||
ctx.fill()
|
||
|
||
// 折线
|
||
ctx.strokeStyle = color
|
||
ctx.lineWidth = 2
|
||
ctx.beginPath()
|
||
for (var j = 0; j < len; j++) {
|
||
var x2 = 24 + (j / (len - 1)) * (w - 24)
|
||
var t2 = history[j]
|
||
if (t2 < 0) t2 = minT
|
||
var y3 = h - ((t2 - minT) / (maxT - minT)) * (h - pad * 2) - pad
|
||
if (j === 0) ctx.moveTo(x2, y3)
|
||
else ctx.lineTo(x2, y3)
|
||
}
|
||
ctx.stroke()
|
||
|
||
// 最新数据点
|
||
var lastIdx = len - 1
|
||
var lastX = 24 + (lastIdx / (len - 1)) * (w - 24)
|
||
var lastT = history[lastIdx]
|
||
if (lastT < 0) lastT = minT
|
||
var lastY = h - ((lastT - minT) / (maxT - minT)) * (h - pad * 2) - pad
|
||
ctx.fillStyle = color
|
||
ctx.beginPath()
|
||
ctx.arc(lastX, lastY, 3, 0, Math.PI * 2)
|
||
ctx.fill()
|
||
|
||
// 最新温度标注
|
||
ctx.font = "bold 10px sans-serif"
|
||
ctx.fillStyle = color
|
||
var label = lastT + "°C"
|
||
var labelX = lastX + 6
|
||
if (labelX + 30 > w) labelX = lastX - 30
|
||
ctx.fillText(label, labelX, lastY + 3)
|
||
}
|
||
}
|
||
|
||
Connections {
|
||
target: root.applet
|
||
function onTempHistoryChanged() { mergedTempCanvas.requestPaint() }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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: 32
|
||
font.pixelSize: 11
|
||
text: getIgpuVendorName()
|
||
|
||
background: Rectangle {
|
||
color: root.currentPrimeMode === "intel" ? root.accentBlue : (parent.hovered ? root.accentBlueLight : root.cardBackground)
|
||
radius: 6
|
||
border.width: 1
|
||
border.color: root.currentPrimeMode === "intel" ? root.accentBlue : root.cardBorder
|
||
}
|
||
contentItem: Text {
|
||
text: parent.text
|
||
color: root.currentPrimeMode === "intel" ? "white" : root.accentBlue
|
||
font.pixelSize: 11
|
||
font.bold: root.currentPrimeMode === "intel"
|
||
horizontalAlignment: Text.AlignHCenter
|
||
}
|
||
onClicked: root.requestSwitch("intel")
|
||
}
|
||
|
||
// 按需按钮
|
||
Button {
|
||
Layout.fillWidth: true
|
||
Layout.preferredHeight: 32
|
||
font.pixelSize: 11
|
||
text: qsTr("On-Demand")
|
||
|
||
background: Rectangle {
|
||
color: root.currentPrimeMode === "on-demand" ? root.accentBlue : (parent.hovered ? root.accentBlueLight : root.cardBackground)
|
||
radius: 6
|
||
border.width: 1
|
||
border.color: root.currentPrimeMode === "on-demand" ? root.accentBlue : root.cardBorder
|
||
}
|
||
contentItem: Text {
|
||
text: parent.text
|
||
color: root.currentPrimeMode === "on-demand" ? "white" : root.accentBlue
|
||
font.pixelSize: 11
|
||
font.bold: root.currentPrimeMode === "on-demand"
|
||
horizontalAlignment: Text.AlignHCenter
|
||
}
|
||
onClicked: root.requestSwitch("on-demand")
|
||
}
|
||
|
||
// NVIDIA 按钮
|
||
Button {
|
||
Layout.fillWidth: true
|
||
Layout.preferredHeight: 32
|
||
font.pixelSize: 11
|
||
text: qsTr("NVIDIA")
|
||
|
||
background: Rectangle {
|
||
color: root.currentPrimeMode === "nvidia" ? root.accentBlue : (parent.hovered ? root.accentBlueLight : root.cardBackground)
|
||
radius: 6
|
||
border.width: 1
|
||
border.color: root.currentPrimeMode === "nvidia" ? root.accentBlue : root.cardBorder
|
||
}
|
||
contentItem: Text {
|
||
text: parent.text
|
||
color: root.currentPrimeMode === "nvidia" ? "white" : root.accentBlue
|
||
font.pixelSize: 11
|
||
font.bold: root.currentPrimeMode === "nvidia"
|
||
horizontalAlignment: Text.AlignHCenter
|
||
}
|
||
onClicked: root.requestSwitch("nvidia")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// GPU 切换确认覆盖层
|
||
Rectangle {
|
||
id: switchOverlay
|
||
anchors.fill: parent
|
||
color: Qt.rgba(0, 0, 0, 0.7)
|
||
radius: 12
|
||
visible: root.pendingSwitchMode.length > 0
|
||
z: 100
|
||
|
||
ColumnLayout {
|
||
anchors.centerIn: parent
|
||
width: parent.width - 48
|
||
spacing: 16
|
||
|
||
// 模式标题
|
||
Text {
|
||
Layout.alignment: Qt.AlignHCenter
|
||
text: qsTr("Switch to") + " " + switchModeLabel(root.pendingSwitchMode)
|
||
font.pixelSize: 16
|
||
font.bold: true
|
||
color: "#e8e8e8"
|
||
}
|
||
|
||
// 模式描述
|
||
Text {
|
||
Layout.alignment: Qt.AlignHCenter
|
||
Layout.fillWidth: true
|
||
text: switchModeDesc(root.pendingSwitchMode)
|
||
font.pixelSize: 12
|
||
color: "#a0a0a0"
|
||
horizontalAlignment: Text.AlignHCenter
|
||
wrapMode: Text.WordWrap
|
||
}
|
||
|
||
// 倒计时圆环
|
||
Item {
|
||
Layout.alignment: Qt.AlignHCenter
|
||
width: 64
|
||
height: 64
|
||
|
||
// 背景圆环
|
||
Rectangle {
|
||
anchors.fill: parent
|
||
radius: 32
|
||
color: "transparent"
|
||
border.width: 3
|
||
border.color: Qt.rgba(1, 1, 1, 0.1)
|
||
}
|
||
|
||
// 进度圆环
|
||
Rectangle {
|
||
anchors.fill: parent
|
||
radius: 32
|
||
color: "transparent"
|
||
border.width: 3
|
||
border.color: root.accentBlue
|
||
// 用 ConicGradient 效果模拟进度(简化用 opacity)
|
||
opacity: root.switchCountdown / 15
|
||
|
||
Behavior on opacity {
|
||
NumberAnimation { duration: 1000 }
|
||
}
|
||
}
|
||
|
||
// 倒计时数字
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: root.switchCountdown
|
||
font.pixelSize: 24
|
||
font.bold: true
|
||
color: "#e8e8e8"
|
||
}
|
||
}
|
||
|
||
// 提示文字
|
||
Text {
|
||
Layout.alignment: Qt.AlignHCenter
|
||
text: qsTr("Auto-cancel if not confirmed")
|
||
font.pixelSize: 10
|
||
color: "#666666"
|
||
}
|
||
|
||
// 按钮行
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
spacing: 12
|
||
|
||
// 取消按钮
|
||
Button {
|
||
Layout.fillWidth: true
|
||
Layout.preferredHeight: 34
|
||
font.pixelSize: 12
|
||
|
||
background: Rectangle {
|
||
color: parent.hovered ? Qt.rgba(1, 1, 1, 0.1) : Qt.rgba(1, 1, 1, 0.05)
|
||
radius: 8
|
||
border.width: 1
|
||
border.color: Qt.rgba(1, 1, 1, 0.15)
|
||
}
|
||
contentItem: Text {
|
||
text: qsTr("Cancel")
|
||
color: "#cccccc"
|
||
font.pixelSize: 12
|
||
horizontalAlignment: Text.AlignHCenter
|
||
}
|
||
onClicked: root.cancelSwitch()
|
||
}
|
||
|
||
// 确认按钮
|
||
Button {
|
||
Layout.fillWidth: true
|
||
Layout.preferredHeight: 34
|
||
font.pixelSize: 12
|
||
|
||
background: Rectangle {
|
||
color: parent.hovered ? Qt.darker(root.accentBlue, 1.2) : root.accentBlue
|
||
radius: 8
|
||
}
|
||
contentItem: Text {
|
||
text: qsTr("Confirm")
|
||
color: "white"
|
||
font.pixelSize: 12
|
||
font.bold: true
|
||
horizontalAlignment: Text.AlignHCenter
|
||
}
|
||
onClicked: root.confirmSwitch()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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"
|
||
}
|
||
|
||
// 获取核显厂商简称
|
||
function getIgpuVendorName() {
|
||
if (!root.ready) return qsTr("Integrated")
|
||
var lines = root.deviceInfo.split("\n").filter(function(l) { return l.trim().length > 0 })
|
||
for (var i = 0; i < lines.length; i++) {
|
||
if (!/nvidia/i.test(lines[i])) {
|
||
return parseGpuVendorShort(lines[i])
|
||
}
|
||
}
|
||
return qsTr("Integrated")
|
||
}
|
||
|
||
// 构建 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()
|
||
}
|
||
}
|
||
|
||
// 设置窗口(无边框 + 毛玻璃风格)
|
||
Window {
|
||
id: settingsWindow
|
||
title: qsTr("Settings")
|
||
width: 650
|
||
height: 350
|
||
flags: Qt.FramelessWindowHint | Qt.Window
|
||
color: "transparent"
|
||
|
||
// 居中屏幕
|
||
x: (Screen.width - width) / 2
|
||
y: (Screen.height - height) / 2
|
||
|
||
// 毛玻璃风格背景
|
||
Rectangle {
|
||
anchors.fill: parent
|
||
radius: 16
|
||
color: Qt.rgba(0.12, 0.12, 0.14, 0.88)
|
||
border.width: 1
|
||
border.color: Qt.rgba(1, 1, 1, 0.1)
|
||
|
||
ColumnLayout {
|
||
anchors.fill: parent
|
||
anchors.margins: 0
|
||
spacing: 0
|
||
|
||
// 自定义标题栏
|
||
Rectangle {
|
||
Layout.fillWidth: true
|
||
Layout.preferredHeight: 48
|
||
color: "transparent"
|
||
radius: 16
|
||
|
||
Rectangle {
|
||
anchors.fill: parent
|
||
anchors.bottomMargin: 0
|
||
color: Qt.rgba(1, 1, 1, 0.04)
|
||
radius: 16
|
||
|
||
RowLayout {
|
||
anchors.fill: parent
|
||
anchors.leftMargin: 20
|
||
anchors.rightMargin: 16
|
||
spacing: 10
|
||
|
||
Text {
|
||
text: qsTr("General Settings")
|
||
font.pixelSize: 15
|
||
font.bold: true
|
||
color: "#e8e8e8"
|
||
}
|
||
|
||
Item { Layout.fillWidth: true }
|
||
|
||
// 关闭按钮
|
||
Rectangle {
|
||
width: 28
|
||
height: 28
|
||
radius: 14
|
||
color: closeBtnArea.containsMouse ? Qt.rgba(255/255, 95/255, 86/255, 0.8) : "transparent"
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: "✕"
|
||
font.pixelSize: 12
|
||
color: closeBtnArea.containsMouse ? "white" : "#999999"
|
||
}
|
||
|
||
MouseArea {
|
||
id: closeBtnArea
|
||
anchors.fill: parent
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: settingsWindow.close()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 窗口拖拽(使用系统移动,避免幻影)
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
anchors.rightMargin: 44
|
||
onPressed: settingsWindow.startSystemMove()
|
||
}
|
||
}
|
||
|
||
// 分隔线
|
||
Rectangle {
|
||
Layout.fillWidth: true
|
||
Layout.preferredHeight: 1
|
||
color: Qt.rgba(1, 1, 1, 0.08)
|
||
}
|
||
|
||
// 内容区域
|
||
ColumnLayout {
|
||
Layout.fillWidth: true
|
||
Layout.fillHeight: true
|
||
Layout.leftMargin: 24
|
||
Layout.rightMargin: 24
|
||
Layout.topMargin: 20
|
||
spacing: 20
|
||
|
||
// 温度告警通知开关
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
spacing: 12
|
||
|
||
ColumnLayout {
|
||
spacing: 2
|
||
Text {
|
||
text: qsTr("Temperature Alert")
|
||
font.pixelSize: 14
|
||
color: "#e0e0e0"
|
||
}
|
||
Text {
|
||
text: qsTr("Show notification when GPU temperature exceeds threshold")
|
||
font.pixelSize: 11
|
||
color: "#777777"
|
||
Layout.fillWidth: true
|
||
wrapMode: Text.WordWrap
|
||
}
|
||
}
|
||
|
||
Item { Layout.fillWidth: true }
|
||
|
||
Switch {
|
||
checked: root.applet ? root.applet.tempAlertEnabled : false
|
||
onToggled: {
|
||
if (root.applet) {
|
||
root.applet.tempAlertEnabled = checked
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 分隔线
|
||
Rectangle {
|
||
Layout.fillWidth: true
|
||
Layout.preferredHeight: 1
|
||
color: Qt.rgba(1, 1, 1, 0.06)
|
||
}
|
||
|
||
// 温度告警阈值
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
spacing: 12
|
||
|
||
Text {
|
||
text: qsTr("Alert Temperature Threshold")
|
||
font.pixelSize: 14
|
||
color: "#e0e0e0"
|
||
}
|
||
|
||
Item { Layout.fillWidth: true }
|
||
|
||
Text {
|
||
text: "80°C"
|
||
font.pixelSize: 14
|
||
font.bold: true
|
||
color: Qt.rgba(245/255, 158/255, 11/255, 1)
|
||
}
|
||
}
|
||
|
||
Item { Layout.fillHeight: true }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 右键点击显示设置窗口
|
||
TapHandler {
|
||
acceptedButtons: Qt.RightButton
|
||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||
onTapped: {
|
||
settingsWindow.show()
|
||
}
|
||
}
|
||
}
|