diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index eeb7ef736..c6e5f0367 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -73,36 +73,37 @@ Item { } // Build comprehensive tooltip text with all stats - function buildTooltipText() { - let lines = []; + function buildTooltipContent() { + let rows = []; // CPU - lines.push(`${I18n.tr("system-monitor.cpu-usage")}: ${Math.round(SystemStatService.cpuUsage)}% (${SystemStatService.cpuFreq})`); + rows.push([I18n.tr("system-monitor.cpu-usage"), `${Math.round(SystemStatService.cpuUsage)}% (${SystemStatService.cpuFreq})`]); + if (SystemStatService.cpuTemp > 0) { - lines.push(`${I18n.tr("system-monitor.cpu-temp")}: ${Math.round(SystemStatService.cpuTemp)}°C`); + rows.push([I18n.tr("system-monitor.cpu-temp"), `${Math.round(SystemStatService.cpuTemp)}°C`]); } // GPU (if available) if (SystemStatService.gpuAvailable) { - lines.push(`${I18n.tr("system-monitor.gpu-temp")}: ${Math.round(SystemStatService.gpuTemp)}°C`); + rows.push([I18n.tr("system-monitor.gpu-temp"), `${Math.round(SystemStatService.gpuTemp)}°C`]); } // Load Average if (SystemStatService.loadAvg1 >= 0) { - lines.push(`${I18n.tr("system-monitor.load-average")}: ${SystemStatService.loadAvg1.toFixed(2)} · ${SystemStatService.loadAvg5.toFixed(2)} · ${SystemStatService.loadAvg15.toFixed(2)}`); + rows.push([I18n.tr("system-monitor.load-average"), `${SystemStatService.loadAvg1.toFixed(2)} · ${SystemStatService.loadAvg5.toFixed(2)} · ${SystemStatService.loadAvg15.toFixed(2)}`]); } // Memory - lines.push(`${I18n.tr("common.memory")}: ${Math.round(SystemStatService.memPercent)}% (${SystemStatService.formatMemoryGb(SystemStatService.memGb).replace(/[^0-9.]/g, "") + " GB"})`); + rows.push([I18n.tr("common.memory"), `${Math.round(SystemStatService.memPercent)}% (${SystemStatService.formatMemoryGb(SystemStatService.memGb).replace(/[^0-9.]/g, "") + " GB"})`]); // Swap (if available) if (SystemStatService.swapTotalGb > 0) { - lines.push(`${I18n.tr("bar.system-monitor.swap-usage-label")}: ${Math.round(SystemStatService.swapPercent)}% (${SystemStatService.formatMemoryGb(SystemStatService.swapGb).replace(/[^0-9.]/g, "") + " GB"})`); + rows.push([I18n.tr("bar.system-monitor.swap-usage-label"), `${Math.round(SystemStatService.swapPercent)}% (${SystemStatService.formatMemoryGb(SystemStatService.swapGb).replace(/[^0-9.]/g, "") + " GB"})`]); } // Network - lines.push(`${I18n.tr("system-monitor.download-speed")}: ${SystemStatService.formatSpeed(SystemStatService.rxSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")}` + "/s"); - lines.push(`${I18n.tr("system-monitor.upload-speed")}: ${SystemStatService.formatSpeed(SystemStatService.txSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")}` + "/s"); + rows.push([I18n.tr("system-monitor.download-speed"), `${SystemStatService.formatSpeed(SystemStatService.rxSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")}` + "/s"]); + rows.push([I18n.tr("system-monitor.upload-speed"), `${SystemStatService.formatSpeed(SystemStatService.txSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")}` + "/s"]); // Disk const diskPercent = SystemStatService.diskPercents[diskPath]; @@ -110,11 +111,13 @@ Item { const usedGb = SystemStatService.diskUsedGb[diskPath] || 0; const sizeGb = SystemStatService.diskSizeGb[diskPath] || 0; const availGb = SystemStatService.diskAvailGb[diskPath] || 0; - lines.push(`${I18n.tr("system-monitor.disk")}: ${usedGb.toFixed(1)}G / ${sizeGb.toFixed(1)}G Used (${diskPercent}%)`); - lines.push(`Available: ${availGb.toFixed(1)}G`); + rows.push([I18n.tr("system-monitor.disk"), `${usedGb.toFixed(1)}GB/${sizeGb.toFixed(1)}GB (${diskPercent}%)`]); + + // TODO i18n + rows.push(["Available", `${availGb.toFixed(1)}G`]); } - return lines.join("\n"); + return rows; } readonly property color textColor: usePrimaryColor ? Color.mPrimary : Color.mOnSurface @@ -918,7 +921,7 @@ Item { } } onEntered: { - TooltipService.show(root, buildTooltipText(), BarService.getTooltipDirection(root.screen?.name)); + TooltipService.show(root, buildTooltipContent(), BarService.getTooltipDirection(root.screen?.name)); tooltipRefreshTimer.start(); } onExited: { @@ -933,7 +936,7 @@ Item { repeat: true onTriggered: { if (tooltipArea.containsMouse) { - TooltipService.updateText(buildTooltipText()); + TooltipService.updateText(buildTooltipContent()); } } } diff --git a/Modules/Tooltip/Tooltip.qml b/Modules/Tooltip/Tooltip.qml index 0ded79306..2ff045d5a 100644 --- a/Modules/Tooltip/Tooltip.qml +++ b/Modules/Tooltip/Tooltip.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQuick.Layouts import Quickshell import qs.Commons import qs.Widgets @@ -7,9 +8,13 @@ PopupWindow { id: root property string text: "" + property var rows: null // Array of arrays for grid mode: [["col1", "col2"], ["row2col1", "row2col2"]] + property bool isGridMode: rows !== null && Array.isArray(rows) && rows.length > 0 + property int columnCount: isGridMode && rows.length > 0 ? rows[0].length : 0 property string direction: "auto" // "auto", "left", "right", "top", "bottom" property int margin: Style.marginXS // distance from target property int padding: Style.marginM + property int gridPaddingVertical: Style.marginXS // extra vertical padding for grid mode property int delay: 0 property int hideDelay: 0 property int maxWidth: 320 @@ -17,6 +22,13 @@ PopupWindow { property int animationDuration: Style.animationFast property real animationScale: 0.85 + // For measuring grid cell sizes + TextMetrics { + id: cellMetrics + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS + } + // Internal properties property var targetItem: null property real anchorX: 0 @@ -106,9 +118,9 @@ PopupWindow { } } - // Function to show tooltip - function show(target, tipText, customDirection, showDelay, fontFamily) { - if (!target || !tipText || tipText === "") + // Function to show tooltip (content can be string or array of arrays for grid mode) + function show(target, content, customDirection, showDelay, fontFamily) { + if (!target || !content || content === "" || (Array.isArray(content) && content.length === 0)) return; root.delay = showDelay; @@ -124,11 +136,17 @@ PopupWindow { hideImmediately(); } - // Convert \n to
for RichText format - const processedText = tipText.replace(/\n/g, '
'); + // Set content based on type + if (Array.isArray(content)) { + rows = content; + text = ""; + } else { + // Convert \n to
for RichText format + const processedText = content.replace(/\n/g, '
'); + text = processedText; + rows = null; + } - // Set properties - text = processedText; targetItem = target; // Find the correct screen dimensions based on target's global position @@ -166,17 +184,68 @@ PopupWindow { tooltipText.family = fontFamily ? fontFamily : Settings.data.ui.fontDefault; } + // Calculate grid dimensions using TextMetrics + function calculateGridSize() { + if (!rows || rows.length === 0) + return { + width: 0, + height: 0 + }; + + const numCols = rows[0].length; + const numRows = rows.length; + let columnWidths = []; + + // Find max width for each column + for (let col = 0; col < numCols; col++) { + let maxWidth = 0; + for (let row = 0; row < numRows; row++) { + cellMetrics.text = rows[row][col] || ""; + if (cellMetrics.width > maxWidth) { + maxWidth = cellMetrics.width; + } + } + columnWidths.push(maxWidth); + } + + // Calculate total width: sum of columns + spacing between columns + let totalWidth = 0; + for (let i = 0; i < columnWidths.length; i++) { + totalWidth += columnWidths[i]; + } + totalWidth += (numCols - 1) * Style.marginM; // columnSpacing + + // Calculate total height: rows * row height + spacing + extra vertical padding + const rowHeight = cellMetrics.height; + const totalHeight = (numRows * rowHeight) + ((numRows - 1) * 0) + (gridPaddingVertical * 2); // rowSpacing is 0 + + return { + width: totalWidth, + height: totalHeight + }; + } + // Function to position and display the tooltip function positionAndShow() { if (!targetItem || !targetItem.parent) { return; } - // Calculate tooltip dimensions - const tipWidth = Math.min(tooltipText.implicitWidth + (padding * 2), maxWidth); + // Calculate tooltip dimensions based on content mode + let contentWidth, contentHeight; + if (isGridMode) { + const gridSize = calculateGridSize(); + contentWidth = gridSize.width; + contentHeight = gridSize.height; + } else { + contentWidth = tooltipText.implicitWidth; + contentHeight = tooltipText.implicitHeight; + } + + const tipWidth = Math.min(contentWidth + (padding * 2), maxWidth); root.implicitWidth = tipWidth; - const tipHeight = tooltipText.implicitHeight + (padding * 2); + const tipHeight = contentHeight + (padding * 2); root.implicitHeight = tipHeight; // Get target's global position and convert to screen-relative @@ -377,6 +446,7 @@ PopupWindow { visible = false; animatingOut = false; text = ""; + rows = null; isPositioned = false; tooltipContainer.opacity = 1.0; tooltipContainer.scale = 1.0; @@ -392,18 +462,34 @@ PopupWindow { completeHide(); } - // Update text function - function updateText(newText) { + // Update content function (supports both text and rows) + function updateContent(newContent) { if (visible && targetItem) { - // Convert \n to
for RichText format - const processedText = newText.replace(/\n/g, '
'); - text = processedText; + if (Array.isArray(newContent)) { + rows = newContent; + text = ""; + } else { + // Convert \n to
for RichText format + const processedText = newContent.replace(/\n/g, '
'); + text = processedText; + rows = null; + } - // Recalculate dimensions - const tipWidth = Math.min(tooltipText.implicitWidth + (padding * 2), maxWidth); + // Recalculate dimensions based on content mode + let contentWidth, contentHeight; + if (isGridMode) { + const gridSize = calculateGridSize(); + contentWidth = gridSize.width; + contentHeight = gridSize.height; + } else { + contentWidth = tooltipText.implicitWidth; + contentHeight = tooltipText.implicitHeight; + } + + const tipWidth = Math.min(contentWidth + (padding * 2), maxWidth); root.implicitWidth = tipWidth; - const tipHeight = tooltipText.implicitHeight + (padding * 2); + const tipHeight = contentHeight + (padding * 2); root.implicitHeight = tipHeight; // Reposition based on current direction (screen-relative) @@ -506,6 +592,11 @@ PopupWindow { } } + // Backward compatibility alias + function updateText(newText) { + updateContent(newText); + } + // Reset function to clean up state function reset() { // Stop all timers and animations @@ -518,6 +609,7 @@ PopupWindow { visible = false; animatingOut = false; text = ""; + rows = null; isPositioned = false; // Reset to defaults @@ -547,11 +639,13 @@ PopupWindow { border.width: Style.borderS radius: Style.radiusS - // Only show content when we have text - visible: root.text !== "" + // Only show content when we have content + visible: root.text !== "" || root.isGridMode + // Text content (default mode) NText { id: tooltipText + visible: !root.isGridMode anchors.centerIn: parent anchors.margins: root.padding text: root.text @@ -564,6 +658,31 @@ PopupWindow { width: Math.min(implicitWidth, root.maxWidth - (root.padding * 2)) richTextEnabled: true } + + // Grid content (grid mode) + GridLayout { + id: gridContent + visible: root.isGridMode + anchors.centerIn: parent + anchors.leftMargin: root.padding + anchors.rightMargin: root.padding + anchors.topMargin: root.padding + root.gridPaddingVertical + anchors.bottomMargin: root.padding + root.gridPaddingVertical + columns: root.columnCount + rowSpacing: 0 + columnSpacing: Style.marginM + + Repeater { + model: root.isGridMode ? [].concat.apply([], root.rows) : [] + + NText { + text: modelData + pointSize: Style.fontSizeS + family: tooltipText.family + color: Color.mOnSurfaceVariant + } + } + } } } diff --git a/Services/UI/TooltipService.qml b/Services/UI/TooltipService.qml index 485d13f33..832585541 100644 --- a/Services/UI/TooltipService.qml +++ b/Services/UI/TooltipService.qml @@ -15,14 +15,14 @@ Singleton { Tooltip {} } - function show(target, text, direction, delay, fontFamily) { + function show(target, content, direction, delay, fontFamily) { if (!Settings.data.ui.tooltipsEnabled) { return; } - // Don't create if no text - if (!target || !text) { - Logger.i("Tooltip", "No target or text"); + // Don't create if no content + if (!target || !content || (Array.isArray(content) && content.length === 0)) { + Logger.i("Tooltip", "No target or content"); return; } @@ -42,7 +42,7 @@ Singleton { // If we already have a tooltip for this target, just update it if (activeTooltip && activeTooltip.targetItem === target) { - activeTooltip.updateText(text); + activeTooltip.updateContent(content); return activeTooltip; } @@ -78,7 +78,7 @@ Singleton { }); // Show the tooltip - newTooltip.show(target, text, direction || "auto", delay || Style.tooltipDelay, fontFamily); + newTooltip.show(target, content, direction || "auto", delay || Style.tooltipDelay, fontFamily); return newTooltip; } else { @@ -120,9 +120,14 @@ Singleton { } } - function updateText(newText) { + function updateContent(newContent) { if (activeTooltip) { - activeTooltip.updateText(newText); + activeTooltip.updateContent(newContent); } } + + // Backward compatibility alias + function updateText(newText) { + updateContent(newText); + } }