diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md
index 4bad687..789150a 100644
--- a/CHANGELOG.en-US.md
+++ b/CHANGELOG.en-US.md
@@ -3,6 +3,9 @@
English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Before publishing a version, add a `## [VERSION]` section to **both** files: the workflow publishes the section matching the tag from each one and fails if either is missing.
## [Unreleased]
+### Fixes
+- FPS counter: the style saved in Settings now applies as soon as the session starts. Before, the counter only showed up after cycling the "FPS counter" row in the session menu. (#139)
+- Back to the PC: the request to close Big Picture now only goes to Steam's own windows. Before, a full-screen game (SDL ones, like Valve's and many indies) could get it and close. (#140)
## [1.6.0-beta.7]
### Heads-up: beta version, not the stable one
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4bd76a5..b01c52b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,9 @@
Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CHANGELOG.en-US.md`. Antes de publicar uma versão, adicione uma seção `## [VERSÃO]` com o changelog daquela versão **nos dois arquivos**. O workflow publica as duas seções correspondentes à tag na mesma release e falha se faltar alguma.
## [Unreleased]
+### Correções
+- Contador de FPS: o estilo salvo em Ajustes agora vale assim que a sessão começa. Antes, o contador só aparecia depois de alternar a linha "Contador de FPS" no menu da sessão. (#139)
+- Voltar ao PC: o pedido de fechar o Big Picture só vai para janelas da própria Steam. Antes, um jogo em tela cheia (os feitos com SDL, como os da Valve e muitos indies) podia receber o pedido e fechar. (#140)
## [1.6.0-beta.7]
### Atenção: versão beta, não é a estável
diff --git a/docs/TESTING.md b/docs/TESTING.md
index 9531d2e..99f71e3 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -238,6 +238,7 @@ Pré-condições: lançador = Steam Big Picture; Ajustes → "Mandar a Steam par
- [ ] 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 um **jogo SDL em tela cheia sem bordas** aberto (ex.: CS2, Dota 2, Half-Life: Alyx ou um indie feito com SDL) e não salvo: voltar ao PC **não fecha o jogo**; o Big Picture fecha e, se o jogo passou por Big Picture na detecção, o log mostra `Fechar Big Picture: janela em tela cheia não é da Steam, mantida aberta`. 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: ______
@@ -393,3 +394,4 @@ Pré-requisito: RivaTuner Statistics Server instalado, com "Show On-Screen Displ
- [ ] Com o RTSS fechado, ligar Compacto abre o RTSS e o contador aparece.
- [ ] Voltar ao PC: o nosso some e o do Afterburner volta.
- [ ] A escolha e o layout ficam salvos para a próxima sessão.
+- [ ] Com Compacto (ou outro estilo nosso) já salvo em Ajustes, iniciar a sessão: o contador aparece e o do Afterburner some sem precisar mexer na linha do menu.
diff --git a/src/ConsoleMode/Models/Models.cs b/src/ConsoleMode/Models/Models.cs
index 16726e3..6fbd8de 100644
--- a/src/ConsoleMode/Models/Models.cs
+++ b/src/ConsoleMode/Models/Models.cs
@@ -87,6 +87,19 @@ public sealed class AppConfig
/// Turning the TV on / to the PC's input when console mode starts (issue #75).
public TvControlConfig Tv { get; set; } = new();
+ ///
+ /// A copy with other monitor fields and every other setting as it is here. A copy, not a field-by-field
+ /// rebuild: a setting added later is carried along without anyone having to remember it.
+ ///
+ public AppConfig WithMonitors(string focusMonitor, List hideMonitors, Dictionary monitorModes)
+ {
+ var copy = (AppConfig)MemberwiseClone();
+ copy.FocusMonitor = focusMonitor;
+ copy.HideMonitors = hideMonitors;
+ copy.MonitorModes = monitorModes;
+ return copy;
+ }
+
[JsonIgnore]
public string SetupKey =>
$"{FocusMonitor}|{(MonitorModes.TryGetValue(FocusMonitor, out var mode) ? mode.Key : "current")}";
diff --git a/src/ConsoleMode/Services/ConsoleEngine.cs b/src/ConsoleMode/Services/ConsoleEngine.cs
index f55ea61..f90e0fd 100644
--- a/src/ConsoleMode/Services/ConsoleEngine.cs
+++ b/src/ConsoleMode/Services/ConsoleEngine.cs
@@ -407,40 +407,7 @@ private AppConfig ResolveMonitorNames(AppConfig config)
foreach (var (key, mode) in config.MonitorModes)
modes[Monitors.ResolveName(key)] = mode;
- return new AppConfig
- {
- Version = config.Version,
- AppLanguage = config.AppLanguage,
- FocusMonitor = focus,
- HideMonitors = hide,
- HideStrategy = config.HideStrategy,
- FullscreenMode = config.FullscreenMode,
- PlaynitePath = config.PlaynitePath,
- AudioDeviceId = config.AudioDeviceId,
- AudioDeviceName = config.AudioDeviceName,
- AudioAutoSwitch = config.AudioAutoSwitch,
- FpsLimit = config.FpsLimit,
- MonitorModes = modes,
- HdrEnable = config.HdrEnable,
- VrrEnable = config.VrrEnable,
- UiMode = config.UiMode,
- TourDone = config.TourDone,
- ConfirmedSetup = config.ConfirmedSetup,
- CheckUpdates = config.CheckUpdates,
- BetaUpdates = config.BetaUpdates,
- HomeShortcut = config.HomeShortcut,
- MenuShortcut = config.MenuShortcut,
- ExitShortcut = config.ExitShortcut,
- ShortcutsOnboardingDone = config.ShortcutsOnboardingDone,
- HomeButtonShortPress = config.HomeButtonShortPress,
- CloseSteamOnRestore = config.CloseSteamOnRestore,
- AutoStartOnController = config.AutoStartOnController,
- InterfaceSounds = config.InterfaceSounds,
- ConsoleBackground = config.ConsoleBackground,
- ConsoleBackgroundImage = config.ConsoleBackgroundImage,
- SkippedUpdateVersion = config.SkippedUpdateVersion,
- Tv = config.Tv
- };
+ return config.WithMonitors(focus, hide, modes);
}
private void MoveToFocus(string monitorName, nint[] handles, ScreenRect? rect)
diff --git a/src/ConsoleMode/Services/LaunchService.cs b/src/ConsoleMode/Services/LaunchService.cs
index 32f46d7..08c2213 100644
--- a/src/ConsoleMode/Services/LaunchService.cs
+++ b/src/ConsoleMode/Services/LaunchService.cs
@@ -136,7 +136,8 @@ public bool IsPlayniteActive(ConsoleRuntimeState state)
///
/// Leaves Big Picture the clean way, through Steam's own URL (back to the desktop client), so it does not
/// stay open on the desk once the screens are back. Halfway through the wait, if it is still there, the
- /// window is asked to close (WM_CLOSE: the X button, not a kill). Returns whether it is gone.
+ /// window is asked to close (WM_CLOSE: the X button, not a kill), and only if it belongs to Steam: a game in
+ /// full screen can pass for Big Picture in . Returns whether it is gone.
///
public bool CloseBigPicture(ConsoleRuntimeState state, int timeoutMs = 6000)
{
@@ -155,12 +156,27 @@ public bool CloseBigPicture(ConsoleRuntimeState state, int timeoutMs = 6000)
{
nudged = true;
foreach (var handle in handles.Concat(GetBigPictureHandles()).Distinct())
- NativeWindows.PostMessage(handle, NativeWindows.WmClose, 0, 0);
+ {
+ if (IsSteamWindow(handle)) NativeWindows.PostMessage(handle, NativeWindows.WmClose, 0, 0);
+ else AppLog.Write("Fechar Big Picture: janela em tela cheia não é da Steam, mantida aberta");
+ }
}
}
return !IsBigPictureActive(state);
}
+ private static bool IsSteamWindow(nint handle)
+ {
+ NativeWindows.GetWindowThreadProcessId(handle, out var processId);
+ if (processId == 0) return false;
+ try
+ {
+ using var process = Process.GetProcessById(unchecked((int)processId));
+ return SteamShutdown.IsSteamProcess(process.ProcessName);
+ }
+ catch { return false; /* gone, or not ours to read: not known to be Steam, so left alone */ }
+ }
+
public bool IsBigPictureActive(ConsoleRuntimeState state)
{
if (state.CachedBigPictureHandle != 0)
diff --git a/src/ConsoleMode/Services/SteamShutdown.cs b/src/ConsoleMode/Services/SteamShutdown.cs
index 4faf25d..86d1b63 100644
--- a/src/ConsoleMode/Services/SteamShutdown.cs
+++ b/src/ConsoleMode/Services/SteamShutdown.cs
@@ -5,10 +5,17 @@ namespace ConsoleMode.Services;
///
public static class SteamShutdown
{
+ ///
+ /// Steam's own processes. Only their windows may be asked to close: a game is an SDL_app in full
+ /// screen just like Big Picture, and it must never get the WM_CLOSE meant for Steam.
+ ///
+ public static bool IsSteamProcess(string? processName) =>
+ string.Equals(processName, "steam", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(processName, "steamwebhelper", StringComparison.OrdinalIgnoreCase);
+
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)) &&
+ IsSteamProcess(processName) &&
string.Equals(title.Trim(), "Steam", StringComparison.OrdinalIgnoreCase) &&
(string.Equals(className, "Chrome_WidgetWin_1", StringComparison.Ordinal) ||
string.Equals(className, "vguiPopupWindow", StringComparison.Ordinal) ||
diff --git a/tests/ConsoleMode.Tests/AppConfigTests.cs b/tests/ConsoleMode.Tests/AppConfigTests.cs
new file mode 100644
index 0000000..8345f17
--- /dev/null
+++ b/tests/ConsoleMode.Tests/AppConfigTests.cs
@@ -0,0 +1,61 @@
+using System.Reflection;
+using ConsoleMode.Models;
+using ConsoleMode.Services;
+
+namespace ConsoleMode.Tests;
+
+public class AppConfigTests
+{
+ [Fact]
+ public void Copy_for_a_session_swaps_only_the_monitor_fields()
+ {
+ var config = new AppConfig { FocusMonitor = "id-tv", HideMonitors = ["id-desk"] };
+ var hide = new List { @"\\.\DISPLAY1" };
+ var modes = new Dictionary();
+
+ var copy = config.WithMonitors(@"\\.\DISPLAY2", hide, modes);
+
+ Assert.Equal(@"\\.\DISPLAY2", copy.FocusMonitor);
+ Assert.Same(hide, copy.HideMonitors);
+ Assert.Same(modes, copy.MonitorModes);
+ Assert.Equal("id-tv", config.FocusMonitor);
+ Assert.Equal(["id-desk"], config.HideMonitors);
+ }
+
+ // The session used to get a field-by-field copy that forgot the FPS counter: the saved style
+ // was never applied when console mode started.
+ [Fact]
+ public void Copy_for_a_session_keeps_the_fps_counter()
+ {
+ var layout = new FpsOverlayLayout { SingleLine = true };
+ var config = new AppConfig { FpsOverlay = RtssOverlay.Compact, FpsOverlayLayout = layout };
+
+ var copy = config.WithMonitors("", [], []);
+
+ Assert.Equal(RtssOverlay.Compact, copy.FpsOverlay);
+ Assert.Same(layout, copy.FpsOverlayLayout);
+ }
+
+ [Fact]
+ public void Copy_for_a_session_keeps_every_other_setting()
+ {
+ string[] swapped = [nameof(AppConfig.FocusMonitor), nameof(AppConfig.HideMonitors), nameof(AppConfig.MonitorModes), nameof(AppConfig.SetupKey)];
+ var config = new AppConfig();
+ var properties = typeof(AppConfig).GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(p => p.CanWrite && !swapped.Contains(p.Name))
+ .ToList();
+ foreach (var property in properties)
+ {
+ // A value that differs from the default, so a property left behind shows up.
+ if (property.PropertyType == typeof(string)) property.SetValue(config, "changed-" + property.Name);
+ else if (property.PropertyType == typeof(bool)) property.SetValue(config, !(bool)property.GetValue(config)!);
+ else if (property.PropertyType == typeof(int)) property.SetValue(config, (int)property.GetValue(config)! + 7);
+ else property.SetValue(config, Activator.CreateInstance(property.PropertyType));
+ }
+
+ var copy = config.WithMonitors("", [], []);
+
+ foreach (var property in properties)
+ Assert.Equal(property.GetValue(config), property.GetValue(copy));
+ }
+}
diff --git a/tests/ConsoleMode.Tests/SteamShutdownTests.cs b/tests/ConsoleMode.Tests/SteamShutdownTests.cs
index 27150d1..379a3e7 100644
--- a/tests/ConsoleMode.Tests/SteamShutdownTests.cs
+++ b/tests/ConsoleMode.Tests/SteamShutdownTests.cs
@@ -17,6 +17,17 @@ public class SteamShutdownTests
public void Only_unowned_visible_steam_client_windows_are_close_candidates(string process, string title, string className, bool visible, bool owned, bool expected) =>
Assert.Equal(expected, SteamShutdown.IsClientWindowCandidate(process, title, className, visible, owned));
+ [Theory]
+ [InlineData("steam", true)]
+ [InlineData("steamwebhelper", true)]
+ [InlineData("SteamWebHelper", true)]
+ [InlineData("cs2", false)]
+ [InlineData("steamapp", false)]
+ [InlineData("", false)]
+ [InlineData(null, false)]
+ public void Only_steams_own_processes_may_be_asked_to_close(string? process, bool expected) =>
+ Assert.Equal(expected, SteamShutdown.IsSteamProcess(process));
+
[Fact]
public void Steam_window_is_closed_to_tray_when_big_picture_was_the_launcher_and_no_game_is_running()
{