Compare commits

...

8 Commits

50 changed files with 2863 additions and 854 deletions

View File

@@ -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",

View File

@@ -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>

View File

@@ -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();

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

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

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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>

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}
}
}

View 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();
});
}
}
}

View File

@@ -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>

View File

@@ -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");
}
}
}

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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();
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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)),
};
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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>

View File

@@ -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;
}
}
}
}

View File

@@ -1,7 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

View File

@@ -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)
{
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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',