diff --git a/graphicsdriverapplet.cpp b/graphicsdriverapplet.cpp index a72a260..352c41b 100644 --- a/graphicsdriverapplet.cpp +++ b/graphicsdriverapplet.cpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include #include @@ -25,7 +27,10 @@ GraphicsDriverApplet::GraphicsDriverApplet(QObject *parent) : DApplet(parent) , m_ready(false) , m_lastAlertTime(0) + , m_tempAlertEnabled(true) { + QSettings settings("deepin", "graphics-driver-applet"); + m_tempAlertEnabled = settings.value("tempAlertEnabled", true).toBool(); } GraphicsDriverApplet::~GraphicsDriverApplet() @@ -96,6 +101,20 @@ QStringList GraphicsDriverApplet::gpuProcesses() const return m_gpuProcesses; } +bool GraphicsDriverApplet::tempAlertEnabled() const +{ + return m_tempAlertEnabled; +} + +void GraphicsDriverApplet::setTempAlertEnabled(bool enabled) +{ + if (m_tempAlertEnabled == enabled) return; + m_tempAlertEnabled = enabled; + QSettings settings("deepin", "graphics-driver-applet"); + settings.setValue("tempAlertEnabled", enabled); + emit tempAlertEnabledChanged(); +} + void GraphicsDriverApplet::refreshDeviceInfo() { detectGpus(); @@ -168,6 +187,11 @@ void GraphicsDriverApplet::detectGpus() } } + // 排序:核显(isPrimary)排在前面 + std::stable_sort(m_gpus.begin(), m_gpus.end(), [](const GpuInfo &a, const GpuInfo &b) { + return a.isPrimary > b.isPrimary; + }); + // 生成摘要信息 QStringList parts; QStringList driverParts; @@ -555,6 +579,8 @@ void GraphicsDriverApplet::readGpuStats() // 温度告警:检测是否有 GPU 温度超过 80°C,30 秒内不重复告警 void GraphicsDriverApplet::checkTempAlert() { + if (!m_tempAlertEnabled) return; + qint64 now = QDateTime::currentSecsSinceEpoch(); if (now - m_lastAlertTime < 30) return; diff --git a/graphicsdriverapplet.h b/graphicsdriverapplet.h index 7a845ab..84463ab 100644 --- a/graphicsdriverapplet.h +++ b/graphicsdriverapplet.h @@ -46,6 +46,7 @@ class GraphicsDriverApplet : public DApplet Q_PROPERTY(bool ready READ ready NOTIFY readyChanged) Q_PROPERTY(QVariantList tempHistory READ tempHistory NOTIFY tempHistoryChanged) Q_PROPERTY(QStringList gpuProcesses READ gpuProcesses NOTIFY gpuProcessesChanged) + Q_PROPERTY(bool tempAlertEnabled READ tempAlertEnabled WRITE setTempAlertEnabled NOTIFY tempAlertEnabledChanged) public: explicit GraphicsDriverApplet(QObject *parent = nullptr); @@ -63,6 +64,8 @@ public: bool ready() const; QVariantList tempHistory() const; QStringList gpuProcesses() const; + bool tempAlertEnabled() const; + void setTempAlertEnabled(bool enabled); Q_INVOKABLE void refreshDeviceInfo(); Q_INVOKABLE void refreshStats(); @@ -78,6 +81,7 @@ signals: void readyChanged(); void tempHistoryChanged(); void gpuProcessesChanged(); + void tempAlertEnabledChanged(); private: void detectGpus(); @@ -105,6 +109,7 @@ private: QStringList m_gpuProcesses; // 温度告警冷却时间戳 qint64 m_lastAlertTime; + bool m_tempAlertEnabled; }; DS_END_NAMESPACE diff --git a/package/driverview.qml b/package/driverview.qml index f085061..4223d11 100644 --- a/package/driverview.qml +++ b/package/driverview.qml @@ -5,7 +5,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 -import Qt.labs.platform 1.1 as LP +import QtQuick.Window 2.15 import org.deepin.ds 1.0 import org.deepin.ds.dock 1.0 import org.deepin.dtk 1.0 @@ -125,8 +125,8 @@ AppletItem { // 弹出窗口 PanelPopup { id: driverPopup - width: 360 - height: 420 + width: 480 + height: 440 popupX: DockPanelPositioner.x popupY: DockPanelPositioner.y @@ -180,6 +180,46 @@ AppletItem { } 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() } + } + } } // 分隔线 @@ -292,7 +332,7 @@ AppletItem { delegate: Rectangle { id: gpuCard width: gpuColumn.width - height: 120 + height: 78 color: root.cardBackground radius: 10 border.width: 1 @@ -360,48 +400,15 @@ AppletItem { 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 } + + 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 @@ -465,12 +472,23 @@ AppletItem { } } - Text { - text: parseDriverInfo(modelData) - font.pixelSize: 11 - color: root.secondaryText + RowLayout { Layout.fillWidth: true - elide: Text.ElideRight + 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 + } } } } @@ -484,142 +502,48 @@ AppletItem { 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" : "—" + 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 !== "-" - } - } + // GPU 使用率进度条 + RowLayout { + spacing: 8 - // 温度趋势迷你图 - 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) + Text { + text: "GPU" + font.pixelSize: 11 + color: root.tertiaryText } - ctx.stroke() - } - // 温度历史变化时重绘 - Connections { - target: root.applet - function onTempHistoryChanged() { tempCanvas.requestPaint() } + Rectangle { + Layout.preferredWidth: 140 + 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 !== "-" + } } } + } } } @@ -655,6 +579,179 @@ AppletItem { } } + // 合并温度趋势图 + 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.25) + 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.06) + 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 @@ -772,34 +869,6 @@ AppletItem { 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() - } - } - } } } @@ -903,53 +972,181 @@ AppletItem { } } - // 右键菜单(使用 Qt.labs.platform 原生菜单,通过 dock 的 MenuHelper 弹出) + // 设置窗口(无边框 + 毛玻璃风格) + 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: { - 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") - } - } + settingsWindow.show() } } } diff --git a/translations/org.deepin.ds.graphics-driver_zh_CN.ts b/translations/org.deepin.ds.graphics-driver_zh_CN.ts index 010e976..49cd5e9 100644 --- a/translations/org.deepin.ds.graphics-driver_zh_CN.ts +++ b/translations/org.deepin.ds.graphics-driver_zh_CN.ts @@ -128,5 +128,30 @@ Fan 风扇 + + + Settings + 设置 + + + + General Settings + 常规设置 + + + + Show notification when GPU temperature exceeds threshold + GPU 温度超过阈值时显示通知 + + + + Alert Temperature Threshold + 告警温度阈值 + + + + Temperature Trend + 温度趋势 + diff --git a/translations/org.deepin.ds.graphics-driver_zh_TW.ts b/translations/org.deepin.ds.graphics-driver_zh_TW.ts index f7816e3..0c16911 100644 --- a/translations/org.deepin.ds.graphics-driver_zh_TW.ts +++ b/translations/org.deepin.ds.graphics-driver_zh_TW.ts @@ -128,5 +128,30 @@ Fan 風扇 + + + Settings + 設定 + + + + General Settings + 一般設定 + + + + Show notification when GPU temperature exceeds threshold + GPU 溫度超過閾值時顯示通知 + + + + Alert Temperature Threshold + 告警溫度閾值 + + + + Temperature Trend + 溫度趨勢 +