diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Commands/CloseWindowCommand.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Commands/CloseWindowCommand.cs new file mode 100644 index 0000000000..bae6c7fa39 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Commands/CloseWindowCommand.cs @@ -0,0 +1,35 @@ +// 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.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.WindowWalker.Components; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Commands; + +internal sealed partial class CloseWindowCommand : InvokableCommand +{ + private readonly Window _window; + + public CloseWindowCommand(Window window) + { + _window = window; + } + + public override ICommandResult Invoke() + { + if (!_window.IsWindow) + { + ExtensionHost.LogMessage(new LogMessage() { Message = $"Can not close the window '{_window.Title}' ({_window.Hwnd}), because it doesn't exist." }); + } + + _window.CloseThisWindow(); + return CommandResult.Dismiss(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/ContextMenuHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/ContextMenuHelper.cs new file mode 100644 index 0000000000..bbaca1acc3 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/ContextMenuHelper.cs @@ -0,0 +1,91 @@ +// 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.Windows; +using System.Windows.Input; +using Microsoft.CmdPal.Ext.WindowWalker.Commands; +using Microsoft.CmdPal.Ext.WindowWalker.Helpers; +using Microsoft.CmdPal.Ext.WindowWalker.Properties; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Components; + +internal class ContextMenuHelper +{ + internal static List GetContextMenuResults(in WindowWalkerListItem listItem) + { + if (!(listItem?.Window is Window windowData)) + { + return new List(0); + } + + var contextMenu = new List() + { + new(new CloseWindowCommand(windowData)) + { + // AcceleratorKey = Key.F4, + // AcceleratorModifiers = ModifierKeys.Control, + Icon = new("\xE8BB"), + Title = $"{Resources.wox_plugin_windowwalker_Close} (Ctrl+F4)", + }, + }; + + // Hide menu if Explorer.exe is the shell process or the process name is ApplicationFrameHost.exe + // In the first case we would crash the windows ui and in the second case we would kill the generic process for uwp apps. + if (!windowData.Process.IsShellProcess && !(windowData.Process.IsUwpApp && string.Equals(windowData.Process.Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase)) + && !(windowData.Process.IsFullAccessDenied && SettingsManager.Instance.HideKillProcessOnElevatedProcesses)) + { + contextMenu.Add(new ContextMenuResult + { + AcceleratorKey = Key.Delete, + AcceleratorModifiers = ModifierKeys.Control, + FontFamily = "Segoe Fluent Icons,Segoe MDL2 Assets", + Glyph = "\xE74D", // E74D => Symbol: Delete + Title = $"{Resources.wox_plugin_windowwalker_Kill} (Ctrl+Delete)", + Action = _ => KillProcessCommand(windowData), + }); + } + + return contextMenu; + } + + /// + /// Method to initiate killing the process of a window + /// + /// Window data + /// True if the PT Run window should close, otherwise false. + private static bool KillProcessCommand(Window window) + { + // Validate process + if (!window.IsWindow || !window.Process.DoesExist || !window.Process.Name.Equals(WindowProcess.GetProcessNameFromProcessID(window.Process.ProcessID), StringComparison.Ordinal)) + { + Log.Debug($"Can not kill process '{window.Process.Name}' ({window.Process.ProcessID}) of the window '{window.Title}' ({window.Hwnd}), because it doesn't exist.", typeof(ContextMenuHelper)); + return false; + } + + // Request user confirmation + if (WindowWalkerSettings.Instance.ConfirmKillProcess) + { + string messageBody = $"{Resources.wox_plugin_windowwalker_KillMessage}\n" + + $"{window.Process.Name} ({window.Process.ProcessID})\n\n" + + $"{(window.Process.IsUwpApp ? Resources.wox_plugin_windowwalker_KillMessageUwp : Resources.wox_plugin_windowwalker_KillMessageQuestion)}"; + MessageBoxResult messageBoxResult = MessageBox.Show( + messageBody, + Resources.wox_plugin_windowwalker_plugin_name + " - " + Resources.wox_plugin_windowwalker_KillMessageTitle, + MessageBoxButton.YesNo, + MessageBoxImage.Warning); + + if (messageBoxResult == MessageBoxResult.No) + { + return false; + } + } + + // Kill process + window.Process.KillThisProcess(WindowWalkerSettings.Instance.KillProcessTree); + return !WindowWalkerSettings.Instance.OpenAfterKillAndClose; + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/FuzzyMatching.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/FuzzyMatching.cs new file mode 100644 index 0000000000..35d8981262 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/FuzzyMatching.cs @@ -0,0 +1,135 @@ +// 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. + +// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Microsoft.Plugin.WindowWalker.Components +{ + /// + /// Class housing fuzzy matching methods + /// + internal static class FuzzyMatching + { + /// + /// Finds the best match (the one with the most + /// number of letters adjacent to each other) and + /// returns the index location of each of the letters + /// of the matches + /// + /// The text to search inside of + /// the text to search for + /// returns the index location of each of the letters of the matches + internal static List FindBestFuzzyMatch(string text, string searchText) + { + ArgumentNullException.ThrowIfNull(searchText); + + ArgumentNullException.ThrowIfNull(text); + + // Using CurrentCulture since this is user facing + searchText = searchText.ToLower(CultureInfo.CurrentCulture); + text = text.ToLower(CultureInfo.CurrentCulture); + + // Create a grid to march matches like + // eg. + // a b c a d e c f g + // a x x + // c x x + bool[,] matches = new bool[text.Length, searchText.Length]; + for (int firstIndex = 0; firstIndex < text.Length; firstIndex++) + { + for (int secondIndex = 0; secondIndex < searchText.Length; secondIndex++) + { + matches[firstIndex, secondIndex] = + searchText[secondIndex] == text[firstIndex] ? + true : + false; + } + } + + // use this table to get all the possible matches + List> allMatches = GetAllMatchIndexes(matches); + + // return the score that is the max + int maxScore = allMatches.Count > 0 ? CalculateScoreForMatches(allMatches[0]) : 0; + List bestMatch = allMatches.Count > 0 ? allMatches[0] : new List(); + + foreach (var match in allMatches) + { + int score = CalculateScoreForMatches(match); + if (score > maxScore) + { + bestMatch = match; + maxScore = score; + } + } + + return bestMatch; + } + + /// + /// Gets all the possible matches to the search string with in the text + /// + /// a table showing the matches as generated by + /// a two dimensional array with the first dimension the text and the second + /// one the search string and each cell marked as an intersection between the two + /// a list of the possible combinations that match the search text + internal static List> GetAllMatchIndexes(bool[,] matches) + { + ArgumentNullException.ThrowIfNull(matches); + + List> results = new List>(); + + for (int secondIndex = 0; secondIndex < matches.GetLength(1); secondIndex++) + { + for (int firstIndex = 0; firstIndex < matches.GetLength(0); firstIndex++) + { + if (secondIndex == 0 && matches[firstIndex, secondIndex]) + { + results.Add(new List { firstIndex }); + } + else if (matches[firstIndex, secondIndex]) + { + var tempList = results.Where(x => x.Count == secondIndex && x[x.Count - 1] < firstIndex).Select(x => x.ToList()).ToList(); + + foreach (var pathSofar in tempList) + { + pathSofar.Add(firstIndex); + } + + results.AddRange(tempList); + } + } + + results = results.Where(x => x.Count == secondIndex + 1).ToList(); + } + + return results.Where(x => x.Count == matches.GetLength(1)).ToList(); + } + + /// + /// Calculates the score for a string + /// + /// the index of the matches + /// an integer representing the score + internal static int CalculateScoreForMatches(List matches) + { + ArgumentNullException.ThrowIfNull(matches); + + var score = 0; + + for (int currentIndex = 1; currentIndex < matches.Count; currentIndex++) + { + var previousIndex = currentIndex - 1; + + score -= matches[currentIndex] - matches[previousIndex]; + } + + return score == 0 ? -10000 : score; + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/LivePreview.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/LivePreview.cs new file mode 100644 index 0000000000..0474d6c13e --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/LivePreview.cs @@ -0,0 +1,60 @@ +// 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. + +// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/ +using System; + +using Wox.Plugin.Common.Win32; + +namespace Microsoft.Plugin.WindowWalker.Components +{ + /// + /// Class containing methods to control the live preview + /// + internal class LivePreview + { + /// + /// Makes sure that a window is excluded from the live preview + /// + /// handle to the window to exclude + public static void SetWindowExclusionFromLivePreview(IntPtr hwnd) + { + uint renderPolicy = (uint)DwmNCRenderingPolicies.Enabled; + + _ = NativeMethods.DwmSetWindowAttribute( + hwnd, + 12, + ref renderPolicy, + sizeof(uint)); + } + + /// + /// Activates the live preview + /// + /// the window to show by making all other windows transparent + /// the window which should not be transparent but is not the target window + public static void ActivateLivePreview(IntPtr targetWindow, IntPtr windowToSpare) + { + _ = NativeMethods.DwmpActivateLivePreview( + true, + targetWindow, + windowToSpare, + LivePreviewTrigger.Superbar, + IntPtr.Zero); + } + + /// + /// Deactivates the live preview + /// + public static void DeactivateLivePreview() + { + _ = NativeMethods.DwmpActivateLivePreview( + false, + IntPtr.Zero, + IntPtr.Zero, + LivePreviewTrigger.AltTab, + IntPtr.Zero); + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/OpenWindows.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/OpenWindows.cs new file mode 100644 index 0000000000..37a6186fdf --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/OpenWindows.cs @@ -0,0 +1,133 @@ +// 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. + +// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; + +using Wox.Plugin.Common.Win32; + +namespace Microsoft.Plugin.WindowWalker.Components +{ + /// + /// Class that represents the state of the desktops windows + /// + internal class OpenWindows + { + /// + /// Used to enforce single execution of EnumWindows + /// + private static readonly object _enumWindowsLock = new(); + + /// + /// PowerLauncher main executable + /// + private static readonly string _powerLauncherExe = Path.GetFileName(Environment.ProcessPath); + + /// + /// List of all the open windows + /// + private readonly List windows = new List(); + + /// + /// An instance of the class OpenWindows + /// + private static OpenWindows instance; + + /// + /// Gets the list of all open windows + /// + internal List Windows => new List(windows); + + /// + /// Gets an instance property of this class that makes sure that + /// the first instance gets created and that all the requests + /// end up at that one instance + /// + internal static OpenWindows Instance + { + get + { + if (instance == null) + { + instance = new OpenWindows(); + } + + return instance; + } + } + + /// + /// Initializes a new instance of the class. + /// Private constructor to make sure there is never + /// more than one instance of this class + /// + private OpenWindows() + { + } + + /// + /// Updates the list of open windows + /// + internal void UpdateOpenWindowsList(CancellationToken cancellationToken) + { + var tokenHandle = GCHandle.Alloc(cancellationToken); + try + { + var tokenHandleParam = GCHandle.ToIntPtr(tokenHandle); + lock (_enumWindowsLock) + { + windows.Clear(); + EnumWindowsProc callbackptr = new EnumWindowsProc(WindowEnumerationCallBack); + _ = NativeMethods.EnumWindows(callbackptr, tokenHandleParam); + } + } + finally + { + if (tokenHandle.IsAllocated) + { + tokenHandle.Free(); + } + } + } + + /// + /// Call back method for window enumeration + /// + /// The handle to the current window being enumerated + /// Value being passed from the caller (we don't use this but might come in handy + /// in the future + /// true to make sure to continue enumeration + internal bool WindowEnumerationCallBack(IntPtr hwnd, IntPtr lParam) + { + var tokenHandle = GCHandle.FromIntPtr(lParam); + var cancellationToken = (CancellationToken)tokenHandle.Target; + if (cancellationToken.IsCancellationRequested) + { + // Stop enumeration + return false; + } + + Window newWindow = new Window(hwnd); + + if (newWindow.IsWindow && newWindow.Visible && newWindow.IsOwner && + (!newWindow.IsToolWindow || newWindow.IsAppWindow) && !newWindow.TaskListDeleted && + (newWindow.Desktop.IsVisible || !WindowWalkerSettings.Instance.ResultsFromVisibleDesktopOnly || Main.VirtualDesktopHelperInstance.GetDesktopCount() < 2) && + newWindow.ClassName != "Windows.UI.Core.CoreWindow" && newWindow.Process.Name != _powerLauncherExe) + { + // To hide (not add) preloaded uwp app windows that are invisible to the user and other cloaked windows, we check the cloak state. (Issue #13637.) + // (If user asking to see cloaked uwp app windows again we can add an optional plugin setting in the future.) + if (!newWindow.IsCloaked || newWindow.GetWindowCloakState() == Window.WindowCloakState.OtherDesktop) + { + windows.Add(newWindow); + } + } + + return true; + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/ResultHelper.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/ResultHelper.cs new file mode 100644 index 0000000000..7ca5423d32 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/ResultHelper.cs @@ -0,0 +1,185 @@ +// 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 Microsoft.Plugin.WindowWalker.Properties; +using Wox.Infrastructure; +using Wox.Plugin; + +namespace Microsoft.Plugin.WindowWalker.Components +{ + /// + /// Helper class to work with results + /// + internal static class ResultHelper + { + /// + /// Returns a list of all results for the query. + /// + /// List with all search controller matches + /// The path to the result icon + /// List of results + internal static List GetResultList(List searchControllerResults, bool isKeywordSearch, string icon, string infoIcon) + { + if (searchControllerResults == null || searchControllerResults.Count == 0) + { + return new List(); + } + + List resultsList = new List(searchControllerResults.Count); + bool addExplorerInfo = searchControllerResults.Any(x => + string.Equals(x.Result.Process.Name, "explorer.exe", StringComparison.OrdinalIgnoreCase) && + x.Result.Process.IsShellProcess); + + // Process each SearchResult to convert it into a Result. + // Using parallel processing if the operation is CPU-bound and the list is large. + resultsList = searchControllerResults + .AsParallel() + .Select(x => CreateResultFromSearchResult(x, icon)) + .ToList(); + + if (addExplorerInfo && isKeywordSearch && !WindowWalkerSettings.Instance.HideExplorerSettingInfo) + { + resultsList.Add(GetExplorerInfoResult(infoIcon)); + } + + return resultsList; + } + + /// + /// Creates a Result object from a given SearchResult. + /// + /// The SearchResult object to convert. + /// The path to the icon that should be used for the Result. + /// A Result object populated with data from the SearchResult. + private static Result CreateResultFromSearchResult(SearchResult searchResult, string icon) + { + return new Result + { + Title = searchResult.Result.Title, + IcoPath = icon, + SubTitle = GetSubtitle(searchResult.Result), + ContextData = searchResult.Result, + Action = c => + { + searchResult.Result.SwitchToWindow(); + return true; + }, + + // For debugging you can set the second parameter to true to see more information. + ToolTipData = GetToolTip(searchResult.Result, false), + }; + } + + /// + /// Returns the subtitle for a result + /// + /// The window properties of the result + /// String with the subtitle + private static string GetSubtitle(Window window) + { + if (window == null || !(window is Window)) + { + return string.Empty; + } + + string subtitleText = Resources.wox_plugin_windowwalker_Running + ": " + window.Process.Name; + + if (WindowWalkerSettings.Instance.SubtitleShowPid) + { + subtitleText += $" ({window.Process.ProcessID})"; + } + + if (!window.Process.IsResponding) + { + subtitleText += $" [{Resources.wox_plugin_windowwalker_NotResponding}]"; + } + + if (WindowWalkerSettings.Instance.SubtitleShowDesktopName && Main.VirtualDesktopHelperInstance.GetDesktopCount() > 1) + { + subtitleText += $" - {Resources.wox_plugin_windowwalker_Desktop}: {window.Desktop.Name}"; + } + + return subtitleText; + } + + /// + /// Returns the tool tip for a result + /// + /// The window properties of the result + /// Value indicating if a detailed debug tooltip should be returned + /// Tooltip for the result or null of failure + private static ToolTipData GetToolTip(Window window, bool debugToolTip) + { + if (window == null || !(window is Window)) + { + return null; + } + + if (!debugToolTip) + { + string text = $"{Resources.wox_plugin_windowwalker_Process}: {window.Process.Name}"; + text += $"\n{Resources.wox_plugin_windowwalker_ProcessId}: {window.Process.ProcessID}"; + + if (Main.VirtualDesktopHelperInstance.GetDesktopCount() > 1) + { + text += $"\n{Resources.wox_plugin_windowwalker_Desktop}: {window.Desktop.Name}"; + + if (!window.Desktop.IsAllDesktopsView) + { + text += $" ({Resources.wox_plugin_windowwalker_Number} {window.Desktop.Number})"; + } + } + + return new ToolTipData(window.Title, text); + } + else + { + string text = $"hWnd: {window.Hwnd}\n" + + $"Window class: {window.ClassName}\n" + + $"Process ID: {window.Process.ProcessID}\n" + + $"Thread ID: {window.Process.ThreadID}\n" + + $"Process: {window.Process.Name}\n" + + $"Process exists: {window.Process.DoesExist}\n" + + $"Is full access denied: {window.Process.IsFullAccessDenied}\n" + + $"Is uwp app: {window.Process.IsUwpApp}\n" + + $"Is ShellProcess: {window.Process.IsShellProcess}\n" + + $"Is window cloaked: {window.IsCloaked}\n" + + $"Window cloak state: {window.GetWindowCloakState()}\n" + + $"Desktop id: {window.Desktop.Id}\n" + + $"Desktop name: {window.Desktop.Name}\n" + + $"Desktop number: {window.Desktop.Number}\n" + + $"Desktop is visible: {window.Desktop.IsVisible}\n" + + $"Desktop position: {window.Desktop.Position}\n" + + $"Is AllDesktops view: {window.Desktop.IsAllDesktopsView}\n" + + $"Responding: {window.Process.IsResponding}"; + + return new ToolTipData(window.Title, text); + } + } + + /// + /// Returns an information result about the explorer setting + /// + /// The path to the info icon. + /// An object of the type with the information. + private static Result GetExplorerInfoResult(string iIcon) + { + return new Result() + { + Title = Resources.wox_plugin_windowwalker_ExplorerInfoTitle, + IcoPath = iIcon, + SubTitle = Resources.wox_plugin_windowwalker_ExplorerInfoSubTitle, + Action = c => + { + Helper.OpenInShell("rundll32.exe", "shell32.dll,Options_RunDLL 7"); // "shell32.dll,Options_RunDLL 7" opens the view tab in folder options of explorer. + return true; + }, + Score = 100_000, + }; + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchController.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchController.cs new file mode 100644 index 0000000000..4ec3f4025f --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchController.cs @@ -0,0 +1,165 @@ +// 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. + +// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Microsoft.Plugin.WindowWalker.Components +{ + /// + /// Responsible for searching and finding matches for the strings provided. + /// Essentially the UI independent model of the application + /// + internal class SearchController + { + /// + /// the current search text + /// + private string searchText; + + /// + /// Open window search results + /// + private List searchMatches; + + /// + /// Singleton pattern + /// + private static SearchController instance; + + /// + /// Gets or sets the current search text + /// + internal string SearchText + { + get + { + return searchText; + } + + set + { + // Using CurrentCulture since this is user facing + searchText = value.ToLower(CultureInfo.CurrentCulture).Trim(); + } + } + + /// + /// Gets the open window search results + /// + internal List SearchMatches + { + get { return new List(searchMatches).OrderByDescending(x => x.Score).ToList(); } + } + + /// + /// Gets singleton Pattern + /// + internal static SearchController Instance + { + get + { + if (instance == null) + { + instance = new SearchController(); + } + + return instance; + } + } + + /// + /// Initializes a new instance of the class. + /// Initializes the search controller object + /// + private SearchController() + { + searchText = string.Empty; + } + + /// + /// Event handler for when the search text has been updated + /// + internal void UpdateSearchText(string searchText) + { + SearchText = searchText; + SyncOpenWindowsWithModel(); + } + + /// + /// Syncs the open windows with the OpenWindows Model + /// + internal void SyncOpenWindowsWithModel() + { + System.Diagnostics.Debug.Print("Syncing WindowSearch result with OpenWindows Model"); + + List snapshotOfOpenWindows = OpenWindows.Instance.Windows; + + if (string.IsNullOrWhiteSpace(SearchText)) + { + searchMatches = AllOpenWindows(snapshotOfOpenWindows); + } + else + { + searchMatches = FuzzySearchOpenWindows(snapshotOfOpenWindows); + } + } + + /// + /// Search method that matches the title of windows with the user search text + /// + /// what windows are open + /// Returns search results + private List FuzzySearchOpenWindows(List openWindows) + { + List result = new List(); + var searchStrings = new SearchString(searchText, SearchResult.SearchType.Fuzzy); + + foreach (var window in openWindows) + { + var titleMatch = FuzzyMatching.FindBestFuzzyMatch(window.Title, searchStrings.SearchText); + var processMatch = FuzzyMatching.FindBestFuzzyMatch(window.Process.Name, searchStrings.SearchText); + + if ((titleMatch.Count != 0 || processMatch.Count != 0) && window.Title.Length != 0) + { + result.Add(new SearchResult(window, titleMatch, processMatch, searchStrings.SearchType)); + } + } + + System.Diagnostics.Debug.Print("Found " + result.Count + " windows that match the search text"); + + return result; + } + + /// + /// Search method that matches all the windows with a title + /// + /// what windows are open + /// Returns search results + private List AllOpenWindows(List openWindows) + { + List result = new List(); + + foreach (var window in openWindows) + { + if (window.Title.Length != 0) + { + result.Add(new SearchResult(window)); + } + } + + return result.OrderBy(w => w.Result.Title).ToList(); + } + + /// + /// Event args for a window list update event + /// + internal class SearchResultUpdateEventArgs : EventArgs + { + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchResult.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchResult.cs new file mode 100644 index 0000000000..168f11d2f5 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchResult.cs @@ -0,0 +1,148 @@ +// 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. + +// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/ +using System.Collections.Generic; + +namespace Microsoft.Plugin.WindowWalker.Components +{ + /// + /// Contains search result windows with each window including the reason why the result was included + /// + internal class SearchResult + { + /// + /// Gets the actual window reference for the search result + /// + internal Window Result + { + get; + private set; + } + + /// + /// Gets the list of indexes of the matching characters for the search in the title window + /// + internal List SearchMatchesInTitle + { + get; + private set; + } + + /// + /// Gets the list of indexes of the matching characters for the search in the + /// name of the process + /// + internal List SearchMatchesInProcessName + { + get; + private set; + } + + /// + /// Gets the type of match (shortcut, fuzzy or nothing) + /// + internal SearchType SearchResultMatchType + { + get; + private set; + } + + /// + /// Gets a score indicating how well this matches what we are looking for + /// + internal int Score + { + get; + private set; + } + + /// + /// Gets the source of where the best score was found + /// + internal TextType BestScoreSource + { + get; + private set; + } + + /// + /// Initializes a new instance of the class. + /// Constructor + /// + internal SearchResult(Window window, List matchesInTitle, List matchesInProcessName, SearchType matchType) + { + Result = window; + SearchMatchesInTitle = matchesInTitle; + SearchMatchesInProcessName = matchesInProcessName; + SearchResultMatchType = matchType; + CalculateScore(); + } + + /// + /// Initializes a new instance of the class. + /// + internal SearchResult(Window window) + { + Result = window; + SearchMatchesInTitle = new List(); + SearchMatchesInProcessName = new List(); + SearchResultMatchType = SearchType.Empty; + CalculateScore(); + } + + /// + /// Calculates the score for how closely this window matches the search string + /// + /// + /// Higher Score is better + /// + private void CalculateScore() + { + if (FuzzyMatching.CalculateScoreForMatches(SearchMatchesInProcessName) > + FuzzyMatching.CalculateScoreForMatches(SearchMatchesInTitle)) + { + Score = FuzzyMatching.CalculateScoreForMatches(SearchMatchesInProcessName); + BestScoreSource = TextType.ProcessName; + } + else + { + Score = FuzzyMatching.CalculateScoreForMatches(SearchMatchesInTitle); + BestScoreSource = TextType.WindowTitle; + } + } + + /// + /// The type of text that a string represents + /// + internal enum TextType + { + ProcessName, + WindowTitle, + } + + /// + /// The type of search + /// + internal enum SearchType + { + /// + /// the search string is empty, which means all open windows are + /// going to be returned + /// + Empty, + + /// + /// Regular fuzzy match search + /// + Fuzzy, + + /// + /// The user has entered text that has been matched to a shortcut + /// and the shortcut is now being searched + /// + Shortcut, + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchString.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchString.cs new file mode 100644 index 0000000000..fb54278ff0 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/SearchString.cs @@ -0,0 +1,46 @@ +// 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. + +// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/ +namespace Microsoft.Plugin.WindowWalker.Components +{ + /// + /// A class to represent a search string + /// + /// Class was added inorder to be able to attach various context data to + /// a search string + internal class SearchString + { + /// + /// Gets where is the search string coming from (is it a shortcut + /// or direct string, etc...) + /// + internal SearchResult.SearchType SearchType + { + get; + private set; + } + + /// + /// Gets the actual text we are searching for + /// + internal string SearchText + { + get; + private set; + } + + /// + /// Initializes a new instance of the class. + /// Constructor + /// + /// text from search + /// type of search + internal SearchString(string searchText, SearchResult.SearchType searchType) + { + SearchText = searchText; + SearchType = searchType; + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/Window.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/Window.cs new file mode 100644 index 0000000000..7114275747 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/Window.cs @@ -0,0 +1,422 @@ +// 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. + +// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Wox.Plugin.Common.VirtualDesktop.Helper; +using Wox.Plugin.Common.Win32; +using Wox.Plugin.Logger; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Components; + +/// +/// Represents a specific open window +/// +internal class Window +{ + /// + /// The handle to the window + /// + private readonly IntPtr hwnd; + + /// + /// A static cache for the process data of all known windows + /// that we don't have to query the data every time + /// + private static readonly Dictionary _handlesToProcessCache = new Dictionary(); + + /// + /// An instance of that contains the process information for the window + /// + private readonly WindowProcess processInfo; + + /// + /// An instance of that contains the desktop information for the window + /// + private readonly VDesktop desktopInfo; + + /// + /// Gets the title of the window (the string displayed at the top of the window) + /// + internal string Title + { + get + { + int sizeOfTitle = NativeMethods.GetWindowTextLength(hwnd); + if (sizeOfTitle++ > 0) + { + StringBuilder titleBuffer = new StringBuilder(sizeOfTitle); + var numCharactersWritten = NativeMethods.GetWindowText(hwnd, titleBuffer, sizeOfTitle); + if (numCharactersWritten == 0) + { + return string.Empty; + } + + return titleBuffer.ToString(); + } + else + { + return string.Empty; + } + } + } + + /// + /// Gets the handle to the window + /// + internal IntPtr Hwnd + { + get { return hwnd; } + } + + /// + /// Gets the object of with the process information of the window + /// + internal WindowProcess Process + { + get { return processInfo; } + } + + /// + /// Gets the object of with the desktop information of the window + /// + internal VDesktop Desktop + { + get { return desktopInfo; } + } + + /// + /// Gets the name of the class for the window represented + /// + internal string ClassName + { + get + { + return GetWindowClassName(Hwnd); + } + } + + /// + /// Gets a value indicating whether the window is visible (might return false if it is a hidden IE tab) + /// + internal bool Visible + { + get + { + return NativeMethods.IsWindowVisible(Hwnd); + } + } + + /// + /// Gets a value indicating whether the window is cloaked (true) or not (false). + /// (A cloaked window is not visible to the user. But the window is still composed by DWM.) + /// + internal bool IsCloaked + { + get + { + return GetWindowCloakState() != WindowCloakState.None; + } + } + + /// + /// Gets a value indicating whether the specified window handle identifies an existing window. + /// + internal bool IsWindow + { + get + { + return NativeMethods.IsWindow(Hwnd); + } + } + + /// + /// Gets a value indicating whether the window is a toolwindow + /// + internal bool IsToolWindow + { + get + { + return (NativeMethods.GetWindowLong(Hwnd, Win32Constants.GWL_EXSTYLE) & + (uint)ExtendedWindowStyles.WS_EX_TOOLWINDOW) == + (uint)ExtendedWindowStyles.WS_EX_TOOLWINDOW; + } + } + + /// + /// Gets a value indicating whether the window is an appwindow + /// + internal bool IsAppWindow + { + get + { + return (NativeMethods.GetWindowLong(Hwnd, Win32Constants.GWL_EXSTYLE) & + (uint)ExtendedWindowStyles.WS_EX_APPWINDOW) == + (uint)ExtendedWindowStyles.WS_EX_APPWINDOW; + } + } + + /// + /// Gets a value indicating whether the window has ITaskList_Deleted property + /// + internal bool TaskListDeleted + { + get + { + return NativeMethods.GetProp(Hwnd, "ITaskList_Deleted") != IntPtr.Zero; + } + } + + /// + /// Gets a value indicating whether the specified windows is the owner (i.e. doesn't have an owner) + /// + internal bool IsOwner + { + get + { + return NativeMethods.GetWindow(Hwnd, GetWindowCmd.GW_OWNER) == IntPtr.Zero; + } + } + + /// + /// Gets a value indicating whether the window is minimized + /// + internal bool Minimized + { + get + { + return GetWindowSizeState() == WindowSizeState.Minimized; + } + } + + /// + /// Initializes a new instance of the class. + /// Initializes a new Window representation + /// + /// the handle to the window we are representing + internal Window(IntPtr hwnd) + { + // TODO: Add verification as to whether the window handle is valid + this.hwnd = hwnd; + processInfo = CreateWindowProcessInstance(hwnd); + desktopInfo = Main.VirtualDesktopHelperInstance.GetWindowDesktop(hwnd); + } + + /// + /// Switches desktop focus to the window + /// + internal void SwitchToWindow() + { + // The following block is necessary because + // 1) There is a weird flashing behavior when trying + // to use ShowWindow for switching tabs in IE + // 2) SetForegroundWindow fails on minimized windows + // Using Ordinal since this is internal + if (processInfo.Name.ToUpperInvariant().Equals("IEXPLORE.EXE", StringComparison.Ordinal) || !Minimized) + { + NativeMethods.SetForegroundWindow(Hwnd); + } + else + { + if (!NativeMethods.ShowWindow(Hwnd, ShowWindowCommand.Restore)) + { + // ShowWindow doesn't work if the process is running elevated: fallback to SendMessage + _ = NativeMethods.SendMessage(Hwnd, Win32Constants.WM_SYSCOMMAND, Win32Constants.SC_RESTORE); + } + } + + NativeMethods.FlashWindow(Hwnd, true); + } + + /// + /// Helper function to close the window + /// + internal void CloseThisWindowHelper() + { + _ = NativeMethods.SendMessageTimeout(Hwnd, Win32Constants.WM_SYSCOMMAND, Win32Constants.SC_CLOSE, 0, 0x0000, 5000, out _); + } + + /// + /// Closes the window + /// + internal void CloseThisWindow() + { + Thread thread = new(new ThreadStart(CloseThisWindowHelper)); + thread.Start(); + } + + /// + /// Converts the window name to string along with the process name + /// + /// The title of the window + public override string ToString() + { + // Using CurrentCulture since this is user facing + return Title + " (" + processInfo.Name.ToUpper(CultureInfo.CurrentCulture) + ")"; + } + + /// + /// Returns what the window size is + /// + /// The state (minimized, maximized, etc..) of the window + internal WindowSizeState GetWindowSizeState() + { + NativeMethods.GetWindowPlacement(Hwnd, out WINDOWPLACEMENT placement); + + switch (placement.ShowCmd) + { + case ShowWindowCommand.Normal: + return WindowSizeState.Normal; + case ShowWindowCommand.Minimize: + case ShowWindowCommand.ShowMinimized: + return WindowSizeState.Minimized; + case ShowWindowCommand.Maximize: // No need for ShowMaximized here since its also of value 3 + return WindowSizeState.Maximized; + default: + // throw new Exception("Don't know how to handle window state = " + placement.ShowCmd); + return WindowSizeState.Unknown; + } + } + + /// + /// Enum to simplify the state of the window + /// + internal enum WindowSizeState + { + Normal, + Minimized, + Maximized, + Unknown, + } + + /// + /// Returns the window cloak state from DWM + /// (A cloaked window is not visible to the user. But the window is still composed by DWM.) + /// + /// The state (none, app, ...) of the window + internal WindowCloakState GetWindowCloakState() + { + _ = NativeMethods.DwmGetWindowAttribute(Hwnd, (int)DwmWindowAttributes.Cloaked, out int isCloakedState, sizeof(uint)); + + switch (isCloakedState) + { + case (int)DwmWindowCloakStates.None: + return WindowCloakState.None; + case (int)DwmWindowCloakStates.CloakedApp: + return WindowCloakState.App; + case (int)DwmWindowCloakStates.CloakedShell: + return Main.VirtualDesktopHelperInstance.IsWindowCloakedByVirtualDesktopManager(hwnd, Desktop.Id) ? WindowCloakState.OtherDesktop : WindowCloakState.Shell; + case (int)DwmWindowCloakStates.CloakedInherited: + return WindowCloakState.Inherited; + default: + return WindowCloakState.Unknown; + } + } + + /// + /// Enum to simplify the cloak state of the window + /// + internal enum WindowCloakState + { + None, + App, + Shell, + Inherited, + OtherDesktop, + Unknown, + } + + /// + /// Returns the class name of a window. + /// + /// Handle to the window. + /// Class name + private static string GetWindowClassName(IntPtr hwnd) + { + StringBuilder windowClassName = new StringBuilder(300); + var numCharactersWritten = NativeMethods.GetClassName(hwnd, windowClassName, windowClassName.MaxCapacity); + + if (numCharactersWritten == 0) + { + return string.Empty; + } + + return windowClassName.ToString(); + } + + /// + /// Gets an instance of form process cache or creates a new one. A new one will be added to the cache. + /// + /// The handle to the window + /// A new Instance of type + private static WindowProcess CreateWindowProcessInstance(IntPtr hWindow) + { + lock (_handlesToProcessCache) + { + if (_handlesToProcessCache.Count > 7000) + { + Debug.Print("Clearing Process Cache because it's size is " + _handlesToProcessCache.Count); + _handlesToProcessCache.Clear(); + } + + // Add window's process to cache if missing + if (!_handlesToProcessCache.ContainsKey(hWindow)) + { + // Get process ID and name + var processId = WindowProcess.GetProcessIDFromWindowHandle(hWindow); + var threadId = WindowProcess.GetThreadIDFromWindowHandle(hWindow); + var processName = WindowProcess.GetProcessNameFromProcessID(processId); + + if (processName.Length != 0) + { + _handlesToProcessCache.Add(hWindow, new WindowProcess(processId, threadId, processName)); + } + else + { + // For the dwm process we can not receive the name. This is no problem because the window isn't part of result list. + Log.Debug($"Invalid process {processId} ({processName}) for window handle {hWindow}.", typeof(Window)); + _handlesToProcessCache.Add(hWindow, new WindowProcess(0, 0, string.Empty)); + } + } + + // Correct the process data if the window belongs to a uwp app hosted by 'ApplicationFrameHost.exe' + // (This only works if the window isn't minimized. For minimized windows the required child window isn't assigned.) + if (string.Equals(_handlesToProcessCache[hWindow].Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase)) + { + new Task(() => + { + EnumWindowsProc callbackptr = new EnumWindowsProc((IntPtr hwnd, IntPtr lParam) => + { + // Every uwp app main window has at least three child windows. Only the one we are interested in has a class starting with "Windows.UI.Core." and is assigned to the real app process. + // (The other ones have a class name that begins with the string "ApplicationFrame".) + if (GetWindowClassName(hwnd).StartsWith("Windows.UI.Core.", StringComparison.OrdinalIgnoreCase)) + { + var childProcessId = WindowProcess.GetProcessIDFromWindowHandle(hwnd); + var childThreadId = WindowProcess.GetThreadIDFromWindowHandle(hwnd); + var childProcessName = WindowProcess.GetProcessNameFromProcessID(childProcessId); + + // Update process info in cache + _handlesToProcessCache[hWindow].UpdateProcessInfo(childProcessId, childThreadId, childProcessName); + return false; + } + else + { + return true; + } + }); + _ = NativeMethods.EnumChildWindows(hWindow, callbackptr, 0); + }).Start(); + } + + return _handlesToProcessCache[hWindow]; + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/WindowProcess.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/WindowProcess.cs new file mode 100644 index 0000000000..0915b13cbb --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Components/WindowProcess.cs @@ -0,0 +1,244 @@ +// 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.Diagnostics; +using System.Linq; +using System.Text; + +using Wox.Infrastructure; +using Wox.Plugin.Common.Win32; + +namespace Microsoft.Plugin.WindowWalker.Components +{ + /// + /// Represents the process data of an open window. This class is used in the process cache and for the process object of the open window + /// + internal class WindowProcess + { + /// + /// Maximum size of a file name + /// + private const int MaximumFileNameLength = 1000; + + /// + /// An indicator if the window belongs to an 'Universal Windows Platform (UWP)' process + /// + private readonly bool _isUwpApp; + + /// + /// Gets the id of the process + /// + internal uint ProcessID + { + get; private set; + } + + /// + /// Gets a value indicating whether the process is responding or not + /// + internal bool IsResponding + { + get + { + try + { + return Process.GetProcessById((int)ProcessID).Responding; + } + catch (InvalidOperationException) + { + // Thrown when process not exist. + return true; + } + catch (NotSupportedException) + { + // Thrown when process is not running locally. + return true; + } + } + } + + /// + /// Gets the id of the thread + /// + internal uint ThreadID + { + get; private set; + } + + /// + /// Gets the name of the process + /// + internal string Name + { + get; private set; + } + + /// + /// Gets a value indicating whether the window belongs to an 'Universal Windows Platform (UWP)' process + /// + internal bool IsUwpApp + { + get { return _isUwpApp; } + } + + /// + /// Gets a value indicating whether this is the shell process or not + /// The shell process (like explorer.exe) hosts parts of the user interface (like taskbar, start menu, ...) + /// + internal bool IsShellProcess + { + get + { + IntPtr hShellWindow = NativeMethods.GetShellWindow(); + return GetProcessIDFromWindowHandle(hShellWindow) == ProcessID; + } + } + + /// + /// Gets a value indicating whether the process exists on the machine + /// + internal bool DoesExist + { + get + { + try + { + var p = Process.GetProcessById((int)ProcessID); + p.Dispose(); + return true; + } + catch (InvalidOperationException) + { + // Thrown when process not exist. + return false; + } + catch (ArgumentException) + { + // Thrown when process not exist. + return false; + } + } + } + + /// + /// Gets a value indicating whether full access to the process is denied or not + /// + internal bool IsFullAccessDenied + { + get; private set; + } + + /// + /// Initializes a new instance of the class. + /// + /// New process id. + /// New thread id. + /// New process name. + internal WindowProcess(uint pid, uint tid, string name) + { + UpdateProcessInfo(pid, tid, name); + _isUwpApp = string.Equals(Name, "ApplicationFrameHost.exe", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Updates the process information of the instance. + /// + /// New process id. + /// New thread id. + /// New process name. + internal void UpdateProcessInfo(uint pid, uint tid, string name) + { + // TODO: Add verification as to whether the process id and thread id is valid + ProcessID = pid; + ThreadID = tid; + Name = name; + + // Process can be elevated only if process id is not 0 (Dummy value on error) + IsFullAccessDenied = (pid != 0) ? TestProcessAccessUsingAllAccessFlag(pid) : false; + } + + /// + /// Gets the process ID for the window handle + /// + /// The handle to the window + /// The process ID + internal static uint GetProcessIDFromWindowHandle(IntPtr hwnd) + { + _ = NativeMethods.GetWindowThreadProcessId(hwnd, out uint processId); + return processId; + } + + /// + /// Gets the thread ID for the window handle + /// + /// The handle to the window + /// The thread ID + internal static uint GetThreadIDFromWindowHandle(IntPtr hwnd) + { + uint threadId = NativeMethods.GetWindowThreadProcessId(hwnd, out _); + return threadId; + } + + /// + /// Gets the process name for the process ID + /// + /// The id of the process/param> + /// A string representing the process name or an empty string if the function fails + internal static string GetProcessNameFromProcessID(uint pid) + { + IntPtr processHandle = NativeMethods.OpenProcess(ProcessAccessFlags.QueryLimitedInformation, true, (int)pid); + StringBuilder processName = new StringBuilder(MaximumFileNameLength); + + if (NativeMethods.GetProcessImageFileName(processHandle, processName, MaximumFileNameLength) != 0) + { + _ = Win32Helpers.CloseHandleIfNotNull(processHandle); + return processName.ToString().Split('\\').Reverse().ToArray()[0]; + } + else + { + _ = Win32Helpers.CloseHandleIfNotNull(processHandle); + return string.Empty; + } + } + + /// + /// Kills the process by it's id. If permissions are required, they will be requested. + /// + /// Kill process and sub processes. + internal void KillThisProcess(bool killProcessTree) + { + if (IsFullAccessDenied) + { + string killTree = killProcessTree ? " /t" : string.Empty; + Helper.OpenInShell("taskkill.exe", $"/pid {(int)ProcessID} /f{killTree}", null, Helper.ShellRunAsType.Administrator, true); + } + else + { + Process.GetProcessById((int)ProcessID).Kill(killProcessTree); + } + } + + /// + /// Gets a boolean value indicating whether the access to a process using the AllAccess flag is denied or not. + /// + /// The process ID of the process + /// True if denied and false if not. + private static bool TestProcessAccessUsingAllAccessFlag(uint pid) + { + IntPtr processHandle = NativeMethods.OpenProcess(ProcessAccessFlags.AllAccess, true, (int)pid); + + if (Win32Helpers.GetLastError() == 5) + { + // Error 5 = ERROR_ACCESS_DENIED + _ = Win32Helpers.CloseHandleIfNotNull(processHandle); + return true; + } + else + { + _ = Win32Helpers.CloseHandleIfNotNull(processHandle); + return false; + } + } + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs index b6948eb32c..d6c3fd4b4d 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Helpers/SettingsManager.cs @@ -19,13 +19,15 @@ public class SettingsManager private readonly string _filePath; private readonly Settings _settings = new(); + private static SettingsManager? instance; + private readonly ToggleSetting _resultsFromVisibleDesktopOnly = new(nameof(ResultsFromVisibleDesktopOnly), Resources.wox_plugin_windowwalker_SettingResultsVisibleDesktop, Resources.wox_plugin_windowwalker_SettingResultsVisibleDesktop, false); private readonly ToggleSetting _subtitleShowPid = new(nameof(SubtitleShowPid), Resources.wox_plugin_windowwalker_SettingSubtitlePid, Resources.wox_plugin_windowwalker_SettingSubtitlePid, false); private readonly ToggleSetting _subtitleShowDesktopName = new(nameof(SubtitleShowDesktopName), Resources.wox_plugin_windowwalker_SettingSubtitleDesktopName, Resources.wox_plugin_windowwalker_SettingSubtitleDesktopName_Description, true); private readonly ToggleSetting _confirmKillProcess = new(nameof(ConfirmKillProcess), Resources.wox_plugin_windowwalker_SettingConfirmKillProcess, Resources.wox_plugin_windowwalker_SettingConfirmKillProcess, true); private readonly ToggleSetting _killProcessTree = new(nameof(KillProcessTree), Resources.wox_plugin_windowwalker_SettingKillProcessTree, Resources.wox_plugin_windowwalker_SettingKillProcessTree_Description, false); private readonly ToggleSetting _openAfterKillAndClose = new(nameof(OpenAfterKillAndClose), Resources.wox_plugin_windowwalker_SettingOpenAfterKillAndClose, Resources.wox_plugin_windowwalker_SettingOpenAfterKillAndClose_Description, false); - private readonly ToggleSetting _hideKillProcessOnElevatedProcess = new(nameof(HideKillProcessOnElevatedProcess), Resources.wox_plugin_windowwalker_SettingHideKillProcess, Resources.wox_plugin_windowwalker_SettingHideKillProcess, false); + private readonly ToggleSetting _hideKillProcessOnElevatedProcesses = new(nameof(HideKillProcessOnElevatedProcesses), Resources.wox_plugin_windowwalker_SettingHideKillProcess, Resources.wox_plugin_windowwalker_SettingHideKillProcess, false); private readonly ToggleSetting _hideExplorerSettingInfo = new(nameof(HideExplorerSettingInfo), Resources.wox_plugin_windowwalker_SettingExplorerSettingInfo, Resources.wox_plugin_windowwalker_SettingExplorerSettingInfo_Description, false); public bool ResultsFromVisibleDesktopOnly => _resultsFromVisibleDesktopOnly.Value; @@ -40,7 +42,7 @@ public class SettingsManager public bool OpenAfterKillAndClose => _openAfterKillAndClose.Value; - public bool HideKillProcessOnElevatedProcess => _hideKillProcessOnElevatedProcess.Value; + public bool HideKillProcessOnElevatedProcesses => _hideKillProcessOnElevatedProcesses.Value; public bool HideExplorerSettingInfo => _hideExplorerSettingInfo.Value; @@ -79,6 +81,15 @@ public class SettingsManager LoadSettings(); } + internal static SettingsManager Instance + { + get + { + instance ??= new SettingsManager(); + return instance; + } + } + public Settings GetSettings() { return _settings; diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Pages/SettingsPage.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Pages/SettingsPage.cs new file mode 100644 index 0000000000..f0d1f06d81 --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/Pages/SettingsPage.cs @@ -0,0 +1,37 @@ +// 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 Microsoft.CmdPal.Ext.WindowWalker.Helpers; +using Microsoft.CmdPal.Ext.WindowWalker.Properties; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace Microsoft.CmdPal.Ext.WindowWalker.Pages; + +internal sealed partial class SettingsPage : FormPage +{ + private readonly Settings _settings; + private readonly SettingsManager _settingsManager; + + public override IForm[] Forms() + { + var s = _settings.ToForms(); + return s; + } + + public SettingsPage(SettingsManager settingsManager) + { + Name = "Settings Page NEED TO MAKE RESOURCES FOR THIS"; + Icon = new("\uE713"); // Settings icon + _settings = settingsManager.GetSettings(); + _settingsManager = settingsManager; + + _settings.SettingsChanged += SettingsChanged; + } + + private void SettingsChanged(object sender, Settings args) + { + _settingsManager.SaveSettings(); + } +} diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/WalkerTopLevelCommandItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/WalkerTopLevelCommandItem.cs index 88033f9020..00fdecaf15 100644 --- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/WalkerTopLevelCommandItem.cs +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/WalkerTopLevelCommandItem.cs @@ -6,6 +6,7 @@ using System; using System.Data; using System.IO; using Microsoft.CmdPal.Ext.WindowWalker.Helpers; +using Microsoft.CmdPal.Ext.WindowWalker.Pages; using Microsoft.CmdPal.Ext.WindowWalker.Properties; using Microsoft.CmdPal.Extensions; using Microsoft.CmdPal.Extensions.Helpers; @@ -19,5 +20,8 @@ public partial class WalkerTopLevelCommandItem : CommandItem { Title = Resources.window_walker_top_level_command_title; Subtitle = Resources.wox_plugin_windowwalker_plugin_name; + MoreCommands = [ + new CommandContextItem(new SettingsPage(settingsManager)), + ]; } } diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/WindowWalkerListItem.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/WindowWalkerListItem.cs new file mode 100644 index 0000000000..73a575c27e --- /dev/null +++ b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.WindowWalker/WindowWalkerListItem.cs @@ -0,0 +1,26 @@ +// 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.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.WindowWalker.Components; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace Microsoft.CmdPal.Ext.WindowWalker; + +internal partial class WindowWalkerListItem : ListItem +{ + private readonly Window _window; + + public Window Window => _window; + + public WindowWalkerListItem(Window window) + : base(new NoOpCommand()) + { + _window = window; + } +}