Skip to content

Commit

Permalink
[PT Run] VirtualDesktopHelper & WindowWalker improvements (#16325)
Browse files Browse the repository at this point in the history
* Import vdh from poc

* last changes

* push changes

* small change

* add error handling to vdh

* last changes

* make spellchecker happy

* last changes

* last changes

* spell check

* fix settings defaults

* Improve WindowWalkerSettings class

* add comment

* New settings and improvements

* new features

* subtitle and tool tip

* spell fixes

* small fixes

* fixes

* Explorer info

* spell fixes

* fixes and CloseWindow feature

* last changes

* first part of implementing KillProcess

* killProcess Part 2 & Fixes

* text fix and installer

* update access modifiers

* some fixes

* update dev docs

* fix dev docs

* dev doc change

* dev docs: add missed infos

* dev docs: add link

* Update src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/WindowWalkerSettings.cs

* fix build

* resolve feedback

* fix settings

* add tests
  • Loading branch information
htcfreek authored Mar 7, 2022
1 parent 2761159 commit e8363a3
Show file tree
Hide file tree
Showing 33 changed files with 1,765 additions and 104 deletions.
3 changes: 3 additions & 0 deletions .github/actions/spell-check/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ cursorpos
customaction
CUSTOMACTIONTEST
cvd
CVirtual
cwchar
cwd
cxfksword
Expand Down Expand Up @@ -1829,6 +1830,7 @@ RTLREADING
ruleset
RUNACTIVEXCTLS
runas
rundll
rungameid
RUNLEVEL
runsettings
Expand Down Expand Up @@ -2285,6 +2287,7 @@ vcredist
VCRT
vcruntime
vcvars
VDesktop
vdi
VDId
vec
Expand Down
1 change: 1 addition & 0 deletions .pipelines/ci/templates/build-powertoys-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ steps:
**\Wox.Test.dll
**\Microsoft.PowerToys.Run.Plugin.System.UnitTests.dll
**\Microsoft.Plugin.WindowsTerminal.UnitTests.dll
**\Microsoft.Plugin.WindowWalker.UnitTests.dll
!**\obj\**
!**\ref\**
Expand Down
14 changes: 12 additions & 2 deletions PowerToys.sln
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonacoPreviewHandler", "src
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.TimeZone", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.TimeZone\Microsoft.PowerToys.Run.Plugin.TimeZone.csproj", "{F44934A8-36F3-49B0-9465-3831BE041CDE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.WindowWalker.UnitTests", "src\modules\launcher\Plugins\Microsoft.Plugin.WindowWalker.UnitTests\Microsoft.Plugin.WindowWalker.UnitTests.csproj", "{8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Expand Down Expand Up @@ -1080,9 +1082,8 @@ Global
{04B193D7-3E21-46B8-A958-89B63A8A69DE}.Debug|x86.Build.0 = Debug|x64
{04B193D7-3E21-46B8-A958-89B63A8A69DE}.Release|x64.ActiveCfg = Release|x64
{04B193D7-3E21-46B8-A958-89B63A8A69DE}.Release|x64.Build.0 = Release|x64
{04B193D7-3E21-46B8-A958-89B63A8A69DE}.Release|x86.ActiveCfg = Release|x64
{04B193D7-3E21-46B8-A958-89B63A8A69DE}.Release|x86.Build.0 = Release|x64
{04B193D7-3E21-46B8-A958-89B63A8A69DE}.Release|x86.ActiveCfg = Release|Any CPU
{04B193D7-3E21-46B8-A958-89B63A8A69DE}.Release|x86.Build.0 = Release|Any CPU
{F44934A8-36F3-49B0-9465-3831BE041CDE}.Debug|x64.ActiveCfg = Debug|x64
{F44934A8-36F3-49B0-9465-3831BE041CDE}.Debug|x64.Build.0 = Debug|x64
{F44934A8-36F3-49B0-9465-3831BE041CDE}.Debug|x86.ActiveCfg = Debug|x64
Expand All @@ -1091,6 +1092,14 @@ Global
{F44934A8-36F3-49B0-9465-3831BE041CDE}.Release|x64.Build.0 = Release|x64
{F44934A8-36F3-49B0-9465-3831BE041CDE}.Release|x86.ActiveCfg = Release|x64
{F44934A8-36F3-49B0-9465-3831BE041CDE}.Release|x86.Build.0 = Release|x64
{8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}.Debug|x64.ActiveCfg = Debug|x64
{8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}.Debug|x64.Build.0 = Debug|x64
{8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}.Debug|x86.ActiveCfg = Debug|x64
{8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}.Debug|x86.Build.0 = Debug|x64
{8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}.Release|x64.ActiveCfg = Release|x64
{8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}.Release|x64.Build.0 = Release|x64
{8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}.Release|x86.ActiveCfg = Release|x64
{8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}.Release|x86.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1223,6 +1232,7 @@ Global
{F1F6B6B6-9F18-4A17-8B5C-97DF552C53DC} = {2F305555-C296-497E-AC20-5FA1B237996A}
{04B193D7-3E21-46B8-A958-89B63A8A69DE} = {2F305555-C296-497E-AC20-5FA1B237996A}
{F44934A8-36F3-49B0-9465-3831BE041CDE} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
{8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
Expand Down
67 changes: 65 additions & 2 deletions doc/devdocs/modules/launcher/plugins/windowwalker.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,49 @@
# Window Walker plugin
The window walker plugin matches the user entered query with the open windows on the system.
The user can switch to the found windows, close them or kill their process.

![Image of Window Walker plugin](/doc/images/launcher/plugins/windowwalker.png)


## Remarks

### UWP Apps
- The process of an UWP app can't be detected correctly for windows that are minimized while searching. At this time they are assigned to the generic process `ApplicationFrameHost.exe`. If the user searches for such an window while it is not minimized, then the process gets assigned correctly/updated.

### Killing processes
- Killing the Explorer process is only allowed, if each folder window is running in its own process. (See section `File Explorer setting` below.)
- You can only kill elevated processes, if you have admin permissions (UAC).
- If you kill the process of an UWP app window, you kill all instances of the app. All windows are assigned to the same process.
- Windows of UWP apps don't know their process, until they are searched in non-minimized state.

### File Explorer setting
- To kill the Process of an Explorer window, each window has to run in a separate process. Otherwise the process is the same one as the shell process and killing the shell process will crash the shell (Windows ui).
- To enable this behavior the setting `Launch folder windows in a separate process` under `Folder Options > View` has to be enabled.
- From PowerToys Run you can open the `Folder options` dialog by clicking the information message in the search results. The information message is only shown when searching with action keyword for explorer windows and can be hidden in the plugin settings.
- Note: The folder option/process is evaluated in real time. After changing the setting it is enough to search again for the windows.

![Folder options for Window Walker](/doc/images/launcher/plugins/windowwalker_folder_options.png)


## Optional plugin settings
- The optional plugin settings are implemented via the [`ISettingProvider`](/src/modules/launcher/Wox.Plugin/ISettingProvider.cs) interface from `Wox.Plugin` project.
- All available settings for the plugin are defined in the [`WindowWalkerSettings`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/WindowWalkerSettings.cs) class of the plugin. The settings can be accessed everywhere in the plugin code via the static class instance `WindowWalkerSettings.Instance`.
- We have the following settings that the user can configure to change the behavior of the plugin:

| Key | Default value | Name/Description |
|--------------|-----------|------------|
| `ResultsFromVisibleDesktopOnly` | `false` | Show only results from visible desktop |
| `SubtitleShowPid` | `false` | Show process id in subtitle |
| `SubtitleShowDesktopName` | `true` | Show desktop name in subtitle (If two or more desktops exist) |
| `ConfirmKillProcess` | `true` | Request confirmation when killing a process |
| `KillProcessTree` | `false` | Kill process and it's child processes |
| `OpenAfterKillAndClose` | `false` | Stay open after closing windows and killing processes (Not working with kill process confirmation) |
| `HideKillProcessOnElevatedProcesses` | `false` | Hide "kill process" button if additional permissions required |
| `HideExplorerSettingInfo` | `false` | Hide Explorer process information |


## Technical details

### [`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.

Expand All @@ -11,11 +52,33 @@ The window walker plugin matches the user entered query with the open windows on
- It is responsible for updating the search text and performing a fuzzy search on all the open windows.

### [`Window.cs`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/Window.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 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.
- 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), the `SwitchTowindow` function which switches the desktop focus to the selected window and the `CloseThisWindow` function which closes the window. The `SwitchTowindow` action is performed when the user clicks on a window walker plugin result.
- 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`.
- To get the desktop information for a window, we use the common [`VirtualDesktopHelper`](/src/modules/launcher/Wox.Plugin/Common/VirtualDesktop/VirtualDesktopHelper.cs) in `Wox.Plugin` project. The instance of `VirtualDesktopHelper` is cached in the [`Main`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Main.cs) class of the plugin at runtime. The desktop information is stored in a property of the type [`VDesktop`](/src/modules/launcher/Wox.Plugin/Common/VirtualDesktop/VDesktop.cs).

### [`WindowProcess.cs`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/WindowProcess.cs)
- 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.
- 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 instance methods/properties to hold/retrieve the process information we want to know about a window's process.
- Additionally, it contains the method `KillThisProcess` to kill the process. (If the user has not enough permissions to kill a process they are requested via UAC.)

### [`ResultHelper.cs`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/ResultHelper.cs)
- The [`ResultHelper`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/ResultHelper.cs) class contains the code to create the list with all results for PT Run based on the data returned from `SearchController` class.
- There is a special result that is added if the folder windows doesn't run in separate processes and the user searches for Explorer windows using the action keyword.
- This result informs the user that there is a setting that must be enabled to be able to kill Explorer processes.
- The result can be disabled in plugin options. When it is clicked it opens the folder options.

### [`ContextMenuHelper.cs`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/ContextMenuHelper.cs)
- The [`ContextMenuHelper`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/ContextMenuHelper.cs) class provides the code for the context menu items.

### [`WindowWalkerSettings.cs`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/WindowWalkerSettings.cs)
- The [`WindowWalkerSettings`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/WindowWalkerSettings.cs) class provides access to all optional plugin settings.
- The class has a static property called `Instance` that holds an instance of the class itself. This allows us to access the settings from everywhere in the plugin code without having additional parameters in our methods.

### 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.

## [Unit Tests](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker.UnitTests)
We have a [Unit Test project](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker.UnitTests) that executes various test to ensure that the plugin works as expected.

### [`PluginSettingsTests.cs`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker.UnitTests/PluginSettingsTests.cs)
- The [`PluginSettingsTests.cs`](/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker.UnitTests/PluginSettingsTests.cs) class contains tests to validate that all settings exist and that they have the correct default values.
Binary file modified doc/images/launcher/plugins/windowwalker.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 9 additions & 8 deletions installer/PowerToysSetup/Product.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,19 @@
<?define VSCWrkCompFiles=plugin.json;Community.PowerToys.Run.Plugin.VSCodeWorkspaces.dll?>

<?define WindowWlkrCompFiles=plugin.json;Microsoft.Plugin.WindowWalker.deps.json;Microsoft.Plugin.WindowWalker.dll;PowerToys.ManagedTelemetry.dll?>

<?define WindowWlkrImagesCompFiles=windowwalker.dark.png;windowwalker.light.png;info.dark.png;info.light.png?>

<?define RegistryComponentFiles=plugin.json;Microsoft.PowerToys.Run.Plugin.Registry.deps.json;Microsoft.PowerToys.Run.Plugin.Registry.dll;PowerToys.ManagedTelemetry.dll?>

<?define ServiceComponentFiles=plugin.json;Microsoft.PowerToys.Run.Plugin.Service.deps.json;Microsoft.PowerToys.Run.Plugin.Service.dll?>

<?define SystemComponentFiles=plugin.json;Microsoft.PowerToys.Run.Plugin.System.deps.json;Microsoft.PowerToys.Run.Plugin.System.dll?>

<?define TimeZoneComponentFiles=plugin.json;Microsoft.PowerToys.Run.Plugin.TimeZone.deps.json;Microsoft.PowerToys.Run.Plugin.TimeZone.dll;PowerToys.ManagedTelemetry.dll?>

<?define SystemImagesComponentFiles=lock.dark.png;lock.light.png;logoff.dark.png;logoff.light.png;recyclebin.dark.png;recyclebin.light.png;restart.dark.png;restart.light.png;shutdown.dark.png;shutdown.light.png;sleep.dark.png;sleep.light.png;firmwareSettings.dark.png;firmwareSettings.light.png?>

<?define TimeZoneComponentFiles=plugin.json;Microsoft.PowerToys.Run.Plugin.TimeZone.deps.json;Microsoft.PowerToys.Run.Plugin.TimeZone.dll;PowerToys.ManagedTelemetry.dll?>

<?define WinSetCmpFiles=plugin.json;Microsoft.PowerToys.Run.Plugin.WindowsSettings.deps.json;Microsoft.PowerToys.Run.Plugin.WindowsSettings.dll;PowerToys.ManagedTelemetry.dll?>

<?define WinTermCmpFiles=plugin.json;Microsoft.PowerToys.Run.Plugin.WindowsTerminal.deps.json;Microsoft.PowerToys.Run.Plugin.WindowsTerminal.dll;PowerToys.ManagedTelemetry.dll?>
Expand Down Expand Up @@ -1516,12 +1518,11 @@
<File Id="WindowWlkrCompFile_$(var.File)" Source="$(var.BinX64Dir)modules\launcher\Plugins\WindowWalker\$(var.File)" />
</Component>
<?endforeach?>
<Component Id="WindowWalkerImagesComponentLight" Directory="WindowWalkerImagesFolder" >
<File Id="WindowWalkerLightIcon" Source="$(var.BinX64Dir)modules\launcher\Plugins\WindowWalker\Images\windowwalker.light.png" />
</Component>
<Component Id="WindowWalkerImagesComponentDark" Directory="WindowWalkerImagesFolder" >
<File Id="WindowWalkerDarkIcon" Source="$(var.BinX64Dir)modules\launcher\Plugins\WindowWalker\Images\windowwalker.dark.png" />
</Component>
<?foreach File in $(var.WindowWlkrImagesCompFiles)?>
<Component Id="WindowWalkerImagesComp_$(var.File)" Win64="yes" Directory="WindowWalkerImagesFolder">
<File Id="WindowWalkerImagesComp_$(var.File)" Source="$(var.BinX64Dir)modules\launcher\Plugins\WindowWalker\Images\$(var.File)" />
</Component>
<?endforeach?>

<!-- Registry Plugin -->
<?foreach File in $(var.RegistryComponentFiles)?>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<IsPackable>false</IsPackable>
<Platforms>x64</Platforms>
<RootNamespace>Microsoft.Plugin.WindowWalker.UnitTests</RootNamespace>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisMode>Recommended</AnalysisMode>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.3" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Microsoft.Plugin.WindowWalker\Microsoft.Plugin.WindowWalker.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\..\..\codeAnalysis\GlobalSuppressions.cs">
<Link>GlobalSuppressions.cs</Link>
</Compile>
<AdditionalFiles Include="..\..\..\..\codeAnalysis\StyleCop.json">
<Link>StyleCop.json</Link>
</AdditionalFiles>
</ItemGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers">
<Version>1.1.118</Version>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 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.Reflection;
using Microsoft.Plugin.WindowWalker.Components;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Microsoft.Plugin.WindowWalker.UnitTests
{
[TestClass]
public class PluginSettingsTests
{
[TestMethod]
public void SettingsCount()
{
// Setup
PropertyInfo[] settings = WindowWalkerSettings.Instance?.GetType()?.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance);

// Act
var result = settings?.Length;

// Assert
Assert.AreEqual(8, result);
}

[DataTestMethod]
[DataRow("ResultsFromVisibleDesktopOnly")]
[DataRow("SubtitleShowPid")]
[DataRow("SubtitleShowDesktopName")]
[DataRow("ConfirmKillProcess")]
[DataRow("KillProcessTree")]
[DataRow("OpenAfterKillAndClose")]
[DataRow("HideKillProcessOnElevatedProcesses")]
[DataRow("HideExplorerSettingInfo")]
public void DoesSettingExist(string name)
{
// Setup
Type settings = WindowWalkerSettings.Instance?.GetType();

// Act
var result = settings?.GetProperty(name, BindingFlags.NonPublic | BindingFlags.Instance);

// Assert
Assert.IsNotNull(result);
}

[DataTestMethod]
[DataRow("ResultsFromVisibleDesktopOnly", false)]
[DataRow("SubtitleShowPid", false)]
[DataRow("SubtitleShowDesktopName", true)]
[DataRow("ConfirmKillProcess", true)]
[DataRow("KillProcessTree", false)]
[DataRow("OpenAfterKillAndClose", false)]
[DataRow("HideKillProcessOnElevatedProcesses", false)]
[DataRow("HideExplorerSettingInfo", false)]
public void DefaultValues(string name, bool valueExpected)
{
// Setup
WindowWalkerSettings setting = WindowWalkerSettings.Instance;

// Act
PropertyInfo propertyInfo = setting?.GetType()?.GetProperty(name, BindingFlags.NonPublic | BindingFlags.Instance);
var result = propertyInfo?.GetValue(setting);

// Assert
Assert.AreEqual(valueExpected, result);
}
}
}
Loading

0 comments on commit e8363a3

Please sign in to comment.