// 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; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Utilities; using Microsoft.PowerToys.Settings.UI.Services; using Microsoft.PowerToys.Settings.UI.ViewModels; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Automation.Peers; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using Settings.UI.Library; using Windows.Data.Json; using Windows.System; namespace Microsoft.PowerToys.Settings.UI.Views { /// /// Root page. /// public sealed partial class ShellPage : UserControl, IDisposable { /// /// Declaration for the ipc callback function. /// /// message. public delegate void IPCMessageCallback(string msg); /// /// Declaration for opening main window callback function. /// public delegate void MainOpeningCallback(Type type); /// /// Declaration for updating the general settings callback function. /// public delegate bool UpdatingGeneralSettingsCallback(ModuleType moduleType, bool isEnabled); /// /// Declaration for opening oobe window callback function. /// public delegate void OobeOpeningCallback(); /// /// Declaration for opening whats new window callback function. /// public delegate void WhatIsNewOpeningCallback(); /// /// Declaration for opening flyout window callback function. /// public delegate void FlyoutOpeningCallback(POINT? point); /// /// Declaration for disabling hide of flyout window callback function. /// public delegate void DisablingFlyoutHidingCallback(); /// /// Gets or sets a shell handler to be used to update contents of the shell dynamically from page within the frame. /// public static ShellPage ShellHandler { get; set; } /// /// Gets or sets iPC default callback function. /// public static IPCMessageCallback DefaultSndMSGCallback { get; set; } /// /// Gets or sets iPC callback function for restart as admin. /// public static IPCMessageCallback SndRestartAsAdminMsgCallback { get; set; } /// /// Gets or sets iPC callback function for checking updates. /// public static IPCMessageCallback CheckForUpdatesMsgCallback { get; set; } /// /// Gets or sets callback function for opening main window /// public static MainOpeningCallback OpenMainWindowCallback { get; set; } /// /// Gets or sets callback function for updating the general settings /// public static UpdatingGeneralSettingsCallback UpdateGeneralSettingsCallback { get; set; } /// /// Gets or sets callback function for opening oobe window /// public static OobeOpeningCallback OpenOobeWindowCallback { get; set; } /// /// Gets or sets callback function for opening oobe window /// public static WhatIsNewOpeningCallback OpenWhatIsNewWindowCallback { get; set; } /// /// Gets or sets callback function for opening flyout window /// public static FlyoutOpeningCallback OpenFlyoutCallback { get; set; } /// /// Gets or sets callback function for disabling hide of flyout window /// public static DisablingFlyoutHidingCallback DisableFlyoutHidingCallback { get; set; } /// /// Gets view model. /// public ShellViewModel ViewModel { get; } /// /// Gets a collection of functions that handle IPC responses. /// public List> IPCResponseHandleList { get; } = new List>(); public static bool IsElevated { get; set; } public static bool IsUserAnAdmin { get; set; } public Controls.TitleBar TitleBar => AppTitleBar; private static string _version = Helper.GetProductVersion(); private Dictionary _navViewParentLookup = new Dictionary(); private List _searchSuggestions = []; private CancellationTokenSource _searchDebounceCts; private const int SearchDebounceMs = 500; private bool _disposed; // Removed trace id counter per cleanup /// /// Initializes a new instance of the class. /// Shell page constructor. /// public ShellPage() { InitializeComponent(); SetWindowTitle(); var settingsUtils = new SettingsUtils(); ViewModel = new ShellViewModel(SettingsRepository.GetInstance(settingsUtils)); DataContext = ViewModel; ShellHandler = this; ViewModel.Initialize(shellFrame, navigationView, KeyboardAccelerators); // NL moved navigation to general page to the moment when the window is first activated (to not make flyout window disappear) // shellFrame.Navigate(typeof(GeneralPage)); IPCResponseHandleList.Add(ReceiveMessage); IPCResponseService.Instance.RegisterForIPC(); if (_navViewParentLookup.Count > 0) { _navViewParentLookup.Clear(); } var topLevelItems = navigationView.MenuItems.OfType().ToArray(); var newTopLevelItems = new HashSet(); foreach (var parent in topLevelItems) { foreach (var child in parent.MenuItems.OfType()) { var pageType = child.GetValue(NavHelper.NavigateToProperty) as Type; _navViewParentLookup.TryAdd(pageType, parent); _searchSuggestions.Add(child.Content?.ToString()); if (AddNewTagIfNeeded(child, pageType)) { newTopLevelItems.Add(parent); } } } foreach (var parent in newTopLevelItems) { parent.InfoBadge = GetNewInfoBadge(); } } public static int SendDefaultIPCMessage(string msg) { DefaultSndMSGCallback?.Invoke(msg); return 0; } public static int SendCheckForUpdatesIPCMessage(string msg) { CheckForUpdatesMsgCallback?.Invoke(msg); return 0; } public static int SendRestartAdminIPCMessage(string msg) { SndRestartAsAdminMsgCallback?.Invoke(msg); return 0; } /// /// Set Default IPC Message callback function. /// /// delegate function implementation. public static void SetDefaultSndMessageCallback(IPCMessageCallback implementation) { DefaultSndMSGCallback = implementation; } /// /// Set restart as admin IPC callback function. /// /// delegate function implementation. public static void SetRestartAdminSndMessageCallback(IPCMessageCallback implementation) { SndRestartAsAdminMsgCallback = implementation; } /// /// Set check for updates IPC callback function. /// /// delegate function implementation. public static void SetCheckForUpdatesMessageCallback(IPCMessageCallback implementation) { CheckForUpdatesMsgCallback = implementation; } /// /// Set main window opening callback function /// /// delegate function implementation. public static void SetOpenMainWindowCallback(MainOpeningCallback implementation) { OpenMainWindowCallback = implementation; } /// /// Set updating the general settings callback function /// /// delegate function implementation. public static void SetUpdatingGeneralSettingsCallback(UpdatingGeneralSettingsCallback implementation) { UpdateGeneralSettingsCallback = implementation; } /// /// Set oobe opening callback function /// /// delegate function implementation. public static void SetOpenOobeCallback(OobeOpeningCallback implementation) { OpenOobeWindowCallback = implementation; } /// /// Set whats new opening callback function /// /// delegate function implementation. public static void SetOpenWhatIsNewCallback(WhatIsNewOpeningCallback implementation) { OpenWhatIsNewWindowCallback = implementation; } /// /// Set flyout opening callback function /// /// delegate function implementation. public static void SetOpenFlyoutCallback(FlyoutOpeningCallback implementation) { OpenFlyoutCallback = implementation; } /// /// Set disable flyout hiding callback function /// /// delegate function implementation. public static void SetDisableFlyoutHidingCallback(DisablingFlyoutHidingCallback implementation) { DisableFlyoutHidingCallback = implementation; } public static void SetElevationStatus(bool isElevated) { IsElevated = isElevated; } public static void SetIsUserAnAdmin(bool isAdmin) { IsUserAnAdmin = isAdmin; } public static void Navigate(Type type) { NavigationService.Navigate(type); } public void Refresh() { shellFrame.Navigate(typeof(DashboardPage)); } // Tell the current page view model to update public void SignalGeneralDataUpdate() { IRefreshablePage currentPage = shellFrame?.Content as IRefreshablePage; if (currentPage != null) { currentPage.RefreshEnabledState(); } } private bool navigationViewInitialStateProcessed; // avoid announcing initial state of the navigation pane. private void NavigationView_PaneOpened(NavigationView sender, object args) { if (!navigationViewInitialStateProcessed) { navigationViewInitialStateProcessed = true; return; } var peer = FrameworkElementAutomationPeer.FromElement(sender); if (peer == null) { peer = FrameworkElementAutomationPeer.CreatePeerForElement(sender); } if (AutomationPeer.ListenerExists(AutomationEvents.MenuOpened)) { var loader = ResourceLoaderInstance.ResourceLoader; peer.RaiseNotificationEvent( AutomationNotificationKind.ActionCompleted, AutomationNotificationProcessing.ImportantMostRecent, loader.GetString("Shell_NavigationMenu_Announce_Open"), "navigationMenuPaneOpened"); } } private void NavigationView_PaneClosed(NavigationView sender, object args) { if (!navigationViewInitialStateProcessed) { navigationViewInitialStateProcessed = true; return; } var peer = FrameworkElementAutomationPeer.FromElement(sender); if (peer == null) { peer = FrameworkElementAutomationPeer.CreatePeerForElement(sender); } if (AutomationPeer.ListenerExists(AutomationEvents.MenuClosed)) { var loader = ResourceLoaderInstance.ResourceLoader; peer.RaiseNotificationEvent( AutomationNotificationKind.ActionCompleted, AutomationNotificationProcessing.ImportantMostRecent, loader.GetString("Shell_NavigationMenu_Announce_Collapse"), "navigationMenuPaneClosed"); } } private void OOBEItem_Tapped(object sender, TappedRoutedEventArgs e) { OpenOobeWindowCallback(); } private async void FeedbackItem_Tapped(object sender, TappedRoutedEventArgs e) { await Launcher.LaunchUriAsync(new Uri("https://aka.ms/powerToysGiveFeedback")); } private void WhatIsNewItem_Tapped(object sender, TappedRoutedEventArgs e) { OpenWhatIsNewWindowCallback(); } private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args) { NavigationViewItem selectedItem = args.SelectedItem as NavigationViewItem; if (selectedItem != null) { Type pageType = selectedItem.GetValue(NavHelper.NavigateToProperty) as Type; if (pageType != null && _navViewParentLookup.TryGetValue(pageType, out var parentItem) && !parentItem.IsExpanded) { parentItem.IsExpanded = true; ViewModel.Expanding = parentItem; NavigationService.Navigate(pageType); } } } private void ReceiveMessage(JsonObject json) { if (json != null) { IJsonValue whatToShowJson; if (json.TryGetValue("ShowYourself", out whatToShowJson)) { if (whatToShowJson.ValueType == JsonValueType.String && whatToShowJson.GetString().Equals("flyout", StringComparison.Ordinal)) { POINT? p = null; IJsonValue flyoutPointX; IJsonValue flyoutPointY; if (json.TryGetValue("x_position", out flyoutPointX) && json.TryGetValue("y_position", out flyoutPointY)) { if (flyoutPointX.ValueType == JsonValueType.Number && flyoutPointY.ValueType == JsonValueType.Number) { int flyout_x = (int)flyoutPointX.GetNumber(); int flyout_y = (int)flyoutPointY.GetNumber(); p = new POINT(flyout_x, flyout_y); } } OpenFlyoutCallback(p); } else if (whatToShowJson.ValueType == JsonValueType.String) { OpenMainWindowCallback(App.GetPage(whatToShowJson.GetString())); } } } } internal static void EnsurePageIsSelected() { NavigationService.EnsurePageIsSelected(typeof(DashboardPage)); } private void SetWindowTitle() { var loader = ResourceLoaderInstance.ResourceLoader; AppTitleBar.Title = App.IsElevated ? loader.GetString("SettingsWindow_AdminTitle") : loader.GetString("SettingsWindow_Title"); #if DEBUG AppTitleBar.Subtitle = "Debug"; #endif } private void ShellPage_Loaded(object sender, RoutedEventArgs e) { Task.Run(() => { SearchIndexService.BuildIndex(); }) .ContinueWith(_ => { }); } private void NavigationView_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args) { if (args.DisplayMode == NavigationViewDisplayMode.Compact || args.DisplayMode == NavigationViewDisplayMode.Minimal) { AppTitleBar.IsPaneButtonVisible = true; } else { AppTitleBar.IsPaneButtonVisible = false; } } private void PaneToggleBtn_Click(object sender, RoutedEventArgs e) { navigationView.IsPaneOpen = !navigationView.IsPaneOpen; } private async void Close_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e) { await CloseDialog.ShowAsync(); } private void CloseDialog_Click(ContentDialog sender, ContentDialogButtonClickEventArgs args) { const string ptTrayIconWindowClass = "PToyTrayIconWindow"; // Defined in runner/tray_icon.h const nuint ID_CLOSE_MENU_COMMAND = 40001; // Generated resource from runner/runner.base.rc // Exit the XAML application Application.Current.Exit(); // Invoke the exit command from the tray icon IntPtr hWnd = NativeMethods.FindWindow(ptTrayIconWindowClass, ptTrayIconWindowClass); NativeMethods.SendMessage(hWnd, NativeMethods.WM_COMMAND, ID_CLOSE_MENU_COMMAND, 0); } private List _lastSearchResults = new(); private string _lastQueryText = string.Empty; private async void SearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) { // Only respond to user input, not programmatic text changes if (args.Reason != AutoSuggestionBoxTextChangeReason.UserInput) { return; } var query = sender.Text?.Trim() ?? string.Empty; // Debounce: cancel previous pending search _searchDebounceCts?.Cancel(); _searchDebounceCts?.Dispose(); _searchDebounceCts = new CancellationTokenSource(); var token = _searchDebounceCts.Token; if (string.IsNullOrWhiteSpace(query)) { sender.ItemsSource = null; sender.IsSuggestionListOpen = false; _lastSearchResults.Clear(); _lastQueryText = string.Empty; return; } try { await Task.Delay(SearchDebounceMs, token); } catch (TaskCanceledException) { return; // debounce canceled } if (token.IsCancellationRequested) { return; } // Query the index on a background thread to avoid blocking UI List results = null; try { // If the token is already canceled before scheduling, the task won't start. results = await Task.Run(() => SearchIndexService.Search(query, token), token); } catch (OperationCanceledException) { return; } if (token.IsCancellationRequested) { return; } _lastSearchResults = results; _lastQueryText = query; var top = BuildSuggestionItems(query, results); sender.ItemsSource = top; sender.IsSuggestionListOpen = top.Count > 0; } private void SearchBox_SuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args) { // Do not navigate on arrow navigation. Let QuerySubmitted handle commits (Enter/click). // AutoSuggestBox will pass the chosen item via args.ChosenSuggestion to QuerySubmitted. // No action required here. } private void NavigateFromSuggestion(SuggestionItem item) { var queryText = _lastQueryText; if (item.IsShowAll) { // Navigate to full results page var searchParams = new SearchResultsNavigationParams(queryText, _lastSearchResults); NavigationService.Navigate(searchParams); SearchBox.Text = string.Empty; return; } // Navigate to the selected item var pageType = GetPageTypeFromName(item.PageTypeName); if (pageType != null) { if (string.IsNullOrEmpty(item.ElementName)) { NavigationService.Navigate(pageType); } else { var navigationParams = new NavigationParams(item.ElementName, item.ParentElementName); NavigationService.Navigate(pageType, navigationParams); } // Clear the search box after navigation SearchBox.Text = string.Empty; } } private static Type GetPageTypeFromName(string pageTypeName) { if (string.IsNullOrEmpty(pageTypeName)) { return null; } var assembly = typeof(GeneralPage).Assembly; return assembly.GetType($"Microsoft.PowerToys.Settings.UI.Views.{pageTypeName}"); } private void CtrlF_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) { SearchBox.Focus(FocusState.Programmatic); args.Handled = true; // prevent further processing (e.g., unintended navigation) } private void SearchBox_GotFocus(object sender, RoutedEventArgs e) { var box = sender as AutoSuggestBox; var current = box?.Text?.Trim() ?? string.Empty; if (string.IsNullOrEmpty(current)) { return; // nothing to restore } // If current text matches last query and we have results, reconstruct the suggestion list. if (string.Equals(current, _lastQueryText, StringComparison.Ordinal) && _lastSearchResults?.Count > 0) { try { var top = BuildSuggestionItems(current, _lastSearchResults); box.ItemsSource = top; box.IsSuggestionListOpen = top.Count > 0; } catch (Exception ex) { Logger.LogError($"Error restoring suggestion list {ex.Message}"); } } } // Centralized suggestion projection logic used by TextChanged & GotFocus restore. private List BuildSuggestionItems(string query, List results) { results ??= new(); if (results.Count == 0) { var rl = ResourceLoaderInstance.ResourceLoader; var noResultsPrefix = rl.GetString("Shell_Search_NoResults"); if (string.IsNullOrEmpty(noResultsPrefix)) { noResultsPrefix = "No results for"; } var headerText = $"{noResultsPrefix} '{query}'"; return new List { new() { Header = headerText, IsNoResults = true, }, }; } var list = results.Take(5).Select(e => { string subtitle = string.Empty; if (e.Type != EntryType.SettingsPage) { subtitle = SearchIndexService.GetLocalizedPageName(e.PageTypeName); if (string.IsNullOrEmpty(subtitle)) { subtitle = SearchIndexService.Index .Where(x => x.Type == EntryType.SettingsPage && x.PageTypeName == e.PageTypeName) .Select(x => x.Header) .FirstOrDefault() ?? string.Empty; } } return new SuggestionItem { Header = e.Header, Icon = e.Icon, PageTypeName = e.PageTypeName, ElementName = e.ElementName, ParentElementName = e.ParentElementName, Subtitle = subtitle, IsShowAll = false, }; }).ToList(); if (results.Count > 5) { list.Add(new SuggestionItem { IsShowAll = true }); } return list; } private async void SearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) { // If a suggestion is selected, navigate directly if (args.ChosenSuggestion is SuggestionItem chosen) { NavigateFromSuggestion(chosen); return; } var queryText = (args.QueryText ?? _lastQueryText)?.Trim(); if (string.IsNullOrWhiteSpace(queryText)) { NavigationService.Navigate(); return; } // Prefer cached results (from live search); if empty, perform a fresh search var matched = _lastSearchResults?.Count > 0 && string.Equals(_lastQueryText, queryText, StringComparison.Ordinal) ? _lastSearchResults : await Task.Run(() => SearchIndexService.Search(queryText)); var searchParams = new SearchResultsNavigationParams(queryText, matched); NavigationService.Navigate(searchParams); } private bool AddNewTagIfNeeded(NavigationViewItem item, Type pageType) { var newUtility = pageType.GetCustomAttribute(); if (newUtility != null && _version.StartsWith(newUtility.Version, StringComparison.InvariantCulture)) { item.InfoBadge = GetNewInfoBadge(); return true; } return false; } private InfoBadge GetNewInfoBadge() { return new InfoBadge { Style = (Style)Application.Current.Resources["NewInfoBadge"], }; } public void Dispose() { if (_disposed) { return; } _searchDebounceCts?.Cancel(); _searchDebounceCts?.Dispose(); _searchDebounceCts = null; _disposed = true; GC.SuppressFinalize(this); } } }