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 }