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);
+ }
}