From d5d45b11c39b16416ee3207eeeefbe7f2c70c0b8 Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 14 Jul 2025 13:17:06 -0400 Subject: [PATCH] Improve system monitor display --- Services/ProcessMonitorService.qml | 131 ++++++- Widgets/ProcessListDropdown.qml | 556 ++++++++++++++++++++--------- 2 files changed, 499 insertions(+), 188 deletions(-) diff --git a/Services/ProcessMonitorService.qml b/Services/ProcessMonitorService.qml index c2251230..af52b8b2 100644 --- a/Services/ProcessMonitorService.qml +++ b/Services/ProcessMonitorService.qml @@ -12,6 +12,15 @@ Singleton { property bool isUpdating: false property int processUpdateInterval: 3000 + // System information properties + property int totalMemoryKB: 0 + property int usedMemoryKB: 0 + property int totalSwapKB: 0 + property int usedSwapKB: 0 + property int cpuCount: 1 + property real totalCpuUsage: 0.0 + property bool systemInfoAvailable: false + // Sorting options property string sortBy: "cpu" // "cpu", "memory", "name", "pid" property bool sortDescending: true @@ -23,10 +32,32 @@ Singleton { console.log("ProcessMonitorService: Initialization complete") } + // System information monitoring + Process { + id: systemInfoProcess + command: ["bash", "-c", "cat /proc/meminfo; echo '---CPU---'; nproc; echo '---CPUSTAT---'; grep '^cpu ' /proc/stat"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (text.trim()) { + parseSystemInfo(text.trim()) + } + } + } + + onExited: (exitCode) => { + if (exitCode !== 0) { + console.warn("System info check failed with exit code:", exitCode) + root.systemInfoAvailable = false + } + } + } + // Process monitoring with ps command Process { id: processListProcess - command: ["bash", "-c", "ps axo pid,ppid,pcpu,pmem,comm,cmd --sort=-pcpu | head -" + (root.maxProcesses + 1)] + command: ["bash", "-c", "ps axo pid,ppid,pcpu,pmem,rss,comm,cmd --sort=-pcpu | head -" + (root.maxProcesses + 1)] running: false stdout: StdioCollector { @@ -40,21 +71,23 @@ Singleton { const line = lines[i].trim() if (!line) continue - // Parse ps output: PID PPID %CPU %MEM COMMAND CMD + // Parse ps output: PID PPID %CPU %MEM RSS COMMAND CMD const parts = line.split(/\s+/) - if (parts.length >= 6) { + if (parts.length >= 7) { const pid = parseInt(parts[0]) const ppid = parseInt(parts[1]) const cpu = parseFloat(parts[2]) - const memory = parseFloat(parts[3]) - const command = parts[4] - const fullCmd = parts.slice(5).join(' ') + const memoryPercent = parseFloat(parts[3]) + const memoryKB = parseInt(parts[4]) + const command = parts[5] + const fullCmd = parts.slice(6).join(' ') newProcesses.push({ pid: pid, ppid: ppid, cpu: cpu, - memory: memory, + memoryPercent: memoryPercent, + memoryKB: memoryKB, command: command, fullCommand: fullCmd, displayName: command.length > 15 ? command.substring(0, 15) + "..." : command @@ -76,7 +109,7 @@ Singleton { } } - // Process monitoring timer + // System and process monitoring timer Timer { id: processTimer interval: root.processUpdateInterval @@ -84,11 +117,18 @@ Singleton { repeat: true onTriggered: { + updateSystemInfo() updateProcessList() } } // Public functions + function updateSystemInfo() { + if (!systemInfoProcess.running) { + systemInfoProcess.running = true + } + } + function updateProcessList() { if (!root.isUpdating) { root.isUpdating = true @@ -112,7 +152,7 @@ Singleton { sortOption = "--sort=-pcpu" } - processListProcess.command = ["bash", "-c", "ps axo pid,ppid,pcpu,pmem,comm,cmd " + sortOption + " | head -" + (root.maxProcesses + 1)] + processListProcess.command = ["bash", "-c", "ps axo pid,ppid,pcpu,pmem,rss,comm,cmd " + sortOption + " | head -" + (root.maxProcesses + 1)] processListProcess.running = true } } @@ -167,7 +207,76 @@ Singleton { return cpu.toFixed(1) + "%" } - function formatMemoryUsage(memory) { - return memory.toFixed(1) + "%" + function formatMemoryUsage(memoryKB) { + if (memoryKB < 1024) { + return memoryKB.toFixed(0) + " KB" + } else if (memoryKB < 1024 * 1024) { + return (memoryKB / 1024).toFixed(1) + " MB" + } else { + return (memoryKB / (1024 * 1024)).toFixed(1) + " GB" + } + } + + function formatSystemMemory(memoryKB) { + if (memoryKB < 1024 * 1024) { + return (memoryKB / 1024).toFixed(0) + " MB" + } else { + return (memoryKB / (1024 * 1024)).toFixed(1) + " GB" + } + } + + function parseSystemInfo(text) { + const lines = text.split('\n') + let section = 'memory' + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim() + + if (line === '---CPU---') { + section = 'cpucount' + continue + } else if (line === '---CPUSTAT---') { + section = 'cpustat' + continue + } + + if (section === 'memory') { + if (line.startsWith('MemTotal:')) { + root.totalMemoryKB = parseInt(line.split(/\s+/)[1]) + } else if (line.startsWith('MemAvailable:')) { + const availableKB = parseInt(line.split(/\s+/)[1]) + root.usedMemoryKB = root.totalMemoryKB - availableKB + } else if (line.startsWith('SwapTotal:')) { + root.totalSwapKB = parseInt(line.split(/\s+/)[1]) + } else if (line.startsWith('SwapFree:')) { + const freeSwapKB = parseInt(line.split(/\s+/)[1]) + root.usedSwapKB = root.totalSwapKB - freeSwapKB + } + } else if (section === 'cpucount') { + const count = parseInt(line) + if (!isNaN(count)) { + root.cpuCount = count + } + } else if (section === 'cpustat') { + if (line.startsWith('cpu ')) { + const parts = line.split(/\s+/) + if (parts.length >= 8) { + const user = parseInt(parts[1]) + const nice = parseInt(parts[2]) + const system = parseInt(parts[3]) + const idle = parseInt(parts[4]) + const iowait = parseInt(parts[5]) + const irq = parseInt(parts[6]) + const softirq = parseInt(parts[7]) + + const total = user + nice + system + idle + iowait + irq + softirq + const used = total - idle - iowait + root.totalCpuUsage = total > 0 ? (used / total) * 100 : 0 + } + } + } + } + + root.systemInfoAvailable = true } } \ No newline at end of file diff --git a/Widgets/ProcessListDropdown.qml b/Widgets/ProcessListDropdown.qml index 90419f95..b1d53749 100644 --- a/Widgets/ProcessListDropdown.qml +++ b/Widgets/ProcessListDropdown.qml @@ -17,8 +17,8 @@ PanelWindow { visible: isVisible - implicitWidth: 500 - implicitHeight: 500 + implicitWidth: 600 + implicitHeight: 600 WlrLayershell.layer: WlrLayershell.Overlay WlrLayershell.exclusiveZone: -1 @@ -41,8 +41,8 @@ PanelWindow { Rectangle { id: dropdownContent - width: Math.min(500, parent.width - Theme.spacingL * 2) - height: Math.min(500, parent.height - Theme.barHeight - Theme.spacingS * 2) + width: Math.min(600, parent.width - Theme.spacingL * 2) + height: Math.min(600, parent.height - Theme.barHeight - Theme.spacingS * 2) x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL) y: Theme.barHeight + Theme.spacingXS @@ -94,145 +94,191 @@ PanelWindow { anchors.margins: Theme.spacingL spacing: Theme.spacingM - // Header + // System overview and controls Column { Layout.fillWidth: true spacing: Theme.spacingM + // Enhanced system overview with integrated controls Row { width: parent.width - height: 32 + spacing: Theme.spacingM - Text { - id: processTitle - text: "System Processes" - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Medium - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - Item { - width: parent.width - processTitle.width - sortControls.width - Theme.spacingM - height: 1 - } - - // Sort controls - Row { - id: sortControls - spacing: Theme.spacingXS - anchors.verticalCenter: parent.verticalCenter - - Rectangle { - width: cpuButton.width + ramButton.width + Theme.spacingXS - height: 28 - radius: Theme.cornerRadius - color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) - - Row { - anchors.centerIn: parent - spacing: 0 - - Button { - id: cpuButton - text: "CPU" - flat: true - checkable: true - checked: ProcessMonitorService.sortBy === "cpu" - onClicked: ProcessMonitorService.setSortBy("cpu") - font.pixelSize: Theme.fontSizeSmall - hoverEnabled: true - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: parent.clicked() - } - - contentItem: Text { - text: parent.text - font: parent.font - color: parent.checked ? Theme.primary : Theme.surfaceText - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - background: Rectangle { - color: parent.checked ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" - radius: Theme.cornerRadius - - Behavior on color { - ColorAnimation { duration: Theme.shortDuration } - } - } - } - - Button { - id: ramButton - text: "RAM" - flat: true - checkable: true - checked: ProcessMonitorService.sortBy === "memory" - onClicked: ProcessMonitorService.setSortBy("memory") - font.pixelSize: Theme.fontSizeSmall - hoverEnabled: true - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: parent.clicked() - } - - contentItem: Text { - text: parent.text - font: parent.font - color: parent.checked ? Theme.primary : Theme.surfaceText - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - background: Rectangle { - color: parent.checked ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" - radius: Theme.cornerRadius - - Behavior on color { - ColorAnimation { duration: Theme.shortDuration } - } - } - } + // CPU Overview Card (clickable for sorting) + Rectangle { + width: (parent.width - Theme.spacingM * 2) / 3 + height: 80 + radius: Theme.cornerRadiusLarge + color: { + if (ProcessMonitorService.sortBy === "cpu") { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) + } else if (cpuCardMouseArea.containsMouse) { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) + } else { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) } } + border.color: ProcessMonitorService.sortBy === "cpu" ? + Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : + Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) + border.width: ProcessMonitorService.sortBy === "cpu" ? 2 : 1 - Rectangle { - width: 28 - height: 28 - radius: Theme.cornerRadius - color: sortOrderArea.containsMouse ? - Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : - "transparent" + MouseArea { + id: cpuCardMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: ProcessMonitorService.setSortBy("cpu") + } + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + + Behavior on border.color { + ColorAnimation { duration: Theme.shortDuration } + } + + Column { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: 2 Text { - text: ProcessMonitorService.sortDescending ? "↓" : "↑" - font.pixelSize: Theme.fontSizeMedium + text: "CPU" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: ProcessMonitorService.sortBy === "cpu" ? Theme.primary : Theme.secondary + opacity: ProcessMonitorService.sortBy === "cpu" ? 1.0 : 0.8 + } + + Text { + text: ProcessMonitorService.totalCpuUsage.toFixed(1) + "%" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Bold color: Theme.surfaceText - anchors.centerIn: parent } - MouseArea { - id: sortOrderArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: ProcessMonitorService.toggleSortOrder() + Text { + text: ProcessMonitorService.cpuCount + " cores" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + opacity: 0.7 + } + } + } + + // Memory Overview Card (clickable for sorting) + Rectangle { + width: (parent.width - Theme.spacingM * 2) / 3 + height: 80 + radius: Theme.cornerRadiusLarge + color: { + if (ProcessMonitorService.sortBy === "memory") { + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) + } else if (memoryCardMouseArea.containsMouse) { + return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12) + } else { + return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08) + } + } + border.color: ProcessMonitorService.sortBy === "memory" ? + Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : + Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.2) + border.width: ProcessMonitorService.sortBy === "memory" ? 2 : 1 + + MouseArea { + id: memoryCardMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: ProcessMonitorService.setSortBy("memory") + } + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + + Behavior on border.color { + ColorAnimation { duration: Theme.shortDuration } + } + + Column { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + Text { + text: "Memory" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: ProcessMonitorService.sortBy === "memory" ? Theme.primary : Theme.secondary + opacity: ProcessMonitorService.sortBy === "memory" ? 1.0 : 0.8 } - Behavior on color { - ColorAnimation { duration: Theme.shortDuration } + Text { + text: ProcessMonitorService.formatSystemMemory(ProcessMonitorService.usedMemoryKB) + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Bold + color: Theme.surfaceText + } + + Text { + text: "of " + ProcessMonitorService.formatSystemMemory(ProcessMonitorService.totalMemoryKB) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + opacity: 0.7 + } + } + } + + // Swap Overview Card + Rectangle { + width: (parent.width - Theme.spacingM * 2) / 3 + height: 80 + radius: Theme.cornerRadiusLarge + color: ProcessMonitorService.totalSwapKB > 0 ? + Qt.rgba(Theme.tertiary.r, Theme.tertiary.g, Theme.tertiary.b, 0.08) : + Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04) + border.color: ProcessMonitorService.totalSwapKB > 0 ? + Qt.rgba(Theme.tertiary.r, Theme.tertiary.g, Theme.tertiary.b, 0.2) : + Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) + border.width: 1 + + Column { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + Text { + text: "Swap" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: ProcessMonitorService.totalSwapKB > 0 ? Theme.tertiary : Theme.surfaceText + opacity: 0.8 + } + + Text { + text: ProcessMonitorService.totalSwapKB > 0 ? ProcessMonitorService.formatSystemMemory(ProcessMonitorService.usedSwapKB) : "None" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Bold + color: Theme.surfaceText + } + + Text { + text: ProcessMonitorService.totalSwapKB > 0 ? "of " + ProcessMonitorService.formatSystemMemory(ProcessMonitorService.totalSwapKB) : "No swap configured" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + opacity: 0.7 } } } } + // Separator Rectangle { width: parent.width @@ -242,52 +288,106 @@ PanelWindow { } // Headers - Row { + Item { id: columnHeaders Layout.fillWidth: true Layout.leftMargin: 8 - spacing: 8 + height: 24 - // Icon placeholder + Process name + // Process name header Text { text: "Process" font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium color: Theme.surfaceText opacity: 0.7 - width: Theme.iconSize - 4 + 8 + 150 // icon width + spacing + name width + anchors.left: parent.left + anchors.leftMargin: 0 // Left align with content area + anchors.verticalCenter: parent.verticalCenter } - Item { width: parent.parent.width - 280 - 16 } // Flexible spacer to match process list (280 from row + 16 for margins) - - Text { - text: "CPU" - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - color: Theme.surfaceText - opacity: 0.7 - width: 60 - horizontalAlignment: Text.AlignRight + // CPU header - positioned exactly like CPU badge + Rectangle { + width: 80 + height: 20 + color: "transparent" + anchors.right: parent.right + anchors.rightMargin: 200 // Slight adjustment to move right + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "CPU" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + opacity: 0.7 + anchors.centerIn: parent + } } - Text { - text: "RAM" - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - color: Theme.surfaceText - opacity: 0.7 - width: 60 - horizontalAlignment: Text.AlignRight + // RAM header - positioned exactly like memory badge + Rectangle { + width: 80 + height: 20 + color: "transparent" + anchors.right: parent.right + anchors.rightMargin: 112 // Move right by decreasing rightMargin + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "RAM" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + opacity: 0.7 + anchors.centerIn: parent + } } + // PID header - positioned exactly like PID text Text { text: "PID" font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium color: Theme.surfaceText opacity: 0.7 - width: 60 + width: 50 horizontalAlignment: Text.AlignRight + anchors.right: parent.right + anchors.rightMargin: 53 // Move left by increasing rightMargin + anchors.verticalCenter: parent.verticalCenter + } + + // Sort direction arrow - far right + Rectangle { + width: 28 + height: 28 + radius: Theme.cornerRadius + color: sortOrderArea.containsMouse ? + Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : + "transparent" + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + + Text { + text: ProcessMonitorService.sortDescending ? "↓" : "↑" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.centerIn: parent + } + + MouseArea { + id: sortOrderArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: ProcessMonitorService.toggleSortOrder() + } + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } } } @@ -305,15 +405,19 @@ PanelWindow { id: processListView anchors.fill: parent model: ProcessMonitorService.processes - spacing: 2 + spacing: 4 delegate: Rectangle { - width: processListView.width - 16 - height: 36 - radius: Theme.cornerRadius + width: processListView.width + height: 40 + radius: Theme.cornerRadiusLarge color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" + border.color: processMouseArea.containsMouse ? + Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : + "transparent" + border.width: 1 MouseArea { id: processMouseArea @@ -342,15 +446,13 @@ PanelWindow { } } - Row { - anchors.left: parent.left - anchors.leftMargin: 8 - anchors.verticalCenter: parent.verticalCenter - spacing: 8 - width: parent.width - 16 + Item { + anchors.fill: parent + anchors.margins: 8 // Process icon Text { + id: processIcon text: ProcessMonitorService.getProcessIcon(modelData ? modelData.command : "") font.family: Theme.iconFont font.pixelSize: Theme.iconSize - 4 @@ -360,6 +462,7 @@ PanelWindow { return Theme.surfaceText } opacity: 0.8 + anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter } @@ -367,42 +470,70 @@ PanelWindow { Text { text: modelData ? modelData.displayName : "" font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium color: Theme.surfaceText - width: 150 + width: 250 elide: Text.ElideRight + anchors.left: processIcon.right + anchors.leftMargin: 8 anchors.verticalCenter: parent.verticalCenter } - Item { width: parent.width - 280 } // CPU usage - Text { - text: ProcessMonitorService.formatCpuUsage(modelData ? modelData.cpu : 0) - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium + Rectangle { + id: cpuBadge + width: 80 + height: 20 + radius: Theme.cornerRadius color: { - if (modelData && modelData.cpu > 80) return Theme.error - if (modelData && modelData.cpu > 50) return Theme.warning - return Theme.surfaceText + if (modelData && modelData.cpu > 80) return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) + if (modelData && modelData.cpu > 50) return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) + return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) } - width: 60 - horizontalAlignment: Text.AlignRight + anchors.right: parent.right + anchors.rightMargin: 194 // 28 (menu) + 12 + 50 (pid) + 12 + 80 (mem) + 12 spacing anchors.verticalCenter: parent.verticalCenter + + Text { + text: ProcessMonitorService.formatCpuUsage(modelData ? modelData.cpu : 0) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: { + if (modelData && modelData.cpu > 80) return Theme.error + if (modelData && modelData.cpu > 50) return Theme.warning + return Theme.surfaceText + } + anchors.centerIn: parent + } } // Memory usage - Text { - text: ProcessMonitorService.formatMemoryUsage(modelData ? modelData.memory : 0) - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium + Rectangle { + id: memoryBadge + width: 80 + height: 20 + radius: Theme.cornerRadius color: { - if (modelData && modelData.memory > 10) return Theme.error - if (modelData && modelData.memory > 5) return Theme.warning - return Theme.surfaceText + if (modelData && modelData.memoryKB > 1024 * 1024) return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) // > 1GB + if (modelData && modelData.memoryKB > 512 * 1024) return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) // > 512MB + return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) } - width: 60 - horizontalAlignment: Text.AlignRight + anchors.right: parent.right + anchors.rightMargin: 102 // 28 (menu) + 12 + 50 (pid) + 12 spacing anchors.verticalCenter: parent.verticalCenter + + Text { + text: ProcessMonitorService.formatMemoryUsage(modelData ? modelData.memoryKB : 0) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: { + if (modelData && modelData.memoryKB > 1024 * 1024) return Theme.error // > 1GB + if (modelData && modelData.memoryKB > 512 * 1024) return Theme.warning // > 512MB + return Theme.surfaceText + } + anchors.centerIn: parent + } } // PID @@ -411,10 +542,54 @@ PanelWindow { font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceText opacity: 0.7 - width: 60 + width: 50 horizontalAlignment: Text.AlignRight + anchors.right: parent.right + anchors.rightMargin: 40 // 28 (menu) + 12 spacing anchors.verticalCenter: parent.verticalCenter } + + // 3-dot menu button (far right) + Rectangle { + id: menuButton + width: 28 + height: 28 + radius: Theme.cornerRadius + color: menuButtonArea.containsMouse ? + Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : + "transparent" + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "more_vert" + font.family: Theme.iconFont + font.weight: Theme.iconFontWeight + font.pixelSize: Theme.iconSize - 2 + color: Theme.surfaceText + opacity: 0.6 + anchors.centerIn: parent + } + + MouseArea { + id: menuButtonArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (modelData && modelData.pid > 0) { + processContextMenuWindow.processData = modelData + let globalPos = menuButtonArea.mapToGlobal(menuButtonArea.width / 2, menuButtonArea.height) + processContextMenuWindow.show(globalPos.x, globalPos.y) + } + } + } + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + } } } } @@ -674,8 +849,34 @@ PanelWindow { } function show(x, y) { - processContextMenu.x = x - processContextMenu.y = y + // Smart positioning to prevent off-screen cutoff + const menuWidth = 180 + const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2 + + // Get screen dimensions from the monitor + const screenWidth = processContextMenuWindow.screen ? processContextMenuWindow.screen.width : 1920 + const screenHeight = processContextMenuWindow.screen ? processContextMenuWindow.screen.height : 1080 + + // Calculate optimal position + let finalX = x + let finalY = y + + // Check horizontal bounds - if too close to right edge, position to the left + if (x + menuWidth > screenWidth - 20) { + finalX = x - menuWidth + } + + // Check vertical bounds - if too close to bottom edge, position above + if (y + menuHeight > screenHeight - 20) { + finalY = y - menuHeight + } + + // Ensure we don't go off the left or top edges + finalX = Math.max(20, finalX) + finalY = Math.max(20, finalY) + + processContextMenu.x = finalX + processContextMenu.y = finalY processContextMenuWindow.menuVisible = true } @@ -721,6 +922,7 @@ PanelWindow { function show() { isVisible = true + ProcessMonitorService.updateSystemInfo() ProcessMonitorService.updateProcessList() }