diff --git a/CMakeLists.txt b/CMakeLists.txt index c24d8be..9ac5048 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,25 +24,28 @@ set(TRANSLATIONS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/translations) set(TS_FILES ${TRANSLATIONS_DIR}/${PLUGIN_ID}_zh_CN.ts ${TRANSLATIONS_DIR}/${PLUGIN_ID}_zh_TW.ts + ${TRANSLATIONS_DIR}/${PLUGIN_ID}_en.ts ) # 生成 .qm 编译翻译文件 set(QM_FILES ${CMAKE_CURRENT_BINARY_DIR}/${PLUGIN_ID}_zh_CN.qm ${CMAKE_CURRENT_BINARY_DIR}/${PLUGIN_ID}_zh_TW.qm + ${CMAKE_CURRENT_BINARY_DIR}/${PLUGIN_ID}_en.qm ) add_custom_command( OUTPUT ${QM_FILES} COMMAND /usr/lib/qt6/bin/lrelease ${TRANSLATIONS_DIR}/${PLUGIN_ID}_zh_CN.ts -qm ${CMAKE_CURRENT_BINARY_DIR}/${PLUGIN_ID}_zh_CN.qm COMMAND /usr/lib/qt6/bin/lrelease ${TRANSLATIONS_DIR}/${PLUGIN_ID}_zh_TW.ts -qm ${CMAKE_CURRENT_BINARY_DIR}/${PLUGIN_ID}_zh_TW.qm + COMMAND /usr/lib/qt6/bin/lrelease ${TRANSLATIONS_DIR}/${PLUGIN_ID}_en.ts -qm ${CMAKE_CURRENT_BINARY_DIR}/${PLUGIN_ID}_en.qm DEPENDS ${TS_FILES} COMMENT "Compiling translations to .qm files" ) # 手动更新翻译源文件(不加入 ALL,仅在需要时手动 make update_translations) add_custom_target(update_translations - COMMAND /usr/lib/qt6/bin/lupdate ${CMAKE_CURRENT_SOURCE_DIR}/package -ts ${TS_FILES} + COMMAND /usr/lib/qt6/bin/lupdate ${CMAKE_CURRENT_SOURCE_DIR}/package ${CMAKE_CURRENT_SOURCE_DIR}/graphicsdriverapplet.cpp -ts ${TS_FILES} COMMENT "Updating translation source files" ) diff --git a/graphicsdriverapplet.cpp b/graphicsdriverapplet.cpp index 02a5237..a72a260 100644 --- a/graphicsdriverapplet.cpp +++ b/graphicsdriverapplet.cpp @@ -12,6 +12,10 @@ #include #include #include +#include +#include +#include +#include DCORE_USE_NAMESPACE @@ -20,6 +24,7 @@ DS_BEGIN_NAMESPACE GraphicsDriverApplet::GraphicsDriverApplet(QObject *parent) : DApplet(parent) , m_ready(false) + , m_lastAlertTime(0) { } @@ -73,6 +78,24 @@ QStringList GraphicsDriverApplet::gpuPrimary() const return m_gpuPrimary; } +QVariantList GraphicsDriverApplet::tempHistory() const +{ + QVariantList result; + for (const QList &history : m_tempHistory) { + QVariantList gpuHistory; + for (int temp : history) { + gpuHistory << temp; + } + result << QVariant(gpuHistory); + } + return result; +} + +QStringList GraphicsDriverApplet::gpuProcesses() const +{ + return m_gpuProcesses; +} + void GraphicsDriverApplet::refreshDeviceInfo() { detectGpus(); @@ -86,6 +109,7 @@ void GraphicsDriverApplet::refreshStats() void GraphicsDriverApplet::detectGpus() { m_gpus.clear(); + m_tempHistory.clear(); // 通过 lspci 检测显卡 QStringList lines = runCommand("lspci", {"-mm"}); @@ -103,6 +127,10 @@ void GraphicsDriverApplet::detectGpus() gpu.memUsage = -1; gpu.memTotal = -1; gpu.memUsed = -1; + gpu.gpuClock = -1; + gpu.memClock = -1; + gpu.powerDraw = -1; + gpu.fanSpeed = -1; // lspci -mm 格式: "Class" "Vendor" "Device" ... // 提取 PCI ID(行首的 01:00.0 等) @@ -326,10 +354,11 @@ int GraphicsDriverApplet::findDrmCard(const QString &pciId) void GraphicsDriverApplet::readGpuStats() { // 先处理 NVIDIA GPU(单次 nvidia-smi 调用获取所有 NVIDIA GPU 数据) - // nvidia-smi 输出格式: pci.bus_id,temperature.gpu,utilization.gpu,utilization.memory,memory.total,memory.used + // nvidia-smi 输出格式: pci.bus_id,temperature.gpu,utilization.gpu,utilization.memory, + // memory.total,memory.used,clocks.gr,clocks.mem,power.draw,fan.speed QMap nvidiaData; // pciId -> stats QStringList nvLines = runCommand("nvidia-smi", { - "--query-gpu=pci.bus_id,temperature.gpu,utilization.gpu,utilization.memory,memory.total,memory.used", + "--query-gpu=pci.bus_id,temperature.gpu,utilization.gpu,utilization.memory,memory.total,memory.used,clocks.gr,clocks.mem,power.draw,fan.speed", "--format=csv,noheader,nounits" }); for (const QString &line : nvLines) { @@ -344,6 +373,17 @@ void GraphicsDriverApplet::readGpuStats() // 逐个 GPU 读取统计 for (GpuInfo &gpu : m_gpus) { + // 每次刷新前重置实时统计字段,确保数据源不可用时不会残留旧值 + gpu.temperature = -1; + gpu.gpuUsage = -1; + gpu.memUsage = -1; + gpu.memTotal = -1; + gpu.memUsed = -1; + gpu.gpuClock = -1; + gpu.memClock = -1; + gpu.powerDraw = -1; + gpu.fanSpeed = -1; + if (gpu.driver == "nvidia" && nvidiaData.contains(gpu.pciId)) { // NVIDIA: 使用 nvidia-smi 数据 QStringList &parts = nvidiaData[gpu.pciId]; @@ -352,6 +392,19 @@ void GraphicsDriverApplet::readGpuStats() gpu.memUsage = parts[3].trimmed().toInt(); gpu.memTotal = parts[4].trimmed().toInt(); gpu.memUsed = parts[5].trimmed().toInt(); + // 频率 (parts[6]=GPU, parts[7]=显存) + if (parts.size() > 6) gpu.gpuClock = parts[6].trimmed().toInt(); + if (parts.size() > 7) gpu.memClock = parts[7].trimmed().toInt(); + // 功耗 (parts[8]),[N/A] 时保持 -1 + if (parts.size() > 8) { + QString pwr = parts[8].trimmed(); + if (pwr != "[N/A]" && !pwr.isEmpty()) gpu.powerDraw = pwr.toDouble(); + } + // 风扇转速 (parts[9]),[N/A] 时保持 -1 + if (parts.size() > 9) { + QString fan = parts[9].trimmed(); + if (fan != "[N/A]" && !fan.isEmpty()) gpu.fanSpeed = fan.toInt(); + } } else if (gpu.drmCard >= 0) { // AMD / Intel / 其他: 使用 sysfs QString cardPath = QString("/sys/class/drm/card%1/device").arg(gpu.drmCard); @@ -384,16 +437,36 @@ void GraphicsDriverApplet::readGpuStats() if (nameFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QString name = QTextStream(&nameFile).readAll().trimmed(); if (name == gpu.driver || name == "amdgpu" || name == "radeon") { - QFile tempFile(hwmonDir.absoluteFilePath(hwmon + "/temp1_input")); + QString hwmonPath = hwmonDir.absoluteFilePath(hwmon); + // 温度 + QFile tempFile(hwmonPath + "/temp1_input"); if (tempFile.open(QIODevice::ReadOnly | QIODevice::Text)) { int tempMilli = QTextStream(&tempFile).readAll().trimmed().toInt(); gpu.temperature = tempMilli / 1000; } + // 功耗 (µW -> W) + QFile powerFile(hwmonPath + "/power1_average"); + if (powerFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + qint64 microWatts = QTextStream(&powerFile).readAll().trimmed().toLongLong(); + gpu.powerDraw = microWatts / 1000000.0; + } + // 风扇转速 (RPM) + QFile fanFile(hwmonPath + "/fan1_input"); + if (fanFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + gpu.fanSpeed = QTextStream(&fanFile).readAll().trimmed().toInt(); + } break; } } } } + + // GPU 频率: Intel i915 使用 gt/gt0/rps_act_freq_mhz,AMD 使用 hwmon freq1_input + QString gtPath = QString("/sys/class/drm/card%1/gt/gt0/rps_act_freq_mhz").arg(gpu.drmCard); + QFile gtFreqFile(gtPath); + if (gtFreqFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + gpu.gpuClock = QTextStream(>FreqFile).readAll().trimmed().toInt(); + } } // --- 以下为 Intel 核显等无 hwmon/vram sysfs 接口时的回退逻辑 --- @@ -443,18 +516,117 @@ void GraphicsDriverApplet::readGpuStats() } } - // 生成统计数据 (格式: temp|gpuUsage|memUsage|memUsed|memTotal) + // 生成统计数据 (格式: temp|gpuUsage|memUsage|memUsed|memTotal|gpuClock|memClock|powerDraw|fanSpeed) m_gpuStats.clear(); for (const GpuInfo &gpu : m_gpus) { - m_gpuStats << QString("%1|%2|%3|%4|%5") + m_gpuStats << QString("%1|%2|%3|%4|%5|%6|%7|%8|%9") .arg(gpu.temperature) .arg(gpu.gpuUsage) .arg(gpu.memUsage) .arg(gpu.memUsed) - .arg(gpu.memTotal); + .arg(gpu.memTotal) + .arg(gpu.gpuClock) + .arg(gpu.memClock) + .arg(gpu.powerDraw, 0, 'f', 1) + .arg(gpu.fanSpeed); } emit gpuStatsChanged(); + + // 更新温度历史(保留最近 60 个采样点) + if (m_tempHistory.size() != m_gpus.size()) { + m_tempHistory.resize(m_gpus.size()); + } + for (int i = 0; i < m_gpus.size(); ++i) { + m_tempHistory[i].append(m_gpus[i].temperature); + if (m_tempHistory[i].size() > 60) { + m_tempHistory[i].removeFirst(); + } + } + emit tempHistoryChanged(); + + // 温度告警检测 + checkTempAlert(); + + // 读取 NVIDIA 进程列表 + readGpuProcesses(); +} + +// 温度告警:检测是否有 GPU 温度超过 80°C,30 秒内不重复告警 +void GraphicsDriverApplet::checkTempAlert() +{ + qint64 now = QDateTime::currentSecsSinceEpoch(); + if (now - m_lastAlertTime < 30) return; + + for (const GpuInfo &gpu : m_gpus) { + if (gpu.temperature >= 80) { + sendNotification( + tr("GPU Temperature Alert"), + tr("%1 temperature reached %2°C, please check cooling") + .arg(gpu.name) + .arg(gpu.temperature) + ); + m_lastAlertTime = now; + break; + } + } +} + +// 通过 D-Bus 发送桌面通知 +void GraphicsDriverApplet::sendNotification(const QString &summary, const QString &body) +{ + QDBusInterface notify("org.freedesktop.Notifications", + "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", + QDBusConnection::sessionBus()); + if (notify.isValid()) { + notify.call("Notify", "dde-shell-graphics-driver", 0u, + "dialog-warning", summary, body, + QStringList(), QVariantMap(), 5000); + } +} + +// 读取 NVIDIA GPU 上运行的进程列表 +void GraphicsDriverApplet::readGpuProcesses() +{ + m_gpuProcesses.clear(); + + // 仅当存在 NVIDIA GPU 时查询 + bool hasNvidia = false; + for (const GpuInfo &gpu : m_gpus) { + if (gpu.driver == "nvidia") { hasNvidia = true; break; } + } + if (!hasNvidia) { + emit gpuProcessesChanged(); + return; + } + + // nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv,noheader,nounits + QStringList lines = runCommand("nvidia-smi", { + "--query-compute-apps=pid,process_name,used_memory", + "--format=csv,noheader,nounits" + }); + for (const QString &line : lines) { + QStringList parts = line.split(","); + if (parts.size() >= 3) { + m_gpuProcesses << QString("%1|%2|%3") + .arg(parts[0].trimmed()) + .arg(parts[1].trimmed()) + .arg(parts[2].trimmed()); + } + } + + emit gpuProcessesChanged(); +} + +// GPU 切换:调用 prime-select 切换 PRIME 模式(需要 polkit 提权) +bool GraphicsDriverApplet::switchGpu(const QString &mode) +{ + // mode: "nvidia" / "intel" / "on-demand" + QProcess process; + process.start("pkexec", {"prime-select", "set", mode}); + process.waitForFinished(30000); + return process.exitCode() == 0; } D_APPLET_CLASS(GraphicsDriverApplet) diff --git a/graphicsdriverapplet.h b/graphicsdriverapplet.h index f483e69..7a845ab 100644 --- a/graphicsdriverapplet.h +++ b/graphicsdriverapplet.h @@ -9,6 +9,7 @@ #include #include #include +#include DS_BEGIN_NAMESPACE @@ -26,6 +27,10 @@ struct GpuInfo { int memUsage; // 显存使用率 (%),-1 表示不可用 int memTotal; // 显存总量 (MB),-1 表示不可用 int memUsed; // 显存已用 (MB),-1 表示不可用 + int gpuClock; // GPU 频率 (MHz),-1 表示不可用 + int memClock; // 显存频率 (MHz),-1 表示不可用 + double powerDraw; // 功耗 (W),-1 表示不可用 + int fanSpeed; // 风扇转速 (%),-1 表示不可用 }; class GraphicsDriverApplet : public DApplet @@ -39,6 +44,8 @@ class GraphicsDriverApplet : public DApplet Q_PROPERTY(QString gpuMode READ gpuMode NOTIFY gpuModeChanged) Q_PROPERTY(QStringList gpuPrimary READ gpuPrimary NOTIFY gpuPrimaryChanged) Q_PROPERTY(bool ready READ ready NOTIFY readyChanged) + Q_PROPERTY(QVariantList tempHistory READ tempHistory NOTIFY tempHistoryChanged) + Q_PROPERTY(QStringList gpuProcesses READ gpuProcesses NOTIFY gpuProcessesChanged) public: explicit GraphicsDriverApplet(QObject *parent = nullptr); @@ -54,9 +61,12 @@ public: QString gpuMode() const; QStringList gpuPrimary() const; bool ready() const; + QVariantList tempHistory() const; + QStringList gpuProcesses() const; Q_INVOKABLE void refreshDeviceInfo(); Q_INVOKABLE void refreshStats(); + Q_INVOKABLE bool switchGpu(const QString &mode); signals: void deviceInfoChanged(); @@ -66,11 +76,16 @@ signals: void gpuModeChanged(); void gpuPrimaryChanged(); void readyChanged(); + void tempHistoryChanged(); + void gpuProcessesChanged(); private: void detectGpus(); void readGpuStats(); + void readGpuProcesses(); void detectGpuMode(); + void checkTempAlert(); + void sendNotification(const QString &summary, const QString &body); static int findDrmCard(const QString &pciId); static QStringList runCommand(const QString &cmd, const QStringList &args); static QString parseGpuName(const QString &line); @@ -84,6 +99,12 @@ private: QString m_gpuMode; QStringList m_gpuPrimary; bool m_ready; + // 温度历史:每个 GPU 保留最近 60 个温度采样 + QVector> m_tempHistory; + // NVIDIA 进程列表:每行 "pid|name|usedMemory" + QStringList m_gpuProcesses; + // 温度告警冷却时间戳 + qint64 m_lastAlertTime; }; DS_END_NAMESPACE diff --git a/package/driverview.qml b/package/driverview.qml index 85305da..f085061 100644 --- a/package/driverview.qml +++ b/package/driverview.qml @@ -5,6 +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 org.deepin.ds 1.0 import org.deepin.ds.dock 1.0 import org.deepin.dtk 1.0 @@ -25,6 +26,24 @@ AppletItem { 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) @@ -34,7 +53,7 @@ AppletItem { 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 @@ -42,14 +61,14 @@ AppletItem { color: "transparent" radius: width * 0.45 border.width: 1 - border.color: root.secondaryText + border.color: root.iconColor Text { anchors.centerIn: parent text: "GPU" font.pixelSize: root.dockSize * 0.22 font.bold: true - color: root.primaryText + color: root.iconColor } } @@ -272,8 +291,8 @@ AppletItem { delegate: Rectangle { id: gpuCard - width: gpuColumn.width - height: 98 + width: gpuColumn.width + height: 120 color: root.cardBackground radius: 10 border.width: 1 @@ -320,12 +339,65 @@ AppletItem { 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.text: qsTr("GPU: ") + parseGpuName(modelData) + "\n" - + qsTr("Driver: ") + parseDriverName(modelData) + "\n" - + qsTr("Version: ") + (parseDriverVersion(modelData) || qsTr("Unknown")) - ToolTip.visible: cardHover.hovered - ToolTip.delay: 300 + // 自定义深色主题悬浮提示(替代系统默认白色 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 @@ -456,10 +528,96 @@ AppletItem { } Text { - text: gpuUsageStr !== "—" ? gpuUsageStr + "%" : "—" + text: gpuUsageStr !== "-" ? gpuUsageStr + "%" : "-" font.pixelSize: 11 color: root.secondaryText - font.bold: gpuUsageStr !== "—" + 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() } } } } @@ -497,6 +655,124 @@ AppletItem { } } + // 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 @@ -626,4 +902,54 @@ AppletItem { 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") + } + } + } + } } diff --git a/translations/org.deepin.ds.graphics-driver_en.ts b/translations/org.deepin.ds.graphics-driver_en.ts new file mode 100644 index 0000000..1d51667 --- /dev/null +++ b/translations/org.deepin.ds.graphics-driver_en.ts @@ -0,0 +1,132 @@ + + + + + GraphicsDriverApplet + + + GPU Temperature Alert + GPU Temperature Alert + + + + %1 temperature reached %2°C, please check cooling + %1 temperature reached %2°C, please check cooling + + + + driverview + + + + + + No GPU detected + No GPU detected + + + + + + + Unknown + Unknown + + + + Graphics Driver Manager + Graphics Driver Manager + + + + Current Driver: + Current Driver: + + + + PRIME mode: the integrated GPU handles display, the discrete GPU renders on demand. 3D apps use PRIME Render Offload to invoke the discrete GPU. + PRIME mode: the integrated GPU handles display, the discrete GPU renders on demand. 3D apps use PRIME Render Offload to invoke the discrete GPU. + + + + Bumblebee mode: a legacy dual-GPU solution, now deprecated. + Bumblebee mode: a legacy dual-GPU solution, now deprecated. + + + + Solo mode: only one GPU detected. + Solo mode: only one GPU detected. + + + + Hybrid mode: dual GPU detected, but no known switching solution found. + Hybrid mode: dual GPU detected, but no known switching solution found. + + + + GPU: + GPU: + + + + Driver: + Driver: + + + + Version: + Version: + + + + Primary + Primary + + + + Fan + Fan + + + + GPU Processes + GPU Processes + + + + NVIDIA + NVIDIA + + + + Intel + Intel + + + + On-Demand + On-Demand + + + + + Refresh + Refresh + + + + Switch to NVIDIA + Switch to NVIDIA + + + + Switch to Intel + Switch to Intel + + + + Switch to On-Demand + Switch to On-Demand + + + diff --git a/translations/org.deepin.ds.graphics-driver_zh_CN.ts b/translations/org.deepin.ds.graphics-driver_zh_CN.ts index 118c9a1..010e976 100644 --- a/translations/org.deepin.ds.graphics-driver_zh_CN.ts +++ b/translations/org.deepin.ds.graphics-driver_zh_CN.ts @@ -1,72 +1,132 @@ + + GraphicsDriverApplet + + + GPU Temperature Alert + GPU 温度告警 + + + + %1 temperature reached %2°C, please check cooling + %1 温度已达 %2°C,请检查散热 + + driverview - + + + + Unknown 未知 - + + + + No GPU detected 未检测到显卡 - + Graphics Driver Manager 显卡驱动管理 - + Current Driver: 当前驱动: - + + GPU Processes + GPU 进程 + + + + NVIDIA + NVIDIA + + + + Intel + Intel + + + + On-Demand + 按需 + + + + Refresh 刷新 - + + Switch to NVIDIA + 切换到 NVIDIA + + + + Switch to Intel + 切换到 Intel + + + + Switch to On-Demand + 切换到按需模式 + + + GPU: 显卡名称: - + Driver: 驱动: - + Version: 版本号: - + Primary 主显 - + PRIME mode: the integrated GPU handles display, the discrete GPU renders on demand. 3D apps use PRIME Render Offload to invoke the discrete GPU. PRIME 模式:核显负责日常显示,独显按需渲染。运行 3D 应用时通过 PRIME Render Offload 调用独显。 - + Bumblebee mode: a legacy dual-GPU solution, now deprecated. Bumblebee 模式:旧版双 GPU 方案,已淘汰。 - + Solo mode: only one GPU detected. Solo 模式:仅检测到一块 GPU。 - + Hybrid mode: dual GPU detected, but no known switching solution found. Hybrid 模式:检测到双 GPU,但未发现已知切换方案。 + + + Fan + 风扇 + diff --git a/translations/org.deepin.ds.graphics-driver_zh_TW.ts b/translations/org.deepin.ds.graphics-driver_zh_TW.ts index 4d2a205..f7816e3 100644 --- a/translations/org.deepin.ds.graphics-driver_zh_TW.ts +++ b/translations/org.deepin.ds.graphics-driver_zh_TW.ts @@ -1,72 +1,132 @@ + + GraphicsDriverApplet + + + GPU Temperature Alert + GPU 溫度警報 + + + + %1 temperature reached %2°C, please check cooling + %1 溫度已達 %2°C,請檢查散熱 + + driverview - + + + + Unknown 未知 - + + + + No GPU detected 未偵測到顯示卡 - + Graphics Driver Manager 顯示卡驅動管理 - + Current Driver: 目前驅動: - + + GPU Processes + GPU 行程 + + + + NVIDIA + NVIDIA + + + + Intel + Intel + + + + On-Demand + 按需 + + + + Refresh 重新整理 - + + Switch to NVIDIA + 切換到 NVIDIA + + + + Switch to Intel + 切換到 Intel + + + + Switch to On-Demand + 切換到按需模式 + + + GPU: 顯示卡名稱: - + Driver: 驅動: - + Version: 版本號: - + Primary 主顯 - + PRIME mode: the integrated GPU handles display, the discrete GPU renders on demand. 3D apps use PRIME Render Offload to invoke the discrete GPU. PRIME 模式:核顯負責日常顯示,獨顯按需渲染。運行 3D 應用時通過 PRIME Render Offload 調用獨顯。 - + Bumblebee mode: a legacy dual-GPU solution, now deprecated. Bumblebee 模式:舊版雙 GPU 方案,已淘汰。 - + Solo mode: only one GPU detected. Solo 模式:僅偵測到一塊 GPU。 - + Hybrid mode: dual GPU detected, but no known switching solution found. Hybrid 模式:偵測到雙 GPU,但未發現已知切換方案。 + + + Fan + 風扇 +