From b1a9e7333f4f7d58b37835e262893c0ae56afc6d Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 6 Apr 2026 14:56:47 -0400 Subject: [PATCH 1/3] feat(quick-menu): replace tools tab with task manager launcher --- .../app/gamenative/ui/component/QuickMenu.kt | 58 +++++++++++++++++++ .../ui/screen/xserver/XServerScreen.kt | 1 + app/src/main/res/values/strings.xml | 8 +++ 3 files changed, 67 insertions(+) diff --git a/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt b/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt index bd0ea12a56..32aa850681 100644 --- a/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt +++ b/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt @@ -40,6 +40,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ExitToApp import androidx.compose.material.icons.filled.AutoFixHigh +import androidx.compose.material.icons.filled.BarChart import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Gamepad @@ -81,6 +82,7 @@ import app.gamenative.ui.theme.PluviaTheme import app.gamenative.ui.util.adaptivePanelWidth import app.gamenative.utils.MathUtils.normalizedProgress import com.winlator.renderer.GLRenderer +import com.winlator.winhandler.WinHandler import kotlinx.coroutines.delay import kotlin.math.roundToInt @@ -97,6 +99,7 @@ private object QuickMenuTab { const val HUD = 0 const val EFFECTS = 1 const val CONTROLLER = 2 + const val TOOLS = 3 } data class QuickMenuItem( @@ -216,6 +219,7 @@ fun QuickMenu( onDismiss: () -> Unit, onItemSelected: (Int) -> Boolean, renderer: GLRenderer? = null, + winHandler: WinHandler? = null, isPerformanceHudEnabled: Boolean = false, performanceHudConfig: PerformanceHudConfig = PerformanceHudConfig(), onPerformanceHudConfigChanged: (PerformanceHudConfig) -> Unit = {}, @@ -271,6 +275,7 @@ fun QuickMenu( val selectedTabLabelResId = when (selectedTab) { QuickMenuTab.HUD -> R.string.performance_hud QuickMenuTab.EFFECTS -> R.string.screen_effects + QuickMenuTab.TOOLS -> R.string.task_manager else -> R.string.quick_menu_tab_controller } @@ -280,9 +285,11 @@ fun QuickMenu( val controllerScrollState = rememberScrollState() val hudTabFocusRequester = remember { FocusRequester() } val controllerTabFocusRequester = remember { FocusRequester() } + val toolsTabFocusRequester = remember { FocusRequester() } val hudItemFocusRequester = remember { FocusRequester() } val effectsItemFocusRequester = remember { FocusRequester() } val controllerItemFocusRequester = remember { FocusRequester() } + val toolsItemFocusRequester = remember { FocusRequester() } BackHandler(enabled = isVisible) { onDismiss() @@ -403,6 +410,15 @@ fun QuickMenu( modifier = Modifier.width(56.dp), focusRequester = controllerTabFocusRequester, ) + QuickMenuTabButton( + icon = Icons.Default.BarChart, + contentDescriptionResId = R.string.task_manager, + selected = selectedTab == QuickMenuTab.TOOLS, + accentColor = PluviaTheme.colors.accentPurple, + onSelected = { selectedTab = QuickMenuTab.TOOLS }, + modifier = Modifier.width(56.dp), + focusRequester = toolsTabFocusRequester, + ) } Spacer(modifier = Modifier.weight(1f)) @@ -492,6 +508,15 @@ fun QuickMenu( } } + QuickMenuTab.TOOLS -> { + ToolsQuickMenuTab( + winHandler = winHandler, + onDismiss = onDismiss, + firstItemFocusRequester = toolsItemFocusRequester, + modifier = Modifier.fillMaxSize(), + ) + } + else -> { Column( modifier = Modifier @@ -537,6 +562,39 @@ fun QuickMenu( } } +@Composable +private fun ToolsQuickMenuTab( + winHandler: WinHandler?, + onDismiss: () -> Unit, + firstItemFocusRequester: FocusRequester? = null, + modifier: Modifier = Modifier, +) { + val scrollState = rememberScrollState() + val item = QuickMenuItem( + id = -1, + icon = Icons.Default.BarChart, + labelResId = R.string.launch_task_manager, + accentColor = PluviaTheme.colors.accentWarning, + enabled = winHandler != null, + ) + + Column( + modifier = modifier + .verticalScroll(scrollState) + .focusGroup(), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + QuickMenuItemRow( + item = item, + onClick = { + winHandler?.exec("taskmgr.exe") + onDismiss() + }, + focusRequester = firstItemFocusRequester, + ) + } +} + @Composable private fun PerformanceHudQuickMenuTab( isPerformanceHudEnabled: Boolean, diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt index 5031522a08..19e468f7d0 100644 --- a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt @@ -2036,6 +2036,7 @@ fun XServerScreen( onDismiss = dismissOverlayMenu, onItemSelected = onQuickMenuItemSelected, renderer = xServerView?.renderer, + winHandler = xServerView?.getxServer()?.winHandler, isPerformanceHudEnabled = isPerformanceHudEnabled, performanceHudConfig = performanceHudConfig, onPerformanceHudConfigChanged = ::applyPerformanceHudConfig, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index feb1d794b1..c336a3e2c0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -263,6 +263,14 @@ Close General Controller + Tools + Launch Task Manager + File Explorer + Task Manager + Command Prompt + Wine Config + Notepad + Registry Editor Performance HUD Show or hide the in-game overlay. Presets From 318fe81bdf81a3a7cf37252f30c9fb78e9c344aa Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 6 Apr 2026 16:28:10 -0400 Subject: [PATCH 2/3] feat(quick-menu): show running exe memory usage --- .../app/gamenative/ui/component/QuickMenu.kt | 115 +++++++++++++++++- .../ui/screen/xserver/XServerScreen.kt | 36 ++++++ .../java/com/winlator/core/ProcessHelper.java | 68 +++++------ app/src/main/res/values/strings.xml | 3 + 4 files changed, 186 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt b/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt index 32aa850681..58a8c6a3e7 100644 --- a/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt +++ b/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt @@ -73,6 +73,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import app.gamenative.R @@ -82,6 +83,7 @@ import app.gamenative.ui.theme.PluviaTheme import app.gamenative.ui.util.adaptivePanelWidth import app.gamenative.utils.MathUtils.normalizedProgress import com.winlator.renderer.GLRenderer +import com.winlator.winhandler.ProcessInfo import com.winlator.winhandler.WinHandler import kotlinx.coroutines.delay import kotlin.math.roundToInt @@ -220,6 +222,8 @@ fun QuickMenu( onItemSelected: (Int) -> Boolean, renderer: GLRenderer? = null, winHandler: WinHandler? = null, + wineProcesses: List = emptyList(), + isWineProcessesLoading: Boolean = false, isPerformanceHudEnabled: Boolean = false, performanceHudConfig: PerformanceHudConfig = PerformanceHudConfig(), onPerformanceHudConfigChanged: (PerformanceHudConfig) -> Unit = {}, @@ -511,6 +515,8 @@ fun QuickMenu( QuickMenuTab.TOOLS -> { ToolsQuickMenuTab( winHandler = winHandler, + processes = wineProcesses, + isLoadingProcesses = isWineProcessesLoading, onDismiss = onDismiss, firstItemFocusRequester = toolsItemFocusRequester, modifier = Modifier.fillMaxSize(), @@ -565,16 +571,19 @@ fun QuickMenu( @Composable private fun ToolsQuickMenuTab( winHandler: WinHandler?, + processes: List, + isLoadingProcesses: Boolean, onDismiss: () -> Unit, firstItemFocusRequester: FocusRequester? = null, modifier: Modifier = Modifier, ) { val scrollState = rememberScrollState() + val accentColor = PluviaTheme.colors.accentPurple val item = QuickMenuItem( id = -1, icon = Icons.Default.BarChart, labelResId = R.string.launch_task_manager, - accentColor = PluviaTheme.colors.accentWarning, + accentColor = accentColor, enabled = winHandler != null, ) @@ -592,6 +601,34 @@ private fun ToolsQuickMenuTab( }, focusRequester = firstItemFocusRequester, ) + + Spacer(modifier = Modifier.height(8.dp)) + + QuickMenuSectionHeader( + title = stringResource(R.string.tools_wine_processes), + subtitle = if (isLoadingProcesses) { + stringResource(R.string.main_loading) + } else { + stringResource(R.string.tools_wine_processes_running, processes.size) + }, + ) + + if (!isLoadingProcesses && processes.isEmpty()) { + Text( + text = stringResource(R.string.tools_wine_processes_empty), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), + ) + } else { + processes.forEach { process -> + QuickMenuReadOnlyValueRow( + title = process.name, + value = process.formattedMemoryUsage, + accentColor = accentColor, + ) + } + } } } @@ -1543,6 +1580,82 @@ private fun QuickMenuSwitch( } } +@Composable +private fun QuickMenuReadOnlyValueRow( + title: String, + value: String, + accentColor: Color, + modifier: Modifier = Modifier, + focusRequester: FocusRequester? = null, +) { + val interactionSource = remember { MutableInteractionSource() } + val isFocused by interactionSource.collectIsFocusedAsState() + val shape = RoundedCornerShape(14.dp) + + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 2.dp) + .clip(shape) + .background( + if (isFocused) { + Brush.horizontalGradient( + colors = listOf( + accentColor.copy(alpha = 0.14f), + accentColor.copy(alpha = 0.06f), + ), + ) + } else { + Brush.horizontalGradient( + colors = listOf( + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.18f), + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.10f), + ), + ) + }, + ) + .then( + if (isFocused) { + Modifier.border( + width = 2.dp, + color = accentColor.copy(alpha = 0.7f), + shape = shape, + ) + } else { + Modifier + } + ) + .then( + if (focusRequester != null) { + Modifier.focusRequester(focusRequester) + } else { + Modifier + } + ) + .focusable(interactionSource = interactionSource) + .padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + color = if (isFocused) accentColor else MaterialTheme.colorScheme.onSurface, + fontWeight = if (isFocused) FontWeight.SemiBold else FontWeight.Medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + + Text( + text = value, + style = MaterialTheme.typography.labelLarge, + color = if (isFocused) accentColor else MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = if (isFocused) FontWeight.SemiBold else FontWeight.Medium, + ) + } +} + @Composable private fun QuickMenuItemRow( item: QuickMenuItem, diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt index 19e468f7d0..71eb244101 100644 --- a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt @@ -194,6 +194,7 @@ private val isExiting = AtomicBoolean(false) private const val EXIT_PROCESS_TIMEOUT_MS = 30_000L private const val EXIT_PROCESS_POLL_INTERVAL_MS = 1_000L private const val EXIT_PROCESS_RESPONSE_TIMEOUT_MS = 2_000L +private const val QUICK_MENU_PROCESS_POLL_INTERVAL_MS = 2_000L private data class XServerViewReleaseBinding( val xServerView: XServerView, @@ -372,6 +373,8 @@ fun XServerScreen( var showPhysicalControllerDialog by remember { mutableStateOf(false) } var keyboardRequestedFromOverlay by remember { mutableStateOf(false) } var showQuickMenu by remember { mutableStateOf(false) } + var quickMenuWineProcesses by remember { mutableStateOf>(emptyList()) } + var quickMenuWineProcessesLoading by remember { mutableStateOf(false) } var hasPhysicalController by remember { mutableStateOf(false) } var keepPausedForEditor by remember { mutableStateOf(false) } var hasPhysicalKeyboard by remember { mutableStateOf(false) } @@ -788,6 +791,37 @@ fun XServerScreen( showQuickMenu = false } + LaunchedEffect(showQuickMenu) { + if (!showQuickMenu) { + quickMenuWineProcesses = emptyList() + quickMenuWineProcessesLoading = false + return@LaunchedEffect + } + + quickMenuWineProcessesLoading = true + while (showQuickMenu) { + val snapshot = withContext(Dispatchers.IO) { + ProcessHelper.listSubProcesses() + .asSequence() + .filter { it.name.endsWith(".exe", ignoreCase = true) } + .map { + ProcessInfo( + it.pid, + it.name.substringAfterLast('\\').substringAfterLast('/'), + it.rssBytes, + 0, + false, + ) + } + .sortedByDescending { it.memoryUsage } + .toList() + } + quickMenuWineProcesses = snapshot + quickMenuWineProcessesLoading = false + delay(QUICK_MENU_PROCESS_POLL_INTERVAL_MS) + } + } + val onQuickMenuItemSelected: (Int) -> Boolean = { itemId -> when (itemId) { QuickMenuAction.KEYBOARD -> { @@ -2037,6 +2071,8 @@ fun XServerScreen( onItemSelected = onQuickMenuItemSelected, renderer = xServerView?.renderer, winHandler = xServerView?.getxServer()?.winHandler, + wineProcesses = quickMenuWineProcesses, + isWineProcessesLoading = quickMenuWineProcessesLoading, isPerformanceHudEnabled = isPerformanceHudEnabled, performanceHudConfig = performanceHudConfig, onPerformanceHudConfigChanged = ::applyPerformanceHudConfig, diff --git a/app/src/main/java/com/winlator/core/ProcessHelper.java b/app/src/main/java/com/winlator/core/ProcessHelper.java index 0363ea2a84..67788dfbcb 100644 --- a/app/src/main/java/com/winlator/core/ProcessHelper.java +++ b/app/src/main/java/com/winlator/core/ProcessHelper.java @@ -71,11 +71,17 @@ public static class ProcessInfo { public final int pid; public final int ppid; public final String name; + public final long rssBytes; public ProcessInfo(int pid, int ppid, String name) { + this(pid, ppid, name, 0L); + } + + public ProcessInfo(int pid, int ppid, String name, long rssBytes) { this.pid = pid; this.ppid = ppid; this.name = name; + this.rssBytes = rssBytes; } } @@ -111,37 +117,9 @@ public static int exec(String command, String[] envp, File workingDir, Callback< public static List listSubProcesses() { List processes = new ArrayList<>(); - String myUser = null; + String myUser = getCurrentProcessUser(); + if (myUser == null) return processes; - // First get our username using the id command - try { - java.lang.Process idProcess = Runtime.getRuntime().exec("id"); - try ( - InputStreamReader isr = new InputStreamReader(idProcess.getInputStream()); - BufferedReader idReader = new BufferedReader(isr); - ) { - String idOutput = idReader.readLine(); - if (idOutput != null) { - // id output format: uid=10290(u0_a290) gid=10290(u0_a290) ... - int startIndex = idOutput.indexOf('('); - int endIndex = idOutput.indexOf(')'); - if (startIndex != -1 && endIndex != -1) { - myUser = idOutput.substring(startIndex + 1, endIndex); - } - } - } - } catch (IOException e) { - Log.e("ProcessHelper", "Failed to retrieve user id in order to list processes: " + e); - return processes; - } - - if (myUser == null) { - return processes; - } - - // Log.d("ProcessHelper", "Found user value to be: " + myUser); - - // Now get the processes try { java.lang.Process process = Runtime.getRuntime().exec("ps -A -o USER,PID,PPID,VSZ,RSS,WCHAN,ADDR,S,NAME"); try ( @@ -149,21 +127,19 @@ public static List listSubProcesses() { BufferedReader reader = new BufferedReader(isr); ) { String line; - - // Skip header line reader.readLine(); while ((line = reader.readLine()) != null) { - String[] parts = line.trim().split("\\s+"); + String[] parts = line.trim().split("\\s+", 9); if (parts.length >= 9) { String user = parts[0]; int pid = Integer.parseInt(parts[1]); int ppid = Integer.parseInt(parts[2]); + long rssKb = Long.parseLong(parts[4]); String processName = parts[8]; - // Check if process belongs to our app (same user) if (user.equals(myUser) && pid != Process.myPid()) { - ProcessInfo info = new ProcessInfo(pid, ppid, processName); + ProcessInfo info = new ProcessInfo(pid, ppid, processName, rssKb * 1024L); processes.add(info); } } @@ -176,6 +152,28 @@ public static List listSubProcesses() { return processes; } + private static String getCurrentProcessUser() { + try { + java.lang.Process idProcess = Runtime.getRuntime().exec("id"); + try ( + InputStreamReader isr = new InputStreamReader(idProcess.getInputStream()); + BufferedReader idReader = new BufferedReader(isr); + ) { + String idOutput = idReader.readLine(); + if (idOutput != null) { + int startIndex = idOutput.indexOf('('); + int endIndex = idOutput.indexOf(')'); + if (startIndex != -1 && endIndex != -1) { + return idOutput.substring(startIndex + 1, endIndex); + } + } + } + } catch (IOException e) { + Log.e("ProcessHelper", "Failed to retrieve process user: " + e); + } + return null; + } + private static void createDebugThread(final InputStream inputStream) { Executors.newSingleThreadExecutor().execute(() -> { try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c336a3e2c0..1cc5be5829 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -265,6 +265,9 @@ Controller Tools Launch Task Manager + Running EXEs + %1$d running + No running EXEs detected. File Explorer Task Manager Command Prompt From 1604e34d37adcc5a096298830e023d12edfdcb30 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Sun, 12 Apr 2026 22:18:10 -0400 Subject: [PATCH 3/3] feat(ui): streamline quick menu task manager --- .../app/gamenative/ui/component/QuickMenu.kt | 112 ++++++++++-------- .../ui/screen/xserver/XServerScreen.kt | 80 ++++++++++--- app/src/main/res/values-da/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-ko/strings.xml | 1 + app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-ro/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 11 +- 16 files changed, 141 insertions(+), 75 deletions(-) diff --git a/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt b/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt index 58a8c6a3e7..6374c62de0 100644 --- a/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt +++ b/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt @@ -11,8 +11,10 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background +import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.focusGroup @@ -224,6 +226,7 @@ fun QuickMenu( winHandler: WinHandler? = null, wineProcesses: List = emptyList(), isWineProcessesLoading: Boolean = false, + onToolsVisibilityChanged: (Boolean) -> Unit = {}, isPerformanceHudEnabled: Boolean = false, performanceHudConfig: PerformanceHudConfig = PerformanceHudConfig(), onPerformanceHudConfigChanged: (PerformanceHudConfig) -> Unit = {}, @@ -517,7 +520,6 @@ fun QuickMenu( winHandler = winHandler, processes = wineProcesses, isLoadingProcesses = isWineProcessesLoading, - onDismiss = onDismiss, firstItemFocusRequester = toolsItemFocusRequester, modifier = Modifier.fillMaxSize(), ) @@ -554,6 +556,10 @@ fun QuickMenu( } } + LaunchedEffect(isVisible, selectedTab) { + onToolsVisibilityChanged(isVisible && selectedTab == QuickMenuTab.TOOLS) + } + LaunchedEffect(isVisible) { if (isVisible) { repeat(3) { @@ -573,19 +579,11 @@ private fun ToolsQuickMenuTab( winHandler: WinHandler?, processes: List, isLoadingProcesses: Boolean, - onDismiss: () -> Unit, firstItemFocusRequester: FocusRequester? = null, modifier: Modifier = Modifier, ) { val scrollState = rememberScrollState() val accentColor = PluviaTheme.colors.accentPurple - val item = QuickMenuItem( - id = -1, - icon = Icons.Default.BarChart, - labelResId = R.string.launch_task_manager, - accentColor = accentColor, - enabled = winHandler != null, - ) Column( modifier = modifier @@ -593,23 +591,11 @@ private fun ToolsQuickMenuTab( .focusGroup(), verticalArrangement = Arrangement.spacedBy(4.dp), ) { - QuickMenuItemRow( - item = item, - onClick = { - winHandler?.exec("taskmgr.exe") - onDismiss() - }, - focusRequester = firstItemFocusRequester, - ) - - Spacer(modifier = Modifier.height(8.dp)) - QuickMenuSectionHeader( - title = stringResource(R.string.tools_wine_processes), - subtitle = if (isLoadingProcesses) { + title = if (isLoadingProcesses) { stringResource(R.string.main_loading) } else { - stringResource(R.string.tools_wine_processes_running, processes.size) + stringResource(R.string.tools_wine_processes_running_hint, processes.size) }, ) @@ -621,11 +607,15 @@ private fun ToolsQuickMenuTab( modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), ) } else { - processes.forEach { process -> - QuickMenuReadOnlyValueRow( - title = process.name, - value = process.formattedMemoryUsage, + processes.forEachIndexed { index, process -> + QuickMenuProcessRow( + title = process.name + if (process.wow64Process) " *32" else "", + subtitle = process.formattedMemoryUsage, accentColor = accentColor, + onEndProcess = { + winHandler?.killProcess(process.name, process.pid) + }, + focusRequester = if (index == 0) firstItemFocusRequester else null, ) } } @@ -1580,11 +1570,13 @@ private fun QuickMenuSwitch( } } +@OptIn(ExperimentalFoundationApi::class) @Composable -private fun QuickMenuReadOnlyValueRow( +private fun QuickMenuProcessRow( title: String, - value: String, + subtitle: String, accentColor: Color, + onEndProcess: () -> Unit, modifier: Modifier = Modifier, focusRequester: FocusRequester? = null, ) { @@ -1618,7 +1610,7 @@ private fun QuickMenuReadOnlyValueRow( if (isFocused) { Modifier.border( width = 2.dp, - color = accentColor.copy(alpha = 0.7f), + color = accentColor.copy(alpha = 0.8f), shape = shape, ) } else { @@ -1633,26 +1625,52 @@ private fun QuickMenuReadOnlyValueRow( } ) .focusable(interactionSource = interactionSource) + .onPreviewKeyEvent { keyEvent -> + if (keyEvent.nativeKeyEvent.action == KeyEvent.ACTION_DOWN && isFocused) { + when (keyEvent.nativeKeyEvent.keyCode) { + KeyEvent.KEYCODE_BUTTON_A, + KeyEvent.KEYCODE_DPAD_CENTER, + KeyEvent.KEYCODE_ENTER -> { + onEndProcess() + true + } + + else -> false + } + } else { + false + } + } + .selectable( + selected = isFocused, + interactionSource = interactionSource, + indication = null, + onClick = onEndProcess, + ) .padding(horizontal = 16.dp, vertical = 14.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - Text( - text = title, - style = MaterialTheme.typography.bodyLarge, - color = if (isFocused) accentColor else MaterialTheme.colorScheme.onSurface, - fontWeight = if (isFocused) FontWeight.SemiBold else FontWeight.Medium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f), - ) - - Text( - text = value, - style = MaterialTheme.typography.labelLarge, - color = if (isFocused) accentColor else MaterialTheme.colorScheme.onSurfaceVariant, - fontWeight = if (isFocused) FontWeight.SemiBold else FontWeight.Medium, - ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + color = if (isFocused) accentColor else MaterialTheme.colorScheme.onSurface, + fontWeight = if (isFocused) FontWeight.SemiBold else FontWeight.Medium, + maxLines = 1, + modifier = Modifier + .fillMaxWidth() + .basicMarquee(iterations = Int.MAX_VALUE), + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = if (isFocused) accentColor.copy(alpha = 0.92f) else MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } } } diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt index 74a73b700b..b964470481 100644 --- a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt @@ -242,6 +242,48 @@ private fun buildEssentialProcessAllowlist(): Set { return (essentialServices + CORE_WINE_PROCESSES).toSet() } +private suspend fun requestWineProcessSnapshot(winHandler: WinHandler): List? { + val previousListener = winHandler.getOnGetProcessInfoListener() + val lock = Any() + var currentList = mutableListOf() + var expectedCount = 0 + val deferred = CompletableDeferred?>() + + val listener = OnGetProcessInfoListener { index, count, processInfo -> + previousListener?.onGetProcessInfo(index, count, processInfo) + synchronized(lock) { + if (count == 0 && processInfo == null) { + if (!deferred.isCompleted) deferred.complete(emptyList()) + return@synchronized + } + if (index == 0) { + currentList = mutableListOf() + expectedCount = count + if (count == 0 && !deferred.isCompleted) { + deferred.complete(emptyList()) + return@synchronized + } + } + if (processInfo != null) { + currentList.add(processInfo) + } + if (currentList.size >= expectedCount && !deferred.isCompleted) { + deferred.complete(currentList.toList()) + } + } + } + + return try { + winHandler.setOnGetProcessInfoListener(listener) + winHandler.listProcesses() + withTimeoutOrNull(EXIT_PROCESS_RESPONSE_TIMEOUT_MS) { + deferred.await() + } + } finally { + winHandler.setOnGetProcessInfoListener(previousListener) + } +} + // TODO logs in composables are 'unstable' which can cause recomposition (performance issues) @Composable @@ -373,6 +415,7 @@ fun XServerScreen( var showPhysicalControllerDialog by remember { mutableStateOf(false) } var keyboardRequestedFromOverlay by remember { mutableStateOf(false) } var showQuickMenu by remember { mutableStateOf(false) } + var quickMenuToolsVisible by remember { mutableStateOf(false) } var quickMenuWineProcesses by remember { mutableStateOf>(emptyList()) } var quickMenuWineProcessesLoading by remember { mutableStateOf(false) } var hasPhysicalController by remember { mutableStateOf(false) } @@ -791,32 +834,32 @@ fun XServerScreen( showQuickMenu = false } - LaunchedEffect(showQuickMenu) { - if (!showQuickMenu) { + LaunchedEffect(showQuickMenu, quickMenuToolsVisible, xServerView) { + if (!showQuickMenu || !quickMenuToolsVisible) { + quickMenuWineProcesses = emptyList() + quickMenuWineProcessesLoading = false + return@LaunchedEffect + } + + val winHandler = xServerView?.getxServer()?.winHandler + if (winHandler == null) { quickMenuWineProcesses = emptyList() quickMenuWineProcessesLoading = false return@LaunchedEffect } quickMenuWineProcessesLoading = true - while (showQuickMenu) { + while (showQuickMenu && quickMenuToolsVisible) { val snapshot = withContext(Dispatchers.IO) { - ProcessHelper.listSubProcesses() - .asSequence() - .filter { it.name.endsWith(".exe", ignoreCase = true) } - .map { - ProcessInfo( - it.pid, - it.name.substringAfterLast('\\').substringAfterLast('/'), - it.rssBytes, - 0, - false, - ) - } - .sortedByDescending { it.memoryUsage } - .toList() + requestWineProcessSnapshot(winHandler) + ?.sortedWith( + compareByDescending { normalizeProcessName(it.name) !in buildEssentialProcessAllowlist() } + .thenByDescending { it.memoryUsage }, + ) + } + if (snapshot != null) { + quickMenuWineProcesses = snapshot } - quickMenuWineProcesses = snapshot quickMenuWineProcessesLoading = false delay(QUICK_MENU_PROCESS_POLL_INTERVAL_MS) } @@ -2073,6 +2116,7 @@ fun XServerScreen( winHandler = xServerView?.getxServer()?.winHandler, wineProcesses = quickMenuWineProcesses, isWineProcessesLoading = quickMenuWineProcessesLoading, + onToolsVisibilityChanged = { quickMenuToolsVisible = it }, isPerformanceHudEnabled = isPerformanceHudEnabled, performanceHudConfig = performanceHudConfig, onPerformanceHudConfigChanged = ::applyPerformanceHudConfig, diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index af305dd3fd..aeb1067788 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -177,6 +177,7 @@ Skift fuldskærm Skift orientering Jobliste + %1$d kører, tryk/klik på en proces for at lukke den Forstørrelsesglas Logfiler Afslut diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index af5057ffbe..7f55638921 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -242,6 +242,7 @@ Vollbild umschalten Ausrichtung wechseln Task-Manager + %1$d aktiv, tippe/klicke auf einen Prozess, um ihn zu schließen Lupe Logs Beenden diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d8ee4df584..85c97a8495 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -264,6 +264,7 @@ Alternar pantalla completa Alternar orientación Administrador de tareas + %1$d en ejecución, toca/haz clic en un proceso para cerrarlo Lupa Registros Salir diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1b63a8bc9d..d47b837701 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -249,6 +249,7 @@ Basculer en plein écran Basculer l\'orientation Gestionnaire de tâches + %1$d en cours, touchez/cliquez sur un processus pour le fermer Loupe Journaux Quitter diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 9d6be35376..d2f6959d05 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -253,6 +253,7 @@ Attiva/Disattiva Schermo Intero Cambia Orientamento Gestione Attività + %1$d in esecuzione, tocca/fai clic su un processo per chiuderlo Lente d\'ingrandimento Log Esci diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index f9eff06fc9..a9f58a3f41 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -262,6 +262,7 @@ 전체 화면 전환 방향 전환 작업 관리자 + %1$d개 실행 중, 프로세스를 탭/클릭해 종료 돋보기 로그 종료 diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index d3c4748801..8d30fce661 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -261,6 +261,7 @@ Przełącz pełny ekran Przełącz orientację Menedżer zadań + %1$d uruchomionych, stuknij/kliknij proces, aby go zamknąć Lupa Logi Wyjdź diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 80545536e2..582796ef7e 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -177,6 +177,7 @@ Alternar Tela Cheia Alternar Orientação Gerenciador de tarefas + %1$d em execução, toque/clique em um processo para fechá-lo Lupa Logs Sair diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index c484ef78e5..121cfadeb4 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -251,6 +251,7 @@ Comută pe ecran complet Comută orientarea Manager de activități + %1$d în execuție, atinge/clic pe un proces pentru a-l închide Lupă Loguri Ieșire diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c1ed94e4b2..360b4dc2e4 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1165,6 +1165,7 @@ https://gamenative.app Пользовательское Steam Менеджер задач + %1$d запущено, нажмите/кликните по процессу, чтобы закрыть его %1$d процент. Кнопки джойстика Кнопки джойстика diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 5b0890c4d8..c9a60727a9 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -244,6 +244,7 @@ Перемкнути повноекранний режим Перемкнути орієнтацію екрана Диспетчер завдань + %1$d запущено, торкніться/клацніть процес, щоб закрити його Лупа Журнали Вийти з гри diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 09b469cbbc..a4689cb741 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -247,6 +247,7 @@ 切换全屏 切换屏幕方向 任务管理器 + %1$d 个正在运行,点按/点击某个进程以关闭 放大镜 应用日志 退出 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8d5d6fc539..25e5331b9c 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -247,6 +247,7 @@ 切換全屏 切換螢幕方向 工作管理員 + %1$d 個正在執行,點按/點擊某個程序以關閉 放大鏡 應用程序日誌 退出 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1cc5be5829..e51385b8bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -263,17 +263,8 @@ Close General Controller - Tools - Launch Task Manager - Running EXEs - %1$d running + %1$d running, tap/click a process to close No running EXEs detected. - File Explorer - Task Manager - Command Prompt - Wine Config - Notepad - Registry Editor Performance HUD Show or hide the in-game overlay. Presets