// 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") } } } } }