mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 19:27:56 +01:00
727 lines
28 KiB
C#
727 lines
28 KiB
C#
// 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.
|
|
|
|
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;
|
|
using Microsoft.CommandPalette.Extensions;
|
|
using Microsoft.PowerToys.Telemetry;
|
|
using Microsoft.UI.Dispatching;
|
|
using Microsoft.UI.Input;
|
|
using Microsoft.UI.Xaml;
|
|
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;
|
|
|
|
namespace Microsoft.CmdPal.UI.Pages;
|
|
|
|
/// <summary>
|
|
/// An empty page that can be used on its own or navigated to within a Frame.
|
|
/// </summary>
|
|
public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
|
IRecipient<NavigateBackMessage>,
|
|
IRecipient<OpenSettingsMessage>,
|
|
IRecipient<HotkeySummonMessage>,
|
|
IRecipient<ShowDetailsMessage>,
|
|
IRecipient<HideDetailsMessage>,
|
|
IRecipient<ClearSearchMessage>,
|
|
IRecipient<LaunchUriMessage>,
|
|
IRecipient<SettingsWindowClosedMessage>,
|
|
IRecipient<GoHomeMessage>,
|
|
IRecipient<GoBackMessage>,
|
|
IRecipient<ShowConfirmationMessage>,
|
|
IRecipient<ShowToastMessage>,
|
|
IRecipient<NavigateToPageMessage>,
|
|
INotifyPropertyChanged,
|
|
IDisposable
|
|
{
|
|
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
|
|
|
|
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
|
|
|
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
|
|
|
private readonly SlideNavigationTransitionInfo _slideRightTransition = new() { Effect = SlideNavigationTransitionEffect.FromRight };
|
|
private readonly SuppressNavigationTransitionInfo _noAnimation = new();
|
|
|
|
private readonly ToastWindow _toast = new();
|
|
|
|
private readonly SettingsModel _settings;
|
|
private readonly TopLevelCommandManager tlcManager;
|
|
|
|
private readonly CompositeFormat _pageNavigatedAnnouncement;
|
|
|
|
private SettingsWindow? _settingsWindow;
|
|
|
|
private CancellationTokenSource? _focusAfterLoadedCts;
|
|
private WeakReference<Page>? _lastNavigatedPageRef;
|
|
|
|
public ShellViewModel ViewModel { get; private set; }
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
public ShellPage(
|
|
ShellViewModel shellViewModel,
|
|
SettingsModel settingsModel,
|
|
TopLevelCommandManager topLevelCommandManager)
|
|
{
|
|
this.InitializeComponent();
|
|
|
|
ViewModel = shellViewModel;
|
|
_settings = settingsModel;
|
|
tlcManager = topLevelCommandManager;
|
|
|
|
// how we are doing navigation around
|
|
WeakReferenceMessenger.Default.Register<NavigateBackMessage>(this);
|
|
WeakReferenceMessenger.Default.Register<OpenSettingsMessage>(this);
|
|
WeakReferenceMessenger.Default.Register<HotkeySummonMessage>(this);
|
|
WeakReferenceMessenger.Default.Register<SettingsWindowClosedMessage>(this);
|
|
|
|
WeakReferenceMessenger.Default.Register<ShowDetailsMessage>(this);
|
|
WeakReferenceMessenger.Default.Register<HideDetailsMessage>(this);
|
|
|
|
WeakReferenceMessenger.Default.Register<ClearSearchMessage>(this);
|
|
WeakReferenceMessenger.Default.Register<LaunchUriMessage>(this);
|
|
|
|
WeakReferenceMessenger.Default.Register<GoHomeMessage>(this);
|
|
WeakReferenceMessenger.Default.Register<GoBackMessage>(this);
|
|
WeakReferenceMessenger.Default.Register<ShowConfirmationMessage>(this);
|
|
WeakReferenceMessenger.Default.Register<ShowToastMessage>(this);
|
|
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(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), new AsyncNavigationRequest(ViewModel, CancellationToken.None));
|
|
|
|
var pageAnnouncementFormat = ResourceLoaderInstance.GetString("ScreenReader_Announcement_NavigatedToPage0");
|
|
_pageNavigatedAnnouncement = CompositeFormat.Parse(pageAnnouncementFormat);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the default page animation, depending on the settings
|
|
/// </summary>
|
|
private NavigationTransitionInfo DefaultPageAnimation
|
|
{
|
|
get
|
|
{
|
|
return _settings.DisableAnimations ? _noAnimation : _slideRightTransition;
|
|
}
|
|
}
|
|
|
|
public void Receive(NavigateBackMessage message)
|
|
{
|
|
if (RootFrame.CanGoBack)
|
|
{
|
|
if (!message.FromBackspace ||
|
|
_settings.BackspaceGoesBack)
|
|
{
|
|
GoBack();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!message.FromBackspace)
|
|
{
|
|
// If we can't go back then we must be at the top and thus escape again should quit.
|
|
WeakReferenceMessenger.Default.Send<DismissMessage>();
|
|
|
|
PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnEsc());
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Receive(NavigateToPageMessage message)
|
|
{
|
|
// TODO GH #526 This needs more better locking too
|
|
_ = _queue.TryEnqueue(() =>
|
|
{
|
|
// Also hide our details pane about here, if we had one
|
|
HideDetails();
|
|
|
|
// Navigate to the appropriate host page for that VM
|
|
RootFrame.Navigate(
|
|
message.Page switch
|
|
{
|
|
ListViewModel => typeof(ListPage),
|
|
ContentPageViewModel => typeof(ContentPage),
|
|
_ => throw new NotSupportedException(),
|
|
},
|
|
new AsyncNavigationRequest(message.Page, message.CancellationToken),
|
|
message.WithAnimation ? DefaultPageAnimation : _noAnimation);
|
|
|
|
PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth));
|
|
|
|
if (!ViewModel.IsNested)
|
|
{
|
|
// todo BODGY
|
|
RootFrame.BackStack.Clear();
|
|
}
|
|
});
|
|
}
|
|
|
|
public void Receive(ShowConfirmationMessage message)
|
|
{
|
|
DispatcherQueue.TryEnqueue(async () =>
|
|
{
|
|
try
|
|
{
|
|
await HandleConfirmArgsOnUiThread(message.Args);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex.ToString());
|
|
}
|
|
});
|
|
}
|
|
|
|
public void Receive(ShowToastMessage message)
|
|
{
|
|
DispatcherQueue.TryEnqueue(() =>
|
|
{
|
|
_toast.ShowToast(message.Message);
|
|
});
|
|
}
|
|
|
|
// This gets called from the UI thread
|
|
private async Task HandleConfirmArgsOnUiThread(IConfirmationArgs? args)
|
|
{
|
|
if (args is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ConfirmResultViewModel vm = new(args, new(ViewModel.CurrentPage));
|
|
var initializeDialogTask = Task.Run(() => { InitializeConfirmationDialog(vm); });
|
|
await initializeDialogTask;
|
|
|
|
var resourceLoader = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance.ResourceLoader;
|
|
var confirmText = resourceLoader.GetString("ConfirmationDialog_ConfirmButtonText");
|
|
var cancelText = resourceLoader.GetString("ConfirmationDialog_CancelButtonText");
|
|
|
|
var name = string.IsNullOrEmpty(vm.PrimaryCommand.Name) ? confirmText : vm.PrimaryCommand.Name;
|
|
ContentDialog dialog = new()
|
|
{
|
|
Title = vm.Title,
|
|
Content = vm.Description,
|
|
PrimaryButtonText = name,
|
|
CloseButtonText = cancelText,
|
|
XamlRoot = this.XamlRoot,
|
|
};
|
|
|
|
if (vm.IsPrimaryCommandCritical)
|
|
{
|
|
dialog.DefaultButton = ContentDialogButton.Close;
|
|
|
|
// TODO: Maybe we need to style the primary button to be red?
|
|
// dialog.PrimaryButtonStyle = new Style(typeof(Button))
|
|
// {
|
|
// Setters =
|
|
// {
|
|
// new Setter(Button.ForegroundProperty, new SolidColorBrush(Colors.Red)),
|
|
// new Setter(Button.BackgroundProperty, new SolidColorBrush(Colors.Red)),
|
|
// },
|
|
// };
|
|
}
|
|
|
|
var result = await dialog.ShowAsync();
|
|
if (result == ContentDialogResult.Primary)
|
|
{
|
|
var performMessage = new PerformCommandMessage(vm);
|
|
WeakReferenceMessenger.Default.Send(performMessage);
|
|
}
|
|
else
|
|
{
|
|
// cancel
|
|
}
|
|
}
|
|
|
|
private void InitializeConfirmationDialog(ConfirmResultViewModel vm)
|
|
{
|
|
vm.SafeInitializePropertiesSynchronous();
|
|
}
|
|
|
|
public void Receive(OpenSettingsMessage message)
|
|
{
|
|
_ = DispatcherQueue.TryEnqueue(() =>
|
|
{
|
|
OpenSettings();
|
|
});
|
|
}
|
|
|
|
public void OpenSettings()
|
|
{
|
|
if (_settingsWindow is null)
|
|
{
|
|
_settingsWindow = new SettingsWindow();
|
|
}
|
|
|
|
_settingsWindow.Activate();
|
|
_settingsWindow.BringToFront();
|
|
}
|
|
|
|
public void Receive(ShowDetailsMessage message)
|
|
{
|
|
if (ViewModel is not null &&
|
|
ViewModel.CurrentPage is not null)
|
|
{
|
|
if (ViewModel.CurrentPage.PageContext.TryGetTarget(out var pageContext))
|
|
{
|
|
Task.Factory.StartNew(
|
|
() =>
|
|
{
|
|
// TERRIBLE HACK TODO GH #245
|
|
// There's weird wacky bugs with debounce currently.
|
|
if (!ViewModel.IsDetailsVisible)
|
|
{
|
|
ViewModel.Details = message.Details;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasHeroImage)));
|
|
ViewModel.IsDetailsVisible = true;
|
|
return;
|
|
}
|
|
|
|
// GH #322:
|
|
// For inexplicable reasons, if you try to change the details too fast,
|
|
// we'll explode. This seemingly only happens if you change the details
|
|
// while we're also scrolling a new list view item into view.
|
|
_debounceTimer.Debounce(
|
|
() =>
|
|
{
|
|
ViewModel.Details = message.Details;
|
|
|
|
// Trigger a re-evaluation of whether we have a hero image based on
|
|
// the current theme
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasHeroImage)));
|
|
},
|
|
interval: TimeSpan.FromMilliseconds(50),
|
|
immediate: ViewModel.IsDetailsVisible == false);
|
|
ViewModel.IsDetailsVisible = true;
|
|
},
|
|
CancellationToken.None,
|
|
TaskCreationOptions.None,
|
|
pageContext.Scheduler);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Receive(HideDetailsMessage message) => HideDetails();
|
|
|
|
public void Receive(LaunchUriMessage message) => _ = global::Windows.System.Launcher.LaunchUriAsync(message.Uri);
|
|
|
|
private void HideDetails()
|
|
{
|
|
ViewModel.Details = null;
|
|
ViewModel.IsDetailsVisible = false;
|
|
}
|
|
|
|
public void Receive(ClearSearchMessage message) => SearchBox.ClearSearch();
|
|
|
|
public void Receive(HotkeySummonMessage message)
|
|
{
|
|
_ = DispatcherQueue.TryEnqueue(() => SummonOnUiThread(message));
|
|
}
|
|
|
|
public void Receive(SettingsWindowClosedMessage message) => _settingsWindow = null;
|
|
|
|
private void SummonOnUiThread(HotkeySummonMessage message)
|
|
{
|
|
var commandId = message.CommandId;
|
|
var isRoot = string.IsNullOrEmpty(commandId);
|
|
if (isRoot)
|
|
{
|
|
// If this is the hotkey for the root level, then always show us
|
|
WeakReferenceMessenger.Default.Send<ShowWindowMessage>(new(message.Hwnd));
|
|
|
|
// Depending on the settings, either
|
|
// * Go home, or
|
|
// * Select the search text (if we should remain open on this page)
|
|
if (_settings.HotkeyGoesHome)
|
|
{
|
|
GoHome(false);
|
|
}
|
|
else if (_settings.HighlightSearchOnActivate)
|
|
{
|
|
SearchBox.SelectSearch();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
// For a hotkey bound to a command, first lookup the
|
|
// command from our list of top level commands.
|
|
var topLevelCommand = tlcManager.LookupCommand(commandId);
|
|
if (topLevelCommand is not null)
|
|
{
|
|
var command = topLevelCommand.CommandViewModel.Model.Unsafe;
|
|
var isPage = command is not IInvokableCommand;
|
|
|
|
// If the bound command is an invokable command, then
|
|
// we don't want to open the window at all - we want to
|
|
// just do it.
|
|
if (isPage)
|
|
{
|
|
// If we're here, then the bound command was a page
|
|
// of some kind. Let's pop the stack, show the window, and navigate to it.
|
|
GoHome(false);
|
|
|
|
WeakReferenceMessenger.Default.Send<ShowWindowMessage>(new(message.Hwnd));
|
|
}
|
|
|
|
var msg = topLevelCommand.GetPerformCommandMessage();
|
|
msg.WithAnimation = false;
|
|
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(msg);
|
|
|
|
// we can't necessarily SelectSearch() here, because when the page is loaded,
|
|
// we'll fetch the SearchText from the page itself, and that'll stomp the
|
|
// selection we start now.
|
|
// That's probably okay though.
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
|
}
|
|
|
|
public void Receive(GoBackMessage message)
|
|
{
|
|
_ = DispatcherQueue.TryEnqueue(() => GoBack(message.WithAnimation, message.FocusSearch));
|
|
}
|
|
|
|
private void GoBack(bool withAnimation = true, bool focusSearch = true)
|
|
{
|
|
HideDetails();
|
|
|
|
ViewModel.CancelNavigation();
|
|
|
|
// Note: That we restore the VM state below in RootFrame_Navigated call back after this occurs.
|
|
// In the future, we may want to manage the back stack ourselves vs. relying on Frame
|
|
// We could replace Frame with a ContentPresenter, but then have to manage transition animations ourselves.
|
|
// However, then we have more fine-grained control on the back stack, managing the VM cache, and not
|
|
// having that all be a black box, though then we wouldn't cache the XAML page itself, but sometimes that is a drawback.
|
|
// However, we do a good job here, see ForwardStack.Clear below, and BackStack.Clear above about managing that.
|
|
if (withAnimation)
|
|
{
|
|
RootFrame.GoBack();
|
|
}
|
|
else
|
|
{
|
|
RootFrame.GoBack(_noAnimation);
|
|
}
|
|
|
|
// Don't store pages we're navigating away from in the Frame cache
|
|
// TODO: In the future we probably want a short cache (3-5?) of recent VMs in case the user re-navigates
|
|
// back to a recent page they visited (like the Pokedex) so we don't have to reload it from scratch.
|
|
// That'd be retrieved as we re-navigate in the PerformCommandMessage logic above
|
|
RootFrame.ForwardStack.Clear();
|
|
|
|
if (!RootFrame.CanGoBack)
|
|
{
|
|
ViewModel.GoHome();
|
|
}
|
|
|
|
if (focusSearch)
|
|
{
|
|
SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
|
|
SearchBox.SelectSearch();
|
|
}
|
|
}
|
|
|
|
public void Receive(GoHomeMessage message)
|
|
{
|
|
_ = DispatcherQueue.TryEnqueue(() => GoHome(withAnimation: message.WithAnimation, focusSearch: message.FocusSearch));
|
|
}
|
|
|
|
private void GoHome(bool withAnimation = true, bool focusSearch = true)
|
|
{
|
|
while (RootFrame.CanGoBack)
|
|
{
|
|
// don't focus on each step, just at the end
|
|
GoBack(withAnimation, focusSearch: false);
|
|
}
|
|
|
|
// focus search box, even if we were already home
|
|
if (focusSearch)
|
|
{
|
|
SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
|
|
SearchBox.SelectSearch();
|
|
}
|
|
}
|
|
|
|
private void BackButton_Clicked(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) => WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
|
|
|
private void RootFrame_Navigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
|
|
{
|
|
// 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.
|
|
// This is currently used for both forward and backward navigation.
|
|
// As when we go back that we restore ourselves to the proper state within our VM
|
|
if (e.Parameter is AsyncNavigationRequest request)
|
|
{
|
|
if (request.NavigationToken.IsCancellationRequested && e.NavigationMode is not (Microsoft.UI.Xaml.Navigation.NavigationMode.Back or Microsoft.UI.Xaml.Navigation.NavigationMode.Forward))
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (request.TargetViewModel)
|
|
{
|
|
case PageViewModel pageViewModel:
|
|
ViewModel.CurrentPage = pageViewModel;
|
|
break;
|
|
case ShellViewModel:
|
|
// This one is an exception, for now (LoadingPage is tied to ShellViewModel,
|
|
// but ShellViewModel is not PageViewModel.
|
|
ViewModel.CurrentPage = ViewModel.NullPage;
|
|
break;
|
|
default:
|
|
ViewModel.CurrentPage = ViewModel.NullPage;
|
|
Logger.LogWarning($"Invalid navigation target: AsyncNavigationRequest.{nameof(AsyncNavigationRequest.TargetViewModel)} must be {nameof(PageViewModel)}");
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.LogWarning("Unrecognized target for shell navigation: " + e.Parameter);
|
|
}
|
|
|
|
if (e.Content is Page element)
|
|
{
|
|
_lastNavigatedPageRef = new WeakReference<Page>(element);
|
|
element.Loaded += FocusAfterLoaded;
|
|
}
|
|
}
|
|
|
|
private void FocusAfterLoaded(object sender, RoutedEventArgs e)
|
|
{
|
|
var page = (Page)sender;
|
|
page.Loaded -= FocusAfterLoaded;
|
|
|
|
// Only handle focus for the latest navigated page
|
|
if (_lastNavigatedPageRef is null || !_lastNavigatedPageRef.TryGetTarget(out var last) || !ReferenceEquals(page, last))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Cancel any previous pending focus work
|
|
_focusAfterLoadedCts?.Cancel();
|
|
_focusAfterLoadedCts?.Dispose();
|
|
_focusAfterLoadedCts = new CancellationTokenSource();
|
|
var token = _focusAfterLoadedCts.Token;
|
|
|
|
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 () =>
|
|
{
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
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++)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
|
|
if (FocusManager.FindFirstFocusableElement(page) is FrameworkElement frameworkElement)
|
|
{
|
|
var set = frameworkElement.Focus(FocusState.Programmatic);
|
|
if (set)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
await Task.Delay(100, token);
|
|
}
|
|
|
|
token.ThrowIfCancellationRequested();
|
|
|
|
// 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;
|
|
});
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// Swallow cancellation - another FocusAfterLoaded invocation superseded this one
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError("Error during FocusAfterLoaded async focus work", ex);
|
|
}
|
|
},
|
|
token);
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether determines if the current Details have a HeroImage, given the theme
|
|
/// we're currently in. This needs to be evaluated in the view, because the
|
|
/// viewModel doesn't actually know what the current theme is.
|
|
/// </summary>
|
|
public bool HasHeroImage
|
|
{
|
|
get
|
|
{
|
|
var requestedTheme = ActualTheme;
|
|
var iconInfoVM = ViewModel.Details?.HeroImage;
|
|
return iconInfoVM?.HasIcon(requestedTheme == Microsoft.UI.Xaml.ElementTheme.Light) ?? false;
|
|
}
|
|
}
|
|
|
|
private void Command_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
|
{
|
|
if (sender is Button button && button.DataContext is CommandViewModel commandViewModel)
|
|
{
|
|
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(commandViewModel.Model));
|
|
}
|
|
}
|
|
|
|
private static void ShellPage_OnPreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
|
{
|
|
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);
|
|
|
|
var onlyAlt = altPressed && !ctrlPressed && !shiftPressed && !winPressed;
|
|
var onlyCtrl = !altPressed && ctrlPressed && !shiftPressed && !winPressed;
|
|
switch (e.Key)
|
|
{
|
|
case VirtualKey.Left when onlyAlt: // Alt+Left arrow
|
|
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
|
e.Handled = true;
|
|
break;
|
|
case VirtualKey.Home when onlyAlt: // Alt+Home
|
|
WeakReferenceMessenger.Default.Send<GoHomeMessage>(new(WithAnimation: false));
|
|
e.Handled = true;
|
|
break;
|
|
case (VirtualKey)188 when onlyCtrl: // Ctrl+,
|
|
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
|
|
e.Handled = true;
|
|
break;
|
|
default:
|
|
{
|
|
// 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;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
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<ActivateSecondaryCommandMessage>();
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == VirtualKey.Enter)
|
|
{
|
|
WeakReferenceMessenger.Default.Send<ActivateSelectedListItemMessage>();
|
|
e.Handled = true;
|
|
}
|
|
else if (ctrlPressed && e.Key == VirtualKey.K)
|
|
{
|
|
// ctrl+k
|
|
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom));
|
|
e.Handled = true;
|
|
}
|
|
else if (e.Key == VirtualKey.Escape)
|
|
{
|
|
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
private void ShellPage_OnPointerPressed(object sender, PointerRoutedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
var ptr = e.Pointer;
|
|
if (ptr.PointerDeviceType == PointerDeviceType.Mouse)
|
|
{
|
|
var ptrPt = e.GetCurrentPoint(this);
|
|
if (ptrPt.Properties.IsXButton1Pressed)
|
|
{
|
|
WeakReferenceMessenger.Default.Send(new NavigateBackMessage());
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError("Error handling mouse button press event", ex);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_focusAfterLoadedCts?.Cancel();
|
|
_focusAfterLoadedCts?.Dispose();
|
|
_focusAfterLoadedCts = null;
|
|
}
|
|
}
|