Files
JSwitchDisplayApplet/package/driverview.qml
T
Jokul 9e474de717 feat: GPU切换回退保护逻辑
- switchGpu切换前写入备份文件(.gpu-switch-backup)记录之前模式
- init()检测未确认的切换,启动30秒自动回退计时器
- QML弹出'显示是否正常'确认覆盖层,支持手动确认/回退/超时自动回退
- confirmSwitchSuccess()用户确认正常后删除备份
- restoreFromBackup()自动恢复之前模式并发送通知
- primeOffloadEnabled属性检测当前PRIME offload状态
- switchGpu改用environment.d方式,不需要root/pkexec
- 翻译更新:回退确认相关8条新增
2026-07-14 22:15:52 +08:00

1451 lines
58 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: 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") return ""
if (root.applet && root.applet.primeOffloadEnabled) 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()
}
}
}
}
// GPU 切换回退确认覆盖层(启动时检测到未确认的切换)
Rectangle {
id: restoreOverlay
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.75)
radius: 12
visible: root.applet && root.applet.switchPending
z: 100
ColumnLayout {
anchors.centerIn: parent
width: parent.width - 48
spacing: 16
// 警告图标
Text {
Layout.alignment: Qt.AlignHCenter
text: "⚠"
font.pixelSize: 36
color: Qt.rgba(245/255, 158/255, 11/255, 1)
}
// 标题
Text {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Display OK?")
font.pixelSize: 18
font.bold: true
color: "#e8e8e8"
}
// 说明
Text {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
text: qsTr("GPU mode was changed. If display is abnormal, it will auto-revert in %1s.").arg(root.applet ? root.applet.restoreCountdown : 0)
font.pixelSize: 12
color: "#a0a0a0"
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
// 按钮行
RowLayout {
Layout.fillWidth: true
spacing: 12
// 回退按钮
Button {
Layout.fillWidth: true
Layout.preferredHeight: 34
font.pixelSize: 12
background: Rectangle {
color: parent.hovered ? Qt.rgba(220/255, 38/255, 38/255, 0.8) : Qt.rgba(220/255, 38/255, 38/255, 0.3)
radius: 8
border.width: 1
border.color: Qt.rgba(220/255, 38/255, 38/255, 0.6)
}
contentItem: Text {
text: qsTr("Revert")
color: "#ff6b6b"
font.pixelSize: 12
font.bold: true
horizontalAlignment: Text.AlignHCenter
}
onClicked: if (root.applet) root.applet.restoreSwitch()
}
// 确认正常按钮
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("Display OK")
color: "white"
font.pixelSize: 12
font.bold: true
horizontalAlignment: Text.AlignHCenter
}
onClicked: if (root.applet) root.applet.confirmSwitchSuccess()
}
}
}
}
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()
}
}
}