diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs index 9f08fefe0f..82b4b7d59e 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs @@ -68,6 +68,8 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext // `IsLoading` property as a combo of this value and `IsInitialized` public bool ModelIsLoading { get; protected set; } = true; + public bool HasSearchBox { get; protected set; } + public IconInfoViewModel Icon { get; protected set; } public PageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost extensionHost) @@ -144,12 +146,15 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext Icon = new(page.Icon); Icon.InitializeProperties(); + HasSearchBox = page is IListPage; + // Let the UI know about our initial properties too. UpdateProperty(nameof(Name)); UpdateProperty(nameof(Title)); UpdateProperty(nameof(ModelIsLoading)); UpdateProperty(nameof(IsLoading)); UpdateProperty(nameof(Icon)); + UpdateProperty(nameof(HasSearchBox)); page.PropChanged += Model_PropChanged; } diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs index da02f23001..bfda24f7ea 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs @@ -32,6 +32,9 @@ public partial class ShellViewModel : ObservableObject, [ObservableProperty] public partial bool IsDetailsVisible { get; set; } + [ObservableProperty] + public partial bool IsSearchBoxVisible { get; set; } = true; + private PageViewModel _currentPage; public PageViewModel CurrentPage diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs index 42969f11b8..bd767ba0f1 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs @@ -11,6 +11,7 @@ using Microsoft.CmdPal.Core.Common.Helpers; using Microsoft.CmdPal.Core.ViewModels.Messages; using Microsoft.CmdPal.Ext.Apps; using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Properties; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.Extensions.DependencyInjection; @@ -46,6 +47,7 @@ public partial class MainListPage : DynamicListPage, public MainListPage(IServiceProvider serviceProvider) { + Title = Resources.builtin_home_name; Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png"); PlaceholderText = Properties.Resources.builtin_main_list_page_searchbar_placeholder; _serviceProvider = serviceProvider; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs index 6924e48d75..e3a4e31088 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs @@ -285,6 +285,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties { } } + /// + /// Looks up a localized string similar to Home. + /// + public static string builtin_home_name { + get { + return ResourceManager.GetString("builtin_home_name", resourceCulture); + } + } + /// /// Looks up a localized string similar to View log folder. /// diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx index a69fa5b00e..6279a0bfdc 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx @@ -233,4 +233,7 @@ Search for apps, files and commands... + + Home + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs index a217333b4b..5337a126c0 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs @@ -122,31 +122,11 @@ public sealed partial class SearchBar : UserControl, return; } - var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down); - var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down); - var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down); - var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) || - InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down); - if (ctrlPressed && e.Key == VirtualKey.Enter) - { - // ctrl+enter - WeakReferenceMessenger.Default.Send(); - e.Handled = true; - } - else if (e.Key == VirtualKey.Enter) - { - WeakReferenceMessenger.Default.Send(); - e.Handled = true; - } - else if (ctrlPressed && e.Key == VirtualKey.K) - { - // ctrl+k - WeakReferenceMessenger.Default.Send(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom)); - e.Handled = true; - } - else if (ctrlPressed && e.Key == VirtualKey.I) + var ctrlPressed = (InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control) & CoreVirtualKeyStates.Down) == CoreVirtualKeyStates.Down; + if (ctrlPressed && e.Key == VirtualKey.I) { // Today you learned that Ctrl+I in a TextBox will insert a tab + // We don't want that, so we'll suppress it, this way it can be used for other purposes e.Handled = true; } else if (e.Key == VirtualKey.Escape) @@ -280,22 +260,6 @@ public sealed partial class SearchBar : UserControl, _inSuggestion = false; _lastText = null; } - - if (!e.Handled) - { - var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down); - var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down); - var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down); - var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) || - InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down); - - // The CommandBar is responsible for handling all the item keybindings, - // since the bound context item may need to then show another - // context menu - TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key); - WeakReferenceMessenger.Default.Send(msg); - e.Handled = msg.Handled; - } } private void FilterBox_PreviewKeyUp(object sender, KeyRoutedEventArgs e) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs index 2955277466..862bf1726f 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs @@ -30,7 +30,7 @@ public sealed partial class ListPage : Page, { private InputSource _lastInputSource; - private ListViewModel? ViewModel + internal ListViewModel? ViewModel { get => (ListViewModel?)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs new file mode 100644 index 0000000000..66744b4c99 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.UI.Helpers; + +internal static class BindTransformers +{ + public static bool Negate(bool value) => !value; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml index dd7028a997..36b1160ffa 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml @@ -9,6 +9,7 @@ xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels" xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:h="using:Microsoft.CmdPal.UI.Helpers" xmlns:help="using:Microsoft.CmdPal.UI.Helpers" xmlns:labToolkit="using:CommunityToolkit.Labs.WinUI.MarkdownTextBlock" xmlns:markdownImageProviders="using:Microsoft.CmdPal.UI.Helpers.MarkdownImageProviders" @@ -16,7 +17,6 @@ xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls" xmlns:toolkit="using:CommunityToolkit.WinUI.Controls" xmlns:ui="using:CommunityToolkit.WinUI" - xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels" Background="Transparent" mc:Ignorable="d"> @@ -27,8 +27,6 @@ EmptyValue="Collapsed" NotEmptyValue="Visible" /> - - + VerticalAlignment="Stretch" + BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}" + BorderThickness="0,0,0,1"> @@ -354,11 +355,7 @@ - + @@ -367,6 +364,9 @@ @@ -502,7 +502,11 @@ - + @@ -519,6 +523,26 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs index 3b27ea8480..7d13532605 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs @@ -3,12 +3,15 @@ // See the LICENSE file in the project root for more information. using System.ComponentModel; +using System.Globalization; +using System.Text; using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.WinUI; using ManagedCommon; using Microsoft.CmdPal.Core.ViewModels; using Microsoft.CmdPal.Core.ViewModels.Messages; using Microsoft.CmdPal.UI.Events; +using Microsoft.CmdPal.UI.Helpers; using Microsoft.CmdPal.UI.Messages; using Microsoft.CmdPal.UI.Settings; using Microsoft.CmdPal.UI.ViewModels; @@ -17,9 +20,12 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Dispatching; using Microsoft.UI.Input; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation.Peers; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media.Animation; +using Windows.UI.Core; using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue; using VirtualKey = Windows.System.VirtualKey; @@ -55,6 +61,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, private readonly ToastWindow _toast = new(); + private readonly CompositeFormat _pageNavigatedAnnouncement; + private SettingsWindow? _settingsWindow; public ShellViewModel ViewModel { get; private set; } = App.Current.Services.GetService()!; @@ -84,9 +92,13 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, WeakReferenceMessenger.Default.Register(this); AddHandler(PreviewKeyDownEvent, new KeyEventHandler(ShellPage_OnPreviewKeyDown), true); + AddHandler(KeyDownEvent, new KeyEventHandler(ShellPage_OnKeyDown), false); AddHandler(PointerPressedEvent, new PointerEventHandler(ShellPage_OnPointerPressed), true); RootFrame.Navigate(typeof(LoadingPage), ViewModel); + + var pageAnnouncementFormat = ResourceLoaderInstance.GetString("ScreenReader_Announcement_NavigatedToPage0"); + _pageNavigatedAnnouncement = CompositeFormat.Parse(pageAnnouncementFormat); } public void Receive(NavigateBackMessage message) @@ -134,9 +146,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth)); - // Refocus on the Search for continual typing on the next search request - SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic); - if (!ViewModel.IsNested) { // todo BODGY @@ -441,6 +450,78 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, // We just need to reconcile our loading systems a bit more in the future. ViewModel.CurrentPage = page; } + + if (e.Content is Page element) + { + element.Loaded += FocusAfterLoaded; + } + } + + private void FocusAfterLoaded(object sender, RoutedEventArgs e) + { + var page = (Page)sender; + page.Loaded -= FocusAfterLoaded; + + AnnounceNavigationToPage(page); + + var shouldSearchBoxBeVisible = ViewModel.CurrentPage?.HasSearchBox ?? false; + + if (shouldSearchBoxBeVisible || page is not ContentPage) + { + ViewModel.IsSearchBoxVisible = shouldSearchBoxBeVisible; + SearchBox.Focus(FocusState.Programmatic); + SearchBox.SelectSearch(); + } + else + { + _ = Task.Run(async () => + { + await page.DispatcherQueue.EnqueueAsync(async () => + { + // I hate this so much, but it can take a while for the page to be ready to accept focus; + // focusing page with MarkdownTextBlock takes up to 5 attempts (* 100ms delay between attempts) + for (var i = 0; i < 10; i++) + { + if (FocusManager.FindFirstFocusableElement(page) is FrameworkElement frameworkElement) + { + var set = frameworkElement.Focus(FocusState.Programmatic); + if (set) + { + break; + } + } + + await Task.Delay(100); + } + + // Update the search box visibility based on the current page: + // - We do this here after navigation so the focus is not jumping around too much, + // it messes with screen readers if we do it too early + // - Since this should hide the search box on content pages, it's not a problem if we + // wait for the code above to finish trying to focus the content + ViewModel.IsSearchBoxVisible = ViewModel.CurrentPage?.HasSearchBox ?? false; + }); + }); + } + } + + private void AnnounceNavigationToPage(Page page) + { + var pageTitle = page switch + { + ListPage listPage => listPage.ViewModel?.Title, + ContentPage contentPage => contentPage.ViewModel?.Title, + _ => null, + }; + + if (string.IsNullOrEmpty(pageTitle)) + { + pageTitle = ResourceLoaderInstance.GetString("UntitledPageTitle"); + } + + var announcement = string.Format(CultureInfo.CurrentCulture, _pageNavigatedAnnouncement.Format, pageTitle); + + UIHelper.AnnounceActionForAccessibility(RootFrame, announcement, "CommandPalettePageNavigatedTo"); } /// @@ -466,11 +547,54 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, } } - private void ShellPage_OnPreviewKeyDown(object sender, KeyRoutedEventArgs e) + private static void ShellPage_OnPreviewKeyDown(object sender, KeyRoutedEventArgs e) { if (e.Key == VirtualKey.Left && e.KeyStatus.IsMenuKeyDown) { WeakReferenceMessenger.Default.Send(new()); + e.Handled = true; + } + else + { + var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down); + var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down); + var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down); + var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) || + InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down); + + // The CommandBar is responsible for handling all the item keybindings, + // since the bound context item may need to then show another + // context menu + TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key); + WeakReferenceMessenger.Default.Send(msg); + e.Handled = msg.Handled; + } + } + + private static void ShellPage_OnKeyDown(object sender, KeyRoutedEventArgs e) + { + var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down); + if (ctrlPressed && e.Key == VirtualKey.Enter) + { + // ctrl+enter + WeakReferenceMessenger.Default.Send(); + e.Handled = true; + } + else if (e.Key == VirtualKey.Enter) + { + WeakReferenceMessenger.Default.Send(); + e.Handled = true; + } + else if (ctrlPressed && e.Key == VirtualKey.K) + { + // ctrl+k + WeakReferenceMessenger.Default.Send(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom)); + e.Handled = true; + } + else if (e.Key == VirtualKey.Escape) + { + WeakReferenceMessenger.Default.Send(new()); + e.Handled = true; } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw index 0a61f66653..263b3e97dd 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw @@ -450,4 +450,10 @@ Right-click to remove the key combination, thereby deactivating the shortcut. For Developers + + an untitled + + + Navigated to {0} page + \ No newline at end of file