// 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 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 || "") : "" 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.secondaryText Text { anchors.centerIn: parent text: "GPU" font.pixelSize: root.dockSize * 0.22 font.bold: true color: root.primaryText } } // 悬停提示 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: 98 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) } 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 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 !== "—" } } } } } } } // 未检测到 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 } } } } // 刷新按钮 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() } } }