Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: ______
Expand Down
3 changes: 3 additions & 0 deletions src/ConsoleMode/Resources/Strings.en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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?",
Expand Down
3 changes: 3 additions & 0 deletions src/ConsoleMode/Resources/Strings.es-ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}?",
Expand Down
3 changes: 3 additions & 0 deletions src/ConsoleMode/Resources/Strings.pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -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?",
Expand Down
3 changes: 3 additions & 0 deletions src/ConsoleMode/Services/LocalizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
32 changes: 29 additions & 3 deletions src/ConsoleMode/SessionMenuWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,9 @@
<ItemsControl.ItemsPanel><ItemsPanelTemplate><controls:WrapPanel Spacing="16"/></ItemsPanelTemplate></ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:SwitchWindowItem">
<Button Tag="card" Style="{StaticResource WindowCard}" Width="250" Height="178" Padding="18,16"
Command="{Binding ViewModel.SwitchToWindowCommand, ElementName=Page}" CommandParameter="{x:Bind}">
<Grid Width="250" Height="178">
<Button Tag="card" Style="{StaticResource WindowCard}" Padding="18,16"
Command="{Binding ViewModel.SwitchToWindowCommand, ElementName=Page}" CommandParameter="{x:Bind}">
<Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<Grid Width="52" Height="52" HorizontalAlignment="Left">
Expand All @@ -291,7 +292,13 @@
<TextBlock Text="{x:Bind Title}" FontSize="18" FontWeight="SemiBold" TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis"/>
</StackPanel>
</Grid>
</Button>
</Button>
<Button IsTabStop="False" Width="40" Height="40" Padding="0" HorizontalAlignment="Right" VerticalAlignment="Top"
Margin="0,7,7,0" Background="{ThemeResource MenuVeil}" Foreground="White" Click="CloseCardClick"
DataContext="{Binding}">
<FontIcon Glyph="&#xE711;" FontSize="16"/>
</Button>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Expand Down Expand Up @@ -344,6 +351,25 @@
</Grid>
</Grid>

<!-- Confirm closing a window; the cancel action is focused by default. -->
<Grid x:Name="CloseConfirmOverlay" Grid.RowSpan="3" Margin="-56,-36,-56,-30" Visibility="Collapsed"
Background="{ThemeResource MenuPickerVeil}" IsTabStop="False">
<Grid x:Name="CloseConfirmCard" MaxWidth="640" VerticalAlignment="Center" RowSpacing="18" Padding="40,32"
Background="{ThemeResource MenuVeil}" XYFocusKeyboardNavigation="Enabled">
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<TextBlock Text="{x:Bind ViewModel.Texts.SwitcherCloseConfirmTitle, Mode=OneWay}" FontSize="28" FontWeight="SemiBold"
HorizontalAlignment="Center" TextAlignment="Center" TextWrapping="Wrap"/>
<TextBlock x:Name="CloseConfirmBodyText" Grid.Row="1" FontSize="19"
Foreground="{ThemeResource MenuTextSecondary}" HorizontalAlignment="Center" TextAlignment="Center" TextWrapping="Wrap"/>
<StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="14" HorizontalAlignment="Center">
<Button x:Name="CancelCloseButton" MinWidth="180" Height="56" Content="{x:Bind ViewModel.Texts.ShortcutCancel, Mode=OneWay}"
Click="CancelCloseClick"/>
<Button x:Name="ConfirmCloseButton" MinWidth="180" Height="56" Content="{x:Bind ViewModel.Texts.SwitcherCloseConfirmAction, Mode=OneWay}"
Click="ConfirmCloseClick"/>
</StackPanel>
</Grid>
</Grid>

<!-- Busy veil while a blocking change runs -->
<Grid Grid.RowSpan="3" Margin="-56,-36,-56,-30" Visibility="{x:Bind ViewModel.IsSessionMenuBusy, Mode=OneWay}" Background="{ThemeResource MenuVeil}">
<ProgressRing IsActive="True" Width="56" Height="56"/>
Expand Down
114 changes: 104 additions & 10 deletions src/ConsoleMode/SessionMenuWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down Expand Up @@ -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())
{
Expand All @@ -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();
Expand All @@ -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
{
Expand Down Expand Up @@ -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;
}

/// <summary>The card goes away once its window closes: the focus moves to the next one (or the previous, or the first row).</summary>
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);
Comment thread
pullfrog[bot] marked this conversation as resolved.
}

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));
}

/// <summary>Keep focus on the target while the close request is pending, then move to a neighbor if it closes.</summary>
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);
}

/// <summary>Focus the requested card if it remains; otherwise the card now occupying its old position.</summary>
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);
}

/// <summary>Windows won't hand a background process the foreground; this forces it (PlayStation pads need it).</summary>
Expand Down
Loading