diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md
index 8bf8662..4bf1084 100644
--- a/CHANGELOG.en-US.md
+++ b/CHANGELOG.en-US.md
@@ -7,6 +7,7 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Befor
- Settings → TV: console mode turns a Google TV / Android TV on over the network and switches to the PC's HDMI input; it can also put the TV in standby on restore. (#126)
### Fixes
+- Session menu: closing a window through the internal Alt+Tab now asks for confirmation; Cancel keeps the window open and restores focus to its card. (#127)
- Reverted PR #45: restored the previous behavior when stopping a session. (#125)
## [1.6.0-alpha.3]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e12e6c..f111a83 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CH
- Ajustes → TV: o modo console liga a Google TV / Android TV pela rede e troca para a entrada HDMI do PC; também pode colocá-la em espera ao restaurar. (#126)
### Correções
+- Menu da sessão: fechar uma janela pelo Alt+Tab interno agora pede confirmação; Cancelar mantém a janela aberta e devolve o foco ao cartão. (#127)
- Revertida a PR #45: restaurado o comportamento anterior ao encerrar uma sessão. (#125)
## [1.6.0-alpha.3]
diff --git a/docs/TESTING.md b/docs/TESTING.md
index 4c38d8a..7b9e276 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -125,6 +125,7 @@ Pré-condições: Ajustes → "Entrar ao conectar um controle" **ligado**; app n
- [ ] A linha/card em foco ganha contorno branco e cresce um pouco; há som ao mover, confirmar e voltar. Resultado: ______
- [ ] Direcional entre o painel lateral e o grid de janelas (esquerda/direita) e dentro de cada um (cima/baixo) nunca fica "morto"; com o seletor (resolução, áudio, FPS) aberto o foco não escapa para trás. Resultado: ______
- [ ] **Janelas**: lista as janelas abertas (ícone, programa e título) em ordem de frente para trás. A traz a escolhida para a frente (restaura se estiver minimizada) e fecha o menu; X (□ no PlayStation) ou Delete pede para a janela fechar e o card some quando ela fecha; uma que pergunta "salvar?" mantém o card. Resultado: ______
+- [ ] **Confirmação ao fechar janela**: pelo X do controle, Delete e botão de fechar no cartão, aparece um modal com o título correto; Cancelar ou B mantém a janela aberta e devolve o foco ao cartão; confirmar fecha só a janela selecionada. Enquanto o modal está aberto, direcional, A, X e B não acionam opções atrás dele. Resultado: ______
- [ ] **Falha ao ativar uma janela**: abra o menu com o Bloco de Notas aberto, feche o Bloco de Notas por outro meio depois de a lista carregar (por exemplo, `taskkill /IM notepad.exe /F`) e escolha o card obsoleto com A. O menu continua aberto, o foco permanece no card e o log mostra `Janelas: trocar ... => não conseguiu`; uma falha do Windows ao trazer uma janela válida para frente também deve manter o menu aberto. Resultado: ______
- [ ] A lista não mostra o próprio menu, a barra de tarefas, a área de trabalho nem apps UWP suspensos; sem janelas mostra "Nenhuma janela aberta". Resultado: ______
- [ ] Em sessão (Big Picture/Playnite aberto): trocar para outra janela **não** restaura a mesa, e voltar ao Big Picture pelo grid funciona; uma janela aberta na tela de jogo aparece na lista. Resultado: ______
diff --git a/src/ConsoleMode/Resources/Strings.en-US.json b/src/ConsoleMode/Resources/Strings.en-US.json
index 2d4eb52..a1f8075 100644
--- a/src/ConsoleMode/Resources/Strings.en-US.json
+++ b/src/ConsoleMode/Resources/Strings.en-US.json
@@ -96,6 +96,9 @@
"SwitcherEmpty": "No open windows",
"SwitcherOpen": "Open",
"SwitcherCloseWindow": "Close window",
+ "SwitcherCloseConfirmTitle": "Close this window?",
+ "SwitcherCloseConfirmBody": "{0} will be closed. If there are unsaved changes, the app may still ask you to save them.",
+ "SwitcherCloseConfirmAction": "Close window",
"SessionPreviewBadge": "Preview · outside a session",
"ConsoleActiveHint": "Or hold {0} on the controller for 1 second.",
"ChooseRoleFor": "What does the {0} screen do?",
diff --git a/src/ConsoleMode/Resources/Strings.es-ES.json b/src/ConsoleMode/Resources/Strings.es-ES.json
index ca98ecf..875882a 100644
--- a/src/ConsoleMode/Resources/Strings.es-ES.json
+++ b/src/ConsoleMode/Resources/Strings.es-ES.json
@@ -96,6 +96,9 @@
"SwitcherEmpty": "No hay ventanas abiertas",
"SwitcherOpen": "Abrir",
"SwitcherCloseWindow": "Cerrar ventana",
+ "SwitcherCloseConfirmTitle": "¿Cerrar esta ventana?",
+ "SwitcherCloseConfirmBody": "Se cerrará {0}. Si hay cambios sin guardar, la aplicación todavía puede pedirte que los guardes.",
+ "SwitcherCloseConfirmAction": "Cerrar ventana",
"SessionPreviewBadge": "Vista previa · fuera de la sesión",
"ConsoleActiveHint": "O mantén {0} en el mando durante 1 segundo.",
"ChooseRoleFor": "¿Qué hace la pantalla {0}?",
diff --git a/src/ConsoleMode/Resources/Strings.pt-BR.json b/src/ConsoleMode/Resources/Strings.pt-BR.json
index e1d68d8..1dcf2c5 100644
--- a/src/ConsoleMode/Resources/Strings.pt-BR.json
+++ b/src/ConsoleMode/Resources/Strings.pt-BR.json
@@ -96,6 +96,9 @@
"SwitcherEmpty": "Nenhuma janela aberta",
"SwitcherOpen": "Abrir",
"SwitcherCloseWindow": "Fechar janela",
+ "SwitcherCloseConfirmTitle": "Fechar esta janela?",
+ "SwitcherCloseConfirmBody": "{0} será fechado. Se houver alterações sem salvar, o aplicativo ainda poderá pedir que você as salve.",
+ "SwitcherCloseConfirmAction": "Fechar janela",
"SessionPreviewBadge": "Prévia · fora da sessão",
"ConsoleActiveHint": "Ou segure {0} no controle por 1 segundo.",
"ChooseRoleFor": "O que a tela {0} faz?",
diff --git a/src/ConsoleMode/Services/LocalizationService.cs b/src/ConsoleMode/Services/LocalizationService.cs
index 0411804..0cdc86e 100644
--- a/src/ConsoleMode/Services/LocalizationService.cs
+++ b/src/ConsoleMode/Services/LocalizationService.cs
@@ -196,6 +196,9 @@ internal void NotifyAll()
public string SwitcherEmpty => LocalizationService.Get(nameof(SwitcherEmpty));
public string SwitcherOpen => LocalizationService.Get(nameof(SwitcherOpen));
public string SwitcherCloseWindow => LocalizationService.Get(nameof(SwitcherCloseWindow));
+ public string SwitcherCloseConfirmTitle => LocalizationService.Get(nameof(SwitcherCloseConfirmTitle));
+ public string SwitcherCloseConfirmAction => LocalizationService.Get(nameof(SwitcherCloseConfirmAction));
+ public string ShortcutCancel => LocalizationService.Get(nameof(ShortcutCancel));
public string BackgroundCard => LocalizationService.Get(nameof(BackgroundCard));
public string BackgroundDescription => LocalizationService.Get(nameof(BackgroundDescription));
public string BackgroundImageCard => LocalizationService.Get(nameof(BackgroundImageCard));
diff --git a/src/ConsoleMode/SessionMenuWindow.xaml b/src/ConsoleMode/SessionMenuWindow.xaml
index 2029421..5865272 100644
--- a/src/ConsoleMode/SessionMenuWindow.xaml
+++ b/src/ConsoleMode/SessionMenuWindow.xaml
@@ -277,8 +277,9 @@
-
+
+
+
+
@@ -344,6 +351,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ConsoleMode/SessionMenuWindow.xaml.cs b/src/ConsoleMode/SessionMenuWindow.xaml.cs
index 85921de..f74a4b3 100644
--- a/src/ConsoleMode/SessionMenuWindow.xaml.cs
+++ b/src/ConsoleMode/SessionMenuWindow.xaml.cs
@@ -38,6 +38,8 @@ public sealed partial class SessionMenuWindow : Window
private Control? _lastSidebarFocus;
private FocusNavigationDirection _lastDirection;
private Microsoft.UI.Dispatching.DispatcherQueueTimer? _closeTimer;
+ private SwitchWindowItem? _pendingCloseItem;
+ private int _pendingCloseIndex;
public MainViewModel ViewModel { get; }
@@ -76,12 +78,14 @@ public SessionMenuWindow(MainViewModel viewModel, ScreenRect? target)
ReadingOrderFallback = false,
// Focus may only land inside the picker while it is open (it covers everything else); up and
// down stay inside the panel or the grid they start in, so the end of a list is an end.
- SearchRoot = () => ViewModel.IsSessionPickerOpen ? PickerCard
+ SearchRoot = () => CloseConfirmOverlay.Visibility == Visibility.Visible ? CloseConfirmCard
+ : ViewModel.IsSessionPickerOpen ? PickerCard
: _lastDirection is FocusNavigationDirection.Up or FocusNavigationDirection.Down ? RegionOfFocus() : Root,
// Adjust mode on the volume row: Left/Right change it, Up/Down are swallowed.
BeforeMove = direction =>
{
_lastDirection = direction;
+ if (CloseConfirmOverlay.Visibility == Visibility.Visible) return false;
// Left from the first column of windows goes back to the row of the panel you came from.
if (!_editingVolume && direction == FocusNavigationDirection.Left && !ViewModel.IsSessionPickerOpen && IsOnLeftmostCard())
{
@@ -98,11 +102,13 @@ public SessionMenuWindow(MainViewModel viewModel, ScreenRect? target)
// X / Square: close the highlighted window, or mute on the volume row.
_navigator.OptionRequested += () =>
{
+ if (CloseConfirmOverlay.Visibility == Visibility.Visible) return;
if (!CloseFocusedWindow() && IsVolumeFocused()) ViewModel.ToggleMuteCommand.Execute(null);
};
_navigator.BackRequested += () =>
{
- if (_editingVolume) SetEditingVolume(false);
+ if (CloseConfirmOverlay.Visibility == Visibility.Visible) CancelCloseConfirmation();
+ else if (_editingVolume) SetEditingVolume(false);
else ViewModel.SessionMenuBack();
};
_navigator.Start();
@@ -111,6 +117,38 @@ public SessionMenuWindow(MainViewModel viewModel, ScreenRect? target)
// before they ever bubble up to Root.
Root.PreviewKeyDown += (_, e) =>
{
+ if (CloseConfirmOverlay.Visibility == Visibility.Visible)
+ {
+ var confirmDirection = e.Key switch
+ {
+ Windows.System.VirtualKey.Up => FocusNavigationDirection.Up,
+ Windows.System.VirtualKey.Down => FocusNavigationDirection.Down,
+ Windows.System.VirtualKey.Left => FocusNavigationDirection.Left,
+ Windows.System.VirtualKey.Right => FocusNavigationDirection.Right,
+ _ => FocusNavigationDirection.None
+ };
+ if (confirmDirection != FocusNavigationDirection.None)
+ {
+ e.Handled = true;
+ _navigator.Navigate(confirmDirection);
+ return;
+ }
+ if (e.Key == Windows.System.VirtualKey.Tab)
+ {
+ e.Handled = true;
+ var focused = Root.XamlRoot is { } confirmRoot ? FocusManager.GetFocusedElement(confirmRoot) : null;
+ if (ReferenceEquals(focused, CancelCloseButton)) ConfirmCloseButton.Focus(FocusState.Keyboard);
+ else CancelCloseButton.Focus(FocusState.Keyboard);
+ return;
+ }
+ if (e.Key is Windows.System.VirtualKey.Escape or Windows.System.VirtualKey.Back)
+ {
+ e.Handled = true;
+ CancelCloseConfirmation();
+ }
+ else if (e.Key == Windows.System.VirtualKey.Delete) e.Handled = true;
+ return;
+ }
// Arrows go through the same navigator as the D-pad (aligned, then the nearest).
var direction = e.Key switch
{
@@ -431,22 +469,78 @@ private bool CloseFocusedWindow()
if (Root.XamlRoot is not { } root) return false;
if (FocusManager.GetFocusedElement(root) is not FrameworkElement { DataContext: SwitchWindowItem item }) return false;
var index = ViewModel.SwitcherWindows.IndexOf(item);
- _ = CloseAndRefocusAsync(item, index);
+ ShowCloseConfirmation(item, index);
return true;
}
- /// The card goes away once its window closes: the focus moves to the next one (or the previous, or the first row).
+ private void ShowCloseConfirmation(SwitchWindowItem item, int index)
+ {
+ if (CloseConfirmOverlay.Visibility == Visibility.Visible) return;
+ _pendingCloseItem = item;
+ _pendingCloseIndex = index;
+ CloseConfirmBodyText.Text = LocalizationService.Get("SwitcherCloseConfirmBody", item.Title);
+ CloseConfirmOverlay.Visibility = Visibility.Visible;
+ CancelCloseButton.Focus(FocusState.Programmatic);
+ }
+
+ private void CancelCloseConfirmation()
+ {
+ var item = _pendingCloseItem;
+ var index = _pendingCloseIndex;
+ _pendingCloseItem = null;
+ CloseConfirmOverlay.Visibility = Visibility.Collapsed;
+ FocusSwitcherCard(item, index, FocusState.Programmatic);
+ }
+
+ private async void ConfirmCloseClick(object sender, RoutedEventArgs e)
+ {
+ var item = _pendingCloseItem;
+ var index = _pendingCloseIndex;
+ _pendingCloseItem = null;
+ CloseConfirmOverlay.Visibility = Visibility.Collapsed;
+ FocusSwitcherCard(item, index, FocusState.Programmatic);
+ if (item is not null) await CloseAndRefocusAsync(item, index);
+ }
+
+ private void CancelCloseClick(object sender, RoutedEventArgs e) => CancelCloseConfirmation();
+
+ private void CloseCardClick(object sender, RoutedEventArgs e)
+ {
+ if (sender is FrameworkElement { DataContext: SwitchWindowItem item })
+ ShowCloseConfirmation(item, ViewModel.SwitcherWindows.IndexOf(item));
+ }
+
+ /// Keep focus on the target while the close request is pending, then move to a neighbor if it closes.
private async Task CloseAndRefocusAsync(SwitchWindowItem item, int index)
{
await ViewModel.CloseSwitcherWindowAsync(item);
- if (ViewModel.SwitcherWindows.Contains(item)) return; // it asked to save: the card stays, so does the focus
- var count = ViewModel.SwitcherWindows.Count;
- if (count == 0) { FirstRow.Focus(FocusState.Keyboard); return; }
- var next = Math.Clamp(index, 0, count - 1);
+ FocusSwitcherCard(item, index, FocusState.Keyboard);
+ }
+
+ /// Focus the requested card if it remains; otherwise the card now occupying its old position.
+ private void FocusSwitcherCard(SwitchWindowItem? item, int oldIndex, FocusState focusState)
+ {
+ var target = item is not null && ViewModel.SwitcherWindows.Contains(item)
+ ? item
+ : ViewModel.SwitcherWindows.Count == 0 ? null
+ : ViewModel.SwitcherWindows[Math.Clamp(oldIndex, 0, ViewModel.SwitcherWindows.Count - 1)];
+ if (target is null)
+ {
+ FirstRow.Focus(focusState);
+ return;
+ }
+
+ var next = ViewModel.SwitcherWindows.IndexOf(target);
SwitcherList.UpdateLayout();
if (SwitcherList.ContainerFromIndex(next) is { } container
- && FocusManager.FindFirstFocusableElement(container) is Control card)
- card.Focus(FocusState.Keyboard);
+ && FocusManager.FindFirstFocusableElement(container) is Control card
+ && ReferenceEquals(card.DataContext, target))
+ {
+ card.Focus(focusState);
+ return;
+ }
+
+ FirstRow.Focus(focusState);
}
/// Windows won't hand a background process the foreground; this forces it (PlayStation pads need it).