Compare commits

...

12 Commits

Author SHA1 Message Date
ryanbodrug-microsoft
1a2cd2a9c3 Improving performance of Adding and Removing Packaged Apps (#4521) (#4900) (#4901)
* Merging in Theme changes and moving win32Tests to Microsoft.Plugin.Program.UnitTests

* Fixing message format for exception

* Changing test structure.  Need to add unit tests.

* Updating packagerepository comment based on pr feedback

* Fixing potential race condition in ListRepository.  Now internally implemented as a concurrent dictionary.

* Removing unecessary implementation of IRepository interface as this is implemented by the base class.

* Restoring checks for invalid uwp apps based on PR feedback. This was accidentally removed when moving the initialize outside the constructor.

* Fixing comments

* Adding newline to end of file for IProgramRepository
2020-07-10 04:52:39 -07:00
Enrico Giordani
062aed38a0 Update version to 0.19.2 (#4896) 2020-07-09 18:18:12 +02:00
Arjun Balgovind
d0d72412d6 Added dummy key event to prevent Start Menu from popping up (#4874) 2020-07-09 17:59:11 +02:00
vldmr11080
3450d832d4 [FancyZones] Invalidate cached work areas when display resolution or taskbar position changes (#4800)
* Invalidate cached work areas when display resolution or taskbar position changes

* Update comments in code
2020-07-09 17:58:24 +02:00
Divyansh Srivastava
79eda1681b Added fix to update text on navigation using up/down arrow (#4626)
* Added fix to update text on navigation using up/down arrow

* Fix incorrect alignment with ghost text

* Added tests
2020-07-06 09:40:10 +02:00
Arjun
1360359bba Changed flags for newly pressed key after invoking shortcut 2020-07-03 20:23:15 +02:00
Alekhya
1a10c1b4f9 Removed the race condition (#4735) 2020-07-03 19:36:18 +02:00
Arjun Balgovind
ae08b810bb Rework the HotkeyManager and KeyboardHook interop classes (#4710)
* Use GetAsyncKeyState calls and remove additional thread usage

* Removed Environment.Exit
2020-07-03 17:24:01 +02:00
Alekhya
2baaa1f20e Fix for Memory issue with context menu items (#4597)
* Added the inotifyPropertyChanged to all the properties and that stops the memory for shooting up

* some more inotify properties added

(cherry picked from commit 26fa05d9b661dadc5ab0257d540ab838a07c43a6)

* Revert "some more inotify properties added"

This reverts commit 845a94c9b2.

* Removed unnecessary inotifypropertychanged interfaces and cleaned up the code

* removed the ctrl+c from folder plugin

* removed unnecessary init

* Added unit test to check if PropertyChanged is called

* renamed var

* refactored the tests

* formatting and adding comments

* changed access modifier in test

* Used observable collection instead of a list

* clearing the observable collection instead of setting it to a new one
2020-07-02 22:55:19 +02:00
Yevhenii Holovachov
25f0ba19ca [FancyZones] Fixed shift behavior (#4653) 2020-07-02 15:38:25 +02:00
Enrico Giordani
32d873f41d Now working on 0.19.1 (#4602) 2020-07-02 15:37:57 +02:00
Clint Rutkas
772387a27a 0.19 readme update (#4583)
* Update README.md

* Update README.md

* Update README.md

* Update README.md
2020-06-30 18:39:13 +02:00
42 changed files with 1567 additions and 838 deletions

View File

@@ -255,6 +255,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManagerTest", "src\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedCommon", "src\common\ManagedCommon\ManagedCommon.csproj", "{4AED67B6-55FD-486F-B917-E543DEE2CB3C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Plugin.Program.UnitTests", "src\modules\launcher\Plugins\Microsoft.Plugin.Program.UnitTests\Microsoft.Plugin.Program.UnitTests.csproj", "{42851751-CBC8-45A6-97F5-7A0753F7B4D1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -501,6 +503,10 @@ Global
{4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Debug|x64.Build.0 = Debug|x64
{4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Release|x64.ActiveCfg = Release|x64
{4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Release|x64.Build.0 = Release|x64
{42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Debug|x64.ActiveCfg = Debug|x64
{42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Debug|x64.Build.0 = Debug|x64
{42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Release|x64.ActiveCfg = Release|x64
{42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -571,6 +577,7 @@ Global
{E6410BFC-B341-498C-8C67-312C20CDD8D5} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{62173D9A-6724-4C00-A1C8-FB646480A9EC} = {38BDB927-829B-4C65-9CD9-93FB05D66D65}
{4AED67B6-55FD-486F-B917-E543DEE2CB3C} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{42851751-CBC8-45A6-97F5-7A0753F7B4D1} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -72,9 +72,9 @@ PowerToys will now enable two types of files to be previewed: Markdown (.md) & S
- Windows 10 1803 (build 17134) or later.
- Have [.NET Core 3.1 Desktop Runtime](https://dotnet.microsoft.com/download/dotnet-core/thank-you/runtime-desktop-3.1.4-windows-x64-installer). The installer will prompt this but we want to directly make people aware.
### Via Github with MSI [Recommended]
### Via GitHub with MSI [Recommended]
Install from the [Microsoft PowerToys GitHub releases page][github-release-link]. Click on `Assets` to show the files available in the release and then click on `PowerToysSetup-0.18.2-x64.msi` to download the PowerToys installer.
Install from the [Microsoft PowerToys GitHub releases page][github-release-link]. Click on `Assets` to show the files available in the release and then click on `PowerToysSetup-0.19.0-x64.msi` to download the PowerToys installer.
**Note:** After installing, you will have to start PowerToys for the first time. We will improve install experience this moving forward but due to a possible install dependency, we can't start after install currently.
@@ -107,8 +107,9 @@ choco upgrade powertoys
### Known issues
- [#2012 - Uninstalling with old control panel fails](https://github.com/microsoft/PowerToys/issues/2012): Please use the modern settings to uninstall. `Windows 10 Settings -> Apps -> Apps & features`
- [#3384 - PowerToys Settings window is empty](https://github.com/microsoft/PowerToys/issues/3384): Workaround appears to be run as admin. We are proactively looking into this as a hotfix.
- PT Run, Newly installed apps can't be found [#3553](https://github.com/microsoft/PowerToys/issues/3553). We will address this in 0.20.
- PT Run, CPU / Memory, still investigating [#3208](https://github.com/microsoft/PowerToys/issues/3208). We have 2 leads and fixed one item.
- WinKey remapping for PT Run can be quirky [#4578](https://github.com/microsoft/PowerToys/issues/4578)
### Processor support
@@ -120,26 +121,29 @@ We currently support the matrix below.
## What's Happening
### May 2020 Update
### June 2020 Update
Our goals for 0.18 release cycle was three big items, PowerToys Run, Keyboard manager, and migrating to the new settings system. This is also the first time we'll test out the auto-updating system.
Our goals for 0.19 release cycle had one big goal, add in stability / quality fixes. We've addressed over 100 issues across all our utilities. We've improved our installer experience and parts will start coming online in 0.19 and 0.20. In this release, it will be the last time during upgrade you'll see Windows Explorer flash on you. For 0.20, the .NET Core install experience much smoother.
Feedback is critical. We know there are areas for improvement on PT Run. We would love feedback so we can improve. We also would love to know if you want us to be more aggressive on auto-upgrading.
We'd also stress feedback is critical. We know there are areas for improvement on PowerToys Run. We would love feedback so we can improve. We also would love to know if you want us to be more aggressive on auto-upgrading.
- We shipped [v0.18][github-release-link]!
- New Utilities
- PowerToys Run, our new application launcher (use alt-space to activate)
- Keyboard manager, a quick easy way to remap your keyboard
- Fixed [#243](https://github.com/microsoft/PowerToys/issues/243)'s setting issue
- Improved performance on FancyZones [#1264](https://github.com/microsoft/PowerToys/issues/1264)
- Lots of bug fixes!
Lastly, we'd like to thank everyone who filed a bug, gave feedback or made a pull-request. The PowerToys team is extremely grateful to have the support of an amazing active community.
For [0.19](https://github.com/microsoft/PowerToys/issues?q=is%3Aopen+is%3Aissue+project%3Amicrosoft%2FPowerToys%2F4), we are proactively working on:
- We shipped [v0.19][github-release-link]!
- Big push for PowerToys Run search quality fixes
- PowerToys Run can now remap to any key shortcut (minus restricted ones such as WinKey+L)
- Improved FancyZones on Virtual Desktops and multi-thread design
- Installer after 0.19 will no longer restart Windows Explorer
- Fixed [#2012 - Uninstalling with old control panel fails](https://github.com/microsoft/PowerToys/issues/2012)
- Fixed [#3384 - PowerToys Settings window is empty](https://github.com/microsoft/PowerToys/issues/3384)
- Over 100 bug fixes!
- Enable PT Run to be mapped to Win-Keys
- Stability / tech debt fixes
- Performance improvements with FancyZones
- A testing utility for FancyZones to be sure we can test different window configurations.
For [0.20](https://github.com/microsoft/PowerToys/issues?q=is%3Aopen+is%3Aissue+project%3Amicrosoft%2FPowerToys%2F6), we are proactively working on:
- Stability
- Start work on FZ Editor V2
- Start work on OOBE improvements
- Keyboard manager improvements
### Version 1.0 plan

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Version>0.19.0</Version>
<Version>0.19.2</Version>
<DefineConstants>Version=$(Version);</DefineConstants>
</PropertyGroup>
</Project>

View File

@@ -5,15 +5,14 @@ using namespace interop;
HotkeyManager::HotkeyManager()
{
keyboardEventCallback = gcnew KeyboardEventCallback(this, &HotkeyManager::KeyboardEventProc);
keyboardEventCallback = gcnew KeyboardEventCallback(this, &HotkeyManager::KeyboardEventProc);
isActiveCallback = gcnew IsActiveCallback(this, &HotkeyManager::IsActiveProc);
filterKeyboardCallback = gcnew FilterKeyboardEvent(this, &HotkeyManager::FilterKeyboardProc);
keyboardHook = gcnew KeyboardHook(
keyboardEventCallback,
isActiveCallback,
filterKeyboardCallback
);
filterKeyboardCallback);
hotkeys = gcnew Dictionary<HOTKEY_HANDLE, HotkeyCallback ^>();
pressedKeys = gcnew Hotkey();
keyboardHook->Start();
@@ -25,12 +24,20 @@ HotkeyManager::~HotkeyManager()
}
// When all Shortcut keys are pressed, fire the HotkeyCallback event.
void HotkeyManager::KeyboardEventProc(KeyboardEvent^ ev)
void HotkeyManager::KeyboardEventProc(KeyboardEvent ^ ev)
{
// pressedKeys always stores the latest keyboard state
auto pressedKeysHandle = GetHotkeyHandle(pressedKeys);
if (hotkeys->ContainsKey(pressedKeysHandle))
{
hotkeys[pressedKeysHandle]->Invoke();
// After invoking the hotkey send a dummy key to prevent Start Menu from activating
INPUT dummyEvent[1] = {};
dummyEvent[0].type = INPUT_KEYBOARD;
dummyEvent[0].ki.wVk = 0xFF;
dummyEvent[0].ki.dwFlags = KEYEVENTF_KEYUP;
SendInput(1, dummyEvent, sizeof(INPUT));
}
}
@@ -42,22 +49,24 @@ bool HotkeyManager::IsActiveProc()
}
// KeyboardEvent callback is only fired for relevant key events.
bool HotkeyManager::FilterKeyboardProc(KeyboardEvent^ ev)
bool HotkeyManager::FilterKeyboardProc(KeyboardEvent ^ ev)
{
auto oldHandle = GetHotkeyHandle(pressedKeys);
// Updating the pressed keys here so we know if the keypress event
// should be propagated or not.
UpdatePressedKeys(ev);
// Updating the pressed keys here so we know if the keypress event should be propagated or not.
pressedKeys->Win = (GetAsyncKeyState(VK_LWIN) & 0x8000) || (GetAsyncKeyState(VK_RWIN) & 0x8000);
pressedKeys->Ctrl = GetAsyncKeyState(VK_CONTROL) & 0x8000;
pressedKeys->Alt = GetAsyncKeyState(VK_MENU) & 0x8000;
pressedKeys->Shift = GetAsyncKeyState(VK_SHIFT) & 0x8000;
pressedKeys->Key = ev->key;
// Convert to hotkey handle
auto pressedKeysHandle = GetHotkeyHandle(pressedKeys);
// Check if the hotkey matches the pressed keys, and check if the pressed keys aren't duplicate
// (there shouldn't be auto repeating hotkeys)
if (hotkeys->ContainsKey(pressedKeysHandle) && oldHandle != pressedKeysHandle)
// Check if any hotkey matches the pressed keys if the current key event is a key down event
if ((ev->message == WM_KEYDOWN || ev->message == WM_SYSKEYDOWN) && hotkeys->ContainsKey(pressedKeysHandle))
{
return true;
}
return false;
}
@@ -83,51 +92,3 @@ HOTKEY_HANDLE HotkeyManager::GetHotkeyHandle(Hotkey ^ hotkey)
handle |= hotkey->Alt << 11;
return handle;
}
void HotkeyManager::UpdatePressedKey(DWORD code, bool replaceWith, unsigned char replaceWithKey)
{
switch (code)
{
case VK_LWIN:
case VK_RWIN:
pressedKeys->Win = replaceWith;
break;
case VK_CONTROL:
case VK_LCONTROL:
case VK_RCONTROL:
pressedKeys->Ctrl = replaceWith;
break;
case VK_SHIFT:
case VK_LSHIFT:
case VK_RSHIFT:
pressedKeys->Shift = replaceWith;
break;
case VK_MENU:
case VK_LMENU:
case VK_RMENU:
pressedKeys->Alt = replaceWith;
break;
default:
pressedKeys->Key = replaceWithKey;
break;
}
}
void HotkeyManager::UpdatePressedKeys(KeyboardEvent ^ ev)
{
switch (ev->message)
{
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
{
UpdatePressedKey(ev->key, true, ev->key);
}
break;
case WM_KEYUP:
case WM_SYSKEYUP:
{
UpdatePressedKey(ev->key, false, 0);
}
break;
}
}

View File

@@ -26,7 +26,7 @@ public
public
delegate void HotkeyCallback();
typedef unsigned short HOTKEY_HANDLE;
typedef unsigned short HOTKEY_HANDLE;
public
ref class HotkeyManager
@@ -46,12 +46,9 @@ public
IsActiveCallback ^ isActiveCallback;
FilterKeyboardEvent ^ filterKeyboardCallback;
void KeyboardEventProc(KeyboardEvent ^ ev);
bool IsActiveProc();
bool FilterKeyboardProc(KeyboardEvent ^ ev);
HOTKEY_HANDLE GetHotkeyHandle(Hotkey ^ hotkey);
void UpdatePressedKeys(KeyboardEvent ^ ev);
void UpdatePressedKey(DWORD code, bool replaceWith, unsigned char replaceWithKey);
};
}

View File

@@ -15,8 +15,6 @@ KeyboardHook::KeyboardHook(
IsActiveCallback ^ isActiveCallback,
FilterKeyboardEvent ^ filterKeyboardEvent)
{
kbEventDispatch = gcnew Thread(gcnew ThreadStart(this, &KeyboardHook::DispatchProc));
queue = gcnew Queue<KeyboardEvent ^>();
this->keyboardEventCallback = keyboardEventCallback;
this->isActiveCallback = isActiveCallback;
this->filterKeyboardEvent = filterKeyboardEvent;
@@ -24,44 +22,10 @@ KeyboardHook::KeyboardHook(
KeyboardHook::~KeyboardHook()
{
quit = true;
// Notify the DispatchProc thread so that it isn't stuck at the Wait step
Monitor::Enter(queue);
Monitor::Pulse(queue);
Monitor::Exit(queue);
kbEventDispatch->Join();
// Unregister low level hook procedure
UnhookWindowsHookEx(hookHandle);
}
void KeyboardHook::DispatchProc()
{
Monitor::Enter(queue);
quit = false;
while (!quit)
{
if (queue->Count == 0)
{
Monitor::Wait(queue);
continue;
}
auto nextEv = queue->Dequeue();
// Release lock while callback is being invoked
Monitor::Exit(queue);
keyboardEventCallback->Invoke(nextEv);
// Re-aquire lock
Monitor::Enter(queue);
}
Monitor::Exit(queue);
}
void KeyboardHook::Start()
{
hookProc = gcnew HookProcDelegate(this, &KeyboardHook::HookProc);
@@ -85,8 +49,6 @@ void KeyboardHook::Start()
throw std::exception("SetWindowsHookEx failed.");
}
}
kbEventDispatch->Start();
}
LRESULT CALLBACK KeyboardHook::HookProc(int nCode, WPARAM wParam, LPARAM lParam)
@@ -101,10 +63,7 @@ LRESULT CALLBACK KeyboardHook::HookProc(int nCode, WPARAM wParam, LPARAM lParam)
return CallNextHookEx(hookHandle, nCode, wParam, lParam);
}
Monitor::Enter(queue);
queue->Enqueue(ev);
Monitor::Pulse(queue);
Monitor::Exit(queue);
keyboardEventCallback->Invoke(ev);
return 1;
}
return CallNextHookEx(hookHandle, nCode, wParam, lParam);

View File

@@ -33,16 +33,12 @@ public
private:
delegate LRESULT HookProcDelegate(int nCode, WPARAM wParam, LPARAM lParam);
Thread ^ kbEventDispatch;
Queue<KeyboardEvent ^> ^ queue;
KeyboardEventCallback ^ keyboardEventCallback;
IsActiveCallback ^ isActiveCallback;
FilterKeyboardEvent ^ filterKeyboardEvent;
bool quit;
HHOOK hookHandle;
HookProcDelegate ^ hookProc;
void DispatchProc();
LRESULT CALLBACK HookProc(int nCode, WPARAM wParam, LPARAM lParam);
};

View File

@@ -9,13 +9,7 @@
template<uint16_t APIVersion>
bool IsAPIContractVxAvailable()
{
static bool isAPIContractVxAvailableInitialized = false;
static bool isAPIContractVxAvailable = false;
if (!isAPIContractVxAvailableInitialized)
{
isAPIContractVxAvailableInitialized = true;
isAPIContractVxAvailable = winrt::Windows::Foundation::Metadata::ApiInformation::IsApiContractPresent(L"Windows.Foundation.UniversalApiContract", APIVersion);
}
static bool isAPIContractVxAvailable = winrt::Windows::Foundation::Metadata::ApiInformation::IsApiContractPresent(L"Windows.Foundation.UniversalApiContract", APIVersion);
return isAPIContractVxAvailable;
}

View File

@@ -14,7 +14,7 @@ namespace Microsoft.PowerLauncher.Telemetry
/// <summary>
/// Gets The version string. TODO: This should be replaced by a P/Invoke call to get_product_version
/// </summary>
public string Version => "v0.19.0";
public string Version => "v0.19.2";
public double BootTimeMs { get; set; }

View File

@@ -74,9 +74,6 @@ namespace Microsoft.PowerToys.Settings.UI.Runner
MessageBoxButton.OK);
app.Shutdown();
}
// Terminate all threads of the process
Environment.Exit(0);
}
}

View File

@@ -9,7 +9,7 @@
<Identity
Name="f4f787a5-f0ae-47a9-be89-5408b1dd2b47"
Publisher="CN=lamotile"
Version="0.19.0.0" />
Version="0.19.2.0" />
<mp:PhoneIdentity PhoneProductId="f4f787a5-f0ae-47a9-be89-5408b1dd2b47" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>

View File

@@ -638,6 +638,9 @@ LRESULT FancyZones::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lpa
{
if (wparam == SPI_SETWORKAREA)
{
// Changes in taskbar position resulted in different size of work area.
// Invalidate cached work-areas so they can be recreated with latest information.
m_workAreaHandler.Clear();
OnDisplayChange(DisplayChangeType::WorkArea);
}
}
@@ -645,6 +648,8 @@ LRESULT FancyZones::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lpa
case WM_DISPLAYCHANGE:
{
// Display resolution changed. Invalidate cached work-areas so they can be recreated with latest information.
m_workAreaHandler.Clear();
OnDisplayChange(DisplayChangeType::DisplayChange);
}
break;

View File

@@ -173,10 +173,7 @@ void WindowMoveHandlerPrivate::MoveSizeStart(HWND window, HMONITOR monitor, POIN
m_mouseHook->enable();
}
if (m_settings->GetSettings()->shiftDrag)
{
m_shiftHook->enable();
}
m_shiftHook->enable();
// This updates m_dragEnabled depending on if the shift key is being held down.
UpdateDragState(window);

View File

@@ -459,15 +459,15 @@ namespace KeyboardEventHandlers
i++;
}
// key down for original shortcut action key
// key down for original shortcut action key with shortcut flag so that we don't invoke the same shortcut remap again
if (isActionKeyPressed)
{
KeyboardManagerHelper::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, (WORD)it.first.GetActionKey(), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
i++;
}
// Send current key pressed
KeyboardManagerHelper::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, (WORD)data->lParam->vkCode, 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
// Send current key pressed without shortcut flag so that it can be reprocessed in case the physical keys pressed are a different remapped shortcut
KeyboardManagerHelper::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, (WORD)data->lParam->vkCode, 0, 0);
i++;
// Send dummy key since the current key pressed could be a modifier
@@ -541,15 +541,15 @@ namespace KeyboardEventHandlers
i++;
}
// key down for original shortcut action key
// key down for original shortcut action key with shortcut flag so that we don't invoke the same shortcut remap again
if (isActionKeyPressed)
{
KeyboardManagerHelper::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, (WORD)it.first.GetActionKey(), 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
i++;
}
// Send current key pressed
KeyboardManagerHelper::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, (WORD)data->lParam->vkCode, 0, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
// Send current key pressed without shortcut flag so that it can be reprocessed in case the physical keys pressed are a different remapped shortcut
KeyboardManagerHelper::SetKeyEvent(keyEventList, i, INPUT_KEYBOARD, (WORD)data->lParam->vkCode, 0, 0);
i++;
// Send dummy key

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<Platforms>x64</Platforms>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" Version="4.14.3" />
<PackageReference Include="nunit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Plugin.Program\Microsoft.Plugin.Program.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,337 +1,437 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using Wox.Infrastructure;
using Wox.Plugin;
using Microsoft.Plugin.Program.Programs;
using Moq;
using System.IO;
namespace Wox.Test.Plugins
{
[TestFixture]
public class ProgramPluginTest
{
Win32 notepad_appdata = new Win32
{
Name = "Notepad",
ExecutableName = "notepad.exe",
FullPath = "c:\\windows\\system32\\notepad.exe",
LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\accessories\\notepad.lnk"
};
Win32 notepad_users = new Win32
{
Name = "Notepad",
ExecutableName = "notepad.exe",
FullPath = "c:\\windows\\system32\\notepad.exe",
LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\accessories\\notepad.lnk"
};
Win32 azure_command_prompt = new Win32
{
Name = "Microsoft Azure Command Prompt - v2.9",
ExecutableName = "cmd.exe",
FullPath = "c:\\windows\\system32\\cmd.exe",
LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\microsoft azure\\microsoft azure sdk for .net\\v2.9\\microsoft azure command prompt - v2.9.lnk"
};
Win32 visual_studio_command_prompt = new Win32
{
Name = "x64 Native Tools Command Prompt for VS 2019",
ExecutableName = "cmd.exe",
FullPath = "c:\\windows\\system32\\cmd.exe",
LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\visual studio 2019\\visual studio tools\\vc\\x64 native tools command prompt for vs 2019.lnk"
};
Win32 command_prompt = new Win32
{
Name = "Command Prompt",
ExecutableName = "cmd.exe",
FullPath = "c:\\windows\\system32\\cmd.exe",
LnkResolvedPath ="c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\system tools\\command prompt.lnk"
};
Win32 file_explorer = new Win32
{
Name = "File Explorer",
ExecutableName = "File Explorer.lnk",
FullPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\system tools\\file explorer.lnk",
LnkResolvedPath = null
};
Win32 wordpad = new Win32
{
Name = "Wordpad",
ExecutableName = "wordpad.exe",
FullPath = "c:\\program files\\windows nt\\accessories\\wordpad.exe",
LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\accessories\\wordpad.lnk"
};
Win32 wordpad_duplicate = new Win32
{
Name = "WORDPAD",
ExecutableName = "WORDPAD.EXE",
FullPath = "c:\\program files\\windows nt\\accessories\\wordpad.exe",
LnkResolvedPath = null
};
Win32 twitter_pwa = new Win32
{
Name = "Twitter",
FullPath = "c:\\program files (x86)\\google\\chrome\\application\\chrome_proxy.exe",
LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\chrome apps\\twitter.lnk",
Arguments = " --profile-directory=Default --app-id=jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi"
};
Win32 pinned_webpage = new Win32
{
Name = "Web page",
FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\msedge_proxy.exe",
LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\web page.lnk",
Arguments = "--profile-directory=Default --app-id=homljgmgpmcbpjbnjpfijnhipfkiclkd"
};
Win32 edge_named_pinned_webpage = new Win32
{
Name = "edge - Bing",
FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\msedge_proxy.exe",
LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\edge - bing.lnk",
Arguments = " --profile-directory=Default --app-id=aocfnapldcnfbofgmbbllojgocaelgdd"
};
Win32 msedge = new Win32
{
Name = "Microsoft Edge",
ExecutableName = "msedge.exe",
FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\msedge.exe",
LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\microsoft edge.lnk"
};
Win32 chrome = new Win32
{
Name = "Google Chrome",
ExecutableName = "chrome.exe",
FullPath = "c:\\program files (x86)\\google\\chrome\\application\\chrome.exe",
LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\google chrome.lnk"
};
Win32 dummy_proxy_app = new Win32
{
Name = "Proxy App",
ExecutableName = "test_proxy.exe",
FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\test_proxy.exe",
LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\test proxy.lnk"
};
Win32 cmd_run_command = new Win32
{
Name = "cmd",
ExecutableName = "cmd.exe",
FullPath = "c:\\windows\\system32\\cmd.exe",
LnkResolvedPath = null,
AppType = 3 // Run command
};
Win32 cmder_run_command = new Win32
{
Name = "Cmder",
ExecutableName = "Cmder.exe",
FullPath = "c:\\tools\\cmder\\cmder.exe",
LnkResolvedPath = null,
AppType = 3 // Run command
};
Win32 dummy_internetShortcut_app = new Win32
{
Name = "Shop Titans",
ExecutableName = "Shop Titans.url",
FullPath = "steam://rungameid/1258080",
ParentDirectory = "C:\\Users\\temp\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Steam",
LnkResolvedPath = null
};
Win32 dummy_internetShortcut_app_duplicate = new Win32
{
Name = "Shop Titans",
ExecutableName = "Shop Titans.url",
FullPath = "steam://rungameid/1258080",
ParentDirectory = "C:\\Users\\temp\\Desktop",
LnkResolvedPath = null
};
[Test]
public void DedupFunction_whenCalled_mustRemoveDuplicateNotepads()
{
// Arrange
List<Win32> prgms = new List<Win32>();
prgms.Add(notepad_appdata);
prgms.Add(notepad_users);
// Act
Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel());
// Assert
Assert.AreEqual(apps.Length, 1);
}
[Test]
public void DedupFunction_whenCalled_MustRemoveInternetShortcuts()
{
// Arrange
List<Win32> prgms = new List<Win32>();
prgms.Add(dummy_internetShortcut_app);
prgms.Add(dummy_internetShortcut_app_duplicate);
// Act
Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel());
// Assert
Assert.AreEqual(apps.Length, 1);
}
[Test]
public void DedupFunction_whenCalled_mustNotRemovelnkWhichdoesNotHaveExe()
{
// Arrange
List<Win32> prgms = new List<Win32>();
prgms.Add(file_explorer);
// Act
Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel());
// Assert
Assert.AreEqual(apps.Length, 1);
}
[Test]
public void DedupFunction_mustRemoveDuplicates_forExeExtensionsWithoutLnkResolvedPath()
{
// Arrange
List<Win32> prgms = new List<Win32>();
prgms.Add(wordpad);
prgms.Add(wordpad_duplicate);
// Act
Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel());
// Assert
Assert.AreEqual(apps.Length, 1);
Assert.IsTrue(!string.IsNullOrEmpty(apps[0].LnkResolvedPath));
}
[Test]
public void DedupFunction_mustNotRemovePrograms_withSameExeNameAndFullPath()
{
// Arrange
List<Win32> prgms = new List<Win32>();
prgms.Add(azure_command_prompt);
prgms.Add(visual_studio_command_prompt);
prgms.Add(command_prompt);
// Act
Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel());
// Assert
Assert.AreEqual(apps.Length, 3);
}
[Test]
public void FunctionIsWebApplication_ShouldReturnTrue_ForWebApplications()
{
// The IsWebApplication(() function must return true for all PWAs and pinned web pages
Assert.IsTrue(twitter_pwa.IsWebApplication());
Assert.IsTrue(pinned_webpage.IsWebApplication());
Assert.IsTrue(edge_named_pinned_webpage.IsWebApplication());
// Should not filter apps whose executable name ends with proxy.exe
Assert.IsFalse(dummy_proxy_app.IsWebApplication());
}
[TestCase("ignore")]
public void FunctionFilterWebApplication_ShouldReturnFalse_WhenSearchingForTheMainApp(string query)
{
// Irrespective of the query, the FilterWebApplication() Function must not filter main apps such as edge and chrome
Assert.IsFalse(msedge.FilterWebApplication(query));
Assert.IsFalse(chrome.FilterWebApplication(query));
}
[TestCase("edge", ExpectedResult = true)]
[TestCase("EDGE", ExpectedResult = true)]
[TestCase("msedge", ExpectedResult = true)]
[TestCase("Microsoft", ExpectedResult = true)]
[TestCase("edg", ExpectedResult = true)]
[TestCase("Edge page", ExpectedResult = false)]
[TestCase("Edge Web page", ExpectedResult = false)]
public bool EdgeWebSites_ShouldBeFiltered_WhenSearchingForEdge(string query)
{
return pinned_webpage.FilterWebApplication(query);
}
[TestCase("chrome", ExpectedResult = true)]
[TestCase("CHROME", ExpectedResult = true)]
[TestCase("Google", ExpectedResult = true)]
[TestCase("Google Chrome", ExpectedResult = true)]
[TestCase("Google Chrome twitter", ExpectedResult = false)]
public bool ChromeWebSites_ShouldBeFiltered_WhenSearchingForChrome(string query)
{
return twitter_pwa.FilterWebApplication(query);
}
[TestCase("twitter", 0, ExpectedResult = false)]
[TestCase("Twit", 0, ExpectedResult = false)]
[TestCase("TWITTER", 0, ExpectedResult = false)]
[TestCase("web", 1, ExpectedResult = false)]
[TestCase("Page", 1, ExpectedResult = false)]
[TestCase("WEB PAGE", 1, ExpectedResult = false)]
[TestCase("edge", 2, ExpectedResult = false)]
[TestCase("EDGE", 2, ExpectedResult = false)]
public bool PinnedWebPages_ShouldNotBeFiltered_WhenSearchingForThem(string query, int Case)
{
const uint CASE_TWITTER = 0;
const uint CASE_WEB_PAGE = 1;
const uint CASE_EDGE_NAMED_WEBPAGE = 2;
// If the query is a part of the name of the web application, it should not be filtered,
// even if the name is the same as that of the main application, eg: case 2 - edge
if (Case == CASE_TWITTER)
{
return twitter_pwa.FilterWebApplication(query);
}
else if(Case == CASE_WEB_PAGE)
{
return pinned_webpage.FilterWebApplication(query);
}
else if(Case == CASE_EDGE_NAMED_WEBPAGE)
{
return edge_named_pinned_webpage.FilterWebApplication(query);
}
// unreachable code
return true;
}
[TestCase("Command Prompt")]
[TestCase("cmd")]
[TestCase("cmd.exe")]
[TestCase("ignoreQueryText")]
public void Win32Applications_ShouldNotBeFiltered_WhenFilteringRunCommands(string query)
{
// Even if there is an exact match in the name or exe name, win32 applications should never be filtered
Assert.IsTrue(command_prompt.QueryEqualsNameForRunCommands(query));
}
[TestCase("cmd")]
[TestCase("Cmd")]
[TestCase("CMD")]
public void RunCommands_ShouldNotBeFiltered_OnExactMatch(string query)
{
// Partial matches should be filtered as cmd is not equal to cmder
Assert.IsFalse(cmder_run_command.QueryEqualsNameForRunCommands(query));
// the query matches the name (cmd) and is therefore not filtered (case-insensitive)
Assert.IsTrue(cmd_run_command.QueryEqualsNameForRunCommands(query));
}
}
}
using Moq;
using NUnit.Framework;
using System.Collections.Generic;
using System.Linq;
using Wox.Infrastructure;
using Wox.Plugin;
using Microsoft.Plugin.Program;
using System.IO.Packaging;
using Windows.ApplicationModel;
namespace Microsoft.Plugin.Program.UnitTests.Programs
{
using Win32 = Microsoft.Plugin.Program.Programs.Win32;
[TestFixture]
public class Win32Tests
{
static Win32 notepad_appdata = new Win32
{
Name = "Notepad",
ExecutableName = "notepad.exe",
FullPath = "c:\\windows\\system32\\notepad.exe",
LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\accessories\\notepad.lnk",
AppType = 2
};
static Win32 notepad_users = new Win32
{
Name = "Notepad",
ExecutableName = "notepad.exe",
FullPath = "c:\\windows\\system32\\notepad.exe",
LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\accessories\\notepad.lnk",
AppType = 2
};
static Win32 azure_command_prompt = new Win32
{
Name = "Microsoft Azure Command Prompt - v2.9",
ExecutableName = "cmd.exe",
FullPath = "c:\\windows\\system32\\cmd.exe",
LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\microsoft azure\\microsoft azure sdk for .net\\v2.9\\microsoft azure command prompt - v2.9.lnk",
AppType = 2
};
static Win32 visual_studio_command_prompt = new Win32
{
Name = "x64 Native Tools Command Prompt for VS 2019",
ExecutableName = "cmd.exe",
FullPath = "c:\\windows\\system32\\cmd.exe",
LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\visual studio 2019\\visual studio tools\\vc\\x64 native tools command prompt for vs 2019.lnk",
AppType = 2
};
static Win32 command_prompt = new Win32
{
Name = "Command Prompt",
ExecutableName = "cmd.exe",
FullPath = "c:\\windows\\system32\\cmd.exe",
LnkResolvedPath ="c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\system tools\\command prompt.lnk",
AppType = 2
};
static Win32 file_explorer = new Win32
{
Name = "File Explorer",
ExecutableName = "File Explorer.lnk",
FullPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\system tools\\file explorer.lnk",
LnkResolvedPath = null,
AppType = 2
};
static Win32 wordpad = new Win32
{
Name = "Wordpad",
ExecutableName = "wordpad.exe",
FullPath = "c:\\program files\\windows nt\\accessories\\wordpad.exe",
LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\accessories\\wordpad.lnk",
AppType = 2
};
static Win32 wordpad_duplicate = new Win32
{
Name = "WORDPAD",
ExecutableName = "WORDPAD.EXE",
FullPath = "c:\\program files\\windows nt\\accessories\\wordpad.exe",
LnkResolvedPath = null,
AppType = 2
};
static Win32 twitter_pwa = new Win32
{
Name = "Twitter",
FullPath = "c:\\program files (x86)\\google\\chrome\\application\\chrome_proxy.exe",
LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\chrome apps\\twitter.lnk",
Arguments = " --profile-directory=Default --app-id=jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi",
AppType = 0
};
static Win32 pinned_webpage = new Win32
{
Name = "Web page",
FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\msedge_proxy.exe",
LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\web page.lnk",
Arguments = "--profile-directory=Default --app-id=homljgmgpmcbpjbnjpfijnhipfkiclkd",
AppType = 0
};
static Win32 edge_named_pinned_webpage = new Win32
{
Name = "edge - Bing",
FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\msedge_proxy.exe",
LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\edge - bing.lnk",
Arguments = " --profile-directory=Default --app-id=aocfnapldcnfbofgmbbllojgocaelgdd",
AppType = 0
};
static Win32 msedge = new Win32
{
Name = "Microsoft Edge",
ExecutableName = "msedge.exe",
FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\msedge.exe",
LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\microsoft edge.lnk",
AppType = 2
};
static Win32 chrome = new Win32
{
Name = "Google Chrome",
ExecutableName = "chrome.exe",
FullPath = "c:\\program files (x86)\\google\\chrome\\application\\chrome.exe",
LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\google chrome.lnk",
AppType = 2
};
static Win32 dummy_proxy_app = new Win32
{
Name = "Proxy App",
ExecutableName = "test_proxy.exe",
FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\test_proxy.exe",
LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\test proxy.lnk",
AppType = 2
};
static Win32 cmd_run_command = new Win32
{
Name = "cmd",
ExecutableName = "cmd.exe",
FullPath = "c:\\windows\\system32\\cmd.exe",
LnkResolvedPath = null,
AppType = 3 // Run command
};
static Win32 cmder_run_command = new Win32
{
Name = "Cmder",
Description = "Cmder: Lovely Console Emulator",
ExecutableName = "Cmder.exe",
FullPath = "c:\\tools\\cmder\\cmder.exe",
LnkResolvedPath = null,
AppType = 3 // Run command
};
static Win32 dummy_internetShortcut_app = new Win32
{
Name = "Shop Titans",
ExecutableName = "Shop Titans.url",
FullPath = "steam://rungameid/1258080",
ParentDirectory = "C:\\Users\\temp\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Steam",
LnkResolvedPath = null,
AppType = 1
};
static Win32 dummy_internetShortcut_app_duplicate = new Win32
{
Name = "Shop Titans",
ExecutableName = "Shop Titans.url",
FullPath = "steam://rungameid/1258080",
ParentDirectory = "C:\\Users\\temp\\Desktop",
LnkResolvedPath = null,
AppType = 1
};
[Test]
public void DedupFunction_whenCalled_mustRemoveDuplicateNotepads()
{
// Arrange
List<Win32> prgms = new List<Win32>();
prgms.Add(notepad_appdata);
prgms.Add(notepad_users);
// Act
Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel());
// Assert
Assert.AreEqual(apps.Length, 1);
}
[Test]
public void DedupFunction_whenCalled_MustRemoveInternetShortcuts()
{
// Arrange
List<Win32> prgms = new List<Win32>();
prgms.Add(dummy_internetShortcut_app);
prgms.Add(dummy_internetShortcut_app_duplicate);
// Act
Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel());
// Assert
Assert.AreEqual(apps.Length, 1);
}
[Test]
public void DedupFunction_whenCalled_mustNotRemovelnkWhichdoesNotHaveExe()
{
// Arrange
List<Win32> prgms = new List<Win32>();
prgms.Add(file_explorer);
// Act
Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel());
// Assert
Assert.AreEqual(apps.Length, 1);
}
[Test]
public void DedupFunction_mustRemoveDuplicates_forExeExtensionsWithoutLnkResolvedPath()
{
// Arrange
List<Win32> prgms = new List<Win32>();
prgms.Add(wordpad);
prgms.Add(wordpad_duplicate);
// Act
Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel());
// Assert
Assert.AreEqual(apps.Length, 1);
Assert.IsTrue(!string.IsNullOrEmpty(apps[0].LnkResolvedPath));
}
[Test]
public void DedupFunction_mustNotRemovePrograms_withSameExeNameAndFullPath()
{
// Arrange
List<Win32> prgms = new List<Win32>();
prgms.Add(azure_command_prompt);
prgms.Add(visual_studio_command_prompt);
prgms.Add(command_prompt);
// Act
Win32[] apps = Win32.DeduplicatePrograms(prgms.AsParallel());
// Assert
Assert.AreEqual(apps.Length, 3);
}
[Test]
public void FunctionIsWebApplication_ShouldReturnTrue_ForWebApplications()
{
// The IsWebApplication(() function must return true for all PWAs and pinned web pages
Assert.IsTrue(twitter_pwa.IsWebApplication());
Assert.IsTrue(pinned_webpage.IsWebApplication());
Assert.IsTrue(edge_named_pinned_webpage.IsWebApplication());
// Should not filter apps whose executable name ends with proxy.exe
Assert.IsFalse(dummy_proxy_app.IsWebApplication());
}
[TestCase("ignore")]
public void FunctionFilterWebApplication_ShouldReturnFalse_WhenSearchingForTheMainApp(string query)
{
// Irrespective of the query, the FilterWebApplication() Function must not filter main apps such as edge and chrome
Assert.IsFalse(msedge.FilterWebApplication(query));
Assert.IsFalse(chrome.FilterWebApplication(query));
}
[TestCase("edge", ExpectedResult = true)]
[TestCase("EDGE", ExpectedResult = true)]
[TestCase("msedge", ExpectedResult = true)]
[TestCase("Microsoft", ExpectedResult = true)]
[TestCase("edg", ExpectedResult = true)]
[TestCase("Edge page", ExpectedResult = false)]
[TestCase("Edge Web page", ExpectedResult = false)]
public bool EdgeWebSites_ShouldBeFiltered_WhenSearchingForEdge(string query)
{
return pinned_webpage.FilterWebApplication(query);
}
[TestCase("chrome", ExpectedResult = true)]
[TestCase("CHROME", ExpectedResult = true)]
[TestCase("Google", ExpectedResult = true)]
[TestCase("Google Chrome", ExpectedResult = true)]
[TestCase("Google Chrome twitter", ExpectedResult = false)]
public bool ChromeWebSites_ShouldBeFiltered_WhenSearchingForChrome(string query)
{
return twitter_pwa.FilterWebApplication(query);
}
[TestCase("twitter", 0, ExpectedResult = false)]
[TestCase("Twit", 0, ExpectedResult = false)]
[TestCase("TWITTER", 0, ExpectedResult = false)]
[TestCase("web", 1, ExpectedResult = false)]
[TestCase("Page", 1, ExpectedResult = false)]
[TestCase("WEB PAGE", 1, ExpectedResult = false)]
[TestCase("edge", 2, ExpectedResult = false)]
[TestCase("EDGE", 2, ExpectedResult = false)]
public bool PinnedWebPages_ShouldNotBeFiltered_WhenSearchingForThem(string query, int Case)
{
const uint CASE_TWITTER = 0;
const uint CASE_WEB_PAGE = 1;
const uint CASE_EDGE_NAMED_WEBPAGE = 2;
// If the query is a part of the name of the web application, it should not be filtered,
// even if the name is the same as that of the main application, eg: case 2 - edge
if (Case == CASE_TWITTER)
{
return twitter_pwa.FilterWebApplication(query);
}
else if (Case == CASE_WEB_PAGE)
{
return pinned_webpage.FilterWebApplication(query);
}
else if (Case == CASE_EDGE_NAMED_WEBPAGE)
{
return edge_named_pinned_webpage.FilterWebApplication(query);
}
// unreachable code
return true;
}
[TestCase("Command Prompt")]
[TestCase("cmd")]
[TestCase("cmd.exe")]
[TestCase("ignoreQueryText")]
public void Win32Applications_ShouldNotBeFiltered_WhenFilteringRunCommands(string query)
{
// Even if there is an exact match in the name or exe name, win32 applications should never be filtered
Assert.IsTrue(command_prompt.QueryEqualsNameForRunCommands(query));
}
[TestCase("cmd")]
[TestCase("Cmd")]
[TestCase("CMD")]
public void RunCommands_ShouldNotBeFiltered_OnExactMatch(string query)
{
// Partial matches should be filtered as cmd is not equal to cmder
Assert.IsFalse(cmder_run_command.QueryEqualsNameForRunCommands(query));
// the query matches the name (cmd) and is therefore not filtered (case-insensitive)
Assert.IsTrue(cmd_run_command.QueryEqualsNameForRunCommands(query));
}
[Test]
public void WEB_APPLICATION_ReturnContextMenuWithOpenInConsole_WhenContextMenusIsCalled()
{
// Arrange
var mock = new Mock<IPublicAPI>();
mock.Setup(x => x.GetTranslation(It.IsAny<string>())).Returns(It.IsAny<string>());
// Act
List<ContextMenuResult> contextMenuResults = pinned_webpage.ContextMenus(mock.Object);
// Assert
Assert.AreEqual(contextMenuResults.Count, 3);
mock.Verify(x => x.GetTranslation("wox_plugin_program_run_as_administrator"), Times.Once());
mock.Verify(x => x.GetTranslation("wox_plugin_program_open_containing_folder"), Times.Once());
mock.Verify(x => x.GetTranslation("wox_plugin_program_open_in_console"), Times.Once());
}
[Test]
public void INTERNET_SHORTCUT_APPLICATION_ReturnContextMenuWithOpenInConsole_WhenContextMenusIsCalled()
{
// Arrange
var mock = new Mock<IPublicAPI>();
mock.Setup(x => x.GetTranslation(It.IsAny<string>())).Returns(It.IsAny<string>());
// Act
List<ContextMenuResult> contextMenuResults = dummy_internetShortcut_app.ContextMenus(mock.Object);
// Assert
Assert.AreEqual(contextMenuResults.Count, 2);
mock.Verify(x => x.GetTranslation("wox_plugin_program_open_containing_folder"), Times.Once());
mock.Verify(x => x.GetTranslation("wox_plugin_program_open_in_console"), Times.Once());
}
[Test]
public void WIN32_APPLICATION_ReturnContextMenuWithOpenInConsole_WhenContextMenusIsCalled()
{
// Arrange
var mock = new Mock<IPublicAPI>();
mock.Setup(x => x.GetTranslation(It.IsAny<string>())).Returns(It.IsAny<string>());
// Act
List<ContextMenuResult> contextMenuResults = chrome.ContextMenus(mock.Object);
// Assert
Assert.AreEqual(contextMenuResults.Count, 3);
mock.Verify(x => x.GetTranslation("wox_plugin_program_run_as_administrator"), Times.Once());
mock.Verify(x => x.GetTranslation("wox_plugin_program_open_containing_folder"), Times.Once());
mock.Verify(x => x.GetTranslation("wox_plugin_program_open_in_console"), Times.Once());
}
[Test]
public void RUN_COMMAND_ReturnContextMenuWithOpenInConsole_WhenContextMenusIsCalled()
{
// Arrange
var mock = new Mock<IPublicAPI>();
mock.Setup(x => x.GetTranslation(It.IsAny<string>())).Returns(It.IsAny<string>());
// Act
List<ContextMenuResult> contextMenuResults = cmd_run_command.ContextMenus(mock.Object);
// Assert
Assert.AreEqual(contextMenuResults.Count, 3);
mock.Verify(x => x.GetTranslation("wox_plugin_program_run_as_administrator"), Times.Once());
mock.Verify(x => x.GetTranslation("wox_plugin_program_open_containing_folder"), Times.Once());
mock.Verify(x => x.GetTranslation("wox_plugin_program_open_in_console"), Times.Once());
}
[Test]
public void Win32Apps_ShouldSetNameAsTitle_WhileCreatingResult()
{
var mock = new Mock<IPublicAPI>();
mock.Setup(x => x.GetTranslation(It.IsAny<string>())).Returns(It.IsAny<string>());
StringMatcher.Instance = new StringMatcher();
// Act
var result = cmder_run_command.Result("cmder", mock.Object);
// Assert
Assert.IsTrue(result.Title.Equals(cmder_run_command.Name));
Assert.IsFalse(result.Title.Equals(cmder_run_command.Description));
}
}
}

View File

@@ -0,0 +1,148 @@
using Microsoft.Plugin.Program.Storage;
using Moq;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Media.Capture;
using Wox.Infrastructure.Storage;
namespace Microsoft.Plugin.Program.UnitTests.Storage
{
[TestFixture]
class ListRepositoryTests
{
[Test]
public void Contains_ShouldReturnTrue_WhenListIsInitializedWithItem()
{
//Arrange
var itemName = "originalItem1";
IRepository<string> repository = new ListRepository<string>() { itemName };
//Act
var result = repository.Contains(itemName);
//Assert
Assert.IsTrue(result);
}
[Test]
public void Contains_ShouldReturnTrue_WhenListIsUpdatedWithAdd()
{
//Arrange
IRepository<string> repository = new ListRepository<string>();
//Act
var itemName = "newItem";
repository.Add(itemName);
var result = repository.Contains(itemName);
//Assert
Assert.IsTrue(result);
}
[Test]
public void Contains_ShouldReturnFalse_WhenListIsUpdatedWithRemove()
{
//Arrange
var itemName = "originalItem1";
IRepository<string> repository = new ListRepository<string>() { itemName };
//Act
repository.Remove(itemName);
var result = repository.Contains(itemName);
//Assert
Assert.IsFalse(result);
}
[Test]
public async Task Add_ShouldNotThrow_WhenBeingIterated()
{
//Arrange
ListRepository<string> repository = new ListRepository<string>();
var numItems = 1000;
for(var i=0; i<numItems;++i)
{
repository.Add($"OriginalItem_{i}");
}
//Act - Begin iterating on one thread
var iterationTask = Task.Run(() =>
{
var remainingIterations = 10000;
while (remainingIterations > 0)
{
foreach (var item in repository)
{
//keep iterating
}
--remainingIterations;
}
});
//Act - Insert on another thread
var addTask = Task.Run(() =>
{
for (var i = 0; i < numItems; ++i)
{
repository.Add($"NewItem_{i}");
}
});
//Assert that this does not throw. Collections that aren't syncronized will throw an invalidoperatioexception if the list is modified while enumerating
Assert.DoesNotThrowAsync(async () =>
{
await Task.WhenAll(new Task[] { iterationTask, addTask });
});
}
[Test]
public async Task Remove_ShouldNotThrow_WhenBeingIterated()
{
//Arrange
ListRepository<string> repository = new ListRepository<string>();
var numItems = 1000;
for (var i = 0; i < numItems; ++i)
{
repository.Add($"OriginalItem_{i}");
}
//Act - Begin iterating on one thread
var iterationTask = Task.Run(() =>
{
var remainingIterations = 10000;
while (remainingIterations > 0)
{
foreach (var item in repository)
{
//keep iterating
}
--remainingIterations;
}
});
//Act - Remove on another thread
var addTask = Task.Run(() =>
{
for (var i = 0; i < numItems; ++i)
{
repository.Remove($"OriginalItem_{i}");
}
});
//Assert that this does not throw. Collections that aren't syncronized will throw an invalidoperatioexception if the list is modified while enumerating
Assert.DoesNotThrowAsync(async () =>
{
await Task.WhenAll(new Task[] { iterationTask, addTask });
});
}
}
}

View File

@@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.Plugin.Program.UnitTests.Storage
{
class PackageRepositoryTest
{
}
}

View File

@@ -1,207 +1,185 @@
using Microsoft.PowerToys.Settings.UI.Lib;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Timers;
using System.Windows.Controls;
using Wox.Infrastructure.Logger;
using Wox.Infrastructure.Storage;
using Wox.Plugin;
using Microsoft.Plugin.Program.Views;
using Stopwatch = Wox.Infrastructure.Stopwatch;
using Microsoft.Plugin.Program.Programs;
namespace Microsoft.Plugin.Program
{
public class Main : ISettingProvider, IPlugin, IPluginI18n, IContextMenu, ISavable, IReloadable, IDisposable
{
private static readonly object IndexLock = new object();
internal static Programs.Win32[] _win32s { get; set; }
internal static Programs.UWP.Application[] _uwps { get; set; }
internal static Settings _settings { get; set; }
FileSystemWatcher _watcher = null;
System.Timers.Timer _timer = null;
private static bool IsStartupIndexProgramsRequired => _settings.LastIndexTime.AddDays(3) < DateTime.Today;
private static PluginInitContext _context;
private static BinaryStorage<Programs.Win32[]> _win32Storage;
private static BinaryStorage<Programs.UWP.Application[]> _uwpStorage;
private readonly PluginJsonStorage<Settings> _settingsStorage;
using Microsoft.PowerToys.Settings.UI.Lib;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Timers;
using System.Windows.Controls;
using Wox.Infrastructure.Logger;
using Wox.Infrastructure.Storage;
using Wox.Plugin;
using Microsoft.Plugin.Program.Views;
using Stopwatch = Wox.Infrastructure.Stopwatch;
using Windows.ApplicationModel;
using Microsoft.Plugin.Program.Storage;
using Microsoft.Plugin.Program.Programs;
namespace Microsoft.Plugin.Program
{
public class Main : IPlugin, IPluginI18n, IContextMenu, ISavable, IReloadable, IDisposable
{
private static readonly object IndexLock = new object();
internal static Programs.Win32[] _win32s { get; set; }
internal static Settings _settings { get; set; }
private static bool IsStartupIndexProgramsRequired => _settings.LastIndexTime.AddDays(3) < DateTime.Today;
private static PluginInitContext _context;
private static BinaryStorage<Programs.Win32[]> _win32Storage;
private readonly PluginJsonStorage<Settings> _settingsStorage;
private bool _disposed = false;
public Main()
{
_settingsStorage = new PluginJsonStorage<Settings>();
_settings = _settingsStorage.Load();
Stopwatch.Normal("|Microsoft.Plugin.Program.Main|Preload programs cost", () =>
{
_win32Storage = new BinaryStorage<Programs.Win32[]>("Win32");
_win32s = _win32Storage.TryLoad(new Programs.Win32[] { });
_uwpStorage = new BinaryStorage<Programs.UWP.Application[]>("UWP");
_uwps = _uwpStorage.TryLoad(new Programs.UWP.Application[] { });
});
Log.Info($"|Microsoft.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>");
Log.Info($"|Microsoft.Plugin.Program.Main|Number of preload uwps <{_uwps.Length}>");
var a = Task.Run(() =>
{
if (IsStartupIndexProgramsRequired || !_win32s.Any())
Stopwatch.Normal("|Microsoft.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs);
});
var b = Task.Run(() =>
{
if (IsStartupIndexProgramsRequired || !_uwps.Any())
Stopwatch.Normal("|Microsoft.Plugin.Program.Main|Win32Program index cost", IndexUWPPrograms);
});
Task.WaitAll(a, b);
_settings.LastIndexTime = DateTime.Today;
InitializeFileWatchers();
InitializeTimer();
}
public void Save()
{
_settingsStorage.Save();
_win32Storage.Save(_win32s);
_uwpStorage.Save(_uwps);
}
public List<Result> Query(Query query)
{
Programs.Win32[] win32;
Programs.UWP.Application[] uwps;
lock (IndexLock)
{
// just take the reference inside the lock to eliminate query time issues.
win32 = _win32s;
uwps = _uwps;
}
var results1 = win32.AsParallel()
.Where(p => p.Enabled)
.Select(p => p.Result(query.Search, _context.API));
var results2 = uwps.AsParallel()
.Where(p => p.Enabled)
.Select(p => p.Result(query.Search, _context.API));
var result = results1.Concat(results2).Where(r => r != null && r.Score > 0).ToList();
return result;
}
public void Init(PluginInitContext context)
{
_context = context;
_context.API.ThemeChanged += OnThemeChanged;
UpdateUWPIconPath(_context.API.GetCurrentTheme());
}
public void OnThemeChanged(Theme _, Theme currentTheme)
{
UpdateUWPIconPath(currentTheme);
}
public void UpdateUWPIconPath(Theme theme)
{
foreach (UWP.Application app in _uwps)
{
app.UpdatePath(theme);
}
}
public static void IndexWin32Programs()
{
var win32S = Programs.Win32.All(_settings);
lock (IndexLock)
{
_win32s = win32S;
}
}
public static void IndexUWPPrograms()
{
var windows10 = new Version(10, 0);
var support = Environment.OSVersion.Version.Major >= windows10.Major;
var applications = support ? Programs.UWP.All() : new Programs.UWP.Application[] { };
lock (IndexLock)
{
_uwps = applications;
}
}
public static void IndexPrograms()
{
var t1 = Task.Run(() => IndexWin32Programs());
var t2 = Task.Run(() => IndexUWPPrograms());
Task.WaitAll(t1, t2);
_settings.LastIndexTime = DateTime.Today;
}
public Control CreateSettingPanel()
{
return new ProgramSetting(_context, _settings, _win32s, _uwps);
}
public string GetTranslatedPluginTitle()
{
return _context.API.GetTranslation("wox_plugin_program_plugin_name");
}
public string GetTranslatedPluginDescription()
{
return _context.API.GetTranslation("wox_plugin_program_plugin_description");
}
public List<ContextMenuResult> LoadContextMenus(Result selectedResult)
{
var menuOptions = new List<ContextMenuResult>();
var program = selectedResult.ContextData as Programs.IProgram;
if (program != null)
{
menuOptions = program.ContextMenus(_context.API);
}
return menuOptions;
}
public static void StartProcess(Func<ProcessStartInfo, Process> runProcess, ProcessStartInfo info)
{
try
{
runProcess(info);
}
catch (Exception)
{
var name = "Plugin: Program";
var message = $"Unable to start: {info.FileName}";
_context.API.ShowMsg(name, message, string.Empty);
}
}
public void ReloadData()
{
IndexPrograms();
}
public void UpdateSettings(PowerLauncherSettings settings)
{
}
private PackageRepository _packageRepository = new PackageRepository(new PackageCatalogWrapper(), new BinaryStorage<IList<UWP.Application>>("UWP"));
public Main()
{
_settingsStorage = new PluginJsonStorage<Settings>();
_settings = _settingsStorage.Load();
Stopwatch.Normal("|Microsoft.Plugin.Program.Main|Preload programs cost", () =>
{
_win32Storage = new BinaryStorage<Programs.Win32[]>("Win32");
_win32s = _win32Storage.TryLoad(new Programs.Win32[] { });
_packageRepository.Load();
});
Log.Info($"|Microsoft.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>");
var a = Task.Run(() =>
{
if (IsStartupIndexProgramsRequired || !_win32s.Any())
Stopwatch.Normal("|Microsoft.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs);
});
var b = Task.Run(() =>
{
if (IsStartupIndexProgramsRequired || !_packageRepository.Any())
Stopwatch.Normal("|Microsoft.Plugin.Program.Main|Win32Program index cost", _packageRepository.IndexPrograms);
});
Task.WaitAll(a, b);
_settings.LastIndexTime = DateTime.Today;
}
public void Save()
{
_settingsStorage.Save();
_win32Storage.Save(_win32s);
_packageRepository.Save();
}
public List<Result> Query(Query query)
{
Programs.Win32[] win32;
lock (IndexLock)
{
// just take the reference inside the lock to eliminate query time issues.
win32 = _win32s;
}
var results1 = win32.AsParallel()
.Where(p => p.Enabled)
.Select(p => p.Result(query.Search, _context.API));
var results2 = _packageRepository.AsParallel()
.Where(p => p.Enabled)
.Select(p => p.Result(query.Search, _context.API));
var result = results1.Concat(results2).Where(r => r != null && r.Score > 0).ToList();
return result;
}
public void Init(PluginInitContext context)
{
_context = context;
_context.API.ThemeChanged += OnThemeChanged;
UpdateUWPIconPath(_context.API.GetCurrentTheme());
}
public void OnThemeChanged(Theme _, Theme currentTheme)
{
UpdateUWPIconPath(currentTheme);
}
public void UpdateUWPIconPath(Theme theme)
{
foreach (UWP.Application app in _packageRepository)
{
app.UpdatePath(theme);
}
}
public static void IndexWin32Programs()
{
var win32S = Programs.Win32.All(_settings);
lock (IndexLock)
{
_win32s = win32S;
}
}
public void IndexPrograms()
{
var t1 = Task.Run(() => IndexWin32Programs());
var t2 = Task.Run(() => _packageRepository.IndexPrograms());
Task.WaitAll(t1, t2);
_settings.LastIndexTime = DateTime.Today;
}
public string GetTranslatedPluginTitle()
{
return _context.API.GetTranslation("wox_plugin_program_plugin_name");
}
public string GetTranslatedPluginDescription()
{
return _context.API.GetTranslation("wox_plugin_program_plugin_description");
}
public List<ContextMenuResult> LoadContextMenus(Result selectedResult)
{
var menuOptions = new List<ContextMenuResult>();
var program = selectedResult.ContextData as Programs.IProgram;
if (program != null)
{
menuOptions = program.ContextMenus(_context.API);
}
return menuOptions;
}
public static void StartProcess(Func<ProcessStartInfo, Process> runProcess, ProcessStartInfo info)
{
try
{
runProcess(info);
}
catch (Exception)
{
var name = "Plugin: Program";
var message = $"Unable to start: {info.FileName}";
_context.API.ShowMsg(name, message, string.Empty);
}
}
public void ReloadData()
{
IndexPrograms();
}
public void UpdateSettings(PowerLauncherSettings settings)
{
}
public void Dispose()
{
@@ -221,52 +199,5 @@ namespace Microsoft.Plugin.Program
}
}
void InitializeFileWatchers()
{
// Create a new FileSystemWatcher and set its properties.
_watcher = new FileSystemWatcher();
var resolvedPath = Environment.ExpandEnvironmentVariables("%ProgramFiles%");
_watcher.Path = resolvedPath;
//Filter to create and deletes of 'microsoft.system.package.metadata' directories.
_watcher.NotifyFilter = NotifyFilters.DirectoryName | NotifyFilters.FileName;
_watcher.IncludeSubdirectories = true;
// Add event handlers.
_watcher.Created += OnChanged;
_watcher.Deleted += OnChanged;
// Begin watching.
_watcher.EnableRaisingEvents = true;
}
void InitializeTimer()
{
//multiple file writes occur on install / uninstall. Adding a delay before actually indexing.
var delayInterval = 5000;
_timer = new System.Timers.Timer(delayInterval);
_timer.Enabled = true;
_timer.AutoReset = false;
_timer.Elapsed += FileWatchElapsedTimer;
_timer.Stop();
}
//When a watched directory changes then reset the timer.
private void OnChanged(object source, FileSystemEventArgs e)
{
Log.Debug($"|Microsoft.Plugin.Program.Main|Directory Changed: {e.FullPath} {e.ChangeType} - Resetting timer.");
_timer.Stop();
_timer.Start();
}
private void FileWatchElapsedTimer(object sender, ElapsedEventArgs e)
{
Task.Run(() =>
{
Log.Debug($"|Microsoft.Plugin.Program.Main| ReIndexing UWP Programs");
IndexUWPPrograms();
Log.Debug($"|Microsoft.Plugin.Program.Main| Done ReIndexing");
});
}
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
using Windows.ApplicationModel;
using Windows.Foundation;
namespace Microsoft.Plugin.Program.Programs
{
internal interface IPackageCatalog
{
event TypedEventHandler<PackageCatalog, PackageInstallingEventArgs> PackageInstalling;
event TypedEventHandler<PackageCatalog, PackageUninstallingEventArgs> PackageUninstalling;
event TypedEventHandler<PackageCatalog, PackageUpdatingEventArgs> PackageUpdating;
}
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Text;
using Windows.ApplicationModel;
using Windows.Foundation;
namespace Microsoft.Plugin.Program.Programs
{
/// <summary>
/// This is a simple wrapper class around the PackageCatalog to facilitate unit testing.
/// </summary>
internal class PackageCatalogWrapper : IPackageCatalog
{
PackageCatalog _packageCatalog;
public PackageCatalogWrapper()
{
//Opens the catalog of packages that is available for the current user.
_packageCatalog = PackageCatalog.OpenForCurrentUser();
}
//
// Summary:
// Indicates that an app package is installing.
public event TypedEventHandler<PackageCatalog, PackageInstallingEventArgs> PackageInstalling
{
add
{
_packageCatalog.PackageInstalling += value;
}
remove
{
_packageCatalog.PackageInstalling -= value;
}
}
//
// Summary:
// Indicates that an app package is uninstalling.
public event TypedEventHandler<PackageCatalog, PackageUninstallingEventArgs> PackageUninstalling
{
add
{
_packageCatalog.PackageUninstalling += value;
}
remove
{
_packageCatalog.PackageUninstalling -= value;
}
}
//
// Summary:
// Indicates that an app package is updating.
public event TypedEventHandler<PackageCatalog, PackageUpdatingEventArgs> PackageUpdating
{
add
{
_packageCatalog.PackageUpdating += value;
}
remove
{
_packageCatalog.PackageUpdating -= value;
}
}
}
}

View File

@@ -30,7 +30,7 @@ namespace Microsoft.Plugin.Program.Programs
{
public string Name { get; }
public string FullName { get; }
public string FamilyName { get; }
public string FamilyName { get; }
public string Location { get; set; }
public Application[] Apps { get; set; }
@@ -39,24 +39,17 @@ namespace Microsoft.Plugin.Program.Programs
public UWP(Package package)
{
Location = package.InstalledLocation.Path;
Name = package.Id.Name;
FullName = package.Id.FullName;
FamilyName = package.Id.FamilyName;
InitializeAppInfo();
Apps = Apps.Where(a =>
{
var valid =
!string.IsNullOrEmpty(a.UserModelId) &&
!string.IsNullOrEmpty(a.DisplayName);
return valid;
}).ToArray();
}
private void InitializeAppInfo()
public void InitializeAppInfo(string installedLocation)
{
Location = installedLocation;
AppxPackageHelper _helper = new AppxPackageHelper();
var path = Path.Combine(Location, "AppxManifest.xml");
var path = Path.Combine(installedLocation, "AppxManifest.xml");
var namespaces = XmlNamespaces(path);
InitPackageVersion(namespaces);
@@ -75,9 +68,17 @@ namespace Microsoft.Plugin.Program.Programs
{
var app = new Application(_app, this);
apps.Add(app);
}
Apps = apps.Where(a => a.AppListEntry != "none").ToArray();
}
Apps = apps.Where(a =>
{
var valid =
!string.IsNullOrEmpty(a.UserModelId) &&
!string.IsNullOrEmpty(a.DisplayName) &&
a.AppListEntry != "none";
return valid;
}).ToArray();
}
else
{
@@ -153,21 +154,14 @@ namespace Microsoft.Plugin.Program.Programs
try
{
u = new UWP(p);
u.InitializeAppInfo(p.InstalledLocation.Path);
}
#if !DEBUG
catch (Exception e)
{
ProgramLogger.LogException($"|UWP|All|{p.InstalledLocation}|An unexpected error occurred and "
+ $"unable to convert Package to UWP for {p.Id.FullName}", e);
return new Application[] { };
}
#endif
#if DEBUG //make developer aware and implement handling
catch
{
throw;
}
#endif
return u.Apps;
}).ToArray();

View File

@@ -0,0 +1,11 @@
using Windows.ApplicationModel;
namespace Microsoft.Plugin.Program.Storage
{
internal interface IProgramRepository
{
void IndexPrograms();
void Load();
void Save();
}
}

View File

@@ -0,0 +1,89 @@
using Microsoft.Plugin.Program.Logger;
using Microsoft.Plugin.Program.Programs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Wox.Infrastructure.Storage;
namespace Microsoft.Plugin.Program.Storage
{
/// <summary>
/// A repository for storing packaged applications such as UWP apps or appx packaged desktop apps.
/// This repository will also monitor for changes to the PackageCatelog and update the repository accordingly
/// </summary>
internal class PackageRepository : ListRepository<UWP.Application>, IProgramRepository
{
private IStorage<IList<UWP.Application>> _storage;
private IPackageCatalog _packageCatalog;
public PackageRepository(IPackageCatalog packageCatalog, IStorage<IList<UWP.Application>> storage)
{
_storage = storage ?? throw new ArgumentNullException("storage", "StorageRepository requires an initialized storage interface");
_packageCatalog = packageCatalog ?? throw new ArgumentNullException("packageCatalog", "PackageRepository expects an interface to be able to subscribe to package events");
_packageCatalog.PackageInstalling += OnPackageInstalling;
_packageCatalog.PackageUninstalling += OnPackageUninstalling;
}
public void OnPackageInstalling(PackageCatalog p, PackageInstallingEventArgs args)
{
if (args.IsComplete)
{
try
{
var uwp = new UWP(args.Package);
uwp.InitializeAppInfo(args.Package.InstalledLocation.Path);
foreach (var app in uwp.Apps)
{
Add(app);
}
}
//InitializeAppInfo will throw if there is no AppxManifest.xml for the package.
//Note there are sometimes multiple packages per product and this doesn't necessarily mean that we haven't found the app.
//eg. "Could not find file 'C:\\Program Files\\WindowsApps\\Microsoft.WindowsTerminalPreview_2020.616.45.0_neutral_~_8wekyb3d8bbwe\\AppxManifest.xml'."
catch (System.IO.FileNotFoundException e)
{
ProgramLogger.LogException($"|UWP|OnPackageInstalling|{args.Package.InstalledLocation}|{e.Message}", e);
}
}
}
public void OnPackageUninstalling(PackageCatalog p, PackageUninstallingEventArgs args)
{
if (args.Progress == 0)
{
//find apps associated with this package.
var uwp = new UWP(args.Package);
var apps = Items.Where(a => a.Package.Equals(uwp)).ToArray();
foreach (var app in apps)
{
Remove(app);
}
}
}
public void IndexPrograms()
{
var windows10 = new Version(10, 0);
var support = Environment.OSVersion.Version.Major >= windows10.Major;
var applications = support ? Programs.UWP.All() : new Programs.UWP.Application[] { };
Set(applications);
}
public void Save()
{
_storage.Save(Items);
}
public void Load()
{
var items = _storage.TryLoad(new Programs.UWP.Application[] { });
Set(items);
}
}
}

View File

@@ -61,20 +61,6 @@ namespace Microsoft.Plugin.Program.Views.Commands
Enabled = t1.Enabled
}
));
Main._uwps
.Where(t1 => !ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier))
.ToList()
.ForEach(t1 => ProgramSetting.ProgramSettingDisplayList
.Add(
new ProgramSource
{
Name = t1.DisplayName,
Location = t1.Package.Location,
UniqueIdentifier = t1.UniqueIdentifier,
Enabled = t1.Enabled
}
));
}
internal static void SetProgramSourcesStatus(this List<ProgramSource> list, List<ProgramSource> selectedProgramSourcesToDisable, bool status)
@@ -88,11 +74,6 @@ namespace Microsoft.Plugin.Program.Views.Commands
.Where(t1 => selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier && t1.Enabled != status))
.ToList()
.ForEach(t1 => t1.Enabled = status);
Main._uwps
.Where(t1 => selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier && t1.Enabled != status))
.ToList()
.ForEach(t1 => t1.Enabled = status);
}
internal static void StoreDisabledInSettings(this List<ProgramSource> list)
@@ -133,7 +114,7 @@ namespace Microsoft.Plugin.Program.Views.Commands
internal static bool IsReindexRequired(this List<ProgramSource> selectedItems)
{
if (selectedItems.Where(t1 => t1.Enabled && !Main._uwps.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)).Count() > 0
if (selectedItems.Where(t1 => t1.Enabled).Count() > 0
&& selectedItems.Where(t1 => t1.Enabled && !Main._win32s.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)).Count() > 0)
return true;

View File

@@ -51,7 +51,6 @@ namespace Microsoft.Plugin.Program.Views
Task.Run(() =>
{
Dispatcher.Invoke(() => { indexingPanel.Visibility = Visibility.Visible; });
Main.IndexPrograms();
Dispatcher.Invoke(() => { indexingPanel.Visibility = Visibility.Hidden; });
});
}

View File

@@ -10,7 +10,7 @@ namespace Microsoft.PowerLauncher.Telemetry
/// <summary>
/// TODO: This should be replaced by a P/Invoke call to get_product_version
/// </summary>
public string Version => "v0.19.0";
public string Version => "v0.19.2";
public double BootTimeMs { get; set; }

View File

@@ -110,22 +110,31 @@ namespace PowerLauncher
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (Visibility == System.Windows.Visibility.Visible)
if(e.PropertyName == nameof(MainViewModel.MainWindowVisibility))
{
// Not called on first launch
// Additionally called when deactivated by clicking on screen
UpdatePosition();
BringProcessToForeground();
if (!_viewModel.LastQuerySelected)
if (Visibility == System.Windows.Visibility.Visible)
{
_viewModel.LastQuerySelected = true;
// Not called on first launch
// Additionally called when deactivated by clicking on screen
UpdatePosition();
BringProcessToForeground();
if (!_viewModel.LastQuerySelected)
{
_viewModel.LastQuerySelected = true;
}
}
}
}
else if (e.PropertyName == nameof(MainViewModel.SystemQueryText))
{
this._isTextSetProgrammatically = true;
SearchBox.QueryTextBox.Text = _viewModel.SystemQueryText;
if (_viewModel.Results != null)
{
SearchBox.QueryTextBox.Text = MainViewModel.GetSearchText(
_viewModel.Results.SelectedIndex,
_viewModel.SystemQueryText,
_viewModel.QueryText);
}
}
}
@@ -261,32 +270,20 @@ namespace PowerLauncher
}
// To populate the AutoCompleteTextBox as soon as the selection is changed or set.
// Setting it here instead of when the text is changed as there is a delay in executing the query and populating the result
SearchBox.AutoCompleteTextBlock.Text = ListView_FirstItem(_viewModel.QueryText);
// Setting it here instead of when the text is changed as there is a delay in executing the query and populating the result
if (_viewModel.Results != null)
{
SearchBox.AutoCompleteTextBlock.Text = MainViewModel.GetAutoCompleteText(
_viewModel.Results.SelectedIndex,
_viewModel.Results.SelectedItem?.ToString(),
_viewModel.QueryText);
}
}
private const int millisecondsToWait = 100;
private static DateTime s_lastTimeOfTyping;
private bool disposedValue = false;
private string ListView_FirstItem(String input)
{
if (!string.IsNullOrEmpty(input))
{
string selectedItem = _viewModel.Results?.SelectedItem?.ToString();
int selectedIndex = _viewModel.Results.SelectedIndex;
if (selectedItem != null && selectedIndex == 0)
{
if (selectedItem.IndexOf(input, StringComparison.InvariantCultureIgnoreCase) == 0)
{
// Use the same case as the input query for the matched portion of the string
return input + selectedItem.Substring(input.Length);
}
}
}
return string.Empty;
}
private void QueryTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (_isTextSetProgrammatically)

View File

@@ -12,7 +12,7 @@ namespace Wox.Infrastructure.Storage
/// Storage object using binary data
/// Normally, it has better performance, but not readable
/// </summary>
public class BinaryStorage<T>
public class BinaryStorage<T> : IStorage<T>
{
// This storage helper returns whether or not to delete the binary storage items
private static readonly int BINARY_STORAGE = 0;

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Wox.Infrastructure.Storage
{
public interface IRepository<T>
{
void Add(T insertedItem);
void Remove(T removedItem);
bool Contains(T item);
void Set(IList<T> list);
bool Any();
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Wox.Infrastructure.Storage
{
public interface IStorage<T>
{
/// <summary>
/// Saves the data
/// </summary>
/// <param name="data"></param>
void Save(T data);
/// <summary>
/// Attempts to load data, otherwise it will return the default provided
/// </summary>
/// <param name="defaultData"></param>
/// <returns>The loaded data or default</returns>
T TryLoad(T defaultData);
}
}

View File

@@ -0,0 +1,80 @@
using NLog.Filters;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls.Primitives;
using Wox.Infrastructure;
using Wox.Infrastructure.Logger;
namespace Wox.Infrastructure.Storage
{
/// <summary>
/// The intent of this class is to provide a basic subset of 'list' like operations, without exposing callers to the internal representation
/// of the data structure. Currently this is implemented as a list for it's simplicity.
/// </summary>
/// <typeparam name="T"></typeparam>
public class ListRepository<T> : IRepository<T>, IEnumerable<T>
{
public IList<T> Items { get { return _items.Values.ToList(); } }
private ConcurrentDictionary<int, T> _items = new ConcurrentDictionary<int, T>();
public ListRepository()
{
}
public void Set(IList<T> items)
{
//enforce that internal representation
_items = new ConcurrentDictionary<int, T>(items.ToDictionary( i => i.GetHashCode()));
}
public bool Any()
{
return _items.Any();
}
public void Add(T insertedItem)
{
if (!_items.TryAdd(insertedItem.GetHashCode(), insertedItem))
{
Log.Error($"|ListRepository.Add| Item Already Exists <{insertedItem}>");
}
}
public void Remove(T removedItem)
{
if (!_items.TryRemove(removedItem.GetHashCode(), out _))
{
Log.Error($"|ListRepository.Remove| Item Not Found <{removedItem}>");
}
}
public ParallelQuery<T> AsParallel()
{
return _items.Values.AsParallel();
}
public bool Contains(T item)
{
return _items.ContainsKey(item.GetHashCode());
}
public IEnumerator<T> GetEnumerator()
{
return _items.Values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return _items.GetEnumerator();
}
}
}

View File

@@ -2,8 +2,10 @@ using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using static Wox.Infrastructure.StringMatcher;
[assembly: InternalsVisibleToAttribute("Microsoft.Plugin.Program.UnitTests")]
namespace Wox.Infrastructure
{
public class StringMatcher

View File

@@ -11,7 +11,7 @@ namespace Wox.Plugin
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

View File

@@ -0,0 +1,211 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Text;
using Wox.Plugin;
using Wox.ViewModel;
namespace Wox.Test
{
[TestFixture]
class MainViewModelTest
{
[Test]
public void MainViewModel_GetAutoCompleteTextReturnsEmptyString_WhenInputIsNull()
{
// Arrange
int index = 0;
string input = null;
String query = "M";
// Act
string autoCompleteText = MainViewModel.GetAutoCompleteText(index, input, query);
// Assert
Assert.AreEqual(autoCompleteText, string.Empty);
}
[Test]
public void MainViewModel_GetAutoCompleteTextReturnsEmptyString_WhenInputIsEmpty()
{
// Arrange
int index = 0;
string input = string.Empty;
String query = "M";
// Act
string autoCompleteText = MainViewModel.GetAutoCompleteText(index, input, query);
// Assert
Assert.AreEqual(autoCompleteText, string.Empty);
}
[Test]
public void MainViewModel_GetAutoCompleteTextReturnsEmptyString_WhenQueryIsNull()
{
// Arrange
int index = 0;
string input = "M";
String query = null;
// Act
string autoCompleteText = MainViewModel.GetAutoCompleteText(index, input, query);
// Assert
Assert.AreEqual(autoCompleteText, string.Empty);
}
[Test]
public void MainViewModel_GetAutoCompleteTextReturnsEmptyString_WhenQueryIsEmpty()
{
// Arrange
int index = 0;
string input = "M";
String query = string.Empty;
// Act
string autoCompleteText = MainViewModel.GetAutoCompleteText(index, input, query);
// Assert
Assert.AreEqual(autoCompleteText, string.Empty);
}
[Test]
public void MainViewModel_GetAutoCompleteTextReturnsEmptyString_WhenIndexIsNonZero()
{
// Arrange
int index = 2;
string input = "Visual";
String query = "Vis";
// Act
string autoCompleteText = MainViewModel.GetAutoCompleteText(index, input, query);
// Assert
Assert.AreEqual(autoCompleteText, string.Empty);
}
[Test]
public void MainViewModel_GetAutoCompleteTextReturnsMatchingString_WhenIndexIsZeroAndMatch()
{
// Arrange
int index = 0;
string input = "VISUAL";
String query = "VIs";
string ExpectedAutoCompleteText = "VIsUAL";
// Act
string autoCompleteText = MainViewModel.GetAutoCompleteText(index, input, query);
// Assert
Assert.AreEqual(autoCompleteText, ExpectedAutoCompleteText);
}
[Test]
public void MainViewModel_GetAutoCompleteTextReturnsEmptyString_WhenIndexIsZeroAndNoMatch()
{
// Arrange
int index = 0;
string input = "VISUAL";
String query = "Vim";
string ExpectedAutoCompleteText = string.Empty;
// Act
string autoCompleteText = MainViewModel.GetAutoCompleteText(index, input, query);
// Assert
Assert.AreEqual(autoCompleteText, ExpectedAutoCompleteText);
}
[Test]
public void MainViewModel_GetSearchTextReturnsEmptyString_WhenInputIsNull()
{
// Arrange
int index = 0;
string input = null;
String query = "M";
// Act
string autoCompleteText = MainViewModel.GetSearchText(index, input, query);
// Assert
Assert.AreEqual(autoCompleteText, string.Empty);
}
[Test]
public void MainViewModel_GetSearchTextReturnsEmptyString_WhenInputIsEmpty()
{
// Arrange
int index = 0;
string input = string.Empty;
String query = "M";
// Act
string autoCompleteText = MainViewModel.GetSearchText(index, input, query);
// Assert
Assert.AreEqual(autoCompleteText, string.Empty);
}
[Test]
public void MainViewModel_GetSearchTextReturnsInputString_WhenQueryIsNull()
{
// Arrange
int index = 0;
string input = "Visual";
String query = null;
// Act
string autoCompleteText = MainViewModel.GetSearchText(index, input, query);
// Assert
Assert.AreEqual(autoCompleteText, input);
}
[Test]
public void MainViewModel_GetSearchTextReturnsInputString_WhenQueryIsEmpty()
{
// Arrange
int index = 0;
string input = "Visual";
String query = string.Empty;
// Act
string autoCompleteText = MainViewModel.GetSearchText(index, input, query);
// Assert
Assert.AreEqual(autoCompleteText, input);
}
[Test]
public void MainViewModel_GetSearchTextReturnsMatchingStringWithCase_WhenIndexIsZeroAndMatch()
{
// Arrange
int index = 0;
string input = "VISUAL";
String query = "VIs";
string ExpectedAutoCompleteText = "VIsUAL";
// Act
string autoCompleteText = MainViewModel.GetSearchText(index, input, query);
// Assert
Assert.AreEqual(autoCompleteText, ExpectedAutoCompleteText);
}
[Test]
public void MainViewModel_GetSearchTextReturnsInput_WhenIndexIsZeroAndNoMatch()
{
// Arrange
int index = 0;
string input = "VISUAL";
String query = "Vim";
// Act
string autoCompleteText = MainViewModel.GetSearchText(index, input, query);
// Assert
Assert.AreEqual(autoCompleteText, input);
}
}
}

View File

@@ -44,6 +44,7 @@
<ProjectReference Include="..\Wox.Core\Wox.Core.csproj" />
<ProjectReference Include="..\Wox.Infrastructure\Wox.Infrastructure.csproj" />
<ProjectReference Include="..\Wox.Plugin\Wox.Plugin.csproj" />
<ProjectReference Include="..\Wox\Wox.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,58 @@
using System.Windows.Input;
using NUnit.Framework;
using Wox.Plugin;
using Wox.ViewModel;
using System.Runtime.CompilerServices;
namespace Wox.Test
{
[TestFixture]
public class Wox
{
// A Dummy class to test that OnPropertyChanged() is called while we set the variable
public class DummyTestClass : BaseModel
{
public bool isFunctionCalled = false;
private ICommand _item;
public ICommand Item
{
get
{
return this._item;
}
set
{
if (value != this._item)
{
this._item = value;
OnPropertyChanged();
}
}
}
// Overriding the OnPropertyChanged() function to test if it is being called
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
isFunctionCalled = true;
}
}
[Test]
public void AnyVariable_MustCallOnPropertyChanged_WhenSet()
{
// Arrange
DummyTestClass testClass = new DummyTestClass();
// Act
testClass.Item = new RelayCommand(null);
// Assert
Assert.IsTrue(testClass.isFunctionCalled);
}
}
}

View File

@@ -9,11 +9,29 @@ namespace Wox.ViewModel
{
public class ContextMenuItemViewModel : BaseModel
{
private ICommand _command;
public string PluginName { get; set; }
public string Title { get; set; }
public string Glyph { get; set; }
public string FontFamily { get; set; }
public ICommand Command { get; set; }
public ICommand Command {
get
{
return this._command;
}
set
{
// ICommand does not implement the INotifyPropertyChanged interface and must call OnPropertyChanged() to prevent memory leaks
if (value != this._command)
{
this._command = value;
OnPropertyChanged();
}
}
}
public Key AcceleratorKey { get; set; }
public ModifierKeys AcceleratorModifiers { get; set; }
public bool IsAcceleratorKeyEnabled { get; set; }

View File

@@ -709,6 +709,40 @@ namespace Wox.ViewModel
}
}
public static string GetAutoCompleteText(int index, string input, String query)
{
if (!string.IsNullOrEmpty(input) && !string.IsNullOrEmpty(query))
{
if (index == 0)
{
if (input.IndexOf(query, StringComparison.InvariantCultureIgnoreCase) == 0)
{
// Use the same case as the input query for the matched portion of the string
return query + input.Substring(query.Length);
}
}
}
return string.Empty;
}
public static string GetSearchText(int index, String input, string query)
{
if (!string.IsNullOrEmpty(input))
{
if (index == 0 && !string.IsNullOrEmpty(query))
{
if (input.IndexOf(query, StringComparison.InvariantCultureIgnoreCase) == 0)
{
return query + input.Substring(query.Length);
}
}
return input;
}
return string.Empty;
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)

View File

@@ -22,7 +22,7 @@ namespace Wox.ViewModel
Hover
};
public List<ContextMenuItemViewModel> ContextMenuItems { get; set; }
public ObservableCollection<ContextMenuItemViewModel> ContextMenuItems { get; set; } = new ObservableCollection<ContextMenuItemViewModel>();
public ICommand ActivateContextButtonsHoverCommand { get; set; }
public ICommand ActivateContextButtonsSelectionCommand { get; set; }
@@ -46,7 +46,10 @@ namespace Wox.ViewModel
{
Result = result;
}
ContextMenuSelectedIndex = NoSelectionIndex;
LoadContextMenu();
ActivateContextButtonsHoverCommand = new RelayCommand(ActivateContextButtonsHoverAction);
ActivateContextButtonsSelectionCommand = new RelayCommand(ActivateContextButtonsSelectionAction);
DeactivateContextButtonsHoverCommand = new RelayCommand(DeactivateContextButtonsHoverAction);
@@ -64,11 +67,6 @@ namespace Wox.ViewModel
}
public void ActivateContextButtons(ActivationType activationType)
{
if (ContextMenuItems == null)
{
LoadContextMenu();
}
// Result does not contain any context menu items - we don't need to show the context menu ListView at all.
if (ContextMenuItems.Count > 0)
{
@@ -78,14 +76,13 @@ namespace Wox.ViewModel
{
AreContextButtonsActive = false;
}
if (activationType == ActivationType.Selection)
{
IsSelected = true;
EnableContextMenuAcceleratorKeys();
}
else if(activationType == ActivationType.Hover)
else if (activationType == ActivationType.Hover)
{
IsHovered = true;
}
@@ -122,17 +119,17 @@ namespace Wox.ViewModel
else
{
AreContextButtonsActive = false;
}
}
}
public void LoadContextMenu()
{
var results = PluginManager.GetContextMenusForPlugin(Result);
var newItems = new List<ContextMenuItemViewModel>();
ContextMenuItems.Clear();
foreach (var r in results)
{
newItems.Add(new ContextMenuItemViewModel
ContextMenuItems.Add(new ContextMenuItemViewModel()
{
PluginName = r.PluginName,
Title = r.Title,
@@ -155,13 +152,11 @@ namespace Wox.ViewModel
})
});
}
ContextMenuItems = newItems;
}
private void EnableContextMenuAcceleratorKeys()
{
foreach(var i in ContextMenuItems)
foreach (var i in ContextMenuItems)
{
i.IsAcceleratorKeyEnabled = true;
}
@@ -192,7 +187,7 @@ namespace Wox.ViewModel
imagePath = ImageLoader.ErrorIconPath;
}
}
// will get here either when icoPath has value\icon delegate is null\when had exception in delegate
return ImageLoader.Load(imagePath);
}
@@ -201,10 +196,10 @@ namespace Wox.ViewModel
//Returns false if we've already reached the last item.
public bool SelectNextContextButton()
{
if(ContextMenuSelectedIndex == (ContextMenuItems.Count -1))
if (ContextMenuSelectedIndex == (ContextMenuItems.Count - 1))
{
ContextMenuSelectedIndex = NoSelectionIndex;
return false;
return false;
}
ContextMenuSelectedIndex++;

View File

@@ -17,7 +17,7 @@ namespace MarkdownPreviewHandler.Telemetry.Events
/// <summary>
/// Gets The version string. TODO: This should be replaced by a P/Invoke call to get_product_version.
/// </summary>
public string Version => "v0.19.0";
public string Version => "v0.19.2";
/// <inheritdoc/>
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;

View File

@@ -17,7 +17,7 @@ namespace SvgPreviewHandler.Telemetry.Events
/// <summary>
/// Gets The version string. TODO: This should be replaced by a P/Invoke call to get_product_version.
/// </summary>
public string Version => "v0.19.0";
public string Version => "v0.19.2";
/// <inheritdoc/>
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;