diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md
index 4a7cc71..31d6d34 100644
--- a/CHANGELOG.en-US.md
+++ b/CHANGELOG.en-US.md
@@ -4,6 +4,10 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Befor
## [Unreleased]
+### Fixes
+- Returning from Big Picture now closes only Steam's client window to the tray, keeping Steam running; games and other windows are preserved. (#131)
+- Console interface settings: grouped controller options, a saved Automatic/Xbox/PlayStation button layout with a preview, and shortcut editing with cards and larger buttons. (#131)
+
## [1.6.0-alpha.5]
### Fixes
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5074c8d..3f6edb1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CH
## [Unreleased]
+### Correções
+- Voltar do Big Picture agora fecha apenas a janela da Steam para a bandeja, mantendo o cliente em execução; jogos e outras janelas são preservados. (#131)
+- Ajustes da interface Console: opções de controle agrupadas, escolha persistente do layout Automático/Xbox/PlayStation com prévia e editor de atalhos com cartões e botões maiores. (#131)
+
## [1.6.0-alpha.5]
### Correções
diff --git a/docs/GUIDE.md b/docs/GUIDE.md
index d56a51e..94c666f 100644
--- a/docs/GUIDE.md
+++ b/docs/GUIDE.md
@@ -14,10 +14,14 @@ You can also restore anytime from the tray (*Restore setup* / *Show window*). Wi
## Settings in Console mode
+In **System → Controller**, choose the button layout: **Automatic**, **Xbox** or **PlayStation**. The preview and shortcut labels follow the choice, which is saved when you close the app. This changes the displayed symbols; the controller's inputs keep their existing mapping.
+
The **System** tab provides the same settings as desktop mode, including controller shortcuts, Playnite, Android TV, custom FPS and the data folder. Both interfaces share the saved values, which remain after restarting the app.
Open the shortcut action you want to configure, choose **Set** or **Change**, hold the desired combination on one controller and release the buttons. **Remove** disables that shortcut. During capture, controller buttons do not navigate the interface; use **Cancel** or **Esc** to stop capture. Outside capture, **B** leaves the editor. Address and custom FPS fields accept keyboard input.
+When returning from Big Picture, **Send Steam to the tray when going back to the PC** closes the client window while keeping Steam running. With the option off, the desktop client is left open. Steam windows are left alone when a game is running or its state cannot be checked.
+
## Local control API
`consolemode://` links fire and forget. Tools that need an answer — a remote-control agent running as a Windows service, a Stream Deck plugin showing whether console mode is on — can use the named pipe `\\.\pipe\ConsoleMode.Control` while the app is running: send one JSON line, get one back.
diff --git a/docs/GUIDE.pt-BR.md b/docs/GUIDE.pt-BR.md
index 40076b0..bdeba5e 100644
--- a/docs/GUIDE.pt-BR.md
+++ b/docs/GUIDE.pt-BR.md
@@ -14,10 +14,14 @@ Também dá para restaurar a qualquer momento pela bandeja (*Restaurar setup* /
## Configurações no modo Console
+Em **Sistema → Controle**, escolha o layout dos botões: **Automático**, **Xbox** ou **PlayStation**. A prévia e os nomes dos atalhos acompanham a escolha, que fica salva ao fechar o app. Isso muda os símbolos exibidos; as entradas do controle mantêm o mapeamento existente.
+
A aba **Sistema** oferece os mesmos ajustes do modo PC, incluindo os atalhos do controle, Playnite, Android TV, FPS personalizado e pasta de dados. Os valores são compartilhados entre as duas interfaces e permanecem salvos ao reabrir o app.
Abra a ação de atalho que deseja configurar, escolha **Definir** ou **Alterar**, segure a combinação desejada em um único controle e solte os botões. **Remover** desativa aquele atalho. Durante a captura, os botões não navegam pela interface; use **Cancelar** ou **Esc** para interrompê-la. Fora da captura, **B** volta do editor. Campos de endereço e FPS personalizado aceitam digitação por teclado.
+Ao voltar do Big Picture, **Mandar a Steam para a bandeja ao voltar ao PC** fecha a janela do cliente e mantém a Steam em execução. Com a opção desligada, o cliente desktop fica aberto. As janelas da Steam são preservadas quando há jogo rodando ou não é possível verificar esse estado.
+
## API de controle local
Os links `consolemode://` não dão resposta. Ferramentas que precisam de uma — um agente de controle remoto rodando como serviço do Windows, um plugin de Stream Deck que mostra se o modo console está ligado — podem usar o named pipe `\\.\pipe\ConsoleMode.Control` com o app aberto: envie uma linha JSON e receba outra.
diff --git a/docs/TESTING.md b/docs/TESTING.md
index 6918d61..462320f 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -224,19 +224,20 @@ Pré-condições: Ajustes → Interface = **Automático** (padrão).
- [ ] Um controle HID que manda leitura impossível (ex.: Switch Pro pelo driver genérico, com 6 ou mais botões "apertados" sozinhos) é ignorado mesmo quando também aparece em Windows.Gaming.Input: o app não navega nem inicia a sessão sozinho, e o log mostra `Controles: … ignorado: leitura inválida`. Reabrir o app tenta de novo. Resultado: ______
- [ ] Com um controle HID defeituoso e outro controle normal conectados, o defeituoso não gera ações pelo HID nem pela projeção Windows.Gaming.Input, e o controle normal continua navegando. Resultado: ______
-## 3c. Fechar a Steam ao voltar ao PC
+## 3c. Mandar a Steam para a bandeja ao voltar ao PC
-Pré-condições: lançador = Steam Big Picture; Ajustes → "Fechar a Steam ao voltar ao PC" **ligado** (padrão); nenhum jogo aberto.
+Pré-condições: lançador = Steam Big Picture; Ajustes → "Mandar a Steam para a bandeja ao voltar ao PC" **ligado** (padrão); nenhum jogo aberto.
-- [ ] Voltar ao PC por cada caminho (botão "Voltar ao PC", menu da sessão, atalho do controle, `consolemode://stop`, tray → Restaurar): o **Big Picture fecha** e some da mesa, e a **Steam fecha sozinha** em alguns segundos (o ícone sai da bandeja, sem janela de erro). Resultado: ______
-- [ ] Sair pelo próprio Big Picture (Sair → Sair do Big Picture): a mesa volta e a Steam também fecha. Resultado: ______
-- [ ] A Steam **não é encerrada à força**: no Gerenciador de Tarefas ela some sem o aviso "o programa não está respondendo", e o log mostra `Steam: pedindo para fechar (sair normal)`. Resultado: ______
+- [ ] Voltar ao PC por cada caminho (botão "Voltar ao PC", menu da sessão, atalho do controle, `consolemode://stop`, tray → Restaurar): o **Big Picture fecha**, a janela principal some e a **Steam continua em execução na bandeja**. Resultado: ______
+- [ ] Sair pelo próprio Big Picture (Sair → Sair do Big Picture): a mesa volta e a Steam permanece na bandeja. Resultado: ______
+- [ ] O processo da Steam continua no Gerenciador de Tarefas; clicar no ícone da bandeja reabre o cliente sem login nem reinício. Resultado: ______
- [ ] Com um **jogo da Steam rodando**: a Steam fica aberta e o log mostra `Steam: mantida aberta (jogo em execução, id …)`; o Big Picture ainda é fechado. Resultado: ______
- [ ] Com o ajuste **desligado**: o Big Picture fecha, a Steam fica aberta (`Steam: mantida aberta (desligado nos ajustes)`). Resultado: ______
- [ ] Desligar o ajuste, fechar e reabrir o Console Mode e iniciar uma sessão Big Picture: a preferência continua desligada e a Steam fica aberta. Resultado: ______
- [ ] Com o Steam aberto e o estado do jogo indisponível no Registro (valor `RunningAppID` ausente ou ilegível): a Steam fica aberta e o log informa que não foi possível verificar se há jogo em execução. Resultado: ______
- [ ] Steam **já fechada** antes de voltar: nada é aberto nem pedido (`Steam: não estava aberta`); a Steam não é iniciada de novo por engano. Resultado: ______
- [ ] Lançador Playnite ou Modo Xbox: a Steam não é tocada. Resultado: ______
+- [ ] Janelas de jogo, chat/popups da Steam e outros aplicativos não recebem o pedido de fechamento da janela principal. Resultado: ______
- [ ] Se o Big Picture demorar a fechar (travado), a restauração da mesa continua e termina normalmente (o log mostra `Big Picture ainda aberto`). Resultado: ______
## 3c. Controle da TV: Google TV / Android TV (issue #75)
@@ -334,3 +335,25 @@ Pré-condições: 1.6 instalada por cima da 1.5, com a configuração da 1.5 (te
- [ ] Os cartões não exibem o botão X no canto. Quadrado/Delete continuam pedindo confirmação para fechar a janela selecionada. Resultado: ______
- [ ] A logo fornecida do ControlFS aparece no painel fixo do overlay, tanto instalado quanto ausente, inclusive no aplicativo publicado. Resultado: ______
- [ ] Com overlay aberto ou outro aplicativo em primeiro plano, a tela Console ao fundo não reage ao DualShock. Resultado: ______
+
+## Ajustes da interface Console: controle e atalhos
+
+### Layout dos botões
+
+- [ ] Abra **Sistema > Controle > Layout dos botões** com um controle conectado.
+- [ ] Em **Automático**, confirme que os símbolos acompanham o controle Xbox ou PlayStation conectado.
+- [ ] Selecione **Xbox** e confirme que a prévia e as dicas usam A, B, X, Y, LB e RB.
+- [ ] Selecione **PlayStation** e confirme que a prévia e as dicas usam ✕, ○, □, △, L1 e R1.
+- [ ] Feche e abra o app; confirme que a escolha manual continua salva.
+- [ ] Com dois tipos de controle disponíveis, troque o controle conectado em **Automático** e confirme que os símbolos acompanham o novo controle.
+- [ ] Abra o menu da sessão e confirme que as dicas também respeitam o layout escolhido.
+
+### Atalhos
+
+- [ ] Abra cada um dos três atalhos com A e confirme que os cartões ficam legíveis em 1920×1080 e em uma janela de 960×760.
+- [ ] Navegue entre **Definir/Alterar**, **Remover** e **Voltar** usando apenas o controle; confirme que o foco visível acompanha cada ação.
+- [ ] Grave uma combinação, solte os botões e confirme que o valor aparece no cartão e na linha de Ajustes.
+- [ ] Durante a gravação, pressione B/○ sozinho e confirme que a captura é cancelada sem alterar o atalho.
+- [ ] Tente usar uma combinação já atribuída e confirme que a mensagem de conflito aparece inteira e não corta os botões.
+- [ ] Reduza a altura da janela e confirme que o editor rola até **Voltar**, sem esconder os cartões.
+- [ ] Feche o editor durante uma captura e confirme que ela para; reabra-o e confirme que nenhum botão fica preso em captura.
diff --git a/src/ConsoleMode/Controls/ShortcutPicker.cs b/src/ConsoleMode/Controls/ShortcutPicker.cs
index 3f6effe..f5626ce 100644
--- a/src/ConsoleMode/Controls/ShortcutPicker.cs
+++ b/src/ConsoleMode/Controls/ShortcutPicker.cs
@@ -39,21 +39,59 @@ public sealed class ShortcutPicker : StackPanel
private ShortcutCapture? _capture;
/// In the setup dialog: left-aligned under the text instead of a right-hand column.
- public ShortcutPicker(MainViewModel vm, ShortcutSlot slot, bool stacked = false)
+ public ShortcutPicker(MainViewModel vm, ShortcutSlot slot, bool stacked = false, bool consoleStyle = false)
{
_vm = vm;
_slot = slot;
Spacing = 4;
- var side = stacked ? HorizontalAlignment.Left : HorizontalAlignment.Right;
- HorizontalAlignment = side;
-
- var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, HorizontalAlignment = side };
- row.Children.Add(_value);
- row.Children.Add(_set);
- row.Children.Add(_clear);
- Children.Add(row);
+ var side = stacked || consoleStyle ? HorizontalAlignment.Left : HorizontalAlignment.Right;
+ HorizontalAlignment = consoleStyle ? HorizontalAlignment.Stretch : side;
+
+ if (consoleStyle)
+ {
+ _value.FontSize = 23;
+ _value.FontWeight = Microsoft.UI.Text.FontWeights.SemiBold;
+ _value.TextAlignment = TextAlignment.Center;
+ _value.TextWrapping = TextWrapping.Wrap;
+ _value.MinWidth = 0;
+ var badge = new Border
+ {
+ Background = new SolidColorBrush(Color.FromArgb(42, 255, 255, 255)),
+ BorderBrush = new SolidColorBrush(Color.FromArgb(70, 255, 255, 255)),
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(10),
+ Padding = new Thickness(12, 10, 12, 10),
+ Child = _value
+ };
+ Children.Add(badge);
+ _set.Style = Application.Current.Resources["ConsoleSmallButton"] as Style;
+ _clear.Style = Application.Current.Resources["ConsoleSmallButton"] as Style;
+ _set.Height = 52;
+ _clear.Height = 52;
+ _set.FontSize = 18;
+ _clear.FontSize = 18;
+ _set.HorizontalAlignment = HorizontalAlignment.Stretch;
+ _clear.HorizontalAlignment = HorizontalAlignment.Stretch;
+ Children.Add(_set);
+ Children.Add(_clear);
+ }
+ else
+ {
+ var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, HorizontalAlignment = side };
+ row.Children.Add(_value);
+ row.Children.Add(_set);
+ row.Children.Add(_clear);
+ Children.Add(row);
+ }
Children.Add(_captureHint);
Children.Add(_error);
+ _captureHint.MaxWidth = consoleStyle ? 280 : 460;
+ _error.MaxWidth = consoleStyle ? 280 : 460;
+ if (consoleStyle)
+ {
+ _captureHint.FontSize = 18;
+ _error.FontSize = 18;
+ }
_timer = DispatcherQueue.GetForCurrentThread().CreateTimer();
_timer.Interval = TimeSpan.FromMilliseconds(40);
diff --git a/src/ConsoleMode/Models/Models.cs b/src/ConsoleMode/Models/Models.cs
index a8d222e..7f06762 100644
--- a/src/ConsoleMode/Models/Models.cs
+++ b/src/ConsoleMode/Models/Models.cs
@@ -27,6 +27,9 @@ public sealed class AppConfig
/// "auto" | "desktop" | "console": which interface to show (UiModeResolver).
public string UiMode { get; set; } = "auto";
+ /// "auto" | "xbox" | "playstation": which controller symbols to show in the console UI.
+ public string ControllerLayout { get; set; } = "auto";
+
/// The first-run tour was finished or skipped.
public bool TourDone { get; set; }
@@ -58,7 +61,7 @@ public sealed class AppConfig
public bool HomeButtonShortPress { get; set; }
///
- /// Going back to the PC also quits Steam (its own Exit, never a kill), unless a game is running.
+ /// Going back to the PC closes Steam's client window to the tray, unless a game is running.
/// Big Picture itself is always closed.
///
public bool CloseSteamOnRestore { get; set; } = true;
diff --git a/src/ConsoleMode/Native/NativeWindows.cs b/src/ConsoleMode/Native/NativeWindows.cs
index b5742e8..264a582 100644
--- a/src/ConsoleMode/Native/NativeWindows.cs
+++ b/src/ConsoleMode/Native/NativeWindows.cs
@@ -20,6 +20,12 @@ public static class NativeWindows
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(nint hWnd, nint processId);
+ [DllImport("user32.dll")]
+ public static extern uint GetWindowThreadProcessId(nint hWnd, out uint processId);
+
+ [DllImport("user32.dll")]
+ public static extern nint GetWindow(nint hWnd, uint uCmd);
+
[DllImport("user32.dll")]
private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
diff --git a/src/ConsoleMode/Resources/Strings.en-US.json b/src/ConsoleMode/Resources/Strings.en-US.json
index 1722536..1dd97e8 100644
--- a/src/ConsoleMode/Resources/Strings.en-US.json
+++ b/src/ConsoleMode/Resources/Strings.en-US.json
@@ -107,6 +107,12 @@
"ExitShortcutCard": "Controller shortcut to go back to the PC",
"ExitShortcutDescription": "During a session, hold this shortcut for 1 second to restore your desk and go back to the PC.",
"ShortcutHome": "Open Console Mode",
+ "ControllerSection": "Controller",
+ "ControllerLayoutCard": "Button layout",
+ "ControllerLayoutDescription": "Automatic follows the connected controller. You can also lock the Xbox or PlayStation symbols.",
+ "ControllerLayoutAuto": "Automatic",
+ "ControllerLayoutXbox": "Xbox (A, B, X, Y)",
+ "ControllerLayoutPlaystation": "PlayStation (✕, ○, □, △)",
"ShortcutMenu": "Session menu",
"ShortcutExit": "Back to the PC",
"ShortcutNotSet": "Not set",
@@ -122,8 +128,8 @@
"ShortcutsOnboardingBody": "No shortcut is on until you choose it. Pick a button or combination for each action, or leave it without one. Press Set, hold the buttons and let go. The same combination can't be used for two actions. You can change all of this later in Settings.",
"ShortcutsUseSuggested": "Use the suggested shortcuts",
"ShortcutsOnboardingDone": "Done",
- "CloseSteamCard": "Close Steam when going back to the PC",
- "CloseSteamDescription": "When you leave Big Picture or go back to the PC, Steam is asked to quit the normal way (its own Exit, never forced). Big Picture is always closed. Steam stays open if a game is running.",
+ "CloseSteamCard": "Send Steam to the tray when going back to the PC",
+ "CloseSteamDescription": "When you leave Big Picture, Steam receives a normal window-close request so it can move to the tray without exiting the process. If a game is running or its state cannot be checked, the window is left unchanged.",
"AutoStartControllerCard": "Enter when a controller connects",
"AutoStartControllerDescription": "With the app in the tray, turning on or plugging in a controller enters console mode. Ignores the first 15 s after opening and 30 s after restoring.",
"HomeButtonShortPressCard": "Short press of the Xbox button",
diff --git a/src/ConsoleMode/Resources/Strings.es-ES.json b/src/ConsoleMode/Resources/Strings.es-ES.json
index 2010049..a64e9a2 100644
--- a/src/ConsoleMode/Resources/Strings.es-ES.json
+++ b/src/ConsoleMode/Resources/Strings.es-ES.json
@@ -107,6 +107,12 @@
"ExitShortcutCard": "Atajo del mando para volver al PC",
"ExitShortcutDescription": "Durante la sesión, mantén este atajo 1 segundo para restaurar el escritorio y volver al PC.",
"ShortcutHome": "Abrir Console Mode",
+ "ControllerSection": "Mando",
+ "ControllerLayoutCard": "Diseño de botones",
+ "ControllerLayoutDescription": "Automático sigue el mando conectado. También puedes fijar los símbolos de Xbox o PlayStation.",
+ "ControllerLayoutAuto": "Automático",
+ "ControllerLayoutXbox": "Xbox (A, B, X, Y)",
+ "ControllerLayoutPlaystation": "PlayStation (✕, ○, □, △)",
"ShortcutMenu": "Menú de la sesión",
"ShortcutExit": "Volver al PC",
"ShortcutNotSet": "Sin definir",
@@ -122,8 +128,8 @@
"ShortcutsOnboardingBody": "Ningún atajo viene activado: tú eliges. Define un botón o combinación para cada acción, o déjala sin atajo. Pulsa Definir, mantén los botones y suéltalos. La misma combinación no puede servir para dos acciones. Puedes cambiarlo todo después en Ajustes.",
"ShortcutsUseSuggested": "Usar los atajos sugeridos",
"ShortcutsOnboardingDone": "Listo",
- "CloseSteamCard": "Cerrar Steam al volver al PC",
- "CloseSteamDescription": "Al salir de Big Picture o volver al PC, se le pide a Steam que se cierre de la forma normal (su propio «Salir», nunca a la fuerza). Big Picture siempre se cierra. Steam sigue abierta si hay un juego en ejecución.",
+ "CloseSteamCard": "Enviar Steam a la bandeja al volver al PC",
+ "CloseSteamDescription": "Al salir de Big Picture, Steam recibe una solicitud normal para cerrar la ventana y pasar a la bandeja sin finalizar el proceso. Si hay un juego en ejecución o no se puede comprobar su estado, la ventana no se modifica.",
"AutoStartControllerCard": "Entrar al conectar un mando",
"AutoStartControllerDescription": "Con la aplicación en la bandeja, encender o conectar un mando entra en modo consola. Ignora los primeros 15 s tras abrir y 30 s tras restaurar.",
"HomeButtonShortPressCard": "Pulsación corta del botón Xbox",
diff --git a/src/ConsoleMode/Resources/Strings.pt-BR.json b/src/ConsoleMode/Resources/Strings.pt-BR.json
index ed7721a..ebe5804 100644
--- a/src/ConsoleMode/Resources/Strings.pt-BR.json
+++ b/src/ConsoleMode/Resources/Strings.pt-BR.json
@@ -107,6 +107,12 @@
"ExitShortcutCard": "Atalho do controle para voltar ao PC",
"ExitShortcutDescription": "Durante a sessão, segure este atalho por 1 segundo para restaurar a mesa e voltar ao PC.",
"ShortcutHome": "Abrir o Console Mode",
+ "ControllerSection": "Controle",
+ "ControllerLayoutCard": "Layout dos botões",
+ "ControllerLayoutDescription": "Automático acompanha o controle conectado. Você também pode fixar os símbolos do Xbox ou PlayStation.",
+ "ControllerLayoutAuto": "Automático",
+ "ControllerLayoutXbox": "Xbox (A, B, X, Y)",
+ "ControllerLayoutPlaystation": "PlayStation (✕, ○, □, △)",
"ShortcutMenu": "Menu da sessão",
"ShortcutExit": "Voltar ao PC",
"ShortcutNotSet": "Não definido",
@@ -122,8 +128,8 @@
"ShortcutsOnboardingBody": "Nenhum atalho vem ligado: você escolhe. Defina um botão ou combinação para cada ação, ou deixe sem atalho. Aperte Definir, segure os botões e solte. A mesma combinação não pode servir para duas ações. Você muda tudo isso depois em Ajustes.",
"ShortcutsUseSuggested": "Usar os atalhos sugeridos",
"ShortcutsOnboardingDone": "Concluir",
- "CloseSteamCard": "Fechar a Steam ao voltar ao PC",
- "CloseSteamDescription": "Ao sair do Big Picture ou voltar ao PC, a Steam é chamada para fechar do jeito normal (o \"Sair\" dela, nunca à força). O Big Picture sempre é fechado. A Steam fica aberta se houver um jogo rodando.",
+ "CloseSteamCard": "Mandar a Steam para a bandeja ao voltar ao PC",
+ "CloseSteamDescription": "Ao sair do Big Picture, envia o fechamento normal da janela da Steam para ela ir à bandeja, sem encerrar o processo. Se houver um jogo em execução ou não for possível verificar, a janela não é alterada.",
"AutoStartControllerCard": "Entrar ao conectar um controle",
"AutoStartControllerDescription": "Com o app na bandeja, ligar ou conectar um controle entra no modo console. Ignora os primeiros 15 s após abrir e 30 s após restaurar.",
"HomeButtonShortPressCard": "Toque curto no botão Xbox",
diff --git a/src/ConsoleMode/Services/ConsoleEngine.cs b/src/ConsoleMode/Services/ConsoleEngine.cs
index 3d94fee..4d14c32 100644
--- a/src/ConsoleMode/Services/ConsoleEngine.cs
+++ b/src/ConsoleMode/Services/ConsoleEngine.cs
@@ -349,9 +349,7 @@ public void Stop()
}
///
- /// Going back to the PC: Big Picture is closed (it would otherwise be left on the desk monitor when the
- /// screens come back), and Steam is asked to quit its own way, because after Big Picture it is often left
- /// half-working. Never a kill, never when a game is running, and never a reason to fail the restore.
+ /// Going back to the PC closes Big Picture and Steam's client window to the tray; the process remains running.
///
private void CloseSteamForRestore()
{
@@ -363,7 +361,7 @@ private void CloseSteamForRestore()
var appId = SteamSession.RunningAppId();
var decision = SteamShutdown.Decide(State.FullscreenMode, State.CloseSteamOnRestore, SteamSession.IsRunning(), appId);
AppLog.Write(SteamShutdown.Describe(decision, appId));
- if (decision == SteamShutdown.Decision.Shutdown) SteamSession.RequestShutdown();
+ if (decision == SteamShutdown.Decision.CloseToTray) SteamSession.CloseWindowToTray();
}
catch (Exception ex)
{
diff --git a/src/ConsoleMode/Services/LocalizationService.cs b/src/ConsoleMode/Services/LocalizationService.cs
index 0cdc86e..99e2d16 100644
--- a/src/ConsoleMode/Services/LocalizationService.cs
+++ b/src/ConsoleMode/Services/LocalizationService.cs
@@ -169,6 +169,9 @@ internal void NotifyAll()
public string ExitConsoleMode => LocalizationService.Get(nameof(ExitConsoleMode));
public string HintVolume => LocalizationService.Get(nameof(HintVolume));
public string SessionMenuCard => LocalizationService.Get(nameof(SessionMenuCard));
+ public string ControllerSection => LocalizationService.Get(nameof(ControllerSection));
+ public string ControllerLayoutCard => LocalizationService.Get(nameof(ControllerLayoutCard));
+ public string ControllerLayoutDescription => LocalizationService.Get(nameof(ControllerLayoutDescription));
public string ControllerTestCard => LocalizationService.Get(nameof(ControllerTestCard));
public string ControllerTestDescription => LocalizationService.Get(nameof(ControllerTestDescription));
public string ControllerTestSteamHint => LocalizationService.Get(nameof(ControllerTestSteamHint));
diff --git a/src/ConsoleMode/Services/SteamSession.cs b/src/ConsoleMode/Services/SteamSession.cs
index 88a40cd..3d2d4ad 100644
--- a/src/ConsoleMode/Services/SteamSession.cs
+++ b/src/ConsoleMode/Services/SteamSession.cs
@@ -1,22 +1,20 @@
using System.Diagnostics;
using Microsoft.Win32;
+using System.Text;
+using ConsoleMode.Native;
namespace ConsoleMode.Services;
-///
-/// Talks to the Steam client about the end of a session: is it running, is a game running, and the
-/// request to quit. Quitting is Steam's own "-shutdown" (what its Exit menu does): it is asked to close,
-/// never killed, so it can save and leave cleanly.
-///
+/// Checks Steam state and closes only its client window without shutting down its process.
public static class SteamSession
{
private const string SteamKey = @"Software\Valve\Steam";
public static bool IsRunning()
{
- var processes = Process.GetProcessesByName("steam");
+ var processes = Process.GetProcessesByName("steam").Concat(Process.GetProcessesByName("steamwebhelper")).ToArray();
try { return processes.Length > 0; }
- finally { foreach (var p in processes) p.Dispose(); }
+ finally { foreach (var process in processes) process.Dispose(); }
}
/// The id of the game Steam is running, 0 if none, or null if the state is unknown.
@@ -48,31 +46,43 @@ public static bool IsRunning()
if (File.Exists(candidate)) return Path.GetFullPath(candidate);
}
}
- catch (Exception ex)
- {
- AppLog.Write($"Steam: caminho: {ex.Message}");
- }
+ catch (Exception ex) { AppLog.Write($"Steam: caminho: {ex.Message}"); }
return null;
}
- /// Asks Steam to exit the normal way. Returns whether the request was sent; it does not wait.
- public static bool RequestShutdown()
+ /// Closes the Steam client window so Steam can move itself to the tray. Never shuts down the process.
+ public static bool CloseWindowToTray()
{
- var exe = FindSteamExe();
- if (exe is null)
- {
- AppLog.Write("Steam: não achei o steam.exe para fechar");
- return false;
- }
+ var processes = Process.GetProcessesByName("steam").Concat(Process.GetProcessesByName("steamwebhelper")).ToArray();
try
{
- Process.Start(new ProcessStartInfo { FileName = exe, Arguments = "-shutdown", UseShellExecute = false, CreateNoWindow = true })?.Dispose();
- return true;
+ var processNames = processes.ToDictionary(process => process.Id, process => process.ProcessName);
+ nint target = 0;
+ NativeWindows.EnumWindows((hwnd, _) =>
+ {
+ NativeWindows.GetWindowThreadProcessId(hwnd, out var processId);
+ var title = new StringBuilder(256);
+ var className = new StringBuilder(256);
+ NativeWindows.GetWindowText(hwnd, title, title.Capacity);
+ NativeWindows.GetClassName(hwnd, className, className.Capacity);
+ var hasOwner = NativeWindows.GetWindow(hwnd, GwOwner) != 0;
+ if (processNames.TryGetValue(unchecked((int)processId), out var processName) &&
+ SteamShutdown.IsClientWindowCandidate(processName, title.ToString(), className.ToString(), NativeWindows.IsWindowVisible(hwnd), hasOwner))
+ {
+ target = hwnd;
+ return false;
+ }
+ return true;
+ }, 0);
+ return target != 0 && NativeWindows.PostMessage(target, NativeWindows.WmClose, 0, 0);
}
catch (Exception ex)
{
- AppLog.Write($"Steam: pedido de fechar falhou: {ex.Message}");
+ AppLog.Write($"Steam: fechar janela para a bandeja falhou: {ex.Message}");
return false;
}
+ finally { foreach (var process in processes) process.Dispose(); }
}
+
+ private const uint GwOwner = 4;
}
diff --git a/src/ConsoleMode/Services/SteamShutdown.cs b/src/ConsoleMode/Services/SteamShutdown.cs
index 1c6d461..4faf25d 100644
--- a/src/ConsoleMode/Services/SteamShutdown.cs
+++ b/src/ConsoleMode/Services/SteamShutdown.cs
@@ -1,17 +1,23 @@
namespace ConsoleMode.Services;
///
-/// The rule for closing Steam when the user goes back to the PC, kept pure so it is tested: Steam is
-/// asked to quit (its own "Exit", never a kill) only when Big Picture was the launcher, the setting is
-/// on, Steam is actually running and no game is running. A running game means the user may be in the
-/// middle of it, so Steam stays.
+/// Steam's client window is closed to the tray only when Big Picture was the launcher and the setting is enabled.
///
public static class SteamShutdown
{
+ public static bool IsClientWindowCandidate(string processName, string title, string className, bool visible, bool hasOwner) =>
+ visible && !hasOwner &&
+ (string.Equals(processName, "steam", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(processName, "steamwebhelper", StringComparison.OrdinalIgnoreCase)) &&
+ string.Equals(title.Trim(), "Steam", StringComparison.OrdinalIgnoreCase) &&
+ (string.Equals(className, "Chrome_WidgetWin_1", StringComparison.Ordinal) ||
+ string.Equals(className, "vguiPopupWindow", StringComparison.Ordinal) ||
+ string.Equals(className, "SDL_app", StringComparison.Ordinal));
+
public enum Decision
{
- /// Ask Steam to exit.
- Shutdown,
+ /// Close the Steam client window to the tray.
+ CloseToTray,
/// The launcher is not Big Picture: Steam was never ours to close.
NotBigPicture,
/// Turned off in Settings.
@@ -31,13 +37,13 @@ public static Decision Decide(string launcherMode, bool closeSteamSetting, bool
if (!steamRunning) return Decision.NotRunning;
if (runningAppId is null) return Decision.GameStateUnknown;
if (runningAppId > 0) return Decision.GameRunning;
- return Decision.Shutdown;
+ return Decision.CloseToTray;
}
/// The line for the log.
public static string Describe(Decision decision, int? runningAppId) => decision switch
{
- Decision.Shutdown => "Steam: pedindo para fechar (sair normal)",
+ Decision.CloseToTray => "Steam: fechando a janela para a bandeja",
Decision.SettingOff => "Steam: mantida aberta (desligado nos ajustes)",
Decision.NotRunning => "Steam: não estava aberta",
Decision.GameRunning => $"Steam: mantida aberta (jogo em execução, id {runningAppId})",
diff --git a/src/ConsoleMode/ViewModels/MainViewModel.ConsoleUi.cs b/src/ConsoleMode/ViewModels/MainViewModel.ConsoleUi.cs
index defc95f..1d77e21 100644
--- a/src/ConsoleMode/ViewModels/MainViewModel.ConsoleUi.cs
+++ b/src/ConsoleMode/ViewModels/MainViewModel.ConsoleUi.cs
@@ -11,12 +11,15 @@ namespace ConsoleMode.ViewModels;
public partial class MainViewModel
{
public ObservableCollection UiModeOptions { get; } = [];
+ public ObservableCollection ControllerLayoutOptions { get; } = [];
[ObservableProperty] private bool _isConsoleUi;
[ObservableProperty] private ComboOption? _selectedUiMode;
[ObservableProperty] private bool _isRolePanelOpen;
[ObservableProperty] private MonitorRowViewModel? _rolePanelMonitor;
[ObservableProperty] private bool _isPlayStationHints;
+ [ObservableProperty] private ComboOption? _selectedControllerLayout;
+ private ControllerFamily _detectedControllerFamily;
/// Set by App when the session was requested from the controller's Home button.
public bool LaunchedByController { get; set; }
@@ -32,6 +35,14 @@ public partial class MainViewModel
public string HintAlt => IsPlayStationHints ? "△" : "Y";
public string HintOption => IsPlayStationHints ? "□" : "X";
public string HintMenu => IsPlayStationHints ? "OPTIONS" : "☰";
+ public string ControllerLayoutPreview => IsPlayStationHints ? "✕ · ○ · □ · △ · L1/R1" : "A · B · X · Y · LB/RB";
+
+ partial void OnSelectedControllerLayoutChanged(ComboOption? value)
+ {
+ if (value is null) return;
+ RefreshControllerHints();
+ SaveQuietly();
+ }
partial void OnIsConsoleUiChanged(bool value)
{
@@ -74,6 +85,17 @@ private void BuildUiModeOptions()
UiModeOptions.Add(new ComboOption { Text = LocalizationService.Get("UiModeDesktop"), Value = UiModeResolver.Desktop });
UiModeOptions.Add(new ComboOption { Text = LocalizationService.Get("UiModeConsole"), Value = UiModeResolver.Console });
SelectedUiMode = UiModeOptions.FirstOrDefault(o => o.Value == current) ?? UiModeOptions[0];
+ BuildControllerLayoutOptions();
+ }
+
+ private void BuildControllerLayoutOptions()
+ {
+ var current = SelectedControllerLayout?.Value ?? _loadedConfig.ControllerLayout;
+ ControllerLayoutOptions.Clear();
+ ControllerLayoutOptions.Add(new ComboOption { Text = LocalizationService.Get("ControllerLayoutAuto"), Value = "auto" });
+ ControllerLayoutOptions.Add(new ComboOption { Text = LocalizationService.Get("ControllerLayoutXbox"), Value = "xbox" });
+ ControllerLayoutOptions.Add(new ComboOption { Text = LocalizationService.Get("ControllerLayoutPlaystation"), Value = "playstation" });
+ SelectedControllerLayout = ControllerLayoutOptions.FirstOrDefault(o => o.Value == current) ?? ControllerLayoutOptions[0];
}
[ObservableProperty] private bool _controllerDetected;
@@ -98,12 +120,23 @@ private async Task HideControllerToastAsync(int id)
/// Called after the config is applied and whenever the choice changes.
private void ResolveUi()
{
- var family = ControllerInput.DetectFamily();
- IsPlayStationHints = family == ControllerFamily.PlayStation;
- ControllerDetected = family != ControllerFamily.None;
+ _detectedControllerFamily = ControllerInput.DetectFamily();
+ RefreshControllerHints();
+ ControllerDetected = _detectedControllerFamily != ControllerFamily.None;
IsConsoleUi = UiModeResolver.IsConsole(SelectedUiMode?.Value, ControllerDetected, LaunchedByController);
}
+ private void RefreshControllerHints()
+ {
+ IsPlayStationHints = (SelectedControllerLayout?.Value ?? "auto") switch
+ {
+ "playstation" => true,
+ "xbox" => false,
+ _ => _detectedControllerFamily == ControllerFamily.PlayStation
+ };
+ OnPropertyChanged(nameof(ControllerLayoutPreview));
+ }
+
///
/// A pad appeared or went away. Windows lists HID pads asynchronously, so the startup check
/// often misses them; re-resolving here is what makes "Automatic" pick the console interface.
@@ -282,6 +315,7 @@ private void OpenPicker(string key)
"hide" => (LocalizationService.Get("HideOtherScreensCard"), HideStrategies.Select(o => (o.Text, o.Value)), SelectedHideStrategy?.Value),
"language" => (LocalizationService.Get("LanguageCard"), LanguageOptions.Select(o => (o.Text, o.Value)), SelectedLanguage?.Value),
"ui" => (LocalizationService.Get("UiModeCard"), UiModeOptions.Select(o => (o.Text, o.Value)), SelectedUiMode?.Value),
+ "controllerlayout" => (LocalizationService.Get("ControllerLayoutCard"), ControllerLayoutOptions.Select(o => (o.Text, o.Value)), SelectedControllerLayout?.Value),
"tvprovider" => (LocalizationService.Get("TvControlCard"), TvProviders.Select(o => (o.Text, o.Value)), SelectedTvProvider?.Value),
"tvhdmi" => (LocalizationService.Get("TvHdmiCard"), TvHdmiInputs.Select(o => (o.Text, o.Value)), SelectedTvHdmiInput?.Value),
"mode" when FocusRow is not null => (LocalizationService.Get("ResolutionRefreshCard"), FocusRow.Modes.Select(m => (m.Text, m.Key)), FocusRow.SelectedMode?.Key),
@@ -310,6 +344,7 @@ private void PickOption(PickerItem? item)
case "hide": SelectedHideStrategy = HideStrategies.FirstOrDefault(o => o.Value == item.Value) ?? SelectedHideStrategy; break;
case "language": SelectedLanguage = LanguageOptions.FirstOrDefault(o => o.Value == item.Value) ?? SelectedLanguage; break;
case "ui": SelectedUiMode = UiModeOptions.FirstOrDefault(o => o.Value == item.Value) ?? SelectedUiMode; break;
+ case "controllerlayout": SelectedControllerLayout = ControllerLayoutOptions.FirstOrDefault(o => o.Value == item.Value) ?? SelectedControllerLayout; break;
case "tvprovider": SelectedTvProvider = TvProviders.FirstOrDefault(o => o.Value == item.Value) ?? SelectedTvProvider; break;
case "tvhdmi": SelectedTvHdmiInput = TvHdmiInputs.FirstOrDefault(o => o.Value == item.Value) ?? SelectedTvHdmiInput; break;
case "mode":
diff --git a/src/ConsoleMode/ViewModels/MainViewModel.cs b/src/ConsoleMode/ViewModels/MainViewModel.cs
index 98ba468..3294739 100644
--- a/src/ConsoleMode/ViewModels/MainViewModel.cs
+++ b/src/ConsoleMode/ViewModels/MainViewModel.cs
@@ -315,6 +315,7 @@ private void Apply(LoadResult data)
CloseSteamOnRestore = config.CloseSteamOnRestore;
RefreshHomeButtonHint();
SelectedUiMode = UiModeOptions.FirstOrDefault(o => o.Value == config.UiMode) ?? UiModeOptions.FirstOrDefault();
+ SelectedControllerLayout = ControllerLayoutOptions.FirstOrDefault(o => o.Value == config.ControllerLayout) ?? ControllerLayoutOptions[0];
if (config.FpsLimit > 0 && FpsPresets.Contains(config.FpsLimit))
{
@@ -922,6 +923,7 @@ private AppConfig BuildConfig()
ConsoleBackgroundImage = ConsoleBackgroundImage,
CloseSteamOnRestore = CloseSteamOnRestore,
UiMode = SelectedUiMode?.Value ?? _loadedConfig.UiMode,
+ ControllerLayout = SelectedControllerLayout?.Value ?? _loadedConfig.ControllerLayout,
SkippedUpdateVersion = _loadedConfig.SkippedUpdateVersion,
Tv = BuildTvConfig()
};
diff --git a/src/ConsoleMode/Views/ConsoleHomeView.xaml b/src/ConsoleMode/Views/ConsoleHomeView.xaml
index 15ddd55..35995a3 100644
--- a/src/ConsoleMode/Views/ConsoleHomeView.xaml
+++ b/src/ConsoleMode/Views/ConsoleHomeView.xaml
@@ -435,6 +435,8 @@
+
+
-
+