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