/ src / modules / cmdpal / Microsoft.CmdPal.UI / Pages / ShellPage.xaml.cs
ShellPage.xaml.cs
  1  // Copyright (c) Microsoft Corporation
  2  // The Microsoft Corporation licenses this file to you under the MIT license.
  3  // See the LICENSE file in the project root for more information.
  4  
  5  using System.ComponentModel;
  6  using System.Globalization;
  7  using System.Text;
  8  using CommunityToolkit.Mvvm.Messaging;
  9  using CommunityToolkit.WinUI;
 10  using ManagedCommon;
 11  using Microsoft.CmdPal.Core.ViewModels;
 12  using Microsoft.CmdPal.Core.ViewModels.Messages;
 13  using Microsoft.CmdPal.UI.Events;
 14  using Microsoft.CmdPal.UI.Helpers;
 15  using Microsoft.CmdPal.UI.Messages;
 16  using Microsoft.CmdPal.UI.Settings;
 17  using Microsoft.CmdPal.UI.ViewModels;
 18  using Microsoft.CommandPalette.Extensions;
 19  using Microsoft.Extensions.DependencyInjection;
 20  using Microsoft.PowerToys.Telemetry;
 21  using Microsoft.UI.Dispatching;
 22  using Microsoft.UI.Input;
 23  using Microsoft.UI.Xaml;
 24  using Microsoft.UI.Xaml.Controls;
 25  using Microsoft.UI.Xaml.Input;
 26  using Microsoft.UI.Xaml.Media.Animation;
 27  using Windows.UI.Core;
 28  using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
 29  using VirtualKey = Windows.System.VirtualKey;
 30  
 31  namespace Microsoft.CmdPal.UI.Pages;
 32  
 33  /// <summary>
 34  /// An empty page that can be used on its own or navigated to within a Frame.
 35  /// </summary>
 36  public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
 37      IRecipient<NavigateBackMessage>,
 38      IRecipient<OpenSettingsMessage>,
 39      IRecipient<HotkeySummonMessage>,
 40      IRecipient<ShowDetailsMessage>,
 41      IRecipient<HideDetailsMessage>,
 42      IRecipient<ClearSearchMessage>,
 43      IRecipient<LaunchUriMessage>,
 44      IRecipient<SettingsWindowClosedMessage>,
 45      IRecipient<GoHomeMessage>,
 46      IRecipient<GoBackMessage>,
 47      IRecipient<ShowConfirmationMessage>,
 48      IRecipient<ShowToastMessage>,
 49      IRecipient<NavigateToPageMessage>,
 50      INotifyPropertyChanged,
 51      IDisposable
 52  {
 53      private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
 54  
 55      private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
 56  
 57      private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
 58  
 59      private readonly SlideNavigationTransitionInfo _slideRightTransition = new() { Effect = SlideNavigationTransitionEffect.FromRight };
 60      private readonly SuppressNavigationTransitionInfo _noAnimation = new();
 61  
 62      private readonly ToastWindow _toast = new();
 63  
 64      private readonly CompositeFormat _pageNavigatedAnnouncement;
 65  
 66      private SettingsWindow? _settingsWindow;
 67  
 68      private CancellationTokenSource? _focusAfterLoadedCts;
 69      private WeakReference<Page>? _lastNavigatedPageRef;
 70  
 71      public ShellViewModel ViewModel { get; private set; } = App.Current.Services.GetService<ShellViewModel>()!;
 72  
 73      public event PropertyChangedEventHandler? PropertyChanged;
 74  
 75      public ShellPage()
 76      {
 77          this.InitializeComponent();
 78  
 79          // how we are doing navigation around
 80          WeakReferenceMessenger.Default.Register<NavigateBackMessage>(this);
 81          WeakReferenceMessenger.Default.Register<OpenSettingsMessage>(this);
 82          WeakReferenceMessenger.Default.Register<HotkeySummonMessage>(this);
 83          WeakReferenceMessenger.Default.Register<SettingsWindowClosedMessage>(this);
 84  
 85          WeakReferenceMessenger.Default.Register<ShowDetailsMessage>(this);
 86          WeakReferenceMessenger.Default.Register<HideDetailsMessage>(this);
 87  
 88          WeakReferenceMessenger.Default.Register<ClearSearchMessage>(this);
 89          WeakReferenceMessenger.Default.Register<LaunchUriMessage>(this);
 90  
 91          WeakReferenceMessenger.Default.Register<GoHomeMessage>(this);
 92          WeakReferenceMessenger.Default.Register<GoBackMessage>(this);
 93          WeakReferenceMessenger.Default.Register<ShowConfirmationMessage>(this);
 94          WeakReferenceMessenger.Default.Register<ShowToastMessage>(this);
 95          WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
 96  
 97          AddHandler(PreviewKeyDownEvent, new KeyEventHandler(ShellPage_OnPreviewKeyDown), true);
 98          AddHandler(KeyDownEvent, new KeyEventHandler(ShellPage_OnKeyDown), false);
 99          AddHandler(PointerPressedEvent, new PointerEventHandler(ShellPage_OnPointerPressed), true);
100  
101          RootFrame.Navigate(typeof(LoadingPage), new AsyncNavigationRequest(ViewModel, CancellationToken.None));
102  
103          var pageAnnouncementFormat = ResourceLoaderInstance.GetString("ScreenReader_Announcement_NavigatedToPage0");
104          _pageNavigatedAnnouncement = CompositeFormat.Parse(pageAnnouncementFormat);
105      }
106  
107      /// <summary>
108      /// Gets the default page animation, depending on the settings
109      /// </summary>
110      private NavigationTransitionInfo DefaultPageAnimation
111      {
112          get
113          {
114              var settings = App.Current.Services.GetService<SettingsModel>()!;
115              return settings.DisableAnimations ? _noAnimation : _slideRightTransition;
116          }
117      }
118  
119      public void Receive(NavigateBackMessage message)
120      {
121          var settings = App.Current.Services.GetService<SettingsModel>()!;
122  
123          if (RootFrame.CanGoBack)
124          {
125              if (!message.FromBackspace ||
126                  settings.BackspaceGoesBack)
127              {
128                  GoBack();
129              }
130          }
131          else
132          {
133              if (!message.FromBackspace)
134              {
135                  // If we can't go back then we must be at the top and thus escape again should quit.
136                  WeakReferenceMessenger.Default.Send(new DismissMessage());
137  
138                  PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnEsc());
139              }
140          }
141      }
142  
143      public void Receive(NavigateToPageMessage message)
144      {
145          // TODO GH #526 This needs more better locking too
146          _ = _queue.TryEnqueue(() =>
147          {
148              // Also hide our details pane about here, if we had one
149              HideDetails();
150  
151              // Navigate to the appropriate host page for that VM
152              RootFrame.Navigate(
153                  message.Page switch
154                  {
155                      ListViewModel => typeof(ListPage),
156                      ContentPageViewModel => typeof(ContentPage),
157                      _ => throw new NotSupportedException(),
158                  },
159                  new AsyncNavigationRequest(message.Page, message.CancellationToken),
160                  message.WithAnimation ? DefaultPageAnimation : _noAnimation);
161  
162              PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth, message.Page.Id));
163  
164              // Telemetry: Send navigation depth for session max depth tracking
165              WeakReferenceMessenger.Default.Send(new NavigationDepthMessage(RootFrame.BackStackDepth));
166  
167              if (!ViewModel.IsNested)
168              {
169                  // todo BODGY
170                  RootFrame.BackStack.Clear();
171              }
172          });
173      }
174  
175      public void Receive(ShowConfirmationMessage message)
176      {
177          DispatcherQueue.TryEnqueue(async () =>
178          {
179              try
180              {
181                  await HandleConfirmArgsOnUiThread(message.Args);
182              }
183              catch (Exception ex)
184              {
185                  Logger.LogError(ex.ToString());
186              }
187          });
188      }
189  
190      public void Receive(ShowToastMessage message)
191      {
192          DispatcherQueue.TryEnqueue(() =>
193          {
194              _toast.ShowToast(message.Message);
195          });
196      }
197  
198      // This gets called from the UI thread
199      private async Task HandleConfirmArgsOnUiThread(IConfirmationArgs? args)
200      {
201          if (args is null)
202          {
203              return;
204          }
205  
206          ConfirmResultViewModel vm = new(args, new(ViewModel.CurrentPage));
207          var initializeDialogTask = Task.Run(() => { InitializeConfirmationDialog(vm); });
208          await initializeDialogTask;
209  
210          var resourceLoader = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance.ResourceLoader;
211          var confirmText = resourceLoader.GetString("ConfirmationDialog_ConfirmButtonText");
212          var cancelText = resourceLoader.GetString("ConfirmationDialog_CancelButtonText");
213  
214          var name = string.IsNullOrEmpty(vm.PrimaryCommand.Name) ? confirmText : vm.PrimaryCommand.Name;
215          ContentDialog dialog = new()
216          {
217              Title = vm.Title,
218              Content = vm.Description,
219              PrimaryButtonText = name,
220              CloseButtonText = cancelText,
221              XamlRoot = this.XamlRoot,
222          };
223  
224          if (vm.IsPrimaryCommandCritical)
225          {
226              dialog.DefaultButton = ContentDialogButton.Close;
227  
228              // TODO: Maybe we need to style the primary button to be red?
229              // dialog.PrimaryButtonStyle = new Style(typeof(Button))
230              // {
231              //     Setters =
232              //     {
233              //         new Setter(Button.ForegroundProperty, new SolidColorBrush(Colors.Red)),
234              //         new Setter(Button.BackgroundProperty, new SolidColorBrush(Colors.Red)),
235              //     },
236              // };
237          }
238  
239          var result = await dialog.ShowAsync();
240          if (result == ContentDialogResult.Primary)
241          {
242              var performMessage = new PerformCommandMessage(vm);
243              WeakReferenceMessenger.Default.Send(performMessage);
244          }
245          else
246          {
247              // cancel
248          }
249      }
250  
251      private void InitializeConfirmationDialog(ConfirmResultViewModel vm)
252      {
253          vm.SafeInitializePropertiesSynchronous();
254      }
255  
256      public void Receive(OpenSettingsMessage message)
257      {
258          _ = DispatcherQueue.TryEnqueue(() =>
259          {
260              OpenSettings(message.SettingsPageTag);
261          });
262      }
263  
264      public void OpenSettings(string pageTag)
265      {
266          if (_settingsWindow is null)
267          {
268              _settingsWindow = new SettingsWindow();
269          }
270  
271          _settingsWindow.Activate();
272          _settingsWindow.BringToFront();
273          _settingsWindow.Navigate(pageTag);
274      }
275  
276      public void Receive(ShowDetailsMessage message)
277      {
278          if (ViewModel is not null &&
279              ViewModel.CurrentPage is not null)
280          {
281              if (ViewModel.CurrentPage.PageContext.TryGetTarget(out var pageContext))
282              {
283                  Task.Factory.StartNew(
284                      () =>
285                      {
286                          // TERRIBLE HACK TODO GH #245
287                          // There's weird wacky bugs with debounce currently.
288                          if (!ViewModel.IsDetailsVisible)
289                          {
290                              ViewModel.Details = message.Details;
291                              PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasHeroImage)));
292                              ViewModel.IsDetailsVisible = true;
293                              return;
294                          }
295  
296                          // GH #322:
297                          // For inexplicable reasons, if you try to change the details too fast,
298                          // we'll explode. This seemingly only happens if you change the details
299                          // while we're also scrolling a new list view item into view.
300                          _debounceTimer.Debounce(
301                              () =>
302                              {
303                                  ViewModel.Details = message.Details;
304  
305                                  // Trigger a re-evaluation of whether we have a hero image based on
306                                  // the current theme
307                                  PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasHeroImage)));
308                              },
309                              interval: TimeSpan.FromMilliseconds(50),
310                              immediate: ViewModel.IsDetailsVisible == false);
311                          ViewModel.IsDetailsVisible = true;
312                      },
313                      CancellationToken.None,
314                      TaskCreationOptions.None,
315                      pageContext.Scheduler);
316              }
317          }
318      }
319  
320      public void Receive(HideDetailsMessage message) => HideDetails();
321  
322      public void Receive(LaunchUriMessage message) => _ = global::Windows.System.Launcher.LaunchUriAsync(message.Uri);
323  
324      private void HideDetails()
325      {
326          ViewModel.Details = null;
327          ViewModel.IsDetailsVisible = false;
328      }
329  
330      public void Receive(ClearSearchMessage message) => SearchBox.ClearSearch();
331  
332      public void Receive(HotkeySummonMessage message)
333      {
334          _ = DispatcherQueue.TryEnqueue(() => SummonOnUiThread(message));
335      }
336  
337      public void Receive(SettingsWindowClosedMessage message) => _settingsWindow = null;
338  
339      private void SummonOnUiThread(HotkeySummonMessage message)
340      {
341          var settings = App.Current.Services.GetService<SettingsModel>()!;
342          var commandId = message.CommandId;
343          var isRoot = string.IsNullOrEmpty(commandId);
344          if (isRoot)
345          {
346              // If this is the hotkey for the root level, then always show us
347              WeakReferenceMessenger.Default.Send<ShowWindowMessage>(new(message.Hwnd));
348  
349              // Depending on the settings, either
350              // * Go home, or
351              // * Select the search text (if we should remain open on this page)
352              if (settings.AutoGoHomeInterval == TimeSpan.Zero)
353              {
354                  GoHome(false);
355              }
356              else if (settings.HighlightSearchOnActivate)
357              {
358                  SearchBox.SelectSearch();
359              }
360          }
361          else
362          {
363              try
364              {
365                  // For a hotkey bound to a command, first lookup the
366                  // command from our list of toplevel commands.
367                  var tlcManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
368                  var topLevelCommand = tlcManager.LookupCommand(commandId);
369                  if (topLevelCommand is not null)
370                  {
371                      var command = topLevelCommand.CommandViewModel.Model.Unsafe;
372                      var isPage = command is not IInvokableCommand;
373  
374                      // If the bound command is an invokable command, then
375                      // we don't want to open the window at all - we want to
376                      // just do it.
377                      if (isPage)
378                      {
379                          // If we're here, then the bound command was a page
380                          // of some kind. Let's pop the stack, show the window, and navigate to it.
381                          GoHome(false);
382  
383                          WeakReferenceMessenger.Default.Send<ShowWindowMessage>(new(message.Hwnd));
384                      }
385  
386                      var msg = topLevelCommand.GetPerformCommandMessage();
387                      msg.WithAnimation = false;
388                      WeakReferenceMessenger.Default.Send<PerformCommandMessage>(msg);
389  
390                      // we can't necessarily SelectSearch() here, because when the page is loaded,
391                      // we'll fetch the SearchText from the page itself, and that'll stomp the
392                      // selection we start now.
393                      // That's probably okay though.
394                  }
395              }
396              catch
397              {
398              }
399          }
400  
401          WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
402      }
403  
404      public void Receive(GoBackMessage message)
405      {
406          _ = DispatcherQueue.TryEnqueue(() => GoBack(message.WithAnimation, message.FocusSearch));
407      }
408  
409      private void GoBack(bool withAnimation = true, bool focusSearch = true)
410      {
411          HideDetails();
412  
413          ViewModel.CancelNavigation();
414  
415          // Note: That we restore the VM state below in RootFrame_Navigated call back after this occurs.
416          // In the future, we may want to manage the back stack ourselves vs. relying on Frame
417          // We could replace Frame with a ContentPresenter, but then have to manage transition animations ourselves.
418          // However, then we have more fine-grained control on the back stack, managing the VM cache, and not
419          // having that all be a black box, though then we wouldn't cache the XAML page itself, but sometimes that is a drawback.
420          // However, we do a good job here, see ForwardStack.Clear below, and BackStack.Clear above about managing that.
421          if (withAnimation)
422          {
423              RootFrame.GoBack();
424          }
425          else
426          {
427              RootFrame.GoBack(_noAnimation);
428          }
429  
430          // Don't store pages we're navigating away from in the Frame cache
431          // TODO: In the future we probably want a short cache (3-5?) of recent VMs in case the user re-navigates
432          // back to a recent page they visited (like the Pokedex) so we don't have to reload it from  scratch.
433          // That'd be retrieved as we re-navigate in the PerformCommandMessage logic above
434          RootFrame.ForwardStack.Clear();
435  
436          if (!RootFrame.CanGoBack)
437          {
438              ViewModel.GoHome();
439          }
440  
441          if (focusSearch)
442          {
443              SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
444              SearchBox.SelectSearch();
445          }
446      }
447  
448      public void Receive(GoHomeMessage message)
449      {
450          _ = DispatcherQueue.TryEnqueue(() => GoHome(withAnimation: message.WithAnimation, focusSearch: message.FocusSearch));
451      }
452  
453      private void GoHome(bool withAnimation = true, bool focusSearch = true)
454      {
455          while (RootFrame.CanGoBack)
456          {
457              // don't focus on each step, just at the end
458              GoBack(withAnimation, focusSearch: false);
459          }
460  
461          // focus search box, even if we were already home
462          if (focusSearch)
463          {
464              SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
465              SearchBox.SelectSearch();
466          }
467      }
468  
469      private void BackButton_Clicked(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) => WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
470  
471      private void RootFrame_Navigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
472      {
473          // This listens to the root frame to ensure that we also track the content's page VM as well that we passed as a parameter.
474          // This is currently used for both forward and backward navigation.
475          // As when we go back that we restore ourselves to the proper state within our VM
476          if (e.Parameter is AsyncNavigationRequest request)
477          {
478              if (request.NavigationToken.IsCancellationRequested && e.NavigationMode is not (Microsoft.UI.Xaml.Navigation.NavigationMode.Back or Microsoft.UI.Xaml.Navigation.NavigationMode.Forward))
479              {
480                  return;
481              }
482  
483              switch (request.TargetViewModel)
484              {
485                  case PageViewModel pageViewModel:
486                      ViewModel.CurrentPage = pageViewModel;
487                      break;
488                  case ShellViewModel:
489                      // This one is an exception, for now (LoadingPage is tied to ShellViewModel,
490                      // but ShellViewModel is not PageViewModel.
491                      ViewModel.CurrentPage = ViewModel.NullPage;
492                      break;
493                  default:
494                      ViewModel.CurrentPage = ViewModel.NullPage;
495                      Logger.LogWarning($"Invalid navigation target: AsyncNavigationRequest.{nameof(AsyncNavigationRequest.TargetViewModel)} must be {nameof(PageViewModel)}");
496                      break;
497              }
498          }
499          else
500          {
501              Logger.LogWarning("Unrecognized target for shell navigation: " + e.Parameter);
502          }
503  
504          if (e.Content is Page element)
505          {
506              _lastNavigatedPageRef = new WeakReference<Page>(element);
507              element.Loaded += FocusAfterLoaded;
508          }
509      }
510  
511      private void FocusAfterLoaded(object sender, RoutedEventArgs e)
512      {
513          var page = (Page)sender;
514          page.Loaded -= FocusAfterLoaded;
515  
516          // Only handle focus for the latest navigated page
517          if (_lastNavigatedPageRef is null || !_lastNavigatedPageRef.TryGetTarget(out var last) || !ReferenceEquals(page, last))
518          {
519              return;
520          }
521  
522          // Cancel any previous pending focus work
523          _focusAfterLoadedCts?.Cancel();
524          _focusAfterLoadedCts?.Dispose();
525          _focusAfterLoadedCts = new CancellationTokenSource();
526          var token = _focusAfterLoadedCts.Token;
527  
528          AnnounceNavigationToPage(page);
529  
530          var shouldSearchBoxBeVisible = ViewModel.CurrentPage?.HasSearchBox ?? false;
531  
532          if (shouldSearchBoxBeVisible || page is not ContentPage)
533          {
534              ViewModel.IsSearchBoxVisible = shouldSearchBoxBeVisible;
535              SearchBox.Focus(FocusState.Programmatic);
536              SearchBox.SelectSearch();
537          }
538          else
539          {
540              _ = Task.Run(
541                  async () =>
542                  {
543                      if (token.IsCancellationRequested)
544                      {
545                          return;
546                      }
547  
548                      try
549                      {
550                          await page.DispatcherQueue.EnqueueAsync(
551                              async () =>
552                              {
553                                  // I hate this so much, but it can take a while for the page to be ready to accept focus;
554                                  // focusing page with MarkdownTextBlock takes up to 5 attempts (* 100ms delay between attempts)
555                                  for (var i = 0; i < 10; i++)
556                                  {
557                                      token.ThrowIfCancellationRequested();
558  
559                                      if (FocusManager.FindFirstFocusableElement(page) is FrameworkElement frameworkElement)
560                                      {
561                                          var set = frameworkElement.Focus(FocusState.Programmatic);
562                                          if (set)
563                                          {
564                                              break;
565                                          }
566                                      }
567  
568                                      await Task.Delay(100, token);
569                                  }
570  
571                                  token.ThrowIfCancellationRequested();
572  
573                                  // Update the search box visibility based on the current page:
574                                  // - We do this here after navigation so the focus is not jumping around too much,
575                                  //   it messes with screen readers if we do it too early
576                                  // - Since this should hide the search box on content pages, it's not a problem if we
577                                  //   wait for the code above to finish trying to focus the content
578                                  ViewModel.IsSearchBoxVisible = ViewModel.CurrentPage?.HasSearchBox ?? false;
579                              });
580                      }
581                      catch (OperationCanceledException)
582                      {
583                          // Swallow cancellation - another FocusAfterLoaded invocation superseded this one
584                      }
585                      catch (Exception ex)
586                      {
587                          Logger.LogError("Error during FocusAfterLoaded async focus work", ex);
588                      }
589                  },
590                  token);
591          }
592      }
593  
594      private void AnnounceNavigationToPage(Page page)
595      {
596          var pageTitle = page switch
597          {
598              ListPage listPage => listPage.ViewModel?.Title,
599              ContentPage contentPage => contentPage.ViewModel?.Title,
600              _ => null,
601          };
602  
603          if (string.IsNullOrEmpty(pageTitle))
604          {
605              pageTitle = ResourceLoaderInstance.GetString("UntitledPageTitle");
606          }
607  
608          var announcement = string.Format(CultureInfo.CurrentCulture, _pageNavigatedAnnouncement.Format, pageTitle);
609  
610          UIHelper.AnnounceActionForAccessibility(RootFrame, announcement, "CommandPalettePageNavigatedTo");
611      }
612  
613      /// <summary>
614      /// Gets a value indicating whether determines if the current Details have a HeroImage, given the theme
615      /// we're currently in. This needs to be evaluated in the view, because the
616      /// viewModel doesn't actually know what the current theme is.
617      /// </summary>
618      public bool HasHeroImage
619      {
620          get
621          {
622              var requestedTheme = ActualTheme;
623              var iconInfoVM = ViewModel.Details?.HeroImage;
624              return iconInfoVM?.HasIcon(requestedTheme == Microsoft.UI.Xaml.ElementTheme.Light) ?? false;
625          }
626      }
627  
628      private void Command_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
629      {
630          if (sender is Button button && button.DataContext is CommandViewModel commandViewModel)
631          {
632              WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(commandViewModel.Model));
633          }
634      }
635  
636      private static void ShellPage_OnPreviewKeyDown(object sender, KeyRoutedEventArgs e)
637      {
638          var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
639          var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
640          var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
641          var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
642                           InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
643  
644          var onlyAlt = altPressed && !ctrlPressed && !shiftPressed && !winPressed;
645          var onlyCtrl = !altPressed && ctrlPressed && !shiftPressed && !winPressed;
646          switch (e.Key)
647          {
648              case VirtualKey.Left when onlyAlt: // Alt+Left arrow
649                  WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
650                  e.Handled = true;
651                  break;
652              case VirtualKey.Home when onlyAlt: // Alt+Home
653                  WeakReferenceMessenger.Default.Send<GoHomeMessage>(new(WithAnimation: false));
654                  e.Handled = true;
655                  break;
656              case (VirtualKey)188 when onlyCtrl: // Ctrl+,
657                  WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
658                  e.Handled = true;
659                  break;
660              default:
661                  {
662                      // The CommandBar is responsible for handling all the item keybindings,
663                      // since the bound context item may need to then show another
664                      // context menu
665                      TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
666                      WeakReferenceMessenger.Default.Send(msg);
667                      e.Handled = msg.Handled;
668                      break;
669                  }
670          }
671      }
672  
673      private static void ShellPage_OnKeyDown(object sender, KeyRoutedEventArgs e)
674      {
675          var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
676          if (ctrlPressed && e.Key == VirtualKey.Enter)
677          {
678              // ctrl+enter
679              WeakReferenceMessenger.Default.Send<ActivateSecondaryCommandMessage>();
680              e.Handled = true;
681          }
682          else if (e.Key == VirtualKey.Enter)
683          {
684              WeakReferenceMessenger.Default.Send<ActivateSelectedListItemMessage>();
685              e.Handled = true;
686          }
687          else if (ctrlPressed && e.Key == VirtualKey.K)
688          {
689              // ctrl+k
690              WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
691              e.Handled = true;
692          }
693          else if (e.Key == VirtualKey.Escape)
694          {
695              WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
696              e.Handled = true;
697          }
698      }
699  
700      private void ShellPage_OnPointerPressed(object sender, PointerRoutedEventArgs e)
701      {
702          try
703          {
704              var ptr = e.Pointer;
705              if (ptr.PointerDeviceType == PointerDeviceType.Mouse)
706              {
707                  var ptrPt = e.GetCurrentPoint(this);
708                  if (ptrPt.Properties.IsXButton1Pressed)
709                  {
710                      WeakReferenceMessenger.Default.Send(new NavigateBackMessage());
711                  }
712              }
713          }
714          catch (Exception ex)
715          {
716              Logger.LogError("Error handling mouse button press event", ex);
717          }
718      }
719  
720      public void Dispose()
721      {
722          _focusAfterLoadedCts?.Cancel();
723          _focusAfterLoadedCts?.Dispose();
724          _focusAfterLoadedCts = null;
725      }
726  }