mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-01 07:59:36 +02:00
Compare commits
8 Commits
main
...
workspaces
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f94c439a8d | ||
|
|
a6f4357c94 | ||
|
|
6961fc66d0 | ||
|
|
8cd88f9817 | ||
|
|
57bdc9da6e | ||
|
|
81ee6b6efd | ||
|
|
ed1570a0e3 | ||
|
|
b364da81e8 |
@@ -234,8 +234,8 @@
|
||||
"PowerToys.WorkspacesWindowArranger.exe",
|
||||
"PowerToys.WorkspacesEditor.exe",
|
||||
"PowerToys.WorkspacesEditor.dll",
|
||||
"PowerToys.WorkspacesLauncherUI.exe",
|
||||
"PowerToys.WorkspacesLauncherUI.dll",
|
||||
"WinUI3Apps\\PowerToys.WorkspacesLauncherUI.exe",
|
||||
"WinUI3Apps\\PowerToys.WorkspacesLauncherUI.dll",
|
||||
"PowerToys.WorkspacesModuleInterface.dll",
|
||||
"PowerToys.WorkspacesCsharpLibrary.dll",
|
||||
|
||||
|
||||
@@ -1020,7 +1020,7 @@
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/Workspaces/WorkspacesLauncher/WorkspacesLauncher.vcxproj" Id="2cac093e-5fcf-4102-9c2c-ac7dd5d9eb96" />
|
||||
<Project Path="src/modules/Workspaces/WorkspacesLauncherUI/WorkspacesLauncherUI.csproj">
|
||||
<Project Path="src/modules/Workspaces/WorkspacesLauncherUI.WinUI/WorkspacesLauncherUI.WinUI.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
@@ -1103,6 +1103,14 @@
|
||||
<File Path="src/Solution.props" />
|
||||
<File Path="src/Version.props" />
|
||||
</Folder>
|
||||
<Folder Name="/src/" />
|
||||
<Folder Name="/src/modules/" />
|
||||
<Folder Name="/src/modules/Workspaces/">
|
||||
<Project Path="src/modules/Workspaces/WorkspacesLauncherUI.UnitTests/WorkspacesLauncherUI.UnitTests.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Project Path="src/ActionRunner/ActionRunner.vcxproj" Id="d29ddd63-e2cf-4657-9fd5-2aede4257e5d">
|
||||
<BuildDependency Project="src/common/updating/updating.vcxproj" />
|
||||
</Project>
|
||||
|
||||
@@ -46,7 +46,7 @@ void LauncherUIHelper::LaunchUI()
|
||||
GetModuleFileName(NULL, buffer, MAX_PATH);
|
||||
std::wstring path = std::filesystem::path(buffer).parent_path();
|
||||
|
||||
auto res = AppLauncher::LaunchApp(path + L"\\PowerToys.WorkspacesLauncherUI.exe", L"", false);
|
||||
auto res = AppLauncher::LaunchApp(path + L"\\WinUI3Apps\\PowerToys.WorkspacesLauncherUI.exe", L"", false);
|
||||
if (res.isOk())
|
||||
{
|
||||
auto value = res.value();
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using WorkspacesLauncherUI.Data;
|
||||
|
||||
namespace WorkspacesLauncherUI.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for ApplicationWrapper struct field mapping.
|
||||
/// All fields must be accessible and hold correct values after deserialization.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class ApplicationDataModelTests
|
||||
{
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppField_ApplicationName_StoresDisplayName()
|
||||
{
|
||||
var app = new ApplicationWrapper { Application = "Visual Studio Code" };
|
||||
Assert.AreEqual("Visual Studio Code", app.Application);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppField_ExecutablePath_StoresFullPathWithSpaces()
|
||||
{
|
||||
var app = new ApplicationWrapper { ApplicationPath = @"C:\Users\test\AppData\Local\Programs\Microsoft VS Code\Code.exe" };
|
||||
Assert.AreEqual(@"C:\Users\test\AppData\Local\Programs\Microsoft VS Code\Code.exe", app.ApplicationPath);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppField_WindowTitle_StoresActiveWindowTitle()
|
||||
{
|
||||
var app = new ApplicationWrapper { Title = "MyProject - Visual Studio Code" };
|
||||
Assert.AreEqual("MyProject - Visual Studio Code", app.Title);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppField_PackageFullName_StoresUwpPackageIdentifier()
|
||||
{
|
||||
var app = new ApplicationWrapper { PackageFullName = "Microsoft.WindowsTerminal_1.21.0.0_x64__8wekyb3d8bbwe" };
|
||||
Assert.AreEqual("Microsoft.WindowsTerminal_1.21.0.0_x64__8wekyb3d8bbwe", app.PackageFullName);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppField_AppUserModelId_StoresAumidForPackagedApps()
|
||||
{
|
||||
var app = new ApplicationWrapper { AppUserModelId = "Microsoft.WindowsTerminal_8wekyb3d8bbwe!App" };
|
||||
Assert.AreEqual("Microsoft.WindowsTerminal_8wekyb3d8bbwe!App", app.AppUserModelId);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppField_PwaAppId_StoresChromeOrEdgePwaIdentifier()
|
||||
{
|
||||
var app = new ApplicationWrapper { PwaAppId = "fmgjjmmmlfnkbppncijlocphclkkleod" };
|
||||
Assert.AreEqual("fmgjjmmmlfnkbppncijlocphclkkleod", app.PwaAppId);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppField_CliArguments_StoresLaunchArgumentsExactly()
|
||||
{
|
||||
var app = new ApplicationWrapper { CommandLineArguments = "--reuse-window --goto file.ts:42" };
|
||||
Assert.AreEqual("--reuse-window --goto file.ts:42", app.CommandLineArguments);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppField_IsElevated_StoresAdminRunningState()
|
||||
{
|
||||
var app = new ApplicationWrapper { IsElevated = true };
|
||||
Assert.IsTrue(app.IsElevated);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppField_CanLaunchElevated_StoresElevationCapability()
|
||||
{
|
||||
var app = new ApplicationWrapper { CanLaunchElevated = true };
|
||||
Assert.IsTrue(app.CanLaunchElevated);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppField_Minimized_StoresMinimizedWindowState()
|
||||
{
|
||||
var app = new ApplicationWrapper { Minimized = true };
|
||||
Assert.IsTrue(app.Minimized);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppField_Maximized_StoresMaximizedWindowState()
|
||||
{
|
||||
var app = new ApplicationWrapper { Maximized = true };
|
||||
Assert.IsTrue(app.Maximized);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppField_MonitorIndex_StoresTargetDisplayNumber()
|
||||
{
|
||||
var app = new ApplicationWrapper { Monitor = 2 };
|
||||
Assert.AreEqual(2, app.Monitor);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppField_WindowPosition_StoresRectangleCoordinates()
|
||||
{
|
||||
var pos = new PositionWrapper { X = 100, Y = 200, Width = 800, Height = 600 };
|
||||
var app = new ApplicationWrapper { Position = pos };
|
||||
|
||||
Assert.AreEqual(100, app.Position.X);
|
||||
Assert.AreEqual(200, app.Position.Y);
|
||||
Assert.AreEqual(800, app.Position.Width);
|
||||
Assert.AreEqual(600, app.Position.Height);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppDefaults_StringFields_AreNullBeforeDeserialization()
|
||||
{
|
||||
ApplicationWrapper app = default;
|
||||
|
||||
Assert.IsNull(app.Application);
|
||||
Assert.IsNull(app.ApplicationPath);
|
||||
Assert.IsNull(app.Title);
|
||||
Assert.IsNull(app.PackageFullName);
|
||||
Assert.IsNull(app.AppUserModelId);
|
||||
Assert.IsNull(app.PwaAppId);
|
||||
Assert.IsNull(app.CommandLineArguments);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppDefaults_BooleanFields_AreFalseBeforeDeserialization()
|
||||
{
|
||||
ApplicationWrapper app = default;
|
||||
|
||||
Assert.IsFalse(app.IsElevated);
|
||||
Assert.IsFalse(app.CanLaunchElevated);
|
||||
Assert.IsFalse(app.Minimized);
|
||||
Assert.IsFalse(app.Maximized);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppDefaults_MonitorIndex_IsZeroPrimaryMonitor()
|
||||
{
|
||||
ApplicationWrapper app = default;
|
||||
Assert.AreEqual(0, app.Monitor);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppConfig_AdminAppOnSecondMonitor_AllFieldsPopulated()
|
||||
{
|
||||
var app = new ApplicationWrapper
|
||||
{
|
||||
Application = "Registry Editor",
|
||||
ApplicationPath = @"C:\Windows\regedit.exe",
|
||||
Title = "Registry Editor",
|
||||
PackageFullName = string.Empty,
|
||||
AppUserModelId = string.Empty,
|
||||
PwaAppId = string.Empty,
|
||||
CommandLineArguments = string.Empty,
|
||||
IsElevated = true,
|
||||
CanLaunchElevated = true,
|
||||
Minimized = false,
|
||||
Maximized = false,
|
||||
Position = new PositionWrapper { X = 1920, Y = 0, Width = 1024, Height = 768 },
|
||||
Monitor = 1,
|
||||
};
|
||||
|
||||
Assert.IsTrue(app.IsElevated);
|
||||
Assert.AreEqual(1, app.Monitor);
|
||||
Assert.AreEqual(1920, app.Position.X);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppConfig_MinimizedOnThirdMonitor_StateAndMonitorCorrect()
|
||||
{
|
||||
var app = new ApplicationWrapper
|
||||
{
|
||||
Application = "Notepad",
|
||||
ApplicationPath = @"C:\Windows\System32\notepad.exe",
|
||||
Minimized = true,
|
||||
Maximized = false,
|
||||
Position = new PositionWrapper { X = 3840, Y = 0, Width = 800, Height = 600 },
|
||||
Monitor = 2,
|
||||
};
|
||||
|
||||
Assert.IsTrue(app.Minimized);
|
||||
Assert.IsFalse(app.Maximized);
|
||||
Assert.AreEqual(2, app.Monitor);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppConfig_PathWithParenthesesAndSpaces_PreservedExactly()
|
||||
{
|
||||
string complexPath = @"C:\Program Files (x86)\Microsoft Office\root\Office16\WINWORD.EXE";
|
||||
var app = new ApplicationWrapper { ApplicationPath = complexPath };
|
||||
Assert.AreEqual(complexPath, app.ApplicationPath);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void AppConfig_ExplicitEmptyStrings_AreEmptyNotNull()
|
||||
{
|
||||
var app = new ApplicationWrapper
|
||||
{
|
||||
Application = string.Empty,
|
||||
ApplicationPath = string.Empty,
|
||||
Title = string.Empty,
|
||||
PackageFullName = string.Empty,
|
||||
AppUserModelId = string.Empty,
|
||||
PwaAppId = string.Empty,
|
||||
CommandLineArguments = string.Empty,
|
||||
};
|
||||
|
||||
Assert.AreEqual(string.Empty, app.Application);
|
||||
Assert.AreEqual(string.Empty, app.ApplicationPath);
|
||||
Assert.AreEqual(string.Empty, app.PackageFullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using WorkspacesLauncherUI.Utils;
|
||||
|
||||
namespace WorkspacesLauncherUI.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for DashCaseNamingPolicy and StringUtils.
|
||||
/// These utilities control JSON property name mapping for IPC messages.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class IpcJsonPropertyNamingTests
|
||||
{
|
||||
private readonly DashCaseNamingPolicy _policy = DashCaseNamingPolicy.Instance;
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Serialization")]
|
||||
public void NamingPolicy_ApplicationPath_MapsTo_application_path()
|
||||
{
|
||||
Assert.AreEqual("application-path", _policy.ConvertName("ApplicationPath"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Serialization")]
|
||||
public void NamingPolicy_Application_MapsTo_application()
|
||||
{
|
||||
Assert.AreEqual("application", _policy.ConvertName("Application"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Serialization")]
|
||||
public void NamingPolicy_AppUserModelId_MapsTo_app_user_model_id()
|
||||
{
|
||||
Assert.AreEqual("app-user-model-id", _policy.ConvertName("AppUserModelId"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Serialization")]
|
||||
public void NamingPolicy_LowercaseInput_RemainsUnchanged()
|
||||
{
|
||||
Assert.AreEqual("title", _policy.ConvertName("title"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Serialization")]
|
||||
public void NamingPolicy_SingleUppercaseChar_PreservedAsIs()
|
||||
{
|
||||
Assert.AreEqual("X", _policy.ConvertName("X"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Serialization")]
|
||||
public void NamingPolicy_SingleLowercaseChar_PreservedAsIs()
|
||||
{
|
||||
Assert.AreEqual("x", _policy.ConvertName("x"));
|
||||
}
|
||||
|
||||
// Exact IPC property names that must match the C++ side
|
||||
[TestMethod]
|
||||
[TestCategory("Serialization")]
|
||||
public void NamingPolicy_PackageFullName_MatchesCppIpcKey()
|
||||
{
|
||||
Assert.AreEqual("package-full-name", _policy.ConvertName("PackageFullName"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Serialization")]
|
||||
public void NamingPolicy_AppUserModelId_MatchesCppIpcKey()
|
||||
{
|
||||
Assert.AreEqual("app-user-model-id", _policy.ConvertName("AppUserModelId"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Serialization")]
|
||||
public void NamingPolicy_PwaAppId_MatchesCppIpcKey()
|
||||
{
|
||||
Assert.AreEqual("pwa-app-id", _policy.ConvertName("PwaAppId"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Serialization")]
|
||||
public void NamingPolicy_CommandLineArguments_MatchesCppIpcKey()
|
||||
{
|
||||
Assert.AreEqual("command-line-arguments", _policy.ConvertName("CommandLineArguments"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Serialization")]
|
||||
public void NamingPolicy_IsElevated_MatchesCppIpcKey()
|
||||
{
|
||||
Assert.AreEqual("is-elevated", _policy.ConvertName("IsElevated"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Serialization")]
|
||||
public void NamingPolicy_CanLaunchElevated_MatchesCppIpcKey()
|
||||
{
|
||||
Assert.AreEqual("can-launch-elevated", _policy.ConvertName("CanLaunchElevated"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Serialization")]
|
||||
public void NamingPolicy_ApplicationPath_MatchesCppIpcKey()
|
||||
{
|
||||
Assert.AreEqual("application-path", _policy.ConvertName("ApplicationPath"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Serialization")]
|
||||
public void NamingPolicy_Singleton_ReturnsSameInstanceEveryTime()
|
||||
{
|
||||
var instance1 = DashCaseNamingPolicy.Instance;
|
||||
var instance2 = DashCaseNamingPolicy.Instance;
|
||||
Assert.AreSame(instance1, instance2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Serialization")]
|
||||
public void StringConversion_TwoUppercaseLetters_InsertsDashBetween()
|
||||
{
|
||||
Assert.AreEqual("a-b", "AB".UpperCamelCaseToDashCase());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Serialization")]
|
||||
public void StringConversion_AllLowercase_NoTransformation()
|
||||
{
|
||||
Assert.AreEqual("alllowercase", "alllowercase".UpperCamelCaseToDashCase());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Serialization")]
|
||||
public void StringConversion_NumbersInMiddle_PreservedWithDashBeforeNextUpper()
|
||||
{
|
||||
Assert.AreEqual("version2-test", "Version2Test".UpperCamelCaseToDashCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using WorkspacesLauncherUI.Data;
|
||||
|
||||
namespace WorkspacesLauncherUI.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for JSON deserialization of IPC messages received from the C++ launcher engine.
|
||||
/// These messages drive the entire Launcher UI state and must remain stable
|
||||
/// across any future UI or data layer changes.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class IpcMessageDeserializationTests
|
||||
{
|
||||
private const string FullIpcMessage = @"{
|
||||
""processId"": 12345,
|
||||
""apps"": {
|
||||
""appLaunchInfos"": [
|
||||
{
|
||||
""application"": {
|
||||
""application"": ""Visual Studio Code"",
|
||||
""application-path"": ""C:\\Users\\test\\AppData\\Local\\Programs\\Microsoft VS Code\\Code.exe"",
|
||||
""title"": ""MyProject - Visual Studio Code"",
|
||||
""package-full-name"": """",
|
||||
""app-user-model-id"": """",
|
||||
""pwa-app-id"": """",
|
||||
""command-line-arguments"": ""--reuse-window"",
|
||||
""is-elevated"": false,
|
||||
""can-launch-elevated"": true,
|
||||
""minimized"": false,
|
||||
""maximized"": true,
|
||||
""position"": { ""X"": 0, ""Y"": 0, ""width"": 1920, ""height"": 1080 },
|
||||
""monitor"": 0
|
||||
},
|
||||
""state"": 2
|
||||
},
|
||||
{
|
||||
""application"": {
|
||||
""application"": ""Windows Terminal"",
|
||||
""application-path"": ""C:\\Program Files\\WindowsApps\\Microsoft.WindowsTerminal_1.0.0.0_x64__8wekyb3d8bbwe\\wt.exe"",
|
||||
""title"": ""PowerShell"",
|
||||
""package-full-name"": ""Microsoft.WindowsTerminal_1.0.0.0_x64__8wekyb3d8bbwe"",
|
||||
""app-user-model-id"": ""Microsoft.WindowsTerminal_8wekyb3d8bbwe!App"",
|
||||
""pwa-app-id"": """",
|
||||
""command-line-arguments"": """",
|
||||
""is-elevated"": false,
|
||||
""can-launch-elevated"": false,
|
||||
""minimized"": false,
|
||||
""maximized"": false,
|
||||
""position"": { ""X"": 960, ""Y"": 0, ""width"": 960, ""height"": 540 },
|
||||
""monitor"": 0
|
||||
},
|
||||
""state"": 0
|
||||
},
|
||||
{
|
||||
""application"": {
|
||||
""application"": ""Notepad"",
|
||||
""application-path"": ""C:\\Windows\\System32\\notepad.exe"",
|
||||
""title"": ""Untitled - Notepad"",
|
||||
""package-full-name"": """",
|
||||
""app-user-model-id"": """",
|
||||
""pwa-app-id"": """",
|
||||
""command-line-arguments"": """",
|
||||
""is-elevated"": false,
|
||||
""can-launch-elevated"": true,
|
||||
""minimized"": true,
|
||||
""maximized"": false,
|
||||
""position"": { ""X"": 100, ""Y"": 100, ""width"": 800, ""height"": 600 },
|
||||
""monitor"": 1
|
||||
},
|
||||
""state"": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}";
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_WithMultipleApps_ExtractsLauncherProcessId()
|
||||
{
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(FullIpcMessage);
|
||||
Assert.AreEqual(12345, result.LauncherProcessID);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_WithThreeApps_DeserializesAllAppEntries()
|
||||
{
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(FullIpcMessage);
|
||||
Assert.AreEqual(3, result.AppLaunchInfos.AppLaunchInfoList.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_Win32Application_DeserializesAllApplicationFields()
|
||||
{
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(FullIpcMessage);
|
||||
var vscode = result.AppLaunchInfos.AppLaunchInfoList[0];
|
||||
|
||||
Assert.AreEqual("Visual Studio Code", vscode.Application.Application);
|
||||
Assert.AreEqual(@"C:\Users\test\AppData\Local\Programs\Microsoft VS Code\Code.exe", vscode.Application.ApplicationPath);
|
||||
Assert.AreEqual("MyProject - Visual Studio Code", vscode.Application.Title);
|
||||
Assert.AreEqual(string.Empty, vscode.Application.PackageFullName);
|
||||
Assert.AreEqual(string.Empty, vscode.Application.AppUserModelId);
|
||||
Assert.AreEqual(string.Empty, vscode.Application.PwaAppId);
|
||||
Assert.AreEqual("--reuse-window", vscode.Application.CommandLineArguments);
|
||||
Assert.IsFalse(vscode.Application.IsElevated);
|
||||
Assert.IsTrue(vscode.Application.CanLaunchElevated);
|
||||
Assert.IsFalse(vscode.Application.Minimized);
|
||||
Assert.IsTrue(vscode.Application.Maximized);
|
||||
Assert.AreEqual(0, vscode.Application.Monitor);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_Win32Application_DeserializesWindowPosition()
|
||||
{
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(FullIpcMessage);
|
||||
var pos = result.AppLaunchInfos.AppLaunchInfoList[0].Application.Position;
|
||||
|
||||
Assert.AreEqual(0, pos.X);
|
||||
Assert.AreEqual(0, pos.Y);
|
||||
Assert.AreEqual(1920, pos.Width);
|
||||
Assert.AreEqual(1080, pos.Height);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_PackagedUwpApp_DeserializesPackageIdentifiers()
|
||||
{
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(FullIpcMessage);
|
||||
var terminal = result.AppLaunchInfos.AppLaunchInfoList[1];
|
||||
|
||||
Assert.AreEqual("Windows Terminal", terminal.Application.Application);
|
||||
Assert.AreEqual("Microsoft.WindowsTerminal_1.0.0.0_x64__8wekyb3d8bbwe", terminal.Application.PackageFullName);
|
||||
Assert.AreEqual("Microsoft.WindowsTerminal_8wekyb3d8bbwe!App", terminal.Application.AppUserModelId);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_StateValueTwo_MapsToLaunchedAndMovedEnum()
|
||||
{
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(FullIpcMessage);
|
||||
Assert.AreEqual(LaunchingState.LaunchedAndMoved, result.AppLaunchInfos.AppLaunchInfoList[0].State);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_StateValueZero_MapsToWaitingEnum()
|
||||
{
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(FullIpcMessage);
|
||||
Assert.AreEqual(LaunchingState.Waiting, result.AppLaunchInfos.AppLaunchInfoList[1].State);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_StateValueThree_MapsToFailedEnum()
|
||||
{
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(FullIpcMessage);
|
||||
Assert.AreEqual(LaunchingState.Failed, result.AppLaunchInfos.AppLaunchInfoList[2].State);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_MinimizedWindow_DeserializesWindowStateFlags()
|
||||
{
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(FullIpcMessage);
|
||||
var notepad = result.AppLaunchInfos.AppLaunchInfoList[2];
|
||||
|
||||
Assert.IsTrue(notepad.Application.Minimized);
|
||||
Assert.IsFalse(notepad.Application.Maximized);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_SecondaryMonitor_DeserializesMonitorIndex()
|
||||
{
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(FullIpcMessage);
|
||||
Assert.AreEqual(1, result.AppLaunchInfos.AppLaunchInfoList[2].Application.Monitor);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_ProgressiveWebApp_DeserializesPwaIdentifier()
|
||||
{
|
||||
string pwaMessage = @"{
|
||||
""processId"": 100,
|
||||
""apps"": {
|
||||
""appLaunchInfos"": [
|
||||
{
|
||||
""application"": {
|
||||
""application"": ""Gmail"",
|
||||
""application-path"": ""C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"",
|
||||
""title"": ""Gmail"",
|
||||
""package-full-name"": """",
|
||||
""app-user-model-id"": """",
|
||||
""pwa-app-id"": ""fmgjjmmmlfnkbppncijlocphclkkleod"",
|
||||
""command-line-arguments"": """",
|
||||
""is-elevated"": false,
|
||||
""can-launch-elevated"": false,
|
||||
""minimized"": false,
|
||||
""maximized"": false,
|
||||
""position"": { ""X"": 0, ""Y"": 0, ""width"": 800, ""height"": 600 },
|
||||
""monitor"": 0
|
||||
},
|
||||
""state"": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}";
|
||||
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(pwaMessage);
|
||||
var gmail = result.AppLaunchInfos.AppLaunchInfoList[0];
|
||||
|
||||
Assert.AreEqual("fmgjjmmmlfnkbppncijlocphclkkleod", gmail.Application.PwaAppId);
|
||||
Assert.AreEqual(LaunchingState.Launched, gmail.State);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_ElevatedProcess_DeserializesAdminFlags()
|
||||
{
|
||||
string elevatedMessage = @"{
|
||||
""processId"": 200,
|
||||
""apps"": {
|
||||
""appLaunchInfos"": [
|
||||
{
|
||||
""application"": {
|
||||
""application"": ""Registry Editor"",
|
||||
""application-path"": ""C:\\Windows\\regedit.exe"",
|
||||
""title"": ""Registry Editor"",
|
||||
""package-full-name"": """",
|
||||
""app-user-model-id"": """",
|
||||
""pwa-app-id"": """",
|
||||
""command-line-arguments"": """",
|
||||
""is-elevated"": true,
|
||||
""can-launch-elevated"": true,
|
||||
""minimized"": false,
|
||||
""maximized"": false,
|
||||
""position"": { ""X"": 100, ""Y"": 100, ""width"": 1024, ""height"": 768 },
|
||||
""monitor"": 0
|
||||
},
|
||||
""state"": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}";
|
||||
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(elevatedMessage);
|
||||
var regedit = result.AppLaunchInfos.AppLaunchInfoList[0];
|
||||
|
||||
Assert.IsTrue(regedit.Application.IsElevated);
|
||||
Assert.IsTrue(regedit.Application.CanLaunchElevated);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_SingleAppWorkspace_DeserializesSuccessfully()
|
||||
{
|
||||
string singleAppMessage = @"{
|
||||
""processId"": 1,
|
||||
""apps"": {
|
||||
""appLaunchInfos"": [
|
||||
{
|
||||
""application"": {
|
||||
""application"": ""Notepad"",
|
||||
""application-path"": ""C:\\Windows\\System32\\notepad.exe"",
|
||||
""title"": """",
|
||||
""package-full-name"": """",
|
||||
""app-user-model-id"": """",
|
||||
""pwa-app-id"": """",
|
||||
""command-line-arguments"": """",
|
||||
""is-elevated"": false,
|
||||
""can-launch-elevated"": false,
|
||||
""minimized"": false,
|
||||
""maximized"": false,
|
||||
""position"": { ""X"": 0, ""Y"": 0, ""width"": 400, ""height"": 300 },
|
||||
""monitor"": 0
|
||||
},
|
||||
""state"": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}";
|
||||
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(singleAppMessage);
|
||||
|
||||
Assert.AreEqual(1, result.AppLaunchInfos.AppLaunchInfoList.Count);
|
||||
Assert.AreEqual("Notepad", result.AppLaunchInfos.AppLaunchInfoList[0].Application.Application);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_ZeroApps_ReturnsEmptyListWithValidProcessId()
|
||||
{
|
||||
string emptyAppsMessage = @"{
|
||||
""processId"": 42,
|
||||
""apps"": {
|
||||
""appLaunchInfos"": []
|
||||
}
|
||||
}";
|
||||
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(emptyAppsMessage);
|
||||
|
||||
Assert.AreEqual(42, result.LauncherProcessID);
|
||||
Assert.AreEqual(0, result.AppLaunchInfos.AppLaunchInfoList.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
[ExpectedException(typeof(JsonException))]
|
||||
public void IpcMessage_MalformedJson_ThrowsJsonException()
|
||||
{
|
||||
var parser = new AppLaunchData();
|
||||
parser.Deserialize("not valid json {{{");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
[ExpectedException(typeof(JsonException))]
|
||||
public void IpcMessage_EmptyPayload_ThrowsJsonException()
|
||||
{
|
||||
var parser = new AppLaunchData();
|
||||
parser.Deserialize(string.Empty);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_LeftOfPrimaryMonitor_DeserializesNegativeCoordinates()
|
||||
{
|
||||
string negativePositionMessage = @"{
|
||||
""processId"": 1,
|
||||
""apps"": {
|
||||
""appLaunchInfos"": [
|
||||
{
|
||||
""application"": {
|
||||
""application"": ""Notepad"",
|
||||
""application-path"": ""C:\\Windows\\System32\\notepad.exe"",
|
||||
""title"": """",
|
||||
""package-full-name"": """",
|
||||
""app-user-model-id"": """",
|
||||
""pwa-app-id"": """",
|
||||
""command-line-arguments"": """",
|
||||
""is-elevated"": false,
|
||||
""can-launch-elevated"": false,
|
||||
""minimized"": false,
|
||||
""maximized"": false,
|
||||
""position"": { ""X"": -1920, ""Y"": -200, ""width"": 800, ""height"": 600 },
|
||||
""monitor"": 1
|
||||
},
|
||||
""state"": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}";
|
||||
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(negativePositionMessage);
|
||||
var pos = result.AppLaunchInfos.AppLaunchInfoList[0].Application.Position;
|
||||
|
||||
Assert.AreEqual(-1920, pos.X);
|
||||
Assert.AreEqual(-200, pos.Y);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_FourthMonitor_DeserializesHighMonitorIndex()
|
||||
{
|
||||
string multiMonitorMessage = @"{
|
||||
""processId"": 1,
|
||||
""apps"": {
|
||||
""appLaunchInfos"": [
|
||||
{
|
||||
""application"": {
|
||||
""application"": ""App"",
|
||||
""application-path"": ""C:\\app.exe"",
|
||||
""title"": """",
|
||||
""package-full-name"": """",
|
||||
""app-user-model-id"": """",
|
||||
""pwa-app-id"": """",
|
||||
""command-line-arguments"": """",
|
||||
""is-elevated"": false,
|
||||
""can-launch-elevated"": false,
|
||||
""minimized"": false,
|
||||
""maximized"": false,
|
||||
""position"": { ""X"": 3840, ""Y"": 0, ""width"": 1920, ""height"": 1080 },
|
||||
""monitor"": 3
|
||||
},
|
||||
""state"": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}";
|
||||
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(multiMonitorMessage);
|
||||
Assert.AreEqual(3, result.AppLaunchInfos.AppLaunchInfoList[0].Application.Monitor);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_AllFiveStateValues_MapToCorrectEnumMembers()
|
||||
{
|
||||
for (int stateValue = 0; stateValue <= 4; stateValue++)
|
||||
{
|
||||
string template = @"{""processId"": 1,""apps"": {""appLaunchInfos"": [{""application"": {""application"": ""App"",""application-path"": ""C:\\app.exe"",""title"": """",""package-full-name"": """",""app-user-model-id"": """",""pwa-app-id"": """",""command-line-arguments"": """",""is-elevated"": false,""can-launch-elevated"": false,""minimized"": false,""maximized"": false,""position"": { ""X"": 0, ""Y"": 0, ""width"": 100, ""height"": 100 },""monitor"": 0},""state"": STATE_PLACEHOLDER}]}}";
|
||||
string message = template.Replace("STATE_PLACEHOLDER", stateValue.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(message);
|
||||
Assert.AreEqual((LaunchingState)stateValue, result.AppLaunchInfos.AppLaunchInfoList[0].State);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_CommandLineWithSpecialChars_PreservesArgumentsExactly()
|
||||
{
|
||||
string cliMessage = @"{
|
||||
""processId"": 1,
|
||||
""apps"": {
|
||||
""appLaunchInfos"": [
|
||||
{
|
||||
""application"": {
|
||||
""application"": ""VS Code"",
|
||||
""application-path"": ""C:\\Code.exe"",
|
||||
""title"": """",
|
||||
""package-full-name"": """",
|
||||
""app-user-model-id"": """",
|
||||
""pwa-app-id"": """",
|
||||
""command-line-arguments"": ""--new-window --goto C:\\project\\file.ts:42"",
|
||||
""is-elevated"": false,
|
||||
""can-launch-elevated"": false,
|
||||
""minimized"": false,
|
||||
""maximized"": false,
|
||||
""position"": { ""X"": 0, ""Y"": 0, ""width"": 100, ""height"": 100 },
|
||||
""monitor"": 0
|
||||
},
|
||||
""state"": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}";
|
||||
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(cliMessage);
|
||||
Assert.AreEqual(@"--new-window --goto C:\project\file.ts:42", result.AppLaunchInfos.AppLaunchInfoList[0].Application.CommandLineArguments);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_JapaneseAppName_DeserializesUnicodeCorrectly()
|
||||
{
|
||||
string unicodeMessage = @"{
|
||||
""processId"": 1,
|
||||
""apps"": {
|
||||
""appLaunchInfos"": [
|
||||
{
|
||||
""application"": {
|
||||
""application"": ""\u30E1\u30E2\u5E33"",
|
||||
""application-path"": ""C:\\Windows\\System32\\notepad.exe"",
|
||||
""title"": ""\u7121\u984C - \u30E1\u30E2\u5E33"",
|
||||
""package-full-name"": """",
|
||||
""app-user-model-id"": """",
|
||||
""pwa-app-id"": """",
|
||||
""command-line-arguments"": """",
|
||||
""is-elevated"": false,
|
||||
""can-launch-elevated"": false,
|
||||
""minimized"": false,
|
||||
""maximized"": false,
|
||||
""position"": { ""X"": 0, ""Y"": 0, ""width"": 400, ""height"": 300 },
|
||||
""monitor"": 0
|
||||
},
|
||||
""state"": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}";
|
||||
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(unicodeMessage);
|
||||
|
||||
Assert.AreEqual("\u30E1\u30E2\u5E33", result.AppLaunchInfos.AppLaunchInfoList[0].Application.Application);
|
||||
Assert.AreEqual("\u7121\u984C - \u30E1\u30E2\u5E33", result.AppLaunchInfos.AppLaunchInfoList[0].Application.Title);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Deserialization")]
|
||||
public void IpcMessage_TenAppWorkspace_DeserializesAllWithCorrectPositionsAndStates()
|
||||
{
|
||||
var appEntries = new StringBuilder();
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
appEntries.Append(',');
|
||||
}
|
||||
|
||||
string entry = string.Create(CultureInfo.InvariantCulture, $@"{{""application"": {{""application"": ""App{i}"",""application-path"": ""C:\\app{i}.exe"",""title"": ""Window {i}"",""package-full-name"": """",""app-user-model-id"": """",""pwa-app-id"": """",""command-line-arguments"": """",""is-elevated"": false,""can-launch-elevated"": false,""minimized"": false,""maximized"": false,""position"": {{ ""X"": {i * 100}, ""Y"": 0, ""width"": 400, ""height"": 300 }},""monitor"": {i % 3}}},""state"": {i % 5}}}");
|
||||
appEntries.Append(entry);
|
||||
}
|
||||
|
||||
string manyAppsMessage = string.Create(CultureInfo.InvariantCulture, $@"{{""processId"": 9999,""apps"": {{""appLaunchInfos"": [{appEntries}]}}}}");
|
||||
|
||||
var parser = new AppLaunchData();
|
||||
var result = parser.Deserialize(manyAppsMessage);
|
||||
|
||||
Assert.AreEqual(10, result.AppLaunchInfos.AppLaunchInfoList.Count);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
Assert.AreEqual(string.Create(CultureInfo.InvariantCulture, $"App{i}"), result.AppLaunchInfos.AppLaunchInfoList[i].Application.Application);
|
||||
Assert.AreEqual(i * 100, result.AppLaunchInfos.AppLaunchInfoList[i].Application.Position.X);
|
||||
Assert.AreEqual(i % 3, result.AppLaunchInfos.AppLaunchInfoList[i].Application.Monitor);
|
||||
Assert.AreEqual((LaunchingState)(i % 5), result.AppLaunchInfos.AppLaunchInfoList[i].State);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using WorkspacesLauncherUI.Data;
|
||||
|
||||
namespace WorkspacesLauncherUI.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for the LaunchingState enum values and their integer mapping.
|
||||
/// The C++ launcher engine sends state as integer values over IPC.
|
||||
/// These integer values MUST remain stable across the migration.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class LaunchStateEnumContractTests
|
||||
{
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void EnumContract_WaitingState_MapsToIntegerZero()
|
||||
{
|
||||
Assert.AreEqual(0, (int)LaunchingState.Waiting);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void EnumContract_LaunchedState_MapsToIntegerOne()
|
||||
{
|
||||
Assert.AreEqual(1, (int)LaunchingState.Launched);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void EnumContract_LaunchedAndMovedState_MapsToIntegerTwo()
|
||||
{
|
||||
Assert.AreEqual(2, (int)LaunchingState.LaunchedAndMoved);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void EnumContract_FailedState_MapsToIntegerThree()
|
||||
{
|
||||
Assert.AreEqual(3, (int)LaunchingState.Failed);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void EnumContract_CanceledState_MapsToIntegerFour()
|
||||
{
|
||||
Assert.AreEqual(4, (int)LaunchingState.Canceled);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void EnumContract_TotalMemberCount_IsExactlyFiveMatchingCppHeader()
|
||||
{
|
||||
var values = Enum.GetValues(typeof(LaunchingState));
|
||||
Assert.AreEqual(5, values.Length, "LaunchingState must have exactly 5 values to match C++ LaunchingStateEnum.h");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void EnumContract_IntToEnumCast_RoundTripsForAllValues()
|
||||
{
|
||||
for (int i = 0; i <= 4; i++)
|
||||
{
|
||||
var state = (LaunchingState)i;
|
||||
Assert.AreEqual(i, (int)state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using WorkspacesLauncherUI.Data;
|
||||
using WorkspacesLauncherUI.Models;
|
||||
|
||||
namespace WorkspacesLauncherUI.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for the AppLaunching model which drives UI display:
|
||||
/// loading indicator, state glyph, and state color.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class LaunchStatusDisplayLogicTests
|
||||
{
|
||||
[TestMethod]
|
||||
[TestCategory("Model")]
|
||||
public void LoadingSpinner_WhenStateIsWaiting_IsVisible()
|
||||
{
|
||||
var app = new AppLaunching { LaunchState = LaunchingState.Waiting };
|
||||
Assert.IsTrue(app.Loading);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model")]
|
||||
public void LoadingSpinner_WhenStateIsLaunched_RemainsVisibleUntilMoved()
|
||||
{
|
||||
var app = new AppLaunching { LaunchState = LaunchingState.Launched };
|
||||
Assert.IsTrue(app.Loading);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model")]
|
||||
public void LoadingSpinner_WhenStateIsLaunchedAndMoved_IsHidden()
|
||||
{
|
||||
var app = new AppLaunching { LaunchState = LaunchingState.LaunchedAndMoved };
|
||||
Assert.IsFalse(app.Loading);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model")]
|
||||
public void LoadingSpinner_WhenStateIsFailed_IsHidden()
|
||||
{
|
||||
var app = new AppLaunching { LaunchState = LaunchingState.Failed };
|
||||
Assert.IsFalse(app.Loading);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model")]
|
||||
public void LoadingSpinner_WhenStateIsCanceled_IsHidden()
|
||||
{
|
||||
var app = new AppLaunching { LaunchState = LaunchingState.Canceled };
|
||||
Assert.IsFalse(app.Loading);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model")]
|
||||
public void StatusIcon_WhenSuccessful_ShowsGreenCheckmarkGlyph()
|
||||
{
|
||||
var app = new AppLaunching { LaunchState = LaunchingState.LaunchedAndMoved };
|
||||
Assert.AreEqual("\U0000F78C", app.StateGlyph, "LaunchedAndMoved should show checkmark glyph");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model")]
|
||||
public void StatusIcon_WhenFailed_ShowsRedErrorGlyph()
|
||||
{
|
||||
var app = new AppLaunching { LaunchState = LaunchingState.Failed };
|
||||
Assert.AreEqual("\U0000EF2C", app.StateGlyph, "Failed should show error glyph");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model")]
|
||||
public void StatusIcon_WhenCanceled_ShowsRedErrorGlyph()
|
||||
{
|
||||
var app = new AppLaunching { LaunchState = LaunchingState.Canceled };
|
||||
Assert.AreEqual("\U0000EF2C", app.StateGlyph, "Canceled should fall through to default error glyph");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model")]
|
||||
public void StatusColor_WhenSuccessful_IsGreenRgb0_128_0()
|
||||
{
|
||||
var app = new AppLaunching { LaunchState = LaunchingState.LaunchedAndMoved };
|
||||
var color = app.StateColorValue;
|
||||
|
||||
Assert.AreNotEqual(default(Windows.UI.Color), color);
|
||||
Assert.AreEqual(0, color.R, "Green color R component");
|
||||
Assert.AreEqual(128, color.G, "Green color G component");
|
||||
Assert.AreEqual(0, color.B, "Green color B component");
|
||||
Assert.AreEqual(255, color.A, "Green color A component");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model")]
|
||||
public void StatusColor_WhenFailed_IsRedRgb254_0_0()
|
||||
{
|
||||
var app = new AppLaunching { LaunchState = LaunchingState.Failed };
|
||||
var color = app.StateColorValue;
|
||||
|
||||
Assert.AreNotEqual(default(Windows.UI.Color), color);
|
||||
Assert.AreEqual(254, color.R, "Red color R component");
|
||||
Assert.AreEqual(0, color.G, "Red color G component");
|
||||
Assert.AreEqual(0, color.B, "Red color B component");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model")]
|
||||
public void StatusColor_WhenCanceled_IsRedRgb254_0_0()
|
||||
{
|
||||
var app = new AppLaunching { LaunchState = LaunchingState.Canceled };
|
||||
var color = app.StateColorValue;
|
||||
|
||||
Assert.AreNotEqual(default(Windows.UI.Color), color);
|
||||
Assert.AreEqual(254, color.R, "Canceled should fall through to red");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model")]
|
||||
public void AppName_SetToString_ReturnsExactValue()
|
||||
{
|
||||
var app = new AppLaunching { Name = "Test Application" };
|
||||
Assert.AreEqual("Test Application", app.Name);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model")]
|
||||
public void AppName_SetToEmpty_ReturnsEmptyString()
|
||||
{
|
||||
var app = new AppLaunching { Name = string.Empty };
|
||||
Assert.AreEqual(string.Empty, app.Name);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model")]
|
||||
public void DisposeModel_WithActiveState_CompletesCleanly()
|
||||
{
|
||||
var app = new AppLaunching
|
||||
{
|
||||
Name = "Test",
|
||||
AppPath = @"C:\app.exe",
|
||||
LaunchState = LaunchingState.Waiting,
|
||||
};
|
||||
app.Dispose();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model")]
|
||||
public void StateProgression_WaitingToSuccess_TransitionsSpinnerToGreenCheckmark()
|
||||
{
|
||||
var app = new AppLaunching { Name = "Test", LaunchState = LaunchingState.Waiting };
|
||||
Assert.IsTrue(app.Loading);
|
||||
|
||||
app.LaunchState = LaunchingState.Launched;
|
||||
Assert.IsTrue(app.Loading);
|
||||
|
||||
app.LaunchState = LaunchingState.LaunchedAndMoved;
|
||||
Assert.IsFalse(app.Loading);
|
||||
Assert.AreEqual("\U0000F78C", app.StateGlyph);
|
||||
var color = app.StateColorValue;
|
||||
Assert.AreEqual(0, color.R);
|
||||
Assert.AreEqual(128, color.G);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Model")]
|
||||
public void StateProgression_WaitingToFailed_TransitionsSpinnerToRedError()
|
||||
{
|
||||
var app = new AppLaunching { Name = "Test", LaunchState = LaunchingState.Waiting };
|
||||
Assert.IsTrue(app.Loading);
|
||||
|
||||
app.LaunchState = LaunchingState.Failed;
|
||||
Assert.IsFalse(app.Loading);
|
||||
Assert.AreEqual("\U0000EF2C", app.StateGlyph);
|
||||
var color = app.StateColorValue;
|
||||
Assert.AreEqual(254, color.R);
|
||||
Assert.AreEqual(0, color.G);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using WorkspacesLauncherUI.Data;
|
||||
using WorkspacesLauncherUI.Models;
|
||||
using WorkspacesLauncherUI.ViewModels;
|
||||
|
||||
namespace WorkspacesLauncherUI.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for MainViewModel IPC message handling and state management.
|
||||
/// MainViewModel is the core of the Launcher UI — it receives IPC messages
|
||||
/// from the C++ launcher engine and populates the AppsListed collection
|
||||
/// that the UI binds to.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class LauncherViewModelStateManagementTests
|
||||
{
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel")]
|
||||
public void ReceiveIpcMessage_ValidPayload_PopulatesAppsListedCollection()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
string message = CreateIpcMessage(("App1", @"C:\app1.exe", LaunchingState.Waiting), ("App2", @"C:\app2.exe", LaunchingState.Launched));
|
||||
SimulateIpcMessage(message);
|
||||
|
||||
Assert.AreEqual(2, vm.AppsListed.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel")]
|
||||
public void ReceiveIpcMessage_ValidPayload_MapsAppNamesFromJson()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
string message = CreateIpcMessage(("Visual Studio Code", @"C:\Code.exe", LaunchingState.Waiting), ("Windows Terminal", @"C:\wt.exe", LaunchingState.Launched));
|
||||
SimulateIpcMessage(message);
|
||||
|
||||
Assert.AreEqual("Visual Studio Code", vm.AppsListed[0].Name);
|
||||
Assert.AreEqual("Windows Terminal", vm.AppsListed[1].Name);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel")]
|
||||
public void ReceiveIpcMessage_MixedStates_MapsEachAppToCorrectState()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
string message = CreateIpcMessage(
|
||||
("App1", @"C:\app1.exe", LaunchingState.Waiting),
|
||||
("App2", @"C:\app2.exe", LaunchingState.Launched),
|
||||
("App3", @"C:\app3.exe", LaunchingState.LaunchedAndMoved),
|
||||
("App4", @"C:\app4.exe", LaunchingState.Failed));
|
||||
SimulateIpcMessage(message);
|
||||
|
||||
Assert.AreEqual(LaunchingState.Waiting, vm.AppsListed[0].LaunchState);
|
||||
Assert.AreEqual(LaunchingState.Launched, vm.AppsListed[1].LaunchState);
|
||||
Assert.AreEqual(LaunchingState.LaunchedAndMoved, vm.AppsListed[2].LaunchState);
|
||||
Assert.AreEqual(LaunchingState.Failed, vm.AppsListed[3].LaunchState);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel")]
|
||||
public void ReceiveIpcMessage_ValidPayload_PreservesExecutablePaths()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
string message = CreateIpcMessage(("Notepad", @"C:\Windows\System32\notepad.exe", LaunchingState.Waiting));
|
||||
SimulateIpcMessage(message);
|
||||
|
||||
Assert.AreEqual(@"C:\Windows\System32\notepad.exe", vm.AppsListed[0].AppPath);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel")]
|
||||
public void ReceiveIpcMessage_PackagedApp_MapsPackageNameAndAumid()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
string message = @"{
|
||||
""processId"": 1,
|
||||
""apps"": {
|
||||
""appLaunchInfos"": [
|
||||
{
|
||||
""application"": {
|
||||
""application"": ""Terminal"",
|
||||
""application-path"": ""C:\\wt.exe"",
|
||||
""title"": """",
|
||||
""package-full-name"": ""Microsoft.WindowsTerminal_1.0_x64__8wekyb3d8bbwe"",
|
||||
""app-user-model-id"": ""Microsoft.WindowsTerminal_8wekyb3d8bbwe!App"",
|
||||
""pwa-app-id"": """",
|
||||
""command-line-arguments"": """",
|
||||
""is-elevated"": false,
|
||||
""can-launch-elevated"": false,
|
||||
""minimized"": false,
|
||||
""maximized"": false,
|
||||
""position"": { ""X"": 0, ""Y"": 0, ""width"": 100, ""height"": 100 },
|
||||
""monitor"": 0
|
||||
},
|
||||
""state"": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}";
|
||||
SimulateIpcMessage(message);
|
||||
|
||||
Assert.AreEqual("Microsoft.WindowsTerminal_1.0_x64__8wekyb3d8bbwe", vm.AppsListed[0].PackagedName);
|
||||
Assert.AreEqual("Microsoft.WindowsTerminal_8wekyb3d8bbwe!App", vm.AppsListed[0].Aumid);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel")]
|
||||
public void ReceiveIpcMessage_PwaApp_MapsPwaAppIdentifier()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
string message = @"{
|
||||
""processId"": 1,
|
||||
""apps"": {
|
||||
""appLaunchInfos"": [
|
||||
{
|
||||
""application"": {
|
||||
""application"": ""Gmail"",
|
||||
""application-path"": ""C:\\chrome.exe"",
|
||||
""title"": """",
|
||||
""package-full-name"": """",
|
||||
""app-user-model-id"": """",
|
||||
""pwa-app-id"": ""abc123"",
|
||||
""command-line-arguments"": """",
|
||||
""is-elevated"": false,
|
||||
""can-launch-elevated"": false,
|
||||
""minimized"": false,
|
||||
""maximized"": false,
|
||||
""position"": { ""X"": 0, ""Y"": 0, ""width"": 100, ""height"": 100 },
|
||||
""monitor"": 0
|
||||
},
|
||||
""state"": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}";
|
||||
SimulateIpcMessage(message);
|
||||
|
||||
Assert.AreEqual("abc123", vm.AppsListed[0].PwaAppId);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel")]
|
||||
public void ReceiveIpcMessage_AnyUpdate_RaisesPropertyChangedForDataBinding()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
bool propertyChangedFired = false;
|
||||
string changedPropertyName = null;
|
||||
|
||||
vm.PropertyChanged += (sender, args) =>
|
||||
{
|
||||
propertyChangedFired = true;
|
||||
changedPropertyName = args.PropertyName;
|
||||
};
|
||||
|
||||
string message = CreateIpcMessage(("App", @"C:\app.exe", LaunchingState.Waiting));
|
||||
SimulateIpcMessage(message);
|
||||
|
||||
Assert.IsTrue(propertyChangedFired, "PropertyChanged should fire when AppsListed is updated");
|
||||
Assert.AreEqual("AppsListed", changedPropertyName);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel")]
|
||||
public void ReceiveIpcMessage_ProgressUpdates_ReplacesEntireCollectionEachTime()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
|
||||
string msg1 = CreateIpcMessage(("App1", @"C:\app1.exe", LaunchingState.Waiting), ("App2", @"C:\app2.exe", LaunchingState.Waiting));
|
||||
SimulateIpcMessage(msg1);
|
||||
Assert.AreEqual(2, vm.AppsListed.Count);
|
||||
Assert.AreEqual(LaunchingState.Waiting, vm.AppsListed[0].LaunchState);
|
||||
|
||||
string msg2 = CreateIpcMessage(("App1", @"C:\app1.exe", LaunchingState.Launched), ("App2", @"C:\app2.exe", LaunchingState.Waiting));
|
||||
SimulateIpcMessage(msg2);
|
||||
Assert.AreEqual(2, vm.AppsListed.Count);
|
||||
Assert.AreEqual(LaunchingState.Launched, vm.AppsListed[0].LaunchState);
|
||||
|
||||
string msg3 = CreateIpcMessage(("App1", @"C:\app1.exe", LaunchingState.LaunchedAndMoved), ("App2", @"C:\app2.exe", LaunchingState.LaunchedAndMoved));
|
||||
SimulateIpcMessage(msg3);
|
||||
Assert.AreEqual(LaunchingState.LaunchedAndMoved, vm.AppsListed[0].LaunchState);
|
||||
Assert.AreEqual(LaunchingState.LaunchedAndMoved, vm.AppsListed[1].LaunchState);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel")]
|
||||
public void ReceiveIpcMessage_SomeAppsFail_AllowsMixedSuccessAndFailure()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
string message = CreateIpcMessage(
|
||||
("App1", @"C:\app1.exe", LaunchingState.LaunchedAndMoved),
|
||||
("App2", @"C:\app2.exe", LaunchingState.Failed),
|
||||
("App3", @"C:\app3.exe", LaunchingState.LaunchedAndMoved));
|
||||
SimulateIpcMessage(message);
|
||||
|
||||
Assert.AreEqual(LaunchingState.LaunchedAndMoved, vm.AppsListed[0].LaunchState);
|
||||
Assert.AreEqual(LaunchingState.Failed, vm.AppsListed[1].LaunchState);
|
||||
Assert.AreEqual(LaunchingState.LaunchedAndMoved, vm.AppsListed[2].LaunchState);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel")]
|
||||
public void ReceiveIpcMessage_CanceledState_ReflectedInCollection()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
string message = CreateIpcMessage(("App1", @"C:\app1.exe", LaunchingState.LaunchedAndMoved), ("App2", @"C:\app2.exe", LaunchingState.Canceled));
|
||||
SimulateIpcMessage(message);
|
||||
|
||||
Assert.AreEqual(LaunchingState.Canceled, vm.AppsListed[1].LaunchState);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel")]
|
||||
public void ReceiveIpcMessage_EmptyAppList_SetsCollectionToEmpty()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
string message = @"{ ""processId"": 1, ""apps"": { ""appLaunchInfos"": [] } }";
|
||||
SimulateIpcMessage(message);
|
||||
|
||||
Assert.AreEqual(0, vm.AppsListed.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel")]
|
||||
public void ReceiveIpcMessage_CorruptedPayload_GracefullyIgnoredWithoutCrash()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
SimulateIpcMessage("this is not json");
|
||||
|
||||
Assert.AreEqual(0, vm.AppsListed.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel")]
|
||||
public void ReceiveIpcMessage_EmptyString_GracefullyIgnoredWithoutCrash()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
SimulateIpcMessage(string.Empty);
|
||||
Assert.AreEqual(0, vm.AppsListed.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel")]
|
||||
public void DisposeViewModel_SingleCall_CompletesWithoutException()
|
||||
{
|
||||
var vm = new MainViewModel();
|
||||
vm.Dispose();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel")]
|
||||
public void DisposeViewModel_MultipleCalls_RemainsIdempotent()
|
||||
{
|
||||
var vm = new MainViewModel();
|
||||
vm.Dispose();
|
||||
vm.Dispose();
|
||||
}
|
||||
|
||||
private static void SimulateIpcMessage(string message)
|
||||
{
|
||||
App.IPCMessageReceivedCallback?.Invoke(message);
|
||||
}
|
||||
|
||||
private static string CreateIpcMessage(params (string Name, string Path, LaunchingState State)[] apps)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(@"{ ""processId"": 1, ""apps"": { ""appLaunchInfos"": [");
|
||||
|
||||
for (int i = 0; i < apps.Length; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
sb.Append(',');
|
||||
}
|
||||
|
||||
var (name, path, state) = apps[i];
|
||||
string escapedPath = path.Replace(@"\", @"\\");
|
||||
string appJson = string.Create(CultureInfo.InvariantCulture, $@"{{""application"": {{""application"": ""{name}"",""application-path"": ""{escapedPath}"",""title"": """",""package-full-name"": """",""app-user-model-id"": """",""pwa-app-id"": """",""command-line-arguments"": """",""is-elevated"": false,""can-launch-elevated"": false,""minimized"": false,""maximized"": false,""position"": {{ ""X"": 0, ""Y"": 0, ""width"": 100, ""height"": 100 }},""monitor"": 0}},""state"": {(int)state}}}");
|
||||
sb.Append(appJson);
|
||||
}
|
||||
|
||||
sb.Append("]}}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
105
src/modules/Workspaces/WorkspacesLauncherUI.UnitTests/README.md
Normal file
105
src/modules/Workspaces/WorkspacesLauncherUI.UnitTests/README.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# WorkspacesLauncherUI Unit Tests
|
||||
|
||||
Unit tests for the Workspaces Launcher UI (WinUI 3). These validate the data layer, ViewModel, and display logic that drives the workspace launch progress window.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Visual Studio 2022 17.4+ or Visual Studio 2026
|
||||
- .NET SDK (see `global.json` in repo root)
|
||||
- Submodules initialized: `git submodule update --init --recursive`
|
||||
|
||||
## Build
|
||||
|
||||
From this directory:
|
||||
|
||||
```powershell
|
||||
# Quick build (auto-detects platform)
|
||||
& "$env:RepoRoot\tools\build\build.cmd"
|
||||
|
||||
# Or with explicit options
|
||||
& "$env:RepoRoot\tools\build\build.cmd" -Platform arm64 -Configuration Debug
|
||||
```
|
||||
|
||||
If you get NuGet restore errors on first build:
|
||||
|
||||
```powershell
|
||||
& "$env:RepoRoot\tools\build\build-essentials.cmd"
|
||||
```
|
||||
|
||||
## Run Tests
|
||||
|
||||
### Option 1: dotnet test (recommended for CI)
|
||||
|
||||
```powershell
|
||||
dotnet test "<output-dir>\tests\WorkspacesLauncherUI.Tests\PowerToys.WorkspacesLauncherUI.Tests.dll" --verbosity normal
|
||||
```
|
||||
|
||||
The output directory depends on your platform/config. For arm64 Debug:
|
||||
|
||||
```powershell
|
||||
dotnet test "arm64\Debug\tests\WorkspacesLauncherUI.Tests\PowerToys.WorkspacesLauncherUI.Tests.dll" --verbosity normal
|
||||
```
|
||||
|
||||
### Option 2: Visual Studio Test Explorer
|
||||
|
||||
1. Open `PowerToys.slnx` in Visual Studio
|
||||
2. Build the `WorkspacesLauncherUI.UnitTests` project
|
||||
3. Open Test Explorer (`Ctrl+E, T`)
|
||||
4. Run all tests in `PowerToys.WorkspacesLauncherUI.Tests`
|
||||
|
||||
### Option 3: Filter by category
|
||||
|
||||
```powershell
|
||||
dotnet test <dll-path> --filter "TestCategory=Scenario"
|
||||
dotnet test <dll-path> --filter "TestCategory=Deserialization"
|
||||
dotnet test <dll-path> --filter "TestCategory=ViewModel"
|
||||
dotnet test <dll-path> --filter "TestCategory=Model"
|
||||
dotnet test <dll-path> --filter "TestCategory=Serialization"
|
||||
dotnet test <dll-path> --filter "TestCategory=DataModel"
|
||||
dotnet test <dll-path> --filter "TestCategory=Converter"
|
||||
```
|
||||
|
||||
### Generate TRX Report
|
||||
|
||||
```powershell
|
||||
dotnet test <dll-path> --logger "trx;LogFileName=TestResults.trx"
|
||||
```
|
||||
|
||||
Report saved to `TestResults/TestResults.trx`.
|
||||
|
||||
## Test Categories
|
||||
|
||||
| Category | File | What It Validates |
|
||||
|----------|------|-------------------|
|
||||
| `Deserialization` | `IpcMessageDeserializationTests.cs` | C++ launcher engine JSON → C# data models |
|
||||
| `ViewModel` | `LauncherViewModelStateManagementTests.cs` | IPC callback → ObservableCollection pipeline |
|
||||
| `Model` | `LaunchStatusDisplayLogicTests.cs` | Spinner/glyph/color for each launch state |
|
||||
| `Scenario` | `UserWorkflowIntegrationTests.cs` | Full user workflows (launch, cancel, fail) |
|
||||
| `Serialization` | `IpcJsonPropertyNamingTests.cs` | JSON key names match C++ IPC protocol |
|
||||
| `DataModel` | `WindowPositionDataTests.cs` | Window coordinates and equality |
|
||||
| `DataModel` | `ApplicationDataModelTests.cs` | All application fields |
|
||||
| `DataModel` | `LaunchStateEnumContractTests.cs` | Enum integers match `LaunchingStateEnum.h` |
|
||||
| `Converter` | `StatusIndicatorVisibilityTests.cs` | Loading → Visibility toggle |
|
||||
|
||||
## When to Run
|
||||
|
||||
- **After IPC contract changes**: Deserialization + Serialization categories
|
||||
- **After UI state changes**: Model + ViewModel categories
|
||||
- **After dependency updates**: All tests to verify no regressions
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
Follow the naming convention: `{WhatIsUnderTest}_{GivenCondition}_{ExpectedBehavior}`
|
||||
|
||||
Example:
|
||||
```csharp
|
||||
[TestMethod]
|
||||
[TestCategory("ViewModel")]
|
||||
public void ReceiveIpcMessage_NewFieldAdded_DeserializesWithoutBreakingExistingFields()
|
||||
```
|
||||
|
||||
## Note on Color Assertions
|
||||
|
||||
Color tests use `AppLaunching.StateColorValue` (returns `Windows.UI.Color`) instead of
|
||||
`StateColor` (returns `SolidColorBrush`) because WinUI brush creation requires a UI thread.
|
||||
The `StateColorValue` property exposes the same ARGB values for headless test validation.
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using WorkspacesLauncherUI.Converters;
|
||||
|
||||
namespace WorkspacesLauncherUI.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for BooleanToInvertedVisibilityConverter.
|
||||
/// When Loading=true → spinner Visible, glyph Collapsed
|
||||
/// When Loading=false → spinner Collapsed, glyph Visible
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class StatusIndicatorVisibilityTests
|
||||
{
|
||||
private readonly BooleanToInvertedVisibilityConverter _converter = new();
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Converter")]
|
||||
public void Converter_WhenLoadingTrue_HidesStatusGlyph()
|
||||
{
|
||||
var result = _converter.Convert(true, typeof(Visibility), null, "en-US");
|
||||
Assert.AreEqual(Visibility.Collapsed, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Converter")]
|
||||
public void Converter_WhenLoadingFalse_ShowsStatusGlyph()
|
||||
{
|
||||
var result = _converter.Convert(false, typeof(Visibility), null, "en-US");
|
||||
Assert.AreEqual(Visibility.Visible, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Converter")]
|
||||
[ExpectedException(typeof(NotImplementedException))]
|
||||
public void Converter_ReverseConversion_IsNotSupported()
|
||||
{
|
||||
_converter.ConvertBack(Visibility.Visible, typeof(bool), null, "en-US");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Converter")]
|
||||
public void Converter_NullConverterParameter_StillFunctionsCorrectly()
|
||||
{
|
||||
var result = _converter.Convert(true, typeof(Visibility), null, null);
|
||||
Assert.AreEqual(Visibility.Collapsed, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Converter")]
|
||||
public void Converter_MultipleCultures_BehaviorIsCultureInvariant()
|
||||
{
|
||||
var result1 = _converter.Convert(true, typeof(Visibility), null, "en-US");
|
||||
var result2 = _converter.Convert(true, typeof(Visibility), null, "ja-JP");
|
||||
var result3 = _converter.Convert(true, typeof(Visibility), null, "de-DE");
|
||||
|
||||
Assert.AreEqual(Visibility.Collapsed, result1);
|
||||
Assert.AreEqual(Visibility.Collapsed, result2);
|
||||
Assert.AreEqual(Visibility.Collapsed, result3);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using WorkspacesLauncherUI.Data;
|
||||
using WorkspacesLauncherUI.Models;
|
||||
using WorkspacesLauncherUI.ViewModels;
|
||||
|
||||
namespace WorkspacesLauncherUI.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// End-to-end scenario tests that simulate complete user workflows
|
||||
/// through the Launcher UI. These verify the full pipeline:
|
||||
/// IPC JSON message → Deserialization → ViewModel → Model properties.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class UserWorkflowIntegrationTests
|
||||
{
|
||||
[TestMethod]
|
||||
[TestCategory("Scenario")]
|
||||
public void UserLaunchesWorkspace_ThreeApps_AllProgressFromWaitingToSuccess()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
|
||||
SimulateIpcMessage(BuildMessage(
|
||||
1234,
|
||||
App("Visual Studio Code", @"C:\Code.exe", LaunchingState.Waiting),
|
||||
App("Windows Terminal", @"C:\wt.exe", LaunchingState.Waiting),
|
||||
App("Microsoft Edge", @"C:\edge.exe", LaunchingState.Waiting)));
|
||||
|
||||
Assert.AreEqual(3, vm.AppsListed.Count);
|
||||
Assert.IsTrue(vm.AppsListed.All(a => a.Loading), "All apps should show loading spinner initially");
|
||||
|
||||
SimulateIpcMessage(BuildMessage(
|
||||
1234,
|
||||
App("Visual Studio Code", @"C:\Code.exe", LaunchingState.Launched),
|
||||
App("Windows Terminal", @"C:\wt.exe", LaunchingState.Waiting),
|
||||
App("Microsoft Edge", @"C:\edge.exe", LaunchingState.Waiting)));
|
||||
|
||||
Assert.IsTrue(vm.AppsListed[0].Loading, "Launched but not yet moved — still loading");
|
||||
|
||||
SimulateIpcMessage(BuildMessage(
|
||||
1234,
|
||||
App("Visual Studio Code", @"C:\Code.exe", LaunchingState.LaunchedAndMoved),
|
||||
App("Windows Terminal", @"C:\wt.exe", LaunchingState.Launched),
|
||||
App("Microsoft Edge", @"C:\edge.exe", LaunchingState.Waiting)));
|
||||
|
||||
Assert.IsFalse(vm.AppsListed[0].Loading, "Moved app should stop loading");
|
||||
Assert.AreEqual("\U0000F78C", vm.AppsListed[0].StateGlyph, "Moved app should show checkmark");
|
||||
|
||||
SimulateIpcMessage(BuildMessage(
|
||||
1234,
|
||||
App("Visual Studio Code", @"C:\Code.exe", LaunchingState.LaunchedAndMoved),
|
||||
App("Windows Terminal", @"C:\wt.exe", LaunchingState.LaunchedAndMoved),
|
||||
App("Microsoft Edge", @"C:\edge.exe", LaunchingState.LaunchedAndMoved)));
|
||||
|
||||
Assert.IsTrue(vm.AppsListed.All(a => !a.Loading), "All apps should stop loading");
|
||||
Assert.IsTrue(vm.AppsListed.All(a => a.StateGlyph == "\U0000F78C"), "All apps should show checkmark");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Scenario")]
|
||||
public void UserLaunchesWorkspace_OneAppMissing_FailedShowsRedOthersShowGreen()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
|
||||
SimulateIpcMessage(BuildMessage(
|
||||
1234,
|
||||
App("Notepad", @"C:\Windows\notepad.exe", LaunchingState.LaunchedAndMoved),
|
||||
App("Missing App", @"C:\nonexistent\app.exe", LaunchingState.Failed),
|
||||
App("Calculator", @"C:\Windows\calc.exe", LaunchingState.LaunchedAndMoved)));
|
||||
|
||||
Assert.IsFalse(vm.AppsListed[0].Loading);
|
||||
Assert.AreEqual("\U0000F78C", vm.AppsListed[0].StateGlyph);
|
||||
|
||||
Assert.IsFalse(vm.AppsListed[1].Loading);
|
||||
Assert.AreEqual("\U0000EF2C", vm.AppsListed[1].StateGlyph);
|
||||
var color = vm.AppsListed[1].StateColorValue;
|
||||
Assert.AreEqual(254, color.R);
|
||||
|
||||
Assert.AreEqual("\U0000F78C", vm.AppsListed[2].StateGlyph);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Scenario")]
|
||||
public void UserCancelsLaunch_MidProgress_PartialAppsShowCanceledState()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
|
||||
SimulateIpcMessage(BuildMessage(
|
||||
5678,
|
||||
App("App1", @"C:\app1.exe", LaunchingState.LaunchedAndMoved),
|
||||
App("App2", @"C:\app2.exe", LaunchingState.Canceled),
|
||||
App("App3", @"C:\app3.exe", LaunchingState.Canceled)));
|
||||
|
||||
Assert.AreEqual(LaunchingState.LaunchedAndMoved, vm.AppsListed[0].LaunchState);
|
||||
Assert.AreEqual(LaunchingState.Canceled, vm.AppsListed[1].LaunchState);
|
||||
Assert.AreEqual(LaunchingState.Canceled, vm.AppsListed[2].LaunchState);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Scenario")]
|
||||
public void UserLaunchesWorkspace_SingleApp_CompletesFullLifecycle()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
|
||||
SimulateIpcMessage(BuildMessage(
|
||||
100,
|
||||
App("Notepad", @"C:\Windows\System32\notepad.exe", LaunchingState.Waiting)));
|
||||
|
||||
Assert.AreEqual(1, vm.AppsListed.Count);
|
||||
Assert.AreEqual("Notepad", vm.AppsListed[0].Name);
|
||||
Assert.IsTrue(vm.AppsListed[0].Loading);
|
||||
|
||||
SimulateIpcMessage(BuildMessage(
|
||||
100,
|
||||
App("Notepad", @"C:\Windows\System32\notepad.exe", LaunchingState.LaunchedAndMoved)));
|
||||
|
||||
Assert.IsFalse(vm.AppsListed[0].Loading);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Scenario")]
|
||||
public void UserLaunchesWorkspace_ChromeAndEdgePwa_PwaIdsPreserved()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
|
||||
SimulateIpcMessage(BuildMessageFull(
|
||||
300,
|
||||
AppFull("Gmail", @"C:\chrome.exe", string.Empty, string.Empty, "fmgjjmmmlfnkbppncijlocphclkkleod", LaunchingState.LaunchedAndMoved),
|
||||
AppFull("Teams", @"C:\edge.exe", string.Empty, string.Empty, "cifhbcnohmdccbgoicgdjpfamggdegmo", LaunchingState.Launched)));
|
||||
|
||||
Assert.AreEqual("fmgjjmmmlfnkbppncijlocphclkkleod", vm.AppsListed[0].PwaAppId);
|
||||
Assert.AreEqual("cifhbcnohmdccbgoicgdjpfamggdegmo", vm.AppsListed[1].PwaAppId);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Scenario")]
|
||||
public void UserLaunchesWorkspace_AdminApp_ElevatedFlagPreservedInUi()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
|
||||
string message = @"{
|
||||
""processId"": 400,
|
||||
""apps"": {
|
||||
""appLaunchInfos"": [
|
||||
{
|
||||
""application"": {
|
||||
""application"": ""Command Prompt (Admin)"",
|
||||
""application-path"": ""C:\\Windows\\System32\\cmd.exe"",
|
||||
""title"": ""Administrator: Command Prompt"",
|
||||
""package-full-name"": """",
|
||||
""app-user-model-id"": """",
|
||||
""pwa-app-id"": """",
|
||||
""command-line-arguments"": """",
|
||||
""is-elevated"": true,
|
||||
""can-launch-elevated"": true,
|
||||
""minimized"": false,
|
||||
""maximized"": false,
|
||||
""position"": { ""X"": 0, ""Y"": 0, ""width"": 800, ""height"": 600 },
|
||||
""monitor"": 0
|
||||
},
|
||||
""state"": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}";
|
||||
|
||||
SimulateIpcMessage(message);
|
||||
Assert.AreEqual("Command Prompt (Admin)", vm.AppsListed[0].Name);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Scenario")]
|
||||
public void UserLaunchesWorkspace_FifteenApps_AllAppsDisplayedWithLoadingState()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
var apps = new (string Name, string Path, LaunchingState State)[15];
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
apps[i] = ($"App {i}", $@"C:\app{i}.exe", LaunchingState.Waiting);
|
||||
}
|
||||
|
||||
SimulateIpcMessage(BuildMessage(500, apps));
|
||||
|
||||
Assert.AreEqual(15, vm.AppsListed.Count);
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
Assert.AreEqual($"App {i}", vm.AppsListed[i].Name);
|
||||
Assert.IsTrue(vm.AppsListed[i].Loading);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Scenario")]
|
||||
public void UserLaunchesWorkspace_AllAppsMissing_AllShowRedErrorState()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
|
||||
SimulateIpcMessage(BuildMessage(
|
||||
800,
|
||||
App("App1", @"C:\missing1.exe", LaunchingState.Failed),
|
||||
App("App2", @"C:\missing2.exe", LaunchingState.Failed)));
|
||||
|
||||
Assert.IsTrue(vm.AppsListed.All(a => !a.Loading), "Failed apps should not show loading");
|
||||
Assert.IsTrue(vm.AppsListed.All(a => a.StateGlyph == "\U0000EF2C"), "Failed apps should show error glyph");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Scenario")]
|
||||
public void UserLaunchesWorkspace_UwpStoreApp_PackageFieldsMappedToUi()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
|
||||
SimulateIpcMessage(BuildMessageFull(
|
||||
900,
|
||||
AppFull(
|
||||
"Windows Settings",
|
||||
@"C:\Program Files\WindowsApps\windows.immersivecontrolpanel\SystemSettings.exe",
|
||||
"windows.immersivecontrolpanel_10.0.0.0_neutral_cw5n1h2txyewy",
|
||||
"windows.immersivecontrolpanel_cw5n1h2txyewy!microsoft.windows.immersivecontrolpanel",
|
||||
string.Empty,
|
||||
LaunchingState.LaunchedAndMoved)));
|
||||
|
||||
Assert.AreEqual("Windows Settings", vm.AppsListed[0].Name);
|
||||
Assert.AreEqual("windows.immersivecontrolpanel_10.0.0.0_neutral_cw5n1h2txyewy", vm.AppsListed[0].PackagedName);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Scenario")]
|
||||
public void UserLaunchesWorkspace_RapidIpcUpdates_FinalStateIsDisplayed()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
|
||||
for (int i = 0; i <= 4; i++)
|
||||
{
|
||||
SimulateIpcMessage(BuildMessage(
|
||||
1000,
|
||||
App("App", @"C:\app.exe", (LaunchingState)Math.Min(i, 2))));
|
||||
}
|
||||
|
||||
Assert.AreEqual(1, vm.AppsListed.Count);
|
||||
Assert.AreEqual(LaunchingState.LaunchedAndMoved, vm.AppsListed[0].LaunchState);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Scenario")]
|
||||
public void UserLaunchesWorkspace_Win32AndPackagedAndPwa_AllTypesCoexistInList()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
|
||||
SimulateIpcMessage(BuildMessageFull(
|
||||
1100,
|
||||
AppFull("Notepad", @"C:\Windows\notepad.exe", string.Empty, string.Empty, string.Empty, LaunchingState.LaunchedAndMoved),
|
||||
AppFull("Terminal", @"C:\wt.exe", "Microsoft.WindowsTerminal_1.0_x64__8wekyb3d8bbwe", "Microsoft.WindowsTerminal_8wekyb3d8bbwe!App", string.Empty, LaunchingState.LaunchedAndMoved),
|
||||
AppFull("Outlook", @"C:\edge.exe", string.Empty, string.Empty, "pwa_outlook_id", LaunchingState.Launched)));
|
||||
|
||||
Assert.AreEqual(3, vm.AppsListed.Count);
|
||||
Assert.AreEqual(string.Empty, vm.AppsListed[0].PwaAppId);
|
||||
Assert.AreEqual("Microsoft.WindowsTerminal_1.0_x64__8wekyb3d8bbwe", vm.AppsListed[1].PackagedName);
|
||||
Assert.AreEqual("pwa_outlook_id", vm.AppsListed[2].PwaAppId);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("Scenario")]
|
||||
public void UserLaunchesWorkspace_FiveUpdates_UiRefreshedOnEveryIpcMessage()
|
||||
{
|
||||
using var vm = new MainViewModel();
|
||||
int fireCount = 0;
|
||||
|
||||
vm.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == "AppsListed")
|
||||
{
|
||||
fireCount++;
|
||||
}
|
||||
};
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
SimulateIpcMessage(BuildMessage(
|
||||
1200,
|
||||
App("App", @"C:\app.exe", (LaunchingState)Math.Min(i, 2))));
|
||||
}
|
||||
|
||||
Assert.AreEqual(5, fireCount, "PropertyChanged should fire once per IPC message");
|
||||
}
|
||||
|
||||
private static (string Name, string Path, LaunchingState State) App(string name, string path, LaunchingState state)
|
||||
{
|
||||
return (name, path, state);
|
||||
}
|
||||
|
||||
private static (string Name, string Path, string PackageFullName, string Aumid, string PwaAppId, LaunchingState State) AppFull(
|
||||
string name, string path, string packageFullName, string aumid, string pwaAppId, LaunchingState state)
|
||||
{
|
||||
return (name, path, packageFullName, aumid, pwaAppId, state);
|
||||
}
|
||||
|
||||
private static void SimulateIpcMessage(string message)
|
||||
{
|
||||
WorkspacesLauncherUI.App.IPCMessageReceivedCallback?.Invoke(message);
|
||||
}
|
||||
|
||||
private static string BuildMessage(
|
||||
int processId,
|
||||
params (string Name, string Path, LaunchingState State)[] apps)
|
||||
{
|
||||
var fullApps = apps.Select(a => (a.Name, a.Path, string.Empty, string.Empty, string.Empty, a.State)).ToArray();
|
||||
return BuildMessageFull(processId, fullApps);
|
||||
}
|
||||
|
||||
private static string BuildMessageFull(
|
||||
int processId,
|
||||
params (string Name, string Path, string PackageFullName, string Aumid, string PwaAppId, LaunchingState State)[] apps)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(CultureInfo.InvariantCulture, $@"{{ ""processId"": {processId}, ""apps"": {{ ""appLaunchInfos"": [");
|
||||
|
||||
for (int i = 0; i < apps.Length; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
sb.Append(',');
|
||||
}
|
||||
|
||||
var (name, path, packageFullName, aumid, pwaAppId, state) = apps[i];
|
||||
string escapedPath = path.Replace(@"\", @"\\");
|
||||
string appJson = string.Create(CultureInfo.InvariantCulture, $@"{{""application"": {{""application"": ""{name}"",""application-path"": ""{escapedPath}"",""title"": """",""package-full-name"": ""{packageFullName}"",""app-user-model-id"": ""{aumid}"",""pwa-app-id"": ""{pwaAppId}"",""command-line-arguments"": """",""is-elevated"": false,""can-launch-elevated"": false,""minimized"": false,""maximized"": false,""position"": {{ ""X"": 0, ""Y"": 0, ""width"": 100, ""height"": 100 }},""monitor"": 0}},""state"": {(int)state}}}");
|
||||
sb.Append(appJson);
|
||||
}
|
||||
|
||||
sb.Append("]}}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using WorkspacesLauncherUI.Data;
|
||||
|
||||
namespace WorkspacesLauncherUI.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for PositionWrapper struct equality and operator behavior.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class WindowPositionDataTests
|
||||
{
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void PositionEquality_IdenticalCoordinates_ReturnsTrue()
|
||||
{
|
||||
var pos1 = new PositionWrapper { X = 100, Y = 200, Width = 800, Height = 600 };
|
||||
var pos2 = new PositionWrapper { X = 100, Y = 200, Width = 800, Height = 600 };
|
||||
Assert.IsTrue(pos1 == pos2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void PositionEquality_DifferentXCoordinate_ReturnsFalse()
|
||||
{
|
||||
var pos1 = new PositionWrapper { X = 100, Y = 200, Width = 800, Height = 600 };
|
||||
var pos2 = new PositionWrapper { X = 101, Y = 200, Width = 800, Height = 600 };
|
||||
Assert.IsFalse(pos1 == pos2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void PositionEquality_DifferentYCoordinate_ReturnsFalse()
|
||||
{
|
||||
var pos1 = new PositionWrapper { X = 100, Y = 200, Width = 800, Height = 600 };
|
||||
var pos2 = new PositionWrapper { X = 100, Y = 201, Width = 800, Height = 600 };
|
||||
Assert.IsFalse(pos1 == pos2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void PositionEquality_DifferentWidth_ReturnsFalse()
|
||||
{
|
||||
var pos1 = new PositionWrapper { X = 100, Y = 200, Width = 800, Height = 600 };
|
||||
var pos2 = new PositionWrapper { X = 100, Y = 200, Width = 801, Height = 600 };
|
||||
Assert.IsFalse(pos1 == pos2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void PositionEquality_DifferentHeight_ReturnsFalse()
|
||||
{
|
||||
var pos1 = new PositionWrapper { X = 100, Y = 200, Width = 800, Height = 600 };
|
||||
var pos2 = new PositionWrapper { X = 100, Y = 200, Width = 800, Height = 601 };
|
||||
Assert.IsFalse(pos1 == pos2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void PositionInequality_DifferentCoordinates_ReturnsTrue()
|
||||
{
|
||||
var pos1 = new PositionWrapper { X = 0, Y = 0, Width = 1920, Height = 1080 };
|
||||
var pos2 = new PositionWrapper { X = 960, Y = 0, Width = 960, Height = 1080 };
|
||||
Assert.IsTrue(pos1 != pos2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void PositionInequality_IdenticalCoordinates_ReturnsFalse()
|
||||
{
|
||||
var pos1 = new PositionWrapper { X = 0, Y = 0, Width = 1920, Height = 1080 };
|
||||
var pos2 = new PositionWrapper { X = 0, Y = 0, Width = 1920, Height = 1080 };
|
||||
Assert.IsFalse(pos1 != pos2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void PositionEquals_BoxedIdenticalValues_ReturnsTrue()
|
||||
{
|
||||
var pos1 = new PositionWrapper { X = 100, Y = 200, Width = 800, Height = 600 };
|
||||
object pos2 = new PositionWrapper { X = 100, Y = 200, Width = 800, Height = 600 };
|
||||
Assert.IsTrue(pos1.Equals(pos2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void PositionEquals_NullComparison_ReturnsFalse()
|
||||
{
|
||||
var pos = new PositionWrapper { X = 0, Y = 0, Width = 100, Height = 100 };
|
||||
Assert.IsFalse(pos.Equals(null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void PositionEquals_DifferentObjectType_ReturnsFalse()
|
||||
{
|
||||
var pos = new PositionWrapper { X = 0, Y = 0, Width = 100, Height = 100 };
|
||||
Assert.IsFalse(pos.Equals("not a position"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void WindowPosition_LeftOfPrimaryMonitor_StoresNegativeCoordinates()
|
||||
{
|
||||
var pos = new PositionWrapper { X = -1920, Y = -200, Width = 1920, Height = 1080 };
|
||||
Assert.AreEqual(-1920, pos.X);
|
||||
Assert.AreEqual(-200, pos.Y);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void WindowPosition_AllZeroValues_IsValidState()
|
||||
{
|
||||
var pos = new PositionWrapper { X = 0, Y = 0, Width = 0, Height = 0 };
|
||||
Assert.AreEqual(0, pos.X);
|
||||
Assert.AreEqual(0, pos.Y);
|
||||
Assert.AreEqual(0, pos.Width);
|
||||
Assert.AreEqual(0, pos.Height);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void WindowPosition_FourthMonitor4K_StoresLargeCoordinates()
|
||||
{
|
||||
var pos = new PositionWrapper { X = 11520, Y = 0, Width = 3840, Height = 2160 };
|
||||
Assert.AreEqual(11520, pos.X);
|
||||
Assert.AreEqual(3840, pos.Width);
|
||||
Assert.AreEqual(2160, pos.Height);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void WindowPosition_DefaultStruct_AllFieldsAreZero()
|
||||
{
|
||||
PositionWrapper pos = default;
|
||||
Assert.AreEqual(0, pos.X);
|
||||
Assert.AreEqual(0, pos.Y);
|
||||
Assert.AreEqual(0, pos.Width);
|
||||
Assert.AreEqual(0, pos.Height);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DataModel")]
|
||||
public void WindowPosition_TwoDefaultStructs_AreConsideredEqual()
|
||||
{
|
||||
PositionWrapper pos1 = default;
|
||||
PositionWrapper pos2 = default;
|
||||
Assert.IsTrue(pos1 == pos2);
|
||||
Assert.IsTrue(pos1.Equals(pos2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="$(RepoRoot)src\Common.SelfContained.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\WorkspacesLauncherUI.Tests\</OutputPath>
|
||||
<RootNamespace>WorkspacesLauncherUI.UnitTests</RootNamespace>
|
||||
<AssemblyName>PowerToys.WorkspacesLauncherUI.Tests</AssemblyName>
|
||||
<OutputType>Exe</OutputType>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<Platforms>x64;ARM64</Platforms>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="MSTest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WorkspacesCsharpLibrary\WorkspacesCsharpLibrary.csproj" />
|
||||
<ProjectReference Include="..\WorkspacesLauncherUI.WinUI\WorkspacesLauncherUI.WinUI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace WorkspacesLauncherUI.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a boolean to inverted Visibility:
|
||||
/// true → Collapsed (hide the element)
|
||||
/// false → Visible (show the element)
|
||||
///
|
||||
/// Used to show the status glyph (checkmark/X) only when loading is complete.
|
||||
/// The spinner uses the standard BooleanToVisibility (true=Visible),
|
||||
/// and this converter shows the glyph when loading is false.
|
||||
/// </summary>
|
||||
public sealed partial class BooleanToInvertedVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, System.Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is bool boolValue && boolValue)
|
||||
{
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
return Visibility.Visible;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, System.Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace WorkspacesLauncherUI.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a boolean to Visibility:
|
||||
/// true → Visible
|
||||
/// false → Collapsed
|
||||
///
|
||||
/// Used to show the loading spinner while an app is still launching.
|
||||
/// </summary>
|
||||
public sealed partial class BooleanToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, System.Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is bool boolValue && boolValue)
|
||||
{
|
||||
return Visibility.Visible;
|
||||
}
|
||||
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, System.Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
|
||||
namespace WorkspacesLauncherUI.Helpers
|
||||
{
|
||||
internal static class IconHelper
|
||||
{
|
||||
public static BitmapImage TryGetExecutableIcon(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path) || !File.Exists(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using Icon icon = Icon.ExtractAssociatedIcon(path);
|
||||
if (icon is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using Bitmap bitmap = icon.ToBitmap();
|
||||
using MemoryStream stream = new();
|
||||
bitmap.Save(stream, ImageFormat.Png);
|
||||
stream.Position = 0;
|
||||
|
||||
BitmapImage bitmapImage = new();
|
||||
bitmapImage.SetSource(stream.AsRandomAccessStream());
|
||||
return bitmapImage;
|
||||
}
|
||||
catch (Exception ex) when (ex is FileNotFoundException
|
||||
or UnauthorizedAccessException
|
||||
or Win32Exception
|
||||
or ArgumentException
|
||||
or IOException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using ManagedCommon;
|
||||
using Microsoft.Windows.ApplicationModel.Resources;
|
||||
|
||||
namespace WorkspacesLauncherUI
|
||||
{
|
||||
internal static class ResourceLoaderInstance
|
||||
{
|
||||
private static ResourceLoader _resourceLoader;
|
||||
|
||||
internal static ResourceLoader ResourceLoader
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_resourceLoader == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_resourceLoader = new ResourceLoader("PowerToys.WorkspacesLauncherUI.pri");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to load ResourceLoader: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return _resourceLoader;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
|
||||
using WorkspacesLauncherUI.Data;
|
||||
|
||||
namespace WorkspacesLauncherUI.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Model representing an application's launch status in the Launcher UI.
|
||||
/// Drives the display of the spinner (Loading), checkmark/X glyph (StateGlyph),
|
||||
/// and color (StateColor) for each app row.
|
||||
/// </summary>
|
||||
public class AppLaunching : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
public bool Loading => LaunchState == LaunchingState.Waiting || LaunchState == LaunchingState.Launched;
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string AppPath { get; set; }
|
||||
|
||||
public BitmapImage IconImage { get; set; }
|
||||
|
||||
public string PackagedName { get; set; }
|
||||
|
||||
public string Aumid { get; set; }
|
||||
|
||||
public string PwaAppId { get; set; }
|
||||
|
||||
private LaunchingState _launchState;
|
||||
|
||||
public LaunchingState LaunchState
|
||||
{
|
||||
get => _launchState;
|
||||
set
|
||||
{
|
||||
if (_launchState != value)
|
||||
{
|
||||
_launchState = value;
|
||||
_stateColorBrush = null;
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(LaunchState)));
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Loading)));
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(StateGlyph)));
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(StateColor)));
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(StateColorValue)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string StateGlyph
|
||||
{
|
||||
get => LaunchState switch
|
||||
{
|
||||
LaunchingState.LaunchedAndMoved => "\U0000F78C",
|
||||
LaunchingState.Failed => "\U0000EF2C",
|
||||
_ => "\U0000EF2C",
|
||||
};
|
||||
}
|
||||
|
||||
private SolidColorBrush _stateColorBrush;
|
||||
|
||||
public Brush StateColor
|
||||
{
|
||||
get => _stateColorBrush ??= new SolidColorBrush(StateColorValue);
|
||||
}
|
||||
|
||||
public Windows.UI.Color StateColorValue
|
||||
{
|
||||
get => LaunchState switch
|
||||
{
|
||||
LaunchingState.LaunchedAndMoved => Windows.UI.Color.FromArgb(255, 0, 128, 0),
|
||||
LaunchingState.Failed => Windows.UI.Color.FromArgb(255, 254, 0, 0),
|
||||
_ => Windows.UI.Color.FromArgb(255, 254, 0, 0),
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/modules/Workspaces/WorkspacesLauncherUI.WinUI/Program.cs
Normal file
46
src/modules/Workspaces/WorkspacesLauncherUI.WinUI/Program.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace WorkspacesLauncherUI
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Logger.InitializeLogger("\\Workspaces\\WorkspacesLauncherUI");
|
||||
|
||||
WinRT.ComWrappersSupport.InitializeComWrappers();
|
||||
|
||||
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredWorkspacesEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
|
||||
return;
|
||||
}
|
||||
|
||||
const string mutexName = "Local\\PowerToys_Workspaces_LauncherUI_InstanceMutex";
|
||||
bool createdNew;
|
||||
using var mutex = new Mutex(true, mutexName, out createdNew);
|
||||
|
||||
if (!createdNew)
|
||||
{
|
||||
Logger.LogWarning("Another instance of Workspaces Launcher UI is already running. Exiting this instance.");
|
||||
return;
|
||||
}
|
||||
|
||||
Microsoft.UI.Xaml.Application.Start((p) =>
|
||||
{
|
||||
var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
|
||||
SynchronizationContext.SetSynchronizationContext(context);
|
||||
_ = new App();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="CancelButton.Content" xml:space="preserve">
|
||||
<value>Cancel launch</value>
|
||||
</data>
|
||||
<data name="CancelButton.AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Cancel launch</value>
|
||||
</data>
|
||||
<data name="DismissButton.Content" xml:space="preserve">
|
||||
<value>Dismiss</value>
|
||||
</data>
|
||||
<data name="DismissButton.AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Dismiss</value>
|
||||
</data>
|
||||
<data name="LauncherWindowTitle" xml:space="preserve">
|
||||
<value>Your workspace is launching. Waiting on ...</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -10,6 +10,7 @@ using System.ComponentModel;
|
||||
using ManagedCommon;
|
||||
using WorkspacesCsharpLibrary;
|
||||
using WorkspacesLauncherUI.Data;
|
||||
using WorkspacesLauncherUI.Helpers;
|
||||
using WorkspacesLauncherUI.Models;
|
||||
|
||||
namespace WorkspacesLauncherUI.ViewModels
|
||||
@@ -18,9 +19,7 @@ namespace WorkspacesLauncherUI.ViewModels
|
||||
{
|
||||
public ObservableCollection<AppLaunching> AppsListed { get; set; } = new ObservableCollection<AppLaunching>();
|
||||
|
||||
private StatusWindow _snapshotWindow;
|
||||
private int launcherProcessID;
|
||||
private PwaHelper _pwaHelper;
|
||||
private readonly PwaHelper _pwaHelper;
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
@@ -33,7 +32,6 @@ namespace WorkspacesLauncherUI.ViewModels
|
||||
{
|
||||
_pwaHelper = new PwaHelper();
|
||||
|
||||
// receive IPC Message
|
||||
App.IPCMessageReceivedCallback = (string msg) =>
|
||||
{
|
||||
try
|
||||
@@ -51,7 +49,6 @@ namespace WorkspacesLauncherUI.ViewModels
|
||||
|
||||
private void HandleAppLaunchingState(AppLaunchData.AppLaunchDataWrapper appLaunchData)
|
||||
{
|
||||
launcherProcessID = appLaunchData.LauncherProcessID;
|
||||
List<AppLaunching> appLaunchingList = new List<AppLaunching>();
|
||||
foreach (var app in appLaunchData.AppLaunchInfos.AppLaunchInfoList)
|
||||
{
|
||||
@@ -59,6 +56,7 @@ namespace WorkspacesLauncherUI.ViewModels
|
||||
{
|
||||
Name = app.Application.Application,
|
||||
AppPath = app.Application.ApplicationPath,
|
||||
IconImage = IconHelper.TryGetExecutableIcon(app.Application.ApplicationPath),
|
||||
PackagedName = app.Application.PackageFullName,
|
||||
Aumid = app.Application.AppUserModelId,
|
||||
PwaAppId = app.Application.PwaAppId,
|
||||
@@ -70,27 +68,14 @@ namespace WorkspacesLauncherUI.ViewModels
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(AppsListed)));
|
||||
}
|
||||
|
||||
private void SelfDestroy(object source, System.Timers.ElapsedEventArgs e)
|
||||
public void CancelLaunch()
|
||||
{
|
||||
_snapshotWindow.Dispatcher.Invoke(() =>
|
||||
{
|
||||
_snapshotWindow.Close();
|
||||
});
|
||||
App.SendIPCMessage("cancel");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
internal void SetSnapshotWindow(StatusWindow snapshotWindow)
|
||||
{
|
||||
_snapshotWindow = snapshotWindow;
|
||||
}
|
||||
|
||||
internal void CancelLaunch()
|
||||
{
|
||||
App.SendIPCMessage("cancel");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Application
|
||||
x:Class="WorkspacesLauncherUI.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:WorkspacesLauncherUI">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<FontFamily x:Key="SymbolThemeFontFamily">Segoe Fluent Icons, Segoe MDL2 Assets</FontFamily>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using PowerToys.Interop;
|
||||
|
||||
namespace WorkspacesLauncherUI
|
||||
{
|
||||
/// <summary>
|
||||
/// WinUI 3 Application class for the Workspaces Launcher UI.
|
||||
/// Manages the IPC pipe connection to the C++ launcher engine and hosts the status window.
|
||||
/// </summary>
|
||||
public partial class App : Application, IDisposable
|
||||
{
|
||||
private StatusWindow _mainWindow;
|
||||
private TwoWayPipeMessageIPCManaged _ipcManager;
|
||||
private bool _isDisposed;
|
||||
|
||||
public static Action<string> IPCMessageReceivedCallback { get; set; }
|
||||
|
||||
public static DispatcherQueue DispatcherQueue { get; private set; }
|
||||
|
||||
public App()
|
||||
{
|
||||
string languageTag = LanguageHelper.LoadLanguage();
|
||||
if (!string.IsNullOrEmpty(languageTag))
|
||||
{
|
||||
try
|
||||
{
|
||||
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = languageTag;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to set language override: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
this.InitializeComponent();
|
||||
this.UnhandledException += OnUnhandledException;
|
||||
}
|
||||
|
||||
public static void SendIPCMessage(string message)
|
||||
{
|
||||
if ((Current as App)?._ipcManager != null)
|
||||
{
|
||||
(Current as App)._ipcManager.Send(message);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
DispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
_ipcManager = new TwoWayPipeMessageIPCManaged(
|
||||
"\\\\.\\pipe\\powertoys_workspaces_ui_",
|
||||
"\\\\.\\pipe\\powertoys_workspaces_launcher_ui_",
|
||||
(string message) =>
|
||||
{
|
||||
if (IPCMessageReceivedCallback != null && message.Length > 0)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
IPCMessageReceivedCallback(message);
|
||||
});
|
||||
}
|
||||
});
|
||||
_ipcManager.Start();
|
||||
|
||||
_mainWindow = new StatusWindow();
|
||||
_mainWindow.Activate();
|
||||
}
|
||||
|
||||
private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Logger.LogError("Unhandled exception occurred", e.Exception);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
_ipcManager?.End();
|
||||
_ipcManager?.Dispose();
|
||||
_isDisposed = true;
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +1,69 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Window
|
||||
x:Class="WorkspacesLauncherUI.StatusWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="clr-namespace:WorkspacesLauncherUI.Converters"
|
||||
xmlns:converters="using:WorkspacesLauncherUI.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:WorkspacesLauncherUI"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:props="clr-namespace:WorkspacesLauncherUI.Properties"
|
||||
Title="{x:Static props:Resources.LauncherWindowTitle}"
|
||||
Width="360"
|
||||
Height="340"
|
||||
BorderBrush="Red"
|
||||
BorderThickness="4"
|
||||
Closing="Window_Closing"
|
||||
ResizeMode="NoResize"
|
||||
Topmost="True"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Title="Workspaces"
|
||||
mc:Ignorable="d">
|
||||
<Window.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||
<converters:BooleanToInvertedVisibilityConverter x:Key="BooleanToInvertedVisibilityConverter" />
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="4" Background="Transparent">
|
||||
<Grid Margin="4">
|
||||
<Grid.Resources>
|
||||
<converters:BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||
<converters:BooleanToInvertedVisibilityConverter x:Key="BooleanToInvertedVisibilityConverter" />
|
||||
</Grid.Resources>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="1*" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ScrollViewer Grid.ColumnSpan="2">
|
||||
<StackPanel>
|
||||
<ItemsControl ItemsSource="{Binding AppsListed, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
|
||||
<ScrollViewer
|
||||
Grid.ColumnSpan="2"
|
||||
AutomationProperties.Name="Application launch status list"
|
||||
TabIndex="0">
|
||||
<StackPanel AutomationProperties.AccessibilityView="Content" AutomationProperties.LiveSetting="Polite">
|
||||
<ItemsControl AutomationProperties.Name="Applications" ItemsSource="{Binding AppsListed, Mode=OneWay}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid
|
||||
Margin="0,2"
|
||||
Padding="4"
|
||||
AutomationProperties.Name="{Binding Name, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image
|
||||
Width="20"
|
||||
Height="20"
|
||||
Margin="10"
|
||||
Margin="4,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Source="{Binding IconBitmapImage}" />
|
||||
Source="{Binding IconImage}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="{Binding Name, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<ProgressBar
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
Text="{Binding Name, Mode=OneWay}" />
|
||||
<ProgressRing
|
||||
Grid.Column="2"
|
||||
Width="20"
|
||||
Height="20"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsIndeterminate="True"
|
||||
Visibility="{Binding Loading, Mode=OneWay, Converter={StaticResource BoolToVis}, UpdateSourceTrigger=PropertyChanged}" />
|
||||
AutomationProperties.Name="Loading"
|
||||
IsActive="True"
|
||||
Visibility="{Binding Loading, Mode=OneWay, Converter={StaticResource BoolToVis}}" />
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Width="20"
|
||||
@@ -70,34 +71,37 @@
|
||||
Margin="10"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{DynamicResource SymbolThemeFontFamily}"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
FontSize="20"
|
||||
Foreground="{Binding StateColor}"
|
||||
Text="{Binding StateGlyph}"
|
||||
Visibility="{Binding Loading, Mode=OneWay, Converter={StaticResource BooleanToInvertedVisibilityConverter}, UpdateSourceTrigger=PropertyChanged}" />
|
||||
Visibility="{Binding Loading, Mode=OneWay, Converter={StaticResource BooleanToInvertedVisibilityConverter}}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<Button
|
||||
x:Name="CancelButton"
|
||||
x:Uid="CancelButton"
|
||||
Grid.Row="1"
|
||||
Margin="4"
|
||||
HorizontalAlignment="Stretch"
|
||||
AutomationProperties.Name="{x:Static props:Resources.CancelLaunch}"
|
||||
Click="CancelButtonClicked"
|
||||
Content="{x:Static props:Resources.CancelLaunch}" />
|
||||
Click="CancelButton_Click"
|
||||
TabIndex="1" />
|
||||
|
||||
<Button
|
||||
x:Name="DismissButton"
|
||||
x:Uid="DismissButton"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="4"
|
||||
HorizontalAlignment="Stretch"
|
||||
AutomationProperties.Name="{x:Static props:Resources.Dismiss}"
|
||||
Click="DismissButtonClicked"
|
||||
Content="{x:Static props:Resources.Dismiss}"
|
||||
Style="{DynamicResource AccentButtonStyle}" />
|
||||
Click="DismissButton_Click"
|
||||
Style="{ThemeResource AccentButtonStyle}"
|
||||
TabIndex="2" />
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
using WorkspacesLauncherUI.ViewModels;
|
||||
|
||||
namespace WorkspacesLauncherUI
|
||||
{
|
||||
/// <summary>
|
||||
/// Status window showing workspace launch progress.
|
||||
/// Displays a list of apps with their launch state (loading/success/failed).
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA1001:Types that own disposable fields should be disposable", Justification = "WinUI Window does not support IDisposable; ViewModel is disposed on window close.")]
|
||||
public sealed partial class StatusWindow : Window
|
||||
{
|
||||
private readonly MainViewModel _viewModel;
|
||||
|
||||
public StatusWindow()
|
||||
{
|
||||
_viewModel = new MainViewModel();
|
||||
this.InitializeComponent();
|
||||
|
||||
// WinUI Window is not a DependencyObject — set DataContext on root content
|
||||
if (this.Content is FrameworkElement rootElement)
|
||||
{
|
||||
rootElement.DataContext = _viewModel;
|
||||
}
|
||||
|
||||
this.Closed += Window_Closed;
|
||||
|
||||
// Configure window size and behavior to match WPF original (360x340, non-resizable, topmost)
|
||||
var appWindow = this.AppWindow;
|
||||
appWindow.Resize(new Windows.Graphics.SizeInt32(360, 340));
|
||||
appWindow.SetIcon("Assets/Workspaces/Workspaces.ico");
|
||||
|
||||
// Set title from resources
|
||||
try
|
||||
{
|
||||
this.Title = ResourceLoaderInstance.ResourceLoader?.GetString("LauncherWindowTitle") ?? "Workspaces";
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to load window title resource: " + ex.Message);
|
||||
this.Title = "Workspaces";
|
||||
}
|
||||
|
||||
if (appWindow.Presenter is OverlappedPresenter presenter)
|
||||
{
|
||||
presenter.IsResizable = false;
|
||||
presenter.IsAlwaysOnTop = true;
|
||||
}
|
||||
|
||||
// Center on screen
|
||||
CenterOnScreen(appWindow);
|
||||
}
|
||||
|
||||
private static void CenterOnScreen(AppWindow appWindow)
|
||||
{
|
||||
var displayArea = DisplayArea.GetFromWindowId(appWindow.Id, DisplayAreaFallback.Nearest);
|
||||
if (displayArea != null)
|
||||
{
|
||||
int centerX = (displayArea.WorkArea.Width - appWindow.Size.Width) / 2;
|
||||
int centerY = (displayArea.WorkArea.Height - appWindow.Size.Height) / 2;
|
||||
appWindow.Move(new Windows.Graphics.PointInt32(centerX, centerY));
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_viewModel.CancelLaunch();
|
||||
Close();
|
||||
}
|
||||
|
||||
private void DismissButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
private void Window_Closed(object sender, WindowEventArgs args)
|
||||
{
|
||||
_viewModel?.Dispose();
|
||||
(Application.Current as System.IDisposable)?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="$(RepoRoot)src\Common.SelfContained.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>WorkspacesLauncherUI</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<Platforms>x64;ARM64</Platforms>
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
|
||||
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
|
||||
<AssemblyName>PowerToys.WorkspacesLauncherUI</AssemblyName>
|
||||
<ApplicationIcon>..\Assets\Workspaces\Workspaces.ico</ApplicationIcon>
|
||||
<ProjectPriFileName>PowerToys.WorkspacesLauncherUI.pri</ProjectPriFileName>
|
||||
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Remove="Views\App.xaml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Include="Views\App.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- See https://learn.microsoft.com/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component for more info -->
|
||||
<PropertyGroup>
|
||||
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
|
||||
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
|
||||
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\Assets\**\*.*">
|
||||
<Link>Assets\Workspaces\%(Filename)%(Extension)</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
<ProjectReference Include="..\WorkspacesCsharpLibrary\WorkspacesCsharpLibrary.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
<PackageReference Include="System.Drawing.Common" />
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="PowerToys.WorkspacesLauncherUI.app"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 compatibility for unpackaged WinUI 3 apps -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>
|
||||
</startup>
|
||||
<runtime>
|
||||
<AppContextSwitchOverrides value = "Switch.System.Windows.DoNotScaleForDpiChanges=false"/>
|
||||
</runtime>
|
||||
</configuration>
|
||||
@@ -1,57 +0,0 @@
|
||||
<Application
|
||||
x:Class="WorkspacesLauncherUI.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:WorkspacesLauncherUI"
|
||||
Exit="OnExit"
|
||||
Startup="OnStartup"
|
||||
ThemeMode="System">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<FontFamily x:Key="SymbolThemeFontFamily">Segoe Fluent Icons, Segoe MDL2 Assets</FontFamily>
|
||||
<Style x:Key="HeadingTextBlock" TargetType="TextBlock" />
|
||||
<Style
|
||||
x:Key="SubtleButtonStyle"
|
||||
BasedOn="{StaticResource {x:Type Button}}"
|
||||
TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border
|
||||
x:Name="Border"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="4"
|
||||
SnapsToDevicePixels="True">
|
||||
<ContentPresenter
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Focusable="False"
|
||||
RecognizesAccessKey="True"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
|
||||
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
|
||||
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorDisabledBrush}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -1,147 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
|
||||
using ManagedCommon;
|
||||
using PowerToys.Interop;
|
||||
using WorkspacesLauncherUI.ViewModels;
|
||||
|
||||
namespace WorkspacesLauncherUI
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application, IDisposable
|
||||
{
|
||||
private static Mutex _instanceMutex;
|
||||
|
||||
// Create an instance of the IPC wrapper.
|
||||
private static TwoWayPipeMessageIPCManaged ipcmanager;
|
||||
|
||||
private StatusWindow _mainWindow;
|
||||
|
||||
private MainViewModel _mainViewModel;
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
public static Action<string> IPCMessageReceivedCallback { get; set; }
|
||||
|
||||
public App()
|
||||
{
|
||||
}
|
||||
|
||||
public static void SendIPCMessage(string message)
|
||||
{
|
||||
if (ipcmanager != null)
|
||||
{
|
||||
ipcmanager.Send(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStartup(object sender, StartupEventArgs e)
|
||||
{
|
||||
Logger.InitializeLogger("\\Workspaces\\WorkspacesLauncherUI");
|
||||
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
|
||||
|
||||
var languageTag = LanguageHelper.LoadLanguage();
|
||||
|
||||
if (!string.IsNullOrEmpty(languageTag))
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(languageTag);
|
||||
}
|
||||
catch (CultureNotFoundException ex)
|
||||
{
|
||||
Logger.LogError("CultureNotFoundException: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
const string appName = "Local\\PowerToys_Workspaces_LauncherUI_InstanceMutex";
|
||||
bool createdNew;
|
||||
_instanceMutex = new Mutex(true, appName, out createdNew);
|
||||
if (!createdNew)
|
||||
{
|
||||
Logger.LogWarning("Another instance of Workspaces Launcher UI is already running. Exiting this instance.");
|
||||
_instanceMutex = null;
|
||||
Shutdown(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (PowerToys.GPOWrapperProjection.GPOWrapper.GetConfiguredWorkspacesEnabledValue() == PowerToys.GPOWrapperProjection.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
|
||||
Shutdown(0);
|
||||
return;
|
||||
}
|
||||
|
||||
ipcmanager = new TwoWayPipeMessageIPCManaged("\\\\.\\pipe\\powertoys_workspaces_ui_", "\\\\.\\pipe\\powertoys_workspaces_launcher_ui_", (string message) =>
|
||||
{
|
||||
if (IPCMessageReceivedCallback != null && message.Length > 0)
|
||||
{
|
||||
IPCMessageReceivedCallback(message);
|
||||
}
|
||||
});
|
||||
ipcmanager.Start();
|
||||
|
||||
if (_mainViewModel == null)
|
||||
{
|
||||
_mainViewModel = new MainViewModel();
|
||||
}
|
||||
|
||||
// normal start of editor
|
||||
if (_mainWindow == null)
|
||||
{
|
||||
_mainWindow = new StatusWindow(_mainViewModel);
|
||||
}
|
||||
|
||||
// reset main window owner to keep it on the top
|
||||
_mainWindow.ShowActivated = true;
|
||||
_mainWindow.Topmost = true;
|
||||
_mainWindow.Show();
|
||||
}
|
||||
|
||||
private void OnExit(object sender, ExitEventArgs e)
|
||||
{
|
||||
if (_instanceMutex != null)
|
||||
{
|
||||
_instanceMutex.ReleaseMutex();
|
||||
}
|
||||
|
||||
Dispose();
|
||||
}
|
||||
|
||||
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs args)
|
||||
{
|
||||
Logger.LogError("Unhandled exception occurred", args.ExceptionObject as Exception);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
ipcmanager?.End();
|
||||
ipcmanager?.Dispose();
|
||||
|
||||
_instanceMutex?.Dispose();
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace WorkspacesLauncherUI.Converters
|
||||
{
|
||||
public class BooleanToInvertedVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if ((bool)value)
|
||||
{
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
return Visibility.Visible;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Windows.Automation.Peers;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace WorkspacesLauncherUI
|
||||
{
|
||||
public class HeadingTextBlock : TextBlock
|
||||
{
|
||||
protected override AutomationPeer OnCreateAutomationPeer()
|
||||
{
|
||||
return new HeadingTextBlockAutomationPeer(this);
|
||||
}
|
||||
|
||||
internal sealed class HeadingTextBlockAutomationPeer : TextBlockAutomationPeer
|
||||
{
|
||||
public HeadingTextBlockAutomationPeer(HeadingTextBlock owner)
|
||||
: base(owner)
|
||||
{
|
||||
}
|
||||
|
||||
protected override AutomationControlType GetAutomationControlTypeCore()
|
||||
{
|
||||
return AutomationControlType.Header;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using ManagedCommon;
|
||||
using WorkspacesCsharpLibrary.Models;
|
||||
using WorkspacesLauncherUI.Data;
|
||||
|
||||
namespace WorkspacesLauncherUI.Models
|
||||
{
|
||||
public class AppLaunching : BaseApplication, IDisposable
|
||||
{
|
||||
public bool Loading => LaunchState == LaunchingState.Waiting || LaunchState == LaunchingState.Launched;
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public LaunchingState LaunchState { get; set; }
|
||||
|
||||
public string StateGlyph
|
||||
{
|
||||
get => LaunchState switch
|
||||
{
|
||||
LaunchingState.LaunchedAndMoved => "\U0000F78C",
|
||||
LaunchingState.Failed => "\U0000EF2C",
|
||||
_ => "\U0000EF2C",
|
||||
};
|
||||
}
|
||||
|
||||
public System.Windows.Media.Brush StateColor
|
||||
{
|
||||
get => LaunchState switch
|
||||
{
|
||||
LaunchingState.LaunchedAndMoved => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 0, 128, 0)),
|
||||
LaunchingState.Failed => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 254, 0, 0)),
|
||||
_ => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 254, 0, 0)),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace WorkspacesLauncherUI.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WorkspacesLauncherUI.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Cancel launch.
|
||||
/// </summary>
|
||||
public static string CancelLaunch {
|
||||
get {
|
||||
return ResourceManager.GetString("CancelLaunch", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Dismiss.
|
||||
/// </summary>
|
||||
public static string Dismiss {
|
||||
get {
|
||||
return ResourceManager.GetString("Dismiss", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Your workspace is launching. Waiting on ....
|
||||
/// </summary>
|
||||
public static string LauncherWindowTitle {
|
||||
get {
|
||||
return ResourceManager.GetString("LauncherWindowTitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="CancelLaunch" xml:space="preserve">
|
||||
<value>Cancel launch</value>
|
||||
</data>
|
||||
<data name="Dismiss" xml:space="preserve">
|
||||
<value>Dismiss</value>
|
||||
</data>
|
||||
<data name="LauncherWindowTitle" xml:space="preserve">
|
||||
<value>Your workspace is launching. Waiting on ...</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,26 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace WorkspacesLauncherUI.Properties {
|
||||
|
||||
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.1.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
|
||||
|
||||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||
|
||||
public static Settings Default {
|
||||
get {
|
||||
return defaultInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
|
||||
<Profiles>
|
||||
<Profile Name="(Default)" />
|
||||
</Profiles>
|
||||
<Settings />
|
||||
</SettingsFile>
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Windows;
|
||||
|
||||
using WorkspacesLauncherUI.ViewModels;
|
||||
|
||||
namespace WorkspacesLauncherUI
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for SnapshotWindow.xaml
|
||||
/// </summary>
|
||||
public partial class StatusWindow : Window
|
||||
{
|
||||
private MainViewModel _mainViewModel;
|
||||
|
||||
public StatusWindow(MainViewModel mainViewModel)
|
||||
{
|
||||
_mainViewModel = mainViewModel;
|
||||
_mainViewModel.SetSnapshotWindow(this);
|
||||
this.DataContext = _mainViewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void CancelButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_mainViewModel.CancelLaunch();
|
||||
Close();
|
||||
}
|
||||
|
||||
private void DismissButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="$(RepoRoot)src\Common.SelfContained.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyTitle>PowerToys.WorkspacesLauncherUI</AssemblyTitle>
|
||||
<AssemblyDescription>PowerToys Workspaces Launcher UI</AssemblyDescription>
|
||||
<Description>PowerToys Workspaces Launcher UI</Description>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
|
||||
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{9C53CC25-0623-4569-95BC-B05410675EE3}</ProjectGuid>
|
||||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ApplicationIcon>..\Assets\Workspaces\Workspaces.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<AssemblyName>PowerToys.WorkspacesLauncherUI</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\Assets\**\*.*">
|
||||
<Link>Assets\Workspaces\%(Filename)%(Extension)</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<COMReference Include="IWshRuntimeLibrary">
|
||||
<WrapperTool>tlbimp</WrapperTool>
|
||||
<VersionMinor>0</VersionMinor>
|
||||
<VersionMajor>1</VersionMajor>
|
||||
<Guid>f935dc20-1cf0-11d0-adb9-00c04fd58a0b</Guid>
|
||||
<Lcid>0</Lcid>
|
||||
<Isolated>false</Isolated>
|
||||
<EmbedInteropTypes>true</EmbedInteropTypes>
|
||||
</COMReference>
|
||||
<COMReference Include="Shell32">
|
||||
<WrapperTool>tlbimp</WrapperTool>
|
||||
<VersionMinor>0</VersionMinor>
|
||||
<VersionMajor>1</VersionMajor>
|
||||
<Guid>50a7e9b0-70ef-11d1-b75a-00a0c90564fe</Guid>
|
||||
<Lcid>0</Lcid>
|
||||
<Isolated>false</Isolated>
|
||||
<EmbedInteropTypes>true</EmbedInteropTypes>
|
||||
</COMReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<None Include="app.manifest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\GPOWrapperProjection\GPOWrapperProjection.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
<ProjectReference Include="..\WorkspacesCsharpLibrary\WorkspacesCsharpLibrary.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Properties\Settings.Designer.cs">
|
||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Settings.settings</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="Properties\Settings.settings">
|
||||
<Generator>SettingsSingleFileGenerator</Generator>
|
||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,74 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<!-- UAC Manifest Options
|
||||
If you want to change the Windows User Account Control level replace the
|
||||
requestedExecutionLevel node with one of the following.
|
||||
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
|
||||
|
||||
Specifying requestedExecutionLevel element will disable file and registry virtualization.
|
||||
Remove this element if your application requires this virtualization for backwards
|
||||
compatibility.
|
||||
-->
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on
|
||||
and is designed to work with. Uncomment the appropriate elements
|
||||
and Windows will automatically select the most compatible environment. -->
|
||||
|
||||
<!-- Windows Vista -->
|
||||
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
|
||||
|
||||
<!-- Windows 7 -->
|
||||
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
|
||||
|
||||
<!-- Windows 8 -->
|
||||
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
|
||||
|
||||
<!-- Windows 8.1 -->
|
||||
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<!-- Indicates that the application is DPI-aware and will not be automatically scaled by Windows at higher
|
||||
DPIs. Windows Presentation Foundation (WPF) applications are automatically DPI-aware and do not need
|
||||
to opt in. Windows Forms applications targeting .NET Framework 4.6 that opt into this setting, should
|
||||
also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config. -->
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->
|
||||
<!--
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
-->
|
||||
|
||||
</assembly>
|
||||
@@ -445,8 +445,8 @@ function Test-CoreFiles {
|
||||
'PowerToys.WorkspacesWindowArranger.exe',
|
||||
'PowerToys.WorkspacesEditor.exe',
|
||||
'PowerToys.WorkspacesEditor.dll',
|
||||
'PowerToys.WorkspacesLauncherUI.exe',
|
||||
'PowerToys.WorkspacesLauncherUI.dll',
|
||||
'WinUI3Apps\PowerToys.WorkspacesLauncherUI.exe',
|
||||
'WinUI3Apps\PowerToys.WorkspacesLauncherUI.dll',
|
||||
'PowerToys.WorkspacesModuleInterface.dll',
|
||||
'PowerToys.WorkspacesCsharpLibrary.dll',
|
||||
|
||||
|
||||
Reference in New Issue
Block a user