// 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") 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: 2000 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 列表区域 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 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 Text { text: parseGpuName(modelData) font.pixelSize: 13 font.bold: true color: root.primaryText Layout.fillWidth: true elide: Text.ElideRight } 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 厂商简称 function parseGpuVendorShort(info) { var name = info.toLowerCase() if (name.indexOf("nvidia") !== -1) return "NV" if (name.indexOf("amd") !== -1 || name.indexOf("ati") !== -1 || name.indexOf("advanced micro") !== -1) return "AMD" if (name.indexOf("intel") !== -1) return "Intel" if (name.indexOf("qualcomm") !== -1) return "QC" if (name.indexOf("microsoft") !== -1) 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() } } }