// 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);
}
}
}