mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
[PT Run] WindowWalker: Refactor code, fix some bugs, hide UWP non-windows, prepare code for new features (#15441)
* Import files from old PR #15329 * Improvements * hide uwp non-windows (#13637) * update debug tool tip * fix spelling and comments * disable tool tip * fix doc links * remove obsolete using * Update docs * fix spelling * rename elevation property and test method * Add property <DoesExist> to WindowProcess class * Close process handles correctly if not used anymore * cleanup coed * fix bug with sticky notes process * add window class to tool tip * small change * make nativeMethods static class * fix broken uwpApp property of WindowProcess class * rename method * Revert making NativeMethods class static. It contains instance members. * improve loggign * fix merge mistakes * fixes * remove obsolete delegate * Improve SearchController to speed up search (#15561) * add <IsShellProcess> property to <WindowProcess> class * reorder code * disable debug tool tip * Update devdocs * remove obsolete event handler * update var name
This commit is contained in:
1
.github/actions/spell-check/expect.txt
vendored
1
.github/actions/spell-check/expect.txt
vendored
@@ -723,6 +723,7 @@ HOLDESC
|
|||||||
homepage
|
homepage
|
||||||
homljgmgpmcbpjbnjpfijnhipfkiclkd
|
homljgmgpmcbpjbnjpfijnhipfkiclkd
|
||||||
HOOKPROC
|
HOOKPROC
|
||||||
|
Hostbackdropbrush
|
||||||
hostname
|
hostname
|
||||||
hotkeycontrol
|
hotkeycontrol
|
||||||
hotkeys
|
hotkeys
|
||||||
|
|||||||
@@ -3,16 +3,19 @@ The window walker plugin matches the user entered query with the open windows on
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
### [`OpenWindows.cs`](src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/OpenWindows.cs)
|
### [`OpenWindows.cs`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/OpenWindows.cs)
|
||||||
- The window walker plugin uses the `EnumWindows` function to enumerate all the open windows in the [`OpenWindows.cs`](src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/OpenWindows.cs) class.
|
- The window walker plugin uses the `EnumWindows` function to enumerate all the open windows in the [`OpenWindows.cs`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/OpenWindows.cs) class.
|
||||||
|
|
||||||
|
### [`SearchController.cs`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/SearchController.cs)
|
||||||
|
- The [`SearchController`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/SearchController.cs) encapsulates the functions needed to search and find matches.
|
||||||
|
- It is responsible for updating the search text and performing a fuzzy search on all the open windows.
|
||||||
|
|
||||||
### [`SearchController.cs`](src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/SearchController.cs)
|
### [`Window.cs`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/Window.cs)
|
||||||
- The [`SearchController`](src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/SearchController.cs) encapsulates the functions needed to search and find matches.
|
- The [`Window`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/Window.cs) class represents a specific window and has functions to get the name of the window, the state of the window (whether it is visible or not), and the `SwitchTowindow` function which switches the desktop focus to the selected window. This action is performed when the user clicks on a window walker plugin result.
|
||||||
- It is responsible for updating the search text and performing a fuzzy search on all the open windows in an asynchronous manner.
|
- The `Window` class holds a static cache with the process information of all windows we know so far and each window instance has a property which holds its process information (name, file, ...). The process data in the cache and the window property are of the type `WindowProcess`.
|
||||||
|
|
||||||
### [`Window.cs`](src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/Window.cs)
|
### [`WindowProcess.cs`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/WindowProcess.cs)
|
||||||
- The [`Window`](src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/Window.cs) class represents a specific window and has functions to get the name of the process, the state of the window (whether it is visible or not), and the `SwitchTowindow` function which switches the desktop focus to the selected window. This action is performed when the user clicks on a window walker plugin result.
|
- The [`WindowProcess`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/WindowProcess.cs) class represents a specific process for a window. It contains static methods to query process information from the system. And it contains instance methods and properties to hold/retrieve the process information we want to know about a window's process.
|
||||||
|
|
||||||
### Score
|
### Score
|
||||||
The window walker plugin uses [`FuzzyMatching`](src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/FuzzyMatching.cs) to get the matching indices and calculates the score by creating a 2 dimensional array of the window and the query text.
|
The window walker plugin uses [`FuzzyMatching`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/FuzzyMatching.cs) to get the matching indices and calculates the score by creating a 2 dimensional array of the window and the query text.
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Window attribute
|
/// DWM window attribute (Windows 7 and earlier: The values between ExcludedFromPeek and Last aren't supported.)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Flags]
|
[Flags]
|
||||||
public enum DwmWindowAttribute
|
public enum DwmWindowAttribute
|
||||||
@@ -266,9 +266,32 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
HasIconicBitmap,
|
HasIconicBitmap,
|
||||||
DisallowPeek,
|
DisallowPeek,
|
||||||
ExcludedFromPeek,
|
ExcludedFromPeek,
|
||||||
|
Cloak,
|
||||||
|
Cloaked,
|
||||||
|
FreezeRepresentation,
|
||||||
|
PassiveUpdateMode,
|
||||||
|
UseHostbackdropbrush,
|
||||||
|
UseImmersiveDarkMode,
|
||||||
|
WindowCornerPreference,
|
||||||
|
BorderColor,
|
||||||
|
CaptionColor,
|
||||||
|
TextColor,
|
||||||
|
VisibleFrameBorderThickness,
|
||||||
Last,
|
Last,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flags for describing the window cloak state (Windows 7 and earlier: This value is not supported.)
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum DwmWindowCloakState
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
CloakedApp = 1,
|
||||||
|
CloakedShell = 2,
|
||||||
|
CloakedInherited = 4,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Flags for accessing the process in trying to get icon for the process
|
/// Flags for accessing the process in trying to get icon for the process
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -869,7 +892,7 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
[DllImport("user32.dll", SetLastError = true, BestFitMapping = false)]
|
[DllImport("user32.dll", SetLastError = true, BestFitMapping = false)]
|
||||||
public static extern IntPtr GetProp(IntPtr hWnd, string lpString);
|
public static extern IntPtr GetProp(IntPtr hWnd, string lpString);
|
||||||
|
|
||||||
[DllImport("kernel32.dll")]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
public static extern IntPtr OpenProcess(ProcessAccessFlags dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, int dwProcessId);
|
public static extern IntPtr OpenProcess(ProcessAccessFlags dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, int dwProcessId);
|
||||||
|
|
||||||
[DllImport("dwmapi.dll", EntryPoint = "#113", CallingConvention = CallingConvention.StdCall)]
|
[DllImport("dwmapi.dll", EntryPoint = "#113", CallingConvention = CallingConvention.StdCall)]
|
||||||
@@ -890,5 +913,37 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
public static extern int SendMessage(IntPtr hWnd, int msg, int wParam);
|
public static extern int SendMessage(IntPtr hWnd, int msg, int wParam);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public static extern bool CloseHandle(IntPtr hObject);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern IntPtr GetShellWindow();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the last Win32 Error code thrown by a native method if enabled for this method.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The error code as int value.</returns>
|
||||||
|
public static int GetLastWin32Error()
|
||||||
|
{
|
||||||
|
return Marshal.GetLastWin32Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate that the handle is not null and close it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="handle">Handle to close.</param>
|
||||||
|
/// <returns>Zero if native method fails and nonzero if the native method succeeds.</returns>
|
||||||
|
public static bool CloseHandleIfNotNull(IntPtr handle)
|
||||||
|
{
|
||||||
|
if (handle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
// Return true if there is nothing to close.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CloseHandle(handle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,6 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly string _powerLauncherExe = Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName);
|
private static readonly string _powerLauncherExe = Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Delegate handler for open windows updates
|
|
||||||
/// </summary>
|
|
||||||
public delegate void OpenWindowsUpdateEventHandler(object sender, SearchController.SearchResultUpdateEventArgs e);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// List of all the open windows
|
/// List of all the open windows
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -93,9 +88,14 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
|
|
||||||
if (newWindow.IsWindow && newWindow.Visible && newWindow.IsOwner &&
|
if (newWindow.IsWindow && newWindow.Visible && newWindow.IsOwner &&
|
||||||
(!newWindow.IsToolWindow || newWindow.IsAppWindow) && !newWindow.TaskListDeleted &&
|
(!newWindow.IsToolWindow || newWindow.IsAppWindow) && !newWindow.TaskListDeleted &&
|
||||||
newWindow.ClassName != "Windows.UI.Core.CoreWindow" && newWindow.ProcessName != _powerLauncherExe)
|
newWindow.ClassName != "Windows.UI.Core.CoreWindow" && newWindow.ProcessInfo.Name != _powerLauncherExe)
|
||||||
{
|
{
|
||||||
windows.Add(newWindow);
|
// To hide (not add) preloaded uwp app windows that are invisible to the user we check the cloak state in DWM to be "none". (Issue #13637.)
|
||||||
|
// (If user asking to see these windows again we can add an optional plugin setting in the future.)
|
||||||
|
if (!newWindow.IsCloaked)
|
||||||
|
{
|
||||||
|
windows.Add(newWindow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.Plugin.WindowWalker.Components
|
namespace Microsoft.Plugin.WindowWalker.Components
|
||||||
{
|
{
|
||||||
@@ -24,7 +23,7 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Open window search results
|
/// Open window search results
|
||||||
/// </summary
|
/// </summary>
|
||||||
private List<SearchResult> searchMatches;
|
private List<SearchResult> searchMatches;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -32,16 +31,6 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static SearchController instance;
|
private static SearchController instance;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Delegate handler for open windows updates
|
|
||||||
/// </summary>
|
|
||||||
public delegate void SearchResultUpdateEventHandler(object sender, SearchResultUpdateEventArgs e);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event raised when there is an update to the list of open windows
|
|
||||||
/// </summary>
|
|
||||||
public event SearchResultUpdateEventHandler OnSearchResultUpdateEventHandler;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the current search text
|
/// Gets or sets the current search text
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -95,16 +84,16 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event handler for when the search text has been updated
|
/// Event handler for when the search text has been updated
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task UpdateSearchText(string searchText)
|
public void UpdateSearchText(string searchText)
|
||||||
{
|
{
|
||||||
SearchText = searchText;
|
SearchText = searchText;
|
||||||
await SyncOpenWindowsWithModelAsync().ConfigureAwait(false);
|
SyncOpenWindowsWithModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Syncs the open windows with the OpenWindows Model
|
/// Syncs the open windows with the OpenWindows Model
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task SyncOpenWindowsWithModelAsync()
|
public void SyncOpenWindowsWithModel()
|
||||||
{
|
{
|
||||||
System.Diagnostics.Debug.Print("Syncing WindowSearch result with OpenWindows Model");
|
System.Diagnostics.Debug.Print("Syncing WindowSearch result with OpenWindows Model");
|
||||||
|
|
||||||
@@ -116,22 +105,8 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
searchMatches = await FuzzySearchOpenWindowsAsync(snapshotOfOpenWindows).ConfigureAwait(false);
|
searchMatches = FuzzySearchOpenWindows(snapshotOfOpenWindows);
|
||||||
}
|
}
|
||||||
|
|
||||||
OnSearchResultUpdateEventHandler?.Invoke(this, new SearchResultUpdateEventArgs());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Redirecting method for Fuzzy searching
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="openWindows">what windows are open</param>
|
|
||||||
/// <returns>Returns search results</returns>
|
|
||||||
private Task<List<SearchResult>> FuzzySearchOpenWindowsAsync(List<Window> openWindows)
|
|
||||||
{
|
|
||||||
return Task.Run(
|
|
||||||
() =>
|
|
||||||
FuzzySearchOpenWindows(openWindows));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -151,7 +126,7 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
foreach (var window in openWindows)
|
foreach (var window in openWindows)
|
||||||
{
|
{
|
||||||
var titleMatch = FuzzyMatching.FindBestFuzzyMatch(window.Title, searchString.SearchText);
|
var titleMatch = FuzzyMatching.FindBestFuzzyMatch(window.Title, searchString.SearchText);
|
||||||
var processMatch = FuzzyMatching.FindBestFuzzyMatch(window.ProcessName, searchString.SearchText);
|
var processMatch = FuzzyMatching.FindBestFuzzyMatch(window.ProcessInfo.Name, searchString.SearchText);
|
||||||
|
|
||||||
if ((titleMatch.Count != 0 || processMatch.Count != 0) &&
|
if ((titleMatch.Count != 0 || processMatch.Count != 0) &&
|
||||||
window.Title.Length != 0)
|
window.Title.Length != 0)
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Wox.Plugin.Logger;
|
||||||
|
|
||||||
namespace Microsoft.Plugin.WindowWalker.Components
|
namespace Microsoft.Plugin.WindowWalker.Components
|
||||||
{
|
{
|
||||||
@@ -18,22 +18,22 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Window
|
public class Window
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Maximum size of a file name
|
|
||||||
/// </summary>
|
|
||||||
private const int MaximumFileNameLength = 1000;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The list of owners of a window so that we don't have to
|
|
||||||
/// constantly query for the process owning a specific window
|
|
||||||
/// </summary>
|
|
||||||
private static readonly Dictionary<IntPtr, string> _handlesToProcessCache = new Dictionary<IntPtr, string>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The handle to the window
|
/// The handle to the window
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IntPtr hwnd;
|
private readonly IntPtr hwnd;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A static cache for the process data of all known windows
|
||||||
|
/// that we don't have to query the data every time
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Dictionary<IntPtr, WindowProcess> _handlesToProcessCache = new Dictionary<IntPtr, WindowProcess>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An instance of <see cref="WindowProcess"/> that contains the process information for the window
|
||||||
|
/// </summary>
|
||||||
|
private readonly WindowProcess processInfo;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the title of the window (the string displayed at the top of the window)
|
/// Gets the title of the window (the string displayed at the top of the window)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -68,63 +68,12 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
get { return hwnd; }
|
get { return hwnd; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public uint ProcessID { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the name of the process
|
/// Gets the object of with the process information of the window
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ProcessName
|
public WindowProcess ProcessInfo
|
||||||
{
|
{
|
||||||
get
|
get { return processInfo; }
|
||||||
{
|
|
||||||
lock (_handlesToProcessCache)
|
|
||||||
{
|
|
||||||
if (_handlesToProcessCache.Count > 7000)
|
|
||||||
{
|
|
||||||
Debug.Print("Clearing Process Cache because it's size is " + _handlesToProcessCache.Count);
|
|
||||||
_handlesToProcessCache.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_handlesToProcessCache.ContainsKey(Hwnd))
|
|
||||||
{
|
|
||||||
var processName = GetProcessNameFromWindowHandle(Hwnd);
|
|
||||||
|
|
||||||
if (processName.Length != 0)
|
|
||||||
{
|
|
||||||
_handlesToProcessCache.Add(
|
|
||||||
Hwnd,
|
|
||||||
processName.ToString().Split('\\').Reverse().ToArray()[0]);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_handlesToProcessCache.Add(Hwnd, string.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_handlesToProcessCache[hwnd].ToUpperInvariant() == "APPLICATIONFRAMEHOST.EXE")
|
|
||||||
{
|
|
||||||
new Task(() =>
|
|
||||||
{
|
|
||||||
NativeMethods.CallBackPtr callbackptr = new NativeMethods.CallBackPtr((IntPtr hwnd, IntPtr lParam) =>
|
|
||||||
{
|
|
||||||
var childProcessId = GetProcessIDFromWindowHandle(hwnd);
|
|
||||||
if (childProcessId != ProcessID)
|
|
||||||
{
|
|
||||||
_handlesToProcessCache[Hwnd] = GetProcessNameFromWindowHandle(hwnd);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
_ = NativeMethods.EnumChildWindows(Hwnd, callbackptr, 0);
|
|
||||||
}).Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _handlesToProcessCache[hwnd];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -134,15 +83,7 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
StringBuilder windowClassName = new StringBuilder(300);
|
return GetWindowClassName(Hwnd);
|
||||||
var numCharactersWritten = NativeMethods.GetClassName(Hwnd, windowClassName, windowClassName.MaxCapacity);
|
|
||||||
|
|
||||||
if (numCharactersWritten == 0)
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return windowClassName.ToString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +98,18 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCloaked
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return GetWindowCloakState() != WindowCloakState.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether the specified window handle identifies an existing window.
|
/// Gets a value indicating whether the specified window handle identifies an existing window.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -236,6 +189,7 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
{
|
{
|
||||||
// TODO: Add verification as to whether the window handle is valid
|
// TODO: Add verification as to whether the window handle is valid
|
||||||
this.hwnd = hwnd;
|
this.hwnd = hwnd;
|
||||||
|
processInfo = CreateWindowProcessInstance(hwnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -248,7 +202,7 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
// to use ShowWindow for switching tabs in IE
|
// to use ShowWindow for switching tabs in IE
|
||||||
// 2) SetForegroundWindow fails on minimized windows
|
// 2) SetForegroundWindow fails on minimized windows
|
||||||
// Using Ordinal since this is internal
|
// Using Ordinal since this is internal
|
||||||
if (ProcessName.ToUpperInvariant().Equals("IEXPLORE.EXE", StringComparison.Ordinal) || !Minimized)
|
if (processInfo.Name.ToUpperInvariant().Equals("IEXPLORE.EXE", StringComparison.Ordinal) || !Minimized)
|
||||||
{
|
{
|
||||||
NativeMethods.SetForegroundWindow(Hwnd);
|
NativeMethods.SetForegroundWindow(Hwnd);
|
||||||
}
|
}
|
||||||
@@ -271,7 +225,7 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
// Using CurrentCulture since this is user facing
|
// Using CurrentCulture since this is user facing
|
||||||
return Title + " (" + ProcessName.ToUpper(CultureInfo.CurrentCulture) + ")";
|
return Title + " (" + processInfo.Name.ToUpper(CultureInfo.CurrentCulture) + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -309,36 +263,125 @@ namespace Microsoft.Plugin.WindowWalker.Components
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the name of the process using the window handle
|
/// Returns the window cloak state from DWM
|
||||||
|
/// (A cloaked window is not visible to the user. But the window is still composed by DWM.)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="hwnd">The handle to the window</param>
|
/// <returns>The state (none, app, ...) of the window</returns>
|
||||||
/// <returns>A string representing the process name or an empty string if the function fails</returns>
|
public WindowCloakState GetWindowCloakState()
|
||||||
private string GetProcessNameFromWindowHandle(IntPtr hwnd)
|
|
||||||
{
|
{
|
||||||
uint processId = GetProcessIDFromWindowHandle(hwnd);
|
_ = NativeMethods.DwmGetWindowAttribute(Hwnd, (int)NativeMethods.DwmWindowAttribute.Cloaked, out int isCloakedState, sizeof(uint));
|
||||||
ProcessID = processId;
|
|
||||||
IntPtr processHandle = NativeMethods.OpenProcess(NativeMethods.ProcessAccessFlags.QueryLimitedInformation, true, (int)processId);
|
|
||||||
StringBuilder processName = new StringBuilder(MaximumFileNameLength);
|
|
||||||
|
|
||||||
if (NativeMethods.GetProcessImageFileName(processHandle, processName, MaximumFileNameLength) != 0)
|
switch (isCloakedState)
|
||||||
{
|
{
|
||||||
return processName.ToString().Split('\\').Reverse().ToArray()[0];
|
case (int)NativeMethods.DwmWindowCloakState.None:
|
||||||
}
|
return WindowCloakState.None;
|
||||||
else
|
case (int)NativeMethods.DwmWindowCloakState.CloakedApp:
|
||||||
{
|
return WindowCloakState.App;
|
||||||
return string.Empty;
|
case (int)NativeMethods.DwmWindowCloakState.CloakedShell:
|
||||||
|
return WindowCloakState.Shell;
|
||||||
|
case (int)NativeMethods.DwmWindowCloakState.CloakedInherited:
|
||||||
|
return WindowCloakState.Inherited;
|
||||||
|
default:
|
||||||
|
return WindowCloakState.Unknown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the process ID for the Window handle
|
/// Enum to simplify the cloak state of the window
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="hwnd">The handle to the window</param>
|
public enum WindowCloakState
|
||||||
/// <returns>The process ID</returns>
|
|
||||||
private static uint GetProcessIDFromWindowHandle(IntPtr hwnd)
|
|
||||||
{
|
{
|
||||||
_ = NativeMethods.GetWindowThreadProcessId(hwnd, out uint processId);
|
None,
|
||||||
return processId;
|
App,
|
||||||
|
Shell,
|
||||||
|
Inherited,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the class name of a window.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hwnd">Handle to the window.</param>
|
||||||
|
/// <returns>Class name</returns>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an instance of <see cref="WindowProcess"/> form process cache or creates a new one. A new one will be added to the cache.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hWindow">The handle to the window</param>
|
||||||
|
/// <returns>A new Instance of type <see cref="WindowProcess"/></returns>
|
||||||
|
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 (_handlesToProcessCache[hWindow].Name.ToUpperInvariant() == "APPLICATIONFRAMEHOST.EXE")
|
||||||
|
{
|
||||||
|
new Task(() =>
|
||||||
|
{
|
||||||
|
NativeMethods.CallBackPtr callbackptr = new NativeMethods.CallBackPtr((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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.Plugin.WindowWalker.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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
|
||||||
|
/// </summary>
|
||||||
|
public class WindowProcess
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum size of a file name
|
||||||
|
/// </summary>
|
||||||
|
private const int MaximumFileNameLength = 1000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An indicator if the window belongs to an 'Universal Windows Platform (UWP)' process
|
||||||
|
/// </summary>
|
||||||
|
private readonly bool _isUwpApp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the id of the process
|
||||||
|
/// </summary>
|
||||||
|
public uint ProcessID
|
||||||
|
{
|
||||||
|
get; private set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the id of the thread
|
||||||
|
/// </summary>
|
||||||
|
public uint ThreadID
|
||||||
|
{
|
||||||
|
get; private set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the process
|
||||||
|
/// </summary>
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get; private set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the window belongs to an 'Universal Windows Platform (UWP)' process
|
||||||
|
/// </summary>
|
||||||
|
public bool IsUwpApp
|
||||||
|
{
|
||||||
|
get { return _isUwpApp; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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, ...)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsShellProcess
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
IntPtr hShellWindow = NativeMethods.GetShellWindow();
|
||||||
|
return GetProcessIDFromWindowHandle(hShellWindow) == ProcessID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the process exists on the machine
|
||||||
|
/// </summary>
|
||||||
|
public 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether full access to the process is denied or not
|
||||||
|
/// </summary>
|
||||||
|
public bool IsFullAccessDenied
|
||||||
|
{
|
||||||
|
get; private set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="WindowProcess"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pid">New process id.</param>
|
||||||
|
/// <param name="tid">New thread id.</param>
|
||||||
|
/// <param name="name">New process name.</param>
|
||||||
|
public WindowProcess(uint pid, uint tid, string name)
|
||||||
|
{
|
||||||
|
UpdateProcessInfo(pid, tid, name);
|
||||||
|
_isUwpApp = Name.ToUpperInvariant().Equals("APPLICATIONFRAMEHOST.EXE", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the process information of the <see cref="WindowProcess"/> instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pid">New process id.</param>
|
||||||
|
/// <param name="tid">New thread id.</param>
|
||||||
|
/// <param name="name">New process name.</param>
|
||||||
|
public void UpdateProcessInfo(uint pid, uint tid, string name)
|
||||||
|
{
|
||||||
|
// TODO: Add verification as to wether 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the process ID for the window handle
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hwnd">The handle to the window</param>
|
||||||
|
/// <returns>The process ID</returns>
|
||||||
|
public static uint GetProcessIDFromWindowHandle(IntPtr hwnd)
|
||||||
|
{
|
||||||
|
_ = NativeMethods.GetWindowThreadProcessId(hwnd, out uint processId);
|
||||||
|
return processId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the thread ID for the window handle
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hwnd">The handle to the window</param>
|
||||||
|
/// <returns>The thread ID</returns>
|
||||||
|
public static uint GetThreadIDFromWindowHandle(IntPtr hwnd)
|
||||||
|
{
|
||||||
|
uint threadId = NativeMethods.GetWindowThreadProcessId(hwnd, out _);
|
||||||
|
return threadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the process name for the process ID
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pid">The id of the process/param>
|
||||||
|
/// <returns>A string representing the process name or an empty string if the function fails</returns>
|
||||||
|
public static string GetProcessNameFromProcessID(uint pid)
|
||||||
|
{
|
||||||
|
IntPtr processHandle = NativeMethods.OpenProcess(NativeMethods.ProcessAccessFlags.QueryLimitedInformation, true, (int)pid);
|
||||||
|
StringBuilder processName = new StringBuilder(MaximumFileNameLength);
|
||||||
|
|
||||||
|
if (NativeMethods.GetProcessImageFileName(processHandle, processName, MaximumFileNameLength) != 0)
|
||||||
|
{
|
||||||
|
_ = NativeMethods.CloseHandleIfNotNull(processHandle);
|
||||||
|
return processName.ToString().Split('\\').Reverse().ToArray()[0];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_ = NativeMethods.CloseHandleIfNotNull(processHandle);
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a boolean value indicating whether the access to a process using the AllAccess flag is denied or not.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pid">The process ID of the process</param>
|
||||||
|
/// <returns>True if denied and false if not.</returns>
|
||||||
|
private static bool TestProcessAccessUsingAllAccessFlag(uint pid)
|
||||||
|
{
|
||||||
|
IntPtr processHandle = NativeMethods.OpenProcess(NativeMethods.ProcessAccessFlags.AllAccess, true, (int)pid);
|
||||||
|
|
||||||
|
if (NativeMethods.GetLastWin32Error() == 5)
|
||||||
|
{
|
||||||
|
// Error 5 = ERROR_ACCESS_DENIED
|
||||||
|
_ = NativeMethods.CloseHandleIfNotNull(processHandle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_ = NativeMethods.CloseHandleIfNotNull(processHandle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,8 +13,6 @@ namespace Microsoft.Plugin.WindowWalker
|
|||||||
{
|
{
|
||||||
public class Main : IPlugin, IPluginI18n
|
public class Main : IPlugin, IPluginI18n
|
||||||
{
|
{
|
||||||
private static List<SearchResult> _results = new List<SearchResult>();
|
|
||||||
|
|
||||||
private string IconPath { get; set; }
|
private string IconPath { get; set; }
|
||||||
|
|
||||||
private PluginInitContext Context { get; set; }
|
private PluginInitContext Context { get; set; }
|
||||||
@@ -25,7 +23,6 @@ namespace Microsoft.Plugin.WindowWalker
|
|||||||
|
|
||||||
static Main()
|
static Main()
|
||||||
{
|
{
|
||||||
SearchController.Instance.OnSearchResultUpdateEventHandler += SearchResultUpdated;
|
|
||||||
OpenWindows.Instance.UpdateOpenWindowsList();
|
OpenWindows.Instance.UpdateOpenWindowsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,18 +34,22 @@ namespace Microsoft.Plugin.WindowWalker
|
|||||||
}
|
}
|
||||||
|
|
||||||
OpenWindows.Instance.UpdateOpenWindowsList();
|
OpenWindows.Instance.UpdateOpenWindowsList();
|
||||||
SearchController.Instance.UpdateSearchText(query.Search).Wait();
|
SearchController.Instance.UpdateSearchText(query.Search);
|
||||||
|
List<SearchResult> searchControllerResults = SearchController.Instance.SearchMatches;
|
||||||
|
|
||||||
return _results.Select(x => new Result()
|
return searchControllerResults.Select(x => new Result()
|
||||||
{
|
{
|
||||||
Title = x.Result.Title,
|
Title = x.Result.Title,
|
||||||
IcoPath = IconPath,
|
IcoPath = IconPath,
|
||||||
SubTitle = Properties.Resources.wox_plugin_windowwalker_running + ": " + x.Result.ProcessName,
|
SubTitle = Properties.Resources.wox_plugin_windowwalker_running + ": " + x.Result.ProcessInfo.Name,
|
||||||
Action = c =>
|
Action = c =>
|
||||||
{
|
{
|
||||||
x.Result.SwitchToWindow();
|
x.Result.SwitchToWindow();
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// For debugging you can remove the comment sign in the next line.
|
||||||
|
// ToolTipData = new ToolTipData(x.Result.Title, $"hWnd: {x.Result.Hwnd}\nWindow class: {x.Result.ClassName}\nProcess ID: {x.Result.ProcessInfo.ProcessID}\nThread ID: {x.Result.ProcessInfo.ThreadID}\nProcess: {x.Result.ProcessInfo.Name}\nProcess exists: {x.Result.ProcessInfo.DoesExist}\nIs full access denied: {x.Result.ProcessInfo.IsFullAccessDenied}\nIs uwp app: {x.Result.ProcessInfo.IsUwpApp}\nIs ShellProcess: {x.Result.ProcessInfo.IsShellProcess}\nIs window cloaked: {x.Result.IsCloaked}\nWindow cloak state: {x.Result.GetWindowCloakState()}"),
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,10 +87,5 @@ namespace Microsoft.Plugin.WindowWalker
|
|||||||
{
|
{
|
||||||
return Properties.Resources.wox_plugin_windowwalker_plugin_description;
|
return Properties.Resources.wox_plugin_windowwalker_plugin_description;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SearchResultUpdated(object sender, SearchController.SearchResultUpdateEventArgs e)
|
|
||||||
{
|
|
||||||
_results = SearchController.Instance.SearchMatches;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user