From 273278fca7ea008f176c3718afbfe3e3be524150 Mon Sep 17 00:00:00 2001 From: Filipe Moreira Date: Wed, 30 Sep 2026 21:01:59 -0300 Subject: [PATCH 1/3] fix: expose desktop settings in console mode --- docs/TESTING.md | 11 +++ src/ConsoleMode/Controls/ShortcutPicker.cs | 6 ++ .../ViewModels/MainViewModel.ConsoleUi.cs | 62 ++++++++++++- src/ConsoleMode/ViewModels/MainViewModel.cs | 17 +++- src/ConsoleMode/Views/ConsoleHomeView.xaml | 87 ++++++++++++++++++- src/ConsoleMode/Views/ConsoleHomeView.xaml.cs | 84 +++++++++++++++++- 6 files changed, 256 insertions(+), 11 deletions(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index 7b9e276..b512712 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -257,6 +257,17 @@ Pré-condições: TV Google TV / Android TV na mesma rede do PC, com **Depuraç - [ ] O arquivo `adbkey.pem` na pasta de dados começa com `dpapi:` (não com `-----BEGIN`). Copiar a pasta de dados para outro usuário do Windows: o log mostra "chave ADB de outro usuário/PC; criando outra" e a TV pede permissão de novo. Resultado: ______ - [ ] TV desligada da tomada: **Jogar agora** espera no máximo ~30 s pela TV e segue (o log mostra o tempo). Resultado: ______ +## 3d. Configurações no modo Console + +- [ ] Em Sistema, as configurações do modo PC também estão disponíveis: caminho do Playnite, fechar a Steam, Android TV, FPS personalizado, atalhos, diagnóstico do controle, tutorial e pasta de dados. Resultado: ______ +- [ ] Configurar, alterar e remover os atalhos de abrir o app, menu da sessão e voltar ao PC; uma combinação em conflito mostra o mesmo erro do modo PC. Durante a captura, os botões não navegam nem iniciam uma sessão. Resultado: ______ +- [ ] Cancelar a captura e sair do editor devolve o foco à configuração; teclado, mouse e controle conseguem abrir e fechar o editor sem acionar opções atrás dele. Resultado: ______ +- [ ] Alterar uma configuração no modo Console, trocar para PC e reabrir o app: o valor permanece igual nas duas interfaces. Resultado: ______ +- [ ] Selecionar e limpar o caminho do Playnite; cancelar o seletor mantém o valor anterior. Resultado: ______ +- [ ] Configurar a Android TV no modo Console: endereço, MAC opcional, entrada HDMI, comando da entrada e espera ao restaurar; Testar agora mostra o resultado e libera o botão após erro ou timeout. Resultado: ______ +- [ ] Selecionar FPS personalizado e digitar o limite; o modo PC mostra o mesmo valor. Sem RTSS disponível, a configuração respeita a mesma indisponibilidade das duas interfaces. Resultado: ______ +- [ ] Copiar o diagnóstico do controle, abrir a pasta de dados e rever o tutorial no modo Console; ao voltar, o foco continua em uma opção visível. Resultado: ______ + ## 4. Regressões - [ ] Interface Desktop: mapa de telas, `Segmented`, chips, tour de 3 passos e Ajustes continuam como antes. Resultado: ______ diff --git a/src/ConsoleMode/Controls/ShortcutPicker.cs b/src/ConsoleMode/Controls/ShortcutPicker.cs index bd520a1..0797694 100644 --- a/src/ConsoleMode/Controls/ShortcutPicker.cs +++ b/src/ConsoleMode/Controls/ShortcutPicker.cs @@ -100,6 +100,12 @@ private void StopCapture() Refresh(); } + /// Stops the active capture when its containing console overlay is dismissed. + public void CancelCapture() => StopCapture(); + + /// Moves controller focus to this shortcut's Set/Change/Cancel action. + public bool FocusCaptureButton() => _set.Focus(FocusState.Keyboard); + private void Poll() { if (_capture is null) return; diff --git a/src/ConsoleMode/ViewModels/MainViewModel.ConsoleUi.cs b/src/ConsoleMode/ViewModels/MainViewModel.ConsoleUi.cs index 2f207ae..defc95f 100644 --- a/src/ConsoleMode/ViewModels/MainViewModel.ConsoleUi.cs +++ b/src/ConsoleMode/ViewModels/MainViewModel.ConsoleUi.cs @@ -37,6 +37,9 @@ partial void OnIsConsoleUiChanged(bool value) { OnPropertyChanged(nameof(IsDesktopHome)); OnPropertyChanged(nameof(IsDesktopSettings)); + OnPropertyChanged(nameof(IsTourMap)); + OnPropertyChanged(nameof(IsTourRole)); + OnPropertyChanged(nameof(IsTourPlay)); if (value) { IsRolePanelOpen = false; IsControllerToastOpen = false; } else { ConsoleTabIndex = HomeTabIndex; IsPickerOpen = false; } } @@ -130,6 +133,10 @@ private void RefreshConsoleTexts() OnPropertyChanged(nameof(HdrText)); OnPropertyChanged(nameof(VrrText)); OnPropertyChanged(nameof(BackgroundModeText)); + OnPropertyChanged(nameof(ConsoleTutorialTitle)); + OnPropertyChanged(nameof(ConsoleTutorialBody)); + OnPropertyChanged(nameof(ConsoleTutorialAction)); + if (IsShortcutEditorOpen) ShortcutEditorTitle = ShortcutName(_shortcutEditorSlot); } /// "desktop" / "console": the on-screen switch, remembered as an explicit choice. @@ -142,6 +149,32 @@ private void SwitchUi(string mode) } [ObservableProperty] private bool _isConsoleSettingsOpen; + [ObservableProperty] private bool _isShortcutEditorOpen; + [ObservableProperty] private string _shortcutEditorTitle = ""; + [ObservableProperty] private bool _isConsoleTutorialOpen; + private ShortcutSlot _shortcutEditorSlot; + + public string ShortcutEditorSlot => _shortcutEditorSlot.ToString().ToLowerInvariant(); + public string ConsoleTutorialTitle => LocalizationService.Get(TourStep switch + { + 2 => "TourRoleTitle", + 3 => "TourPlayTitle", + _ => "TourMapTitle" + }); + public string ConsoleTutorialBody => TourStep switch + { + 2 => LocalizationService.Get("TourRoleDescription"), + 3 => LocalizationService.Get("TourPlayDescription"), + _ => TourMapText + }; + public string ConsoleTutorialAction => LocalizationService.Get(TourStep >= 3 ? "GotIt" : "Next"); + + partial void OnTourStepChanged(int value) + { + OnPropertyChanged(nameof(ConsoleTutorialTitle)); + OnPropertyChanged(nameof(ConsoleTutorialBody)); + OnPropertyChanged(nameof(ConsoleTutorialAction)); + } /// x:Bind helper for on/off rows. public string OnOff(bool value) => LocalizationService.Get(value ? "ToggleOn" : "ToggleOff"); @@ -195,9 +228,32 @@ private void CycleSetting(string spec) case "startup": StartWithWindows = !StartWithWindows; break; case "updates": CheckUpdates = !CheckUpdates; break; case "betas": BetaUpdates = !BetaUpdates; break; + case "closest": CloseSteamOnRestore = !CloseSteamOnRestore; break; + case "tvpower": TvTurnOffOnRestore = !TvTurnOffOnRestore; break; } } + [RelayCommand] + private void OpenShortcutEditor(string slot) + { + if (IsConsoleActive) return; + var target = slot switch + { + "home" => ShortcutSlot.Home, + "menu" => ShortcutSlot.Menu, + "exit" => ShortcutSlot.Exit, + _ => (ShortcutSlot?)null + }; + if (target is null) return; + _shortcutEditorSlot = target.Value; + ShortcutEditorTitle = ShortcutName(target.Value); + OnPropertyChanged(nameof(ShortcutEditorSlot)); + IsShortcutEditorOpen = true; + } + + [RelayCommand] + private void CloseShortcutEditor() => IsShortcutEditorOpen = false; + private static ComboOption? Next(IList options, ComboOption? current, int step) { var index = UiModeResolver.Cycle(options.Count, current is null ? -1 : options.IndexOf(current), step); @@ -222,10 +278,12 @@ private void OpenPicker(string key) { "launch" => (LocalizationService.Get("LaunchCard"), LaunchOptions.Select(o => (o.Text, o.Value)), SelectedLaunch?.Value), "audio" => (LocalizationService.Get("AudioOutputCard"), AudioOptions.Select(o => (o.Text, o.Value)), SelectedAudio?.Value), - "fps" => (LocalizationService.Get("FpsCard"), FpsOptions.Where(o => o.Value != FpsCustomValue.ToString()).Select(o => (o.Text, o.Value)), SelectedFps?.Value), + "fps" => (LocalizationService.Get("FpsCard"), FpsOptions.Select(o => (o.Text, o.Value)), SelectedFps?.Value), "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), + "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), _ => (null, null, null) }; @@ -252,6 +310,8 @@ 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 "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": if (FocusRow?.Modes.FirstOrDefault(m => m.Key == item.Value) is { } mode) { diff --git a/src/ConsoleMode/ViewModels/MainViewModel.cs b/src/ConsoleMode/ViewModels/MainViewModel.cs index 397796a..98ba468 100644 --- a/src/ConsoleMode/ViewModels/MainViewModel.cs +++ b/src/ConsoleMode/ViewModels/MainViewModel.cs @@ -735,9 +735,9 @@ private async Task TestConfirmAsync() [NotifyPropertyChangedFor(nameof(IsTourMap), nameof(IsTourRole), nameof(IsTourPlay))] private int _tourStep; - public bool IsTourMap => TourStep == 1; - public bool IsTourRole => TourStep == 2; - public bool IsTourPlay => TourStep == 3; + public bool IsTourMap => !IsConsoleUi && TourStep == 1; + public bool IsTourRole => !IsConsoleUi && TourStep == 2; + public bool IsTourPlay => !IsConsoleUi && TourStep == 3; public string TourMapText => FocusRow is null ? LocalizationService.Get("TourMapGeneric") @@ -747,6 +747,16 @@ private async Task TestConfirmAsync() private void StartTour() { if (!HasMonitors || IsConsoleActive) return; + if (IsConsoleUi) + { + ConsoleTabIndex = HomeTabIndex; + IsConsoleTutorialOpen = true; + IsStatusOpen = false; + SelectedMonitor = FocusRow ?? SelectedMonitor; + OnPropertyChanged(nameof(TourMapText)); + TourStep = 1; + return; + } ShowPage(settings: false); IsStatusOpen = false; SelectedMonitor = FocusRow ?? SelectedMonitor; @@ -771,6 +781,7 @@ public void OnTourTipClosed(TeachingTip sender, TeachingTipClosedEventArgs args) private void EndTour() { TourStep = 0; + IsConsoleTutorialOpen = false; if (!_loadedConfig.TourDone) { var config = BuildConfig(); diff --git a/src/ConsoleMode/Views/ConsoleHomeView.xaml b/src/ConsoleMode/Views/ConsoleHomeView.xaml index 90a02f6..15ddd55 100644 --- a/src/ConsoleMode/Views/ConsoleHomeView.xaml +++ b/src/ConsoleMode/Views/ConsoleHomeView.xaml @@ -475,24 +475,58 @@ - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ConsoleMode/Views/ConsoleHomeView.xaml.cs b/src/ConsoleMode/Views/ConsoleHomeView.xaml.cs index 4be4433..dedf5a1 100644 --- a/src/ConsoleMode/Views/ConsoleHomeView.xaml.cs +++ b/src/ConsoleMode/Views/ConsoleHomeView.xaml.cs @@ -2,6 +2,7 @@ using System.Numerics; using Microsoft.UI.Xaml.Hosting; using ConsoleMode.Services; +using ConsoleMode.Controls; using ConsoleMode.ViewModels; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -25,6 +26,9 @@ public ConsoleHomeView() { ViewModel = App.ViewModel!; InitializeComponent(); + HomeShortcutPickerHost.Content = new ShortcutPicker(ViewModel, ShortcutSlot.Home); + MenuShortcutPickerHost.Content = new ShortcutPicker(ViewModel, ShortcutSlot.Menu); + ExitShortcutPickerHost.Content = new ShortcutPicker(ViewModel, ShortcutSlot.Exit); Loaded += OnLoaded; Unloaded += (_, _) => { _navigator?.Dispose(); _navigator = null; _clock?.Stop(); }; // Preview (tunnelling): a ScrollViewer would otherwise swallow the arrows to scroll the page. @@ -63,17 +67,30 @@ private void OnLoaded(object sender, RoutedEventArgs e) App.MainWindowInstance is { } w ? WinRT.Interop.WindowNative.GetWindowHandle(w) : 0); _navigator.SearchRoot = () => ViewModel.IsControllerTestOpen ? ControllerTestPanel + : ViewModel.IsConsoleTutorialOpen ? ConsoleTutorialPanel + : ViewModel.IsShortcutEditorOpen ? ShortcutEditorPanel : ViewModel.IsPickerOpen ? PickerPanel : ViewModel.IsConsoleSettingsOpen ? SettingsPanel : ViewModel.IsRolePanelOpen ? RolePanel : Root; _navigator.Sounds = true; _navigator.BackRequested += GoBack; + _navigator.Intercept = action => + { + if (!ViewModel.IsCapturingShortcut) return false; + return true; + }; _navigator.TabRequested += StepTab; - _navigator.MenuRequested += () => { if (ViewModel.CanStart && !ViewModel.IsRolePanelOpen && !ViewModel.IsConsoleSettingsOpen && !ViewModel.IsPickerOpen && !ViewModel.IsUpdateOpen) ViewModel.StartCommand.Execute(null); }; + _navigator.MenuRequested += () => + { + if (ViewModel.CanStart && !ViewModel.IsRolePanelOpen && !ViewModel.IsConsoleSettingsOpen + && !ViewModel.IsPickerOpen && !ViewModel.IsShortcutEditorOpen && !ViewModel.IsConsoleTutorialOpen + && !ViewModel.IsControllerTestOpen && !ViewModel.IsUpdateOpen) + ViewModel.StartCommand.Execute(null); + }; _navigator.AltRequested += () => { - if (ViewModel.IsRolePanelOpen || ViewModel.IsPickerOpen || ViewModel.IsControllerTestOpen || ViewModel.IsUpdateOpen) return; + if (ViewModel.IsRolePanelOpen || ViewModel.IsPickerOpen || ViewModel.IsShortcutEditorOpen || ViewModel.IsConsoleTutorialOpen || ViewModel.IsControllerTestOpen || ViewModel.IsUpdateOpen) return; if (ViewModel.IsConsoleSettingsOpen) ViewModel.CloseConsoleSettingsCommand.Execute(null); else ViewModel.OpenFullSettingsCommand.Execute(null); }; @@ -127,6 +144,31 @@ private void OnViewModelChanged(object? sender, PropertyChangedEventArgs e) else FocusDefault(); }); break; + case nameof(MainViewModel.IsShortcutEditorOpen): + DispatcherQueue.TryEnqueue(() => + { + if (ViewModel.IsShortcutEditorOpen) + FocusShortcutEditorTarget(); + else + { + (HomeShortcutPickerHost.Content as ShortcutPicker)?.CancelCapture(); + (MenuShortcutPickerHost.Content as ShortcutPicker)?.CancelCapture(); + (ExitShortcutPickerHost.Content as ShortcutPicker)?.CancelCapture(); + FocusShortcutEditorRow(); + } + RefreshNavigator(); + }); + break; + case nameof(MainViewModel.IsConsoleTutorialOpen): + DispatcherQueue.TryEnqueue(() => + { + if (ViewModel.IsConsoleTutorialOpen) ConsoleTutorialNextButton.Focus(FocusState.Keyboard); + else FocusDefault(); + }); + break; + case MainViewModel.ShortcutsProperty: + RefreshNavigator(); + break; case nameof(MainViewModel.ConsoleTabIndex): DispatcherQueue.TryEnqueue(() => { @@ -166,6 +208,8 @@ private void FocusDefault() if (!ViewModel.IsConsoleUi) return; if (ViewModel.IsConsoleActive) { RestoreButton.Focus(FocusState.Keyboard); return; } if (ViewModel.IsUpdateOpen) { UpdateNowButton.Focus(FocusState.Keyboard); return; } + if (ViewModel.IsConsoleTutorialOpen) { ConsoleTutorialNextButton.Focus(FocusState.Keyboard); return; } + if (ViewModel.IsShortcutEditorOpen) { FocusShortcutEditorTarget(); return; } if (ViewModel.IsSystemTab) { FirstSettingsRow.Focus(FocusState.Keyboard); return; } if (ViewModel.IsSessionTab) { (FocusManager.FindFirstFocusableElement(ScreenCards) as Control)?.Focus(FocusState.Keyboard); return; } if (ViewModel.CanStart) { PlayButton.Focus(FocusState.Keyboard); return; } @@ -235,7 +279,7 @@ private void AnimateTab(int from, int to) /// LB / RB: the previous / next tab, unless a panel is open over the page. private void StepTab(int step) { - if (ViewModel.IsConsoleActive || ViewModel.IsRolePanelOpen || ViewModel.IsPickerOpen || ViewModel.IsControllerTestOpen) return; + if (ViewModel.IsConsoleActive || ViewModel.IsRolePanelOpen || ViewModel.IsPickerOpen || ViewModel.IsShortcutEditorOpen || ViewModel.IsConsoleTutorialOpen || ViewModel.IsControllerTestOpen) return; if (ViewModel.StepConsoleTab(step)) UiSounds.Play(UiSound.Move); } @@ -243,11 +287,39 @@ private void GoBack() { if (ViewModel.IsControllerTestOpen) ViewModel.CloseControllerTestCommand.Execute(null); else if (ViewModel.IsPickerOpen) ViewModel.ClosePickerCommand.Execute(null); + else if (ViewModel.IsShortcutEditorOpen) ViewModel.CloseShortcutEditorCommand.Execute(null); + else if (ViewModel.IsConsoleTutorialOpen) ViewModel.EndTourCommand.Execute(null); else if (ViewModel.IsRolePanelOpen) ViewModel.CloseRolePanelCommand.Execute(null); else if (!ViewModel.IsHomeTab) ViewModel.SelectConsoleTabCommand.Execute("0"); else if (ViewModel.IsUpdateOpen && !ViewModel.IsUpdating) ViewModel.DismissUpdateCommand.Execute(null); } + private void FocusShortcutEditorTarget() + { + var picker = ViewModel.ShortcutEditorSlot switch + { + "home" => HomeShortcutPickerHost.Content as ShortcutPicker, + "menu" => MenuShortcutPickerHost.Content as ShortcutPicker, + "exit" => ExitShortcutPickerHost.Content as ShortcutPicker, + _ => null + }; + if (picker?.FocusCaptureButton() != true) + (FocusManager.FindFirstFocusableElement(ShortcutEditorPanel) as Control)?.Focus(FocusState.Keyboard); + } + + private void FocusShortcutEditorRow() + { + var row = ViewModel.ShortcutEditorSlot switch + { + "home" => HomeShortcutRow, + "menu" => MenuShortcutRow, + "exit" => ExitShortcutRow, + _ => FirstSettingsRow + }; + if (ViewModel.IsConsoleUi && ViewModel.IsSystemTab && row.Focus(FocusState.Keyboard)) return; + FocusDefault(); + } + private void StartClock() { _clock ??= DispatcherQueue.CreateTimer(); @@ -312,6 +384,12 @@ private void OnKeyDown(object sender, KeyRoutedEventArgs e) StepTab(1); e.Handled = true; } + else if (Root.XamlRoot is { } root && FocusManager.GetFocusedElement(root) is TextBox + && (e.Key is VirtualKey.Left or VirtualKey.Right or VirtualKey.Up or VirtualKey.Down)) + { + // Let text inputs move the caret/selection; the console menu only handles arrows elsewhere. + return; + } else if (_navigator is not null && Arrow(e.Key) is { } direction) { _navigator.Navigate(direction); From b128834ce531b0568c35713f63da55a3e937a363 Mon Sep 17 00:00:00 2001 From: Filipe Moreira Date: Wed, 30 Sep 2026 21:02:43 -0300 Subject: [PATCH 2/3] docs: describe console settings and shortcut capture --- CHANGELOG.en-US.md | 1 + CHANGELOG.md | 1 + docs/GUIDE.md | 6 ++++++ docs/GUIDE.pt-BR.md | 6 ++++++ 4 files changed, 14 insertions(+) diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 4bf1084..fd75bdb 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 +- Console interface: the System tab now supports custom controller shortcuts and desktop settings, including Playnite, Android TV, custom FPS, closing Steam, the tutorial and the data folder. (#128) - 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) diff --git a/CHANGELOG.md b/CHANGELOG.md index f111a83..64d6919 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 +- Interface Console: a aba Sistema agora permite configurar atalhos personalizados e acessar os ajustes do modo PC, incluindo Playnite, Android TV, FPS personalizado, fechar a Steam, tutorial e pasta de dados. (#128) - 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) diff --git a/docs/GUIDE.md b/docs/GUIDE.md index 52fd7a6..d56a51e 100644 --- a/docs/GUIDE.md +++ b/docs/GUIDE.md @@ -12,6 +12,12 @@ Details that don't fit in the [README](../README.md). 🇧🇷 [Guia em portugu You can also restore anytime from the tray (*Restore setup* / *Show window*). With black overlays, **ESC** dismisses the curtains. +## Settings in Console mode + +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. + ## 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 1fdeae2..40076b0 100644 --- a/docs/GUIDE.pt-BR.md +++ b/docs/GUIDE.pt-BR.md @@ -12,6 +12,12 @@ Detalhes que não cabem no [README](../README.pt-BR.md). 🇺🇸 [Guide in Engl Também dá para restaurar a qualquer momento pela bandeja (*Restaurar setup* / *Mostrar janela*). Com cortinas pretas, **ESC** remove o overlay. +## Configurações no modo Console + +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. + ## 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. From c98435c10eb23091a0ddb0072a8232cabd7793b7 Mon Sep 17 00:00:00 2001 From: Filipe Moreira Date: Wed, 30 Sep 2026 21:09:04 -0300 Subject: [PATCH 3/3] fix: allow controller to cancel shortcut capture --- docs/TESTING.md | 2 +- src/ConsoleMode/Controls/ShortcutPicker.cs | 13 +++++++++++++ src/ConsoleMode/Resources/Strings.en-US.json | 1 + src/ConsoleMode/Resources/Strings.es-ES.json | 1 + src/ConsoleMode/Resources/Strings.pt-BR.json | 1 + 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index b512712..5ccaa57 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -261,7 +261,7 @@ Pré-condições: TV Google TV / Android TV na mesma rede do PC, com **Depuraç - [ ] Em Sistema, as configurações do modo PC também estão disponíveis: caminho do Playnite, fechar a Steam, Android TV, FPS personalizado, atalhos, diagnóstico do controle, tutorial e pasta de dados. Resultado: ______ - [ ] Configurar, alterar e remover os atalhos de abrir o app, menu da sessão e voltar ao PC; uma combinação em conflito mostra o mesmo erro do modo PC. Durante a captura, os botões não navegam nem iniciam uma sessão. Resultado: ______ -- [ ] Cancelar a captura e sair do editor devolve o foco à configuração; teclado, mouse e controle conseguem abrir e fechar o editor sem acionar opções atrás dele. Resultado: ______ +- [ ] Na captura, apertar e soltar B/○ sozinho cancela sem alterar o atalho; B/○ de novo fecha o editor e devolve o foco à configuração. B/○ junto com outro botão ainda pode ser salvo como combinação, mas B/○ sozinho não é um atalho válido. Teclado e mouse também conseguem abrir e fechar o editor sem acionar opções atrás dele. Resultado: ______ - [ ] Alterar uma configuração no modo Console, trocar para PC e reabrir o app: o valor permanece igual nas duas interfaces. Resultado: ______ - [ ] Selecionar e limpar o caminho do Playnite; cancelar o seletor mantém o valor anterior. Resultado: ______ - [ ] Configurar a Android TV no modo Console: endereço, MAC opcional, entrada HDMI, comando da entrada e espera ao restaurar; Testar agora mostra o resultado e libera o botão após erro ou timeout. Resultado: ______ diff --git a/src/ConsoleMode/Controls/ShortcutPicker.cs b/src/ConsoleMode/Controls/ShortcutPicker.cs index 0797694..3f6effe 100644 --- a/src/ConsoleMode/Controls/ShortcutPicker.cs +++ b/src/ConsoleMode/Controls/ShortcutPicker.cs @@ -26,6 +26,13 @@ public sealed class ShortcutPicker : StackPanel Foreground = new SolidColorBrush(Color.FromArgb(255, 232, 88, 88)), Visibility = Visibility.Collapsed }; + private readonly TextBlock _captureHint = new() + { + TextWrapping = TextWrapping.Wrap, + MaxWidth = 460, + Opacity = 0.7, + Visibility = Visibility.Collapsed + }; private readonly Button _set = new(); private readonly Button _clear = new(); private readonly DispatcherQueueTimer _timer; @@ -45,6 +52,7 @@ public ShortcutPicker(MainViewModel vm, ShortcutSlot slot, bool stacked = false) row.Children.Add(_set); row.Children.Add(_clear); Children.Add(row); + Children.Add(_captureHint); Children.Add(_error); _timer = DispatcherQueue.GetForCurrentThread().CreateTimer(); @@ -77,6 +85,8 @@ private void Refresh() _clear.Content = LocalizationService.Get("ShortcutClear"); _clear.Visibility = isSet && _capture is null ? Visibility.Visible : Visibility.Collapsed; _value.Opacity = isSet || _capture is not null ? 1 : 0.6; + _captureHint.Text = LocalizationService.Get("ShortcutCaptureHint", _vm.HintBack); + _captureHint.Visibility = _capture is null ? Visibility.Collapsed : Visibility.Visible; } private void StartCapture() @@ -116,6 +126,9 @@ private void Poll() if (captured is not { } mask) return; StopCapture(); + // A lone B/Circle is never a valid shortcut, so use its release to cancel capture. + // Combinations containing B still reach TrySetShortcut unchanged. + if (mask == ControllerShortcuts.B) return; var error = _vm.TrySetShortcut(_slot, mask); _error.Text = error ?? ""; _error.Visibility = error is null ? Visibility.Collapsed : Visibility.Visible; diff --git a/src/ConsoleMode/Resources/Strings.en-US.json b/src/ConsoleMode/Resources/Strings.en-US.json index a1f8075..1722536 100644 --- a/src/ConsoleMode/Resources/Strings.en-US.json +++ b/src/ConsoleMode/Resources/Strings.en-US.json @@ -115,6 +115,7 @@ "ShortcutClear": "Remove", "ShortcutCancel": "Cancel", "ShortcutPressButtons": "Hold the buttons, then let go…", + "ShortcutCaptureHint": "Release to save. Press {0} alone to cancel.", "ShortcutNotAllowed": "Use two or more buttons together (like Select + Y), or the Home button alone. A single button would fire in the middle of a game.", "ShortcutInUse": "{0} is already the shortcut for \"{1}\". Pick a different combination.", "ShortcutsOnboardingTitle": "Set up your controller shortcuts", diff --git a/src/ConsoleMode/Resources/Strings.es-ES.json b/src/ConsoleMode/Resources/Strings.es-ES.json index 875882a..2010049 100644 --- a/src/ConsoleMode/Resources/Strings.es-ES.json +++ b/src/ConsoleMode/Resources/Strings.es-ES.json @@ -115,6 +115,7 @@ "ShortcutClear": "Quitar", "ShortcutCancel": "Cancelar", "ShortcutPressButtons": "Mantén los botones y luego suéltalos…", + "ShortcutCaptureHint": "Suelta para guardar. Pulsa {0} solo para cancelar.", "ShortcutNotAllowed": "Usa dos o más botones a la vez (como Select + Y), o solo el botón Home. Un botón suelto se activaría en mitad del juego.", "ShortcutInUse": "{0} ya es el atajo de \"{1}\". Elige otra combinación.", "ShortcutsOnboardingTitle": "Configura los atajos del mando", diff --git a/src/ConsoleMode/Resources/Strings.pt-BR.json b/src/ConsoleMode/Resources/Strings.pt-BR.json index 1dcf2c5..ed7721a 100644 --- a/src/ConsoleMode/Resources/Strings.pt-BR.json +++ b/src/ConsoleMode/Resources/Strings.pt-BR.json @@ -115,6 +115,7 @@ "ShortcutClear": "Remover", "ShortcutCancel": "Cancelar", "ShortcutPressButtons": "Segure os botões e depois solte…", + "ShortcutCaptureHint": "Solte para salvar. Pressione {0} sozinho para cancelar.", "ShortcutNotAllowed": "Use dois ou mais botões juntos (como Select + Y), ou só o botão Home. Um botão sozinho dispararia no meio do jogo.", "ShortcutInUse": "{0} já é o atalho de \"{1}\". Escolha outra combinação.", "ShortcutsOnboardingTitle": "Configure os atalhos do controle",