mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-29 08:29:10 +01:00
Compare commits
12 Commits
shawn/mcps
...
v0.19.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a2cd2a9c3 | ||
|
|
062aed38a0 | ||
|
|
d0d72412d6 | ||
|
|
3450d832d4 | ||
|
|
79eda1681b | ||
|
|
1360359bba | ||
|
|
1a10c1b4f9 | ||
|
|
ae08b810bb | ||
|
|
2baaa1f20e | ||
|
|
25f0ba19ca | ||
|
|
32d873f41d | ||
|
|
772387a27a |
@@ -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}
|
||||
|
||||
42
README.md
42
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -74,9 +74,6 @@ namespace Microsoft.PowerToys.Settings.UI.Runner
|
||||
MessageBoxButton.OK);
|
||||
app.Shutdown();
|
||||
}
|
||||
|
||||
// Terminate all threads of the process
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Plugin.Program.UnitTests.Storage
|
||||
{
|
||||
class PackageRepositoryTest
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace Microsoft.Plugin.Program.Storage
|
||||
{
|
||||
internal interface IProgramRepository
|
||||
{
|
||||
void IndexPrograms();
|
||||
void Load();
|
||||
void Save();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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; });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
23
src/modules/launcher/Wox.Infrastructure/Storage/IStorage.cs
Normal file
23
src/modules/launcher/Wox.Infrastructure/Storage/IStorage.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
211
src/modules/launcher/Wox.Test/MainViewModelTest.cs
Normal file
211
src/modules/launcher/Wox.Test/MainViewModelTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
58
src/modules/launcher/Wox.Test/WoxTest.cs
Normal file
58
src/modules/launcher/Wox.Test/WoxTest.cs
Normal 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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user