diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 78affbb16f..7e9445f893 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -76,6 +76,7 @@ body:
- System tray interaction
- TextExtractor
- Video Conference Mute
+ - Workspaces
- Welcome / PowerToys Tour window
validations:
required: true
diff --git a/.github/ISSUE_TEMPLATE/translation_issue.yml b/.github/ISSUE_TEMPLATE/translation_issue.yml
index 88114730c6..8a6cd56280 100644
--- a/.github/ISSUE_TEMPLATE/translation_issue.yml
+++ b/.github/ISSUE_TEMPLATE/translation_issue.yml
@@ -50,6 +50,7 @@ body:
- System tray interaction
- TextExtractor
- Video Conference Mute
+ - Workspaces
- Welcome / PowerToys Tour window
validations:
required: true
diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 2bf5ff9492..f5bc0156f8 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -56,11 +56,14 @@ APPBARDATA
appdata
APPEXECLINK
Appium
+applayout
Applicationcan
+APPLICATIONFRAMEHOST
appmanifest
APPNAME
appref
appsettings
+appsfolder
appwindow
appwiz
APSTUDIO
@@ -240,6 +243,7 @@ CONTEXTMENUHANDLER
CONTROLL
CONTROLPARENT
copiedcolorrepresentation
+COREWINDOW
cotaskmem
COULDNOT
countof
@@ -832,6 +836,7 @@ lpwcx
lpwndpl
LReader
LRESULT
+LSTATUS
lstrcmp
lstrcmpi
lstrlen
@@ -1205,6 +1210,8 @@ projectname
PROPBAG
PROPERTYKEY
propkey
+propsys
+PROPVARIANT
propvarutil
prvpane
psapi
@@ -1370,6 +1377,7 @@ sddl
SDKDDK
sdns
searchterm
+SEARCHUI
secpol
SENDCHANGE
sendinput
@@ -1552,7 +1560,9 @@ SYSKEYUP
SYSLIB
SYSMENU
SYSTEMAPPS
+systemsettings
SYSTEMTIME
+SYSTEMWOW
tapp
TApplication
TApplied
@@ -1664,9 +1674,11 @@ urlmon
Usb
USEDEFAULT
USEFILEATTRIBUTES
+USEPOSITION
USERDATA
Userenv
USESHOWWINDOW
+USESIZE
USESTDHANDLES
USRDLL
UType
@@ -1734,6 +1746,7 @@ vswhere
Vtbl
WANTPALM
wbem
+Wbemidl
wbemuuid
WBounds
Wca
@@ -1821,6 +1834,9 @@ WNDCLASSEXW
WNDCLASSW
WNDPROC
workarounds
+WORKSPACESEDITOR
+WORKSPACESLAUNCHER
+WORKSPACESSNAPSHOTTOOL
wox
wparam
wpf
diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json
index 03a30fbe8d..b873c42b90 100644
--- a/.pipelines/ESRPSigning_core.json
+++ b/.pipelines/ESRPSigning_core.json
@@ -189,6 +189,14 @@
"WinUI3Apps\\PowerToys.PowerRenameContextMenu.dll",
"WinUI3Apps\\PowerRenameContextMenuPackage.msix",
+ "PowerToys.WorkspacesSnapshotTool.exe",
+ "PowerToys.WorkspacesLauncher.exe",
+ "PowerToys.WorkspacesEditor.exe",
+ "PowerToys.WorkspacesEditor.dll",
+ "PowerToys.WorkspacesLauncherUI.exe",
+ "PowerToys.WorkspacesLauncherUI.dll",
+ "PowerToys.WorkspacesModuleInterface.dll",
+
"WinUI3Apps\\PowerToys.RegistryPreviewExt.dll",
"WinUI3Apps\\PowerToys.RegistryPreviewUILib.dll",
"WinUI3Apps\\PowerToys.RegistryPreview.dll",
diff --git a/PowerToys.sln b/PowerToys.sln
index d2b3e1c00c..d6c33873e2 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -171,14 +171,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
src\.editorconfig = src\.editorconfig
.vsconfig = .vsconfig
+ src\Common.Dotnet.CsWinRT.props = src\Common.Dotnet.CsWinRT.props
+ src\Common.SelfContained.props = src\Common.SelfContained.props
Cpp.Build.props = Cpp.Build.props
Directory.Build.props = Directory.Build.props
Directory.Build.targets = Directory.Build.targets
Directory.Packages.props = Directory.Packages.props
Solution.props = Solution.props
src\Version.props = src\Version.props
- src\Common.SelfContained.props = src\Common.SelfContained.props
- src\Common.Dotnet.CsWinRT.props = src\Common.Dotnet.CsWinRT.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Settings.UI.Library", "src\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj", "{B1BCC8C6-46B5-4BFA-8F22-20F32D99EC6A}"
@@ -277,6 +277,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "utils", "utils", "{B39DC643
src\common\utils\modulesRegistry.h = src\common\utils\modulesRegistry.h
src\common\utils\MsiUtils.h = src\common\utils\MsiUtils.h
src\common\utils\MsWindowsSettings.h = src\common\utils\MsWindowsSettings.h
+ src\common\utils\OnThreadExecutor.h = src\common\utils\OnThreadExecutor.h
src\common\utils\os-detect.h = src\common\utils\os-detect.h
src\common\utils\package.h = src\common\utils\package.h
src\common\utils\ProcessWaiter.h = src\common\utils\ProcessWaiter.h
@@ -456,7 +457,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FileLocksmithExt", "src\mod
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileLocksmithUI", "src\modules\FileLocksmith\FileLocksmithUI\FileLocksmithUI.csproj", "{E69B044A-2F8A-45AA-AD0B-256C59421807}"
EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FileLocksmithLibInterop", "src\modules\FileLocksmith\FileLocksmithLibInterop\FileLocksmithLibInterop.vcxproj", "{C604B37E-9D0E-4484-8778-E8B31B0E1B3A}"
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerToys.FileLocksmithLib.Interop", "src\modules\FileLocksmith\FileLocksmithLibInterop\FileLocksmithLibInterop.vcxproj", "{C604B37E-9D0E-4484-8778-E8B31B0E1B3A}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GPOWrapper", "src\common\GPOWrapper\GPOWrapper.vcxproj", "{E599C30B-9DC8-4E5A-BF27-93D4CCEDE788}"
EndProject
@@ -583,6 +584,37 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerToys.Settings.DSC.Sche
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerToys.Interop", "src\common\interop\PowerToys.Interop.vcxproj", "{F055103B-F80B-4D0C-BF48-057C55620033}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workspaces", "Workspaces", "{A2221D7E-55E7-4BEA-90D1-4F162D670BBF}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workspaces-common", "workspaces-common", "{BE126CBB-AE12-406A-9837-A05ACFCA57A7}"
+ ProjectSection(SolutionItems) = preProject
+ src\modules\Workspaces\workspaces-common\GuidUtils.h = src\modules\Workspaces\workspaces-common\GuidUtils.h
+ src\modules\Workspaces\workspaces-common\InvokePoint.h = src\modules\Workspaces\workspaces-common\InvokePoint.h
+ src\modules\Workspaces\workspaces-common\MonitorEnumerator.h = src\modules\Workspaces\workspaces-common\MonitorEnumerator.h
+ src\modules\Workspaces\workspaces-common\MonitorUtils.h = src\modules\Workspaces\workspaces-common\MonitorUtils.h
+ src\modules\Workspaces\workspaces-common\VirtualDesktop.h = src\modules\Workspaces\workspaces-common\VirtualDesktop.h
+ src\modules\Workspaces\workspaces-common\WindowEnumerator.h = src\modules\Workspaces\workspaces-common\WindowEnumerator.h
+ src\modules\Workspaces\workspaces-common\WindowFilter.h = src\modules\Workspaces\workspaces-common\WindowFilter.h
+ src\modules\Workspaces\workspaces-common\WindowUtils.h = src\modules\Workspaces\workspaces-common\WindowUtils.h
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WindowProperties", "WindowProperties", "{14CB58B7-D280-4A7A-95DE-4B2DF14EA000}"
+ ProjectSection(SolutionItems) = preProject
+ src\modules\Workspaces\WindowProperties\WorkspacesWindowPropertyUtils.h = src\modules\Workspaces\WindowProperties\WorkspacesWindowPropertyUtils.h
+ EndProjectSection
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesLib", "src\modules\Workspaces\WorkspacesLib\WorkspacesLib.vcxproj", "{B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkspacesLauncherUI", "src\modules\Workspaces\WorkspacesLauncherUI\WorkspacesLauncherUI.csproj", "{9C53CC25-0623-4569-95BC-B05410675EE3}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesModuleInterface", "src\modules\Workspaces\WorkspacesModuleInterface\WorkspacesModuleInterface.vcxproj", "{45285DF2-9742-4ECA-9AC9-58951FC26489}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesSnapshotTool", "src\modules\Workspaces\WorkspacesSnapshotTool\WorkspacesSnapshotTool.vcxproj", "{3D63307B-9D27-44FD-B033-B26F39245B85}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkspacesEditor", "src\modules\Workspaces\WorkspacesEditor\WorkspacesEditor.csproj", "{367D7543-7DBA-4381-99F1-BF6142A996C4}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesLauncher", "src\modules\Workspaces\WorkspacesLauncher\WorkspacesLauncher.vcxproj", "{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2601,6 +2633,78 @@ Global
{F055103B-F80B-4D0C-BF48-057C55620033}.Release|x64.Build.0 = Release|x64
{F055103B-F80B-4D0C-BF48-057C55620033}.Release|x86.ActiveCfg = Release|x64
{F055103B-F80B-4D0C-BF48-057C55620033}.Release|x86.Build.0 = Release|x64
+ {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Debug|ARM64.Build.0 = Debug|ARM64
+ {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Debug|x64.ActiveCfg = Debug|x64
+ {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Debug|x64.Build.0 = Debug|x64
+ {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Debug|x86.ActiveCfg = Debug|x64
+ {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Debug|x86.Build.0 = Debug|x64
+ {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Release|ARM64.ActiveCfg = Release|ARM64
+ {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Release|ARM64.Build.0 = Release|ARM64
+ {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Release|x64.ActiveCfg = Release|x64
+ {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Release|x64.Build.0 = Release|x64
+ {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Release|x86.ActiveCfg = Release|x64
+ {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Release|x86.Build.0 = Release|x64
+ {9C53CC25-0623-4569-95BC-B05410675EE3}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {9C53CC25-0623-4569-95BC-B05410675EE3}.Debug|ARM64.Build.0 = Debug|ARM64
+ {9C53CC25-0623-4569-95BC-B05410675EE3}.Debug|x64.ActiveCfg = Debug|x64
+ {9C53CC25-0623-4569-95BC-B05410675EE3}.Debug|x64.Build.0 = Debug|x64
+ {9C53CC25-0623-4569-95BC-B05410675EE3}.Debug|x86.ActiveCfg = Debug|x64
+ {9C53CC25-0623-4569-95BC-B05410675EE3}.Debug|x86.Build.0 = Debug|x64
+ {9C53CC25-0623-4569-95BC-B05410675EE3}.Release|ARM64.ActiveCfg = Release|ARM64
+ {9C53CC25-0623-4569-95BC-B05410675EE3}.Release|ARM64.Build.0 = Release|ARM64
+ {9C53CC25-0623-4569-95BC-B05410675EE3}.Release|x64.ActiveCfg = Release|x64
+ {9C53CC25-0623-4569-95BC-B05410675EE3}.Release|x64.Build.0 = Release|x64
+ {9C53CC25-0623-4569-95BC-B05410675EE3}.Release|x86.ActiveCfg = Release|x64
+ {9C53CC25-0623-4569-95BC-B05410675EE3}.Release|x86.Build.0 = Release|x64
+ {45285DF2-9742-4ECA-9AC9-58951FC26489}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {45285DF2-9742-4ECA-9AC9-58951FC26489}.Debug|ARM64.Build.0 = Debug|ARM64
+ {45285DF2-9742-4ECA-9AC9-58951FC26489}.Debug|x64.ActiveCfg = Debug|x64
+ {45285DF2-9742-4ECA-9AC9-58951FC26489}.Debug|x64.Build.0 = Debug|x64
+ {45285DF2-9742-4ECA-9AC9-58951FC26489}.Debug|x86.ActiveCfg = Debug|x64
+ {45285DF2-9742-4ECA-9AC9-58951FC26489}.Debug|x86.Build.0 = Debug|x64
+ {45285DF2-9742-4ECA-9AC9-58951FC26489}.Release|ARM64.ActiveCfg = Release|ARM64
+ {45285DF2-9742-4ECA-9AC9-58951FC26489}.Release|ARM64.Build.0 = Release|ARM64
+ {45285DF2-9742-4ECA-9AC9-58951FC26489}.Release|x64.ActiveCfg = Release|x64
+ {45285DF2-9742-4ECA-9AC9-58951FC26489}.Release|x64.Build.0 = Release|x64
+ {45285DF2-9742-4ECA-9AC9-58951FC26489}.Release|x86.ActiveCfg = Release|x64
+ {45285DF2-9742-4ECA-9AC9-58951FC26489}.Release|x86.Build.0 = Release|x64
+ {3D63307B-9D27-44FD-B033-B26F39245B85}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {3D63307B-9D27-44FD-B033-B26F39245B85}.Debug|ARM64.Build.0 = Debug|ARM64
+ {3D63307B-9D27-44FD-B033-B26F39245B85}.Debug|x64.ActiveCfg = Debug|x64
+ {3D63307B-9D27-44FD-B033-B26F39245B85}.Debug|x64.Build.0 = Debug|x64
+ {3D63307B-9D27-44FD-B033-B26F39245B85}.Debug|x86.ActiveCfg = Debug|x64
+ {3D63307B-9D27-44FD-B033-B26F39245B85}.Debug|x86.Build.0 = Debug|x64
+ {3D63307B-9D27-44FD-B033-B26F39245B85}.Release|ARM64.ActiveCfg = Release|ARM64
+ {3D63307B-9D27-44FD-B033-B26F39245B85}.Release|ARM64.Build.0 = Release|ARM64
+ {3D63307B-9D27-44FD-B033-B26F39245B85}.Release|x64.ActiveCfg = Release|x64
+ {3D63307B-9D27-44FD-B033-B26F39245B85}.Release|x64.Build.0 = Release|x64
+ {3D63307B-9D27-44FD-B033-B26F39245B85}.Release|x86.ActiveCfg = Release|x64
+ {3D63307B-9D27-44FD-B033-B26F39245B85}.Release|x86.Build.0 = Release|x64
+ {367D7543-7DBA-4381-99F1-BF6142A996C4}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {367D7543-7DBA-4381-99F1-BF6142A996C4}.Debug|ARM64.Build.0 = Debug|ARM64
+ {367D7543-7DBA-4381-99F1-BF6142A996C4}.Debug|x64.ActiveCfg = Debug|x64
+ {367D7543-7DBA-4381-99F1-BF6142A996C4}.Debug|x64.Build.0 = Debug|x64
+ {367D7543-7DBA-4381-99F1-BF6142A996C4}.Debug|x86.ActiveCfg = Debug|x64
+ {367D7543-7DBA-4381-99F1-BF6142A996C4}.Debug|x86.Build.0 = Debug|x64
+ {367D7543-7DBA-4381-99F1-BF6142A996C4}.Release|ARM64.ActiveCfg = Release|ARM64
+ {367D7543-7DBA-4381-99F1-BF6142A996C4}.Release|ARM64.Build.0 = Release|ARM64
+ {367D7543-7DBA-4381-99F1-BF6142A996C4}.Release|x64.ActiveCfg = Release|x64
+ {367D7543-7DBA-4381-99F1-BF6142A996C4}.Release|x64.Build.0 = Release|x64
+ {367D7543-7DBA-4381-99F1-BF6142A996C4}.Release|x86.ActiveCfg = Release|x64
+ {367D7543-7DBA-4381-99F1-BF6142A996C4}.Release|x86.Build.0 = Release|x64
+ {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Debug|ARM64.Build.0 = Debug|ARM64
+ {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Debug|x64.ActiveCfg = Debug|x64
+ {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Debug|x64.Build.0 = Debug|x64
+ {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Debug|x86.ActiveCfg = Debug|x64
+ {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Debug|x86.Build.0 = Debug|x64
+ {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|ARM64.ActiveCfg = Release|ARM64
+ {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|ARM64.Build.0 = Release|ARM64
+ {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x64.ActiveCfg = Release|x64
+ {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x64.Build.0 = Release|x64
+ {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x86.ActiveCfg = Release|x64
+ {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x86.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2817,6 +2921,15 @@ Global
{C0974915-8A1D-4BF0-977B-9587D3807AB7} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
{1D6893CB-BC0C-46A8-A76C-9728706CA51A} = {557C4636-D7E1-4838-A504-7D19B725EE95}
{F055103B-F80B-4D0C-BF48-057C55620033} = {5A7818A8-109C-4E1C-850D-1A654E234B0E}
+ {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
+ {BE126CBB-AE12-406A-9837-A05ACFCA57A7} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
+ {14CB58B7-D280-4A7A-95DE-4B2DF14EA000} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
+ {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
+ {9C53CC25-0623-4569-95BC-B05410675EE3} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
+ {45285DF2-9742-4ECA-9AC9-58951FC26489} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
+ {3D63307B-9D27-44FD-B033-B26F39245B85} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
+ {367D7543-7DBA-4381-99F1-BF6142A996C4} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
+ {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
diff --git a/doc/images/icons/Workspaces.png b/doc/images/icons/Workspaces.png
new file mode 100644
index 0000000000..18f13f1c0c
Binary files /dev/null and b/doc/images/icons/Workspaces.png differ
diff --git a/installer/PowerToysSetup/Common.wxi b/installer/PowerToysSetup/Common.wxi
index 4d64a0c63c..f036b3797b 100644
--- a/installer/PowerToysSetup/Common.wxi
+++ b/installer/PowerToysSetup/Common.wxi
@@ -18,6 +18,7 @@
+
diff --git a/installer/PowerToysSetup/Resources.wxs b/installer/PowerToysSetup/Resources.wxs
index c4bf93d59b..0f4e10f4a6 100644
--- a/installer/PowerToysSetup/Resources.wxs
+++ b/installer/PowerToysSetup/Resources.wxs
@@ -449,6 +449,15 @@
+
+
+
+
+
+
diff --git a/installer/PowerToysSetupCustomActions/CustomAction.cpp b/installer/PowerToysSetupCustomActions/CustomAction.cpp
index a00b6bdad7..6e931e4749 100644
--- a/installer/PowerToysSetupCustomActions/CustomAction.cpp
+++ b/installer/PowerToysSetupCustomActions/CustomAction.cpp
@@ -1223,7 +1223,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
}
processes.resize(bytes / sizeof(processes[0]));
- std::array processesToTerminate = {
+ std::array processesToTerminate = {
L"PowerToys.PowerLauncher.exe",
L"PowerToys.Settings.exe",
L"PowerToys.AdvancedPaste.exe",
@@ -1255,6 +1255,10 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
L"PowerToys.MouseWithoutBordersService.exe",
L"PowerToys.CropAndLock.exe",
L"PowerToys.EnvironmentVariables.exe",
+ L"PowerToys.WorkspacesSnapshotTool.exe",
+ L"PowerToys.WorkspacesLauncher.exe",
+ L"PowerToys.WorkspacesLauncherUI.exe",
+ L"PowerToys.WorkspacesEditor.exe",
L"PowerToys.exe",
};
diff --git a/src/common/Common.UI/SettingsDeepLink.cs b/src/common/Common.UI/SettingsDeepLink.cs
index c3a81a49a1..41bd2c012e 100644
--- a/src/common/Common.UI/SettingsDeepLink.cs
+++ b/src/common/Common.UI/SettingsDeepLink.cs
@@ -31,6 +31,7 @@ namespace Common.UI
EnvironmentVariables,
Dashboard,
AdvancedPaste,
+ Workspaces,
}
private static string SettingsWindowNameToString(SettingsWindow value)
@@ -77,6 +78,8 @@ namespace Common.UI
return "Dashboard";
case SettingsWindow.AdvancedPaste:
return "AdvancedPaste";
+ case SettingsWindow.Workspaces:
+ return "Workspaces";
default:
{
return string.Empty;
diff --git a/src/common/Display/Display.vcxproj b/src/common/Display/Display.vcxproj
index e69a2ada2d..87b74ba534 100644
--- a/src/common/Display/Display.vcxproj
+++ b/src/common/Display/Display.vcxproj
@@ -24,15 +24,18 @@
NotUsing
- ..\..\..\;%(AdditionalIncludeDirectories)
+ ..\..\..\;..\..\common;.\;%(AdditionalIncludeDirectories)
_LIB;%(PreprocessorDefinitions)
+
+
+
diff --git a/src/common/Display/DisplayUtils.cpp b/src/common/Display/DisplayUtils.cpp
new file mode 100644
index 0000000000..85f9c4b1fc
--- /dev/null
+++ b/src/common/Display/DisplayUtils.cpp
@@ -0,0 +1,143 @@
+#include "DisplayUtils.h"
+
+#include
+#include
+#include
+
+#include
+#include
+
+#include
+
+namespace DisplayUtils
+{
+ std::wstring remove_non_digits(const std::wstring& input)
+ {
+ std::wstring result;
+ std::copy_if(input.begin(), input.end(), std::back_inserter(result), [](wchar_t ch) { return std::iswdigit(ch); });
+ return result;
+ }
+
+ std::pair SplitDisplayDeviceId(const std::wstring& str) noexcept
+ {
+ // format: \\?\DISPLAY#{device id}#{instance id}#{some other id}
+ // example: \\?\DISPLAY#GSM1388#4&125707d6&0&UID8388688#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}
+ // output: { GSM1388, 4&125707d6&0&UID8388688 }
+
+ size_t nameStartPos = str.find_first_of('#');
+ size_t uidStartPos = str.find('#', nameStartPos + 1);
+ size_t uidEndPos = str.find('#', uidStartPos + 1);
+
+ if (nameStartPos == std::string::npos || uidStartPos == std::string::npos || uidEndPos == std::string::npos)
+ {
+ return { str, L"" };
+ }
+
+ return { str.substr(nameStartPos + 1, uidStartPos - nameStartPos - 1), str.substr(uidStartPos + 1, uidEndPos - uidStartPos - 1) };
+ }
+
+ std::pair> GetDisplays()
+ {
+ bool success = true;
+ std::vector result{};
+ auto allMonitors = MonitorEnumerator::Enumerate();
+
+ OnThreadExecutor dpiUnawareThread;
+ dpiUnawareThread.submit(OnThreadExecutor::task_t{ [&] {
+ SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);
+ SetThreadDpiHostingBehavior(DPI_HOSTING_BEHAVIOR_MIXED);
+ } }).wait();
+
+ for (auto& monitorData : allMonitors)
+ {
+ MONITORINFOEX monitorInfo = monitorData.second;
+ MONITORINFOEX dpiUnawareMonitorInfo{};
+
+ dpiUnawareThread.submit(OnThreadExecutor::task_t{ [&] {
+ dpiUnawareMonitorInfo.cbSize = sizeof(dpiUnawareMonitorInfo);
+ if (!GetMonitorInfo(monitorData.first, &dpiUnawareMonitorInfo))
+ {
+ return;
+ }
+ } }).wait();
+
+ UINT dpi = 0;
+ if (DPIAware::GetScreenDPIForMonitor(monitorData.first, dpi) != S_OK)
+ {
+ success = false;
+ break;
+ }
+
+ DisplayUtils::DisplayData data{
+ .monitor = monitorData.first,
+ .dpi = dpi,
+ .monitorRectDpiAware = monitorInfo.rcMonitor,
+ .monitorRectDpiUnaware = dpiUnawareMonitorInfo.rcMonitor,
+ };
+
+ bool foundActiveMonitor = false;
+ DISPLAY_DEVICE displayDevice{ .cb = sizeof(displayDevice) };
+ DWORD displayDeviceIndex = 0;
+ while (EnumDisplayDevicesW(monitorInfo.szDevice, displayDeviceIndex, &displayDevice, EDD_GET_DEVICE_INTERFACE_NAME))
+ {
+ /*
+ * if (WI_IsFlagSet(displayDevice.StateFlags, DISPLAY_DEVICE_ACTIVE) &&
+ WI_IsFlagClear(displayDevice.StateFlags, DISPLAY_DEVICE_MIRRORING_DRIVER))
+ */
+ if (((displayDevice.StateFlags & DISPLAY_DEVICE_ACTIVE) == DISPLAY_DEVICE_ACTIVE) &&
+ (displayDevice.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER) == 0)
+ {
+ // Find display devices associated with the display.
+ foundActiveMonitor = true;
+ break;
+ }
+
+ displayDeviceIndex++;
+ }
+
+ if (foundActiveMonitor)
+ {
+ auto deviceId = SplitDisplayDeviceId(displayDevice.DeviceID);
+ data.id = deviceId.first;
+ data.instanceId = deviceId.second;
+ try
+ {
+ std::wstring numberStr = displayDevice.DeviceName; // \\.\DISPLAY1\Monitor0
+ numberStr = numberStr.substr(0, numberStr.find_last_of('\\')); // \\.\DISPLAY1
+ numberStr = remove_non_digits(numberStr);
+ data.number = std::stoi(numberStr);
+ }
+ catch (...)
+ {
+ success = false;
+ break;
+ }
+ }
+ else
+ {
+ success = false;
+
+ // Use the display name as a fallback value when no proper device was found.
+ data.id = monitorInfo.szDevice;
+ data.instanceId = L"";
+
+ try
+ {
+ std::wstring numberStr = monitorInfo.szDevice; // \\.\DISPLAY1
+ numberStr = remove_non_digits(numberStr);
+ data.number = std::stoi(numberStr);
+ }
+ catch (...)
+ {
+ success = false;
+ break;
+ }
+ }
+
+ result.push_back(data);
+ }
+
+ return { success, result };
+ }
+
+}
diff --git a/src/common/Display/DisplayUtils.h b/src/common/Display/DisplayUtils.h
new file mode 100644
index 0000000000..0b0e86e227
--- /dev/null
+++ b/src/common/Display/DisplayUtils.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include
+#include
+#include
+
+namespace DisplayUtils
+{
+ struct DisplayData
+ {
+ HMONITOR monitor{};
+ std::wstring id;
+ std::wstring instanceId;
+ unsigned int number{};
+ unsigned int dpi{};
+ RECT monitorRectDpiAware{};
+ RECT monitorRectDpiUnaware{};
+ };
+
+ std::pair> GetDisplays();
+};
diff --git a/src/common/Display/MonitorEnumerator.h b/src/common/Display/MonitorEnumerator.h
new file mode 100644
index 0000000000..c603bfee6d
--- /dev/null
+++ b/src/common/Display/MonitorEnumerator.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include
+#include
+#include
+
+class MonitorEnumerator
+{
+public:
+ static std::vector> Enumerate()
+ {
+ MonitorEnumerator inst;
+ EnumDisplayMonitors(NULL, NULL, Callback, reinterpret_cast(&inst));
+ return inst.m_monitors;
+ }
+
+private:
+ MonitorEnumerator() = default;
+ ~MonitorEnumerator() = default;
+
+ static BOOL CALLBACK Callback(HMONITOR monitor, HDC /*hdc*/, LPRECT /*pRect*/, LPARAM param)
+ {
+ MonitorEnumerator* inst = reinterpret_cast(param);
+ MONITORINFOEX mi;
+ mi.cbSize = sizeof(mi);
+ if (GetMonitorInfo(monitor, &mi))
+ {
+ inst->m_monitors.push_back({monitor, mi});
+ }
+
+ return TRUE;
+ }
+
+ std::vector> m_monitors;
+};
\ No newline at end of file
diff --git a/src/common/Display/dpi_aware.cpp b/src/common/Display/dpi_aware.cpp
index 2ed0228ae3..8397430c6d 100644
--- a/src/common/Display/dpi_aware.cpp
+++ b/src/common/Display/dpi_aware.cpp
@@ -1,7 +1,9 @@
#include "dpi_aware.h"
+
#include "monitors.h"
#include
#include
+#include
namespace DPIAware
{
@@ -60,6 +62,24 @@ namespace DPIAware
}
}
+ void Convert(HMONITOR monitor_handle, RECT& rect)
+ {
+ if (monitor_handle == NULL)
+ {
+ const POINT ptZero = { 0, 0 };
+ monitor_handle = MonitorFromPoint(ptZero, MONITOR_DEFAULTTOPRIMARY);
+ }
+
+ UINT dpi_x, dpi_y;
+ if (GetDpiForMonitor(monitor_handle, MDT_EFFECTIVE_DPI, &dpi_x, &dpi_y) == S_OK)
+ {
+ rect.left = static_cast(std::round(rect.left * static_cast(dpi_x) / DEFAULT_DPI));
+ rect.right = static_cast(std::round(rect.right * static_cast(dpi_x) / DEFAULT_DPI));
+ rect.top = static_cast(std::round(rect.top * static_cast(dpi_y) / DEFAULT_DPI));
+ rect.bottom = static_cast(std::round(rect.bottom * static_cast(dpi_y) / DEFAULT_DPI));
+ }
+ }
+
void ConvertByCursorPosition(float& width, float& height)
{
HMONITOR targetMonitor = nullptr;
diff --git a/src/common/Display/dpi_aware.h b/src/common/Display/dpi_aware.h
index f93e8f87ad..a63365aa2f 100644
--- a/src/common/Display/dpi_aware.h
+++ b/src/common/Display/dpi_aware.h
@@ -12,6 +12,7 @@ namespace DPIAware
HRESULT GetScreenDPIForPoint(POINT p, UINT& dpi);
HRESULT GetScreenDPIForCursor(UINT& dpi);
void Convert(HMONITOR monitor_handle, float& width, float& height);
+ void Convert(HMONITOR monitor_handle, RECT& rect);
void ConvertByCursorPosition(float& width, float& height);
void InverseConvert(HMONITOR monitor_handle, float& width, float& height);
void EnableDPIAwarenessForThisProcess();
diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp
index de987ed81e..08c896d0f3 100644
--- a/src/common/GPOWrapper/GPOWrapper.cpp
+++ b/src/common/GPOWrapper/GPOWrapper.cpp
@@ -1,4 +1,4 @@
-#include "pch.h"
+#include "pch.h"
#include "GPOWrapper.h"
#include "GPOWrapper.g.cpp"
@@ -176,6 +176,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast(powertoys_gpo::getAllowedAdvancedPasteOnlineAIModelsValue());
}
+ GpoRuleConfigured GPOWrapper::GetConfiguredWorkspacesEnabledValue()
+ {
+ return static_cast(powertoys_gpo::getConfiguredWorkspacesEnabledValue());
+ }
GpoRuleConfigured GPOWrapper::GetConfiguredMwbClipboardSharingEnabledValue()
{
return static_cast(powertoys_gpo::getConfiguredMwbClipboardSharingEnabledValue());
diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h
index 670124527b..b0392cea1e 100644
--- a/src/common/GPOWrapper/GPOWrapper.h
+++ b/src/common/GPOWrapper/GPOWrapper.h
@@ -1,4 +1,4 @@
-#pragma once
+#pragma once
#include "GPOWrapper.g.h"
#include
@@ -50,6 +50,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
static GpoRuleConfigured GetConfiguredQoiPreviewEnabledValue();
static GpoRuleConfigured GetConfiguredQoiThumbnailsEnabledValue();
static GpoRuleConfigured GetAllowedAdvancedPasteOnlineAIModelsValue();
+ static GpoRuleConfigured GetConfiguredWorkspacesEnabledValue();
static GpoRuleConfigured GetConfiguredMwbClipboardSharingEnabledValue();
static GpoRuleConfigured GetConfiguredMwbFileTransferEnabledValue();
static GpoRuleConfigured GetConfiguredMwbUseOriginalUserInterfaceValue();
diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl
index 27c2420b6e..81ff61121a 100644
--- a/src/common/GPOWrapper/GPOWrapper.idl
+++ b/src/common/GPOWrapper/GPOWrapper.idl
@@ -54,6 +54,7 @@ namespace PowerToys
static GpoRuleConfigured GetConfiguredQoiPreviewEnabledValue();
static GpoRuleConfigured GetConfiguredQoiThumbnailsEnabledValue();
static GpoRuleConfigured GetAllowedAdvancedPasteOnlineAIModelsValue();
+ static GpoRuleConfigured GetConfiguredWorkspacesEnabledValue();
static GpoRuleConfigured GetConfiguredMwbClipboardSharingEnabledValue();
static GpoRuleConfigured GetConfiguredMwbFileTransferEnabledValue();
static GpoRuleConfigured GetConfiguredMwbUseOriginalUserInterfaceValue();
diff --git a/src/common/GPOWrapperProjection/GPOWrapper.cs b/src/common/GPOWrapperProjection/GPOWrapper.cs
index f0c0b8421c..6cb91a69ac 100644
--- a/src/common/GPOWrapperProjection/GPOWrapper.cs
+++ b/src/common/GPOWrapperProjection/GPOWrapper.cs
@@ -61,5 +61,10 @@ namespace PowerToys.GPOWrapperProjection
{
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetRunPluginEnabledValue(pluginID);
}
+
+ public static GpoRuleConfigured GetConfiguredWorkspacesEnabledValue()
+ {
+ return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredWorkspacesEnabledValue();
+ }
}
}
diff --git a/src/common/ManagedCommon/ModuleType.cs b/src/common/ManagedCommon/ModuleType.cs
index de57f5138c..6f15bc3a15 100644
--- a/src/common/ManagedCommon/ModuleType.cs
+++ b/src/common/ManagedCommon/ModuleType.cs
@@ -30,5 +30,6 @@ namespace ManagedCommon
MeasureTool,
ShortcutGuide,
PowerOCR,
+ Workspaces,
}
}
diff --git a/src/common/interop/Constants.cpp b/src/common/interop/Constants.cpp
index 30969a75b4..63e9782346 100644
--- a/src/common/interop/Constants.cpp
+++ b/src/common/interop/Constants.cpp
@@ -147,4 +147,8 @@ namespace winrt::PowerToys::Interop::implementation
{
return CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT;
}
+ hstring Constants::WorkspacesLaunchEditorEvent()
+ {
+ return CommonSharedConstants::WORKSPACES_LAUNCH_EDITOR_EVENT;
+ }
}
diff --git a/src/common/interop/Constants.h b/src/common/interop/Constants.h
index cc8ebc01b1..978ca8ab60 100644
--- a/src/common/interop/Constants.h
+++ b/src/common/interop/Constants.h
@@ -40,6 +40,7 @@ namespace winrt::PowerToys::Interop::implementation
static hstring CropAndLockReparentEvent();
static hstring ShowEnvironmentVariablesSharedEvent();
static hstring ShowEnvironmentVariablesAdminSharedEvent();
+ static hstring WorkspacesLaunchEditorEvent();
};
}
diff --git a/src/common/interop/Constants.idl b/src/common/interop/Constants.idl
index 72d9fc58a0..4c4125b7db 100644
--- a/src/common/interop/Constants.idl
+++ b/src/common/interop/Constants.idl
@@ -37,6 +37,7 @@ namespace PowerToys
static String CropAndLockReparentEvent();
static String ShowEnvironmentVariablesSharedEvent();
static String ShowEnvironmentVariablesAdminSharedEvent();
+ static String WorkspacesLaunchEditorEvent();
}
}
}
\ No newline at end of file
diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h
index c5d44524c9..5237c15737 100644
--- a/src/common/interop/shared_constants.h
+++ b/src/common/interop/shared_constants.h
@@ -43,6 +43,8 @@ namespace CommonSharedConstants
const wchar_t FANCY_ZONES_EDITOR_TOGGLE_EVENT[] = L"Local\\FancyZones-ToggleEditorEvent-1e174338-06a3-472b-874d-073b21c62f14";
+ const wchar_t WORKSPACES_LAUNCH_EDITOR_EVENT[] = L"Local\\Workspaces-LaunchEditorEvent-a55ff427-cf62-4994-a2cd-9f72139296bf";
+
const wchar_t SHOW_HOSTS_EVENT[] = L"Local\\Hosts-ShowHostsEvent-5a0c0aae-5ff5-40f5-95c2-20e37ed671f0";
const wchar_t SHOW_HOSTS_ADMIN_EVENT[] = L"Local\\Hosts-ShowHostsAdminEvent-60ff44e2-efd3-43bf-928a-f4d269f98bec";
@@ -98,6 +100,8 @@ namespace CommonSharedConstants
const wchar_t SHOW_ENVIRONMENT_VARIABLES_EVENT[] = L"Local\\PowerToysEnvironmentVariables-ShowEnvironmentVariablesEvent-1021f616-e951-4d64-b231-a8f972159978";
const wchar_t SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT[] = L"Local\\PowerToysEnvironmentVariables-EnvironmentVariablesAdminEvent-8c95d2ad-047c-49a2-9e8b-b4656326cfb2";
+ const wchar_t WORKSPACES_EXIT_EVENT[] = L"Local\\PowerToys-Workspaces-ExitEvent-29a1566f-f4f8-4d56-9435-d2a437f727c6";
+
// Max DWORD for key code to disable keys.
const DWORD VK_DISABLED = 0x100;
}
diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h
index cc1b3825ce..345286a38b 100644
--- a/src/common/logger/logger_settings.h
+++ b/src/common/logger/logger_settings.h
@@ -69,6 +69,10 @@ struct LogSettings
inline const static std::string environmentVariablesLoggerName = "environment-variables";
inline const static std::wstring cmdNotFoundLogPath = L"Logs\\cmd-not-found-log.txt";
inline const static std::string cmdNotFoundLoggerName = "cmd-not-found";
+ inline const static std::string workspacesLauncherLoggerName = "workspaces-launcher";
+ inline const static std::wstring workspacesLauncherLogPath = L"workspaces-launcher-log.txt";
+ inline const static std::string workspacesSnapshotToolLoggerName = "workspaces-snapshot-tool";
+ inline const static std::wstring workspacesSnapshotToolLogPath = L"workspaces-snapshot-tool-log.txt";
inline const static int retention = 30;
std::wstring logLevel;
LogSettings();
diff --git a/src/common/utils/OnThreadExecutor.h b/src/common/utils/OnThreadExecutor.h
new file mode 100644
index 0000000000..c361a33700
--- /dev/null
+++ b/src/common/utils/OnThreadExecutor.h
@@ -0,0 +1,72 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+// OnThreadExecutor allows its caller to off-load some work to a persistently running background thread.
+// This might come in handy if you use the API which sets thread-wide global state and the state needs
+// to be isolated.
+
+class OnThreadExecutor final
+{
+public:
+ using task_t = std::packaged_task;
+
+ OnThreadExecutor() :
+ _shutdown_request{ false },
+ _worker_thread{ [this] { worker_thread(); } }
+ {
+ }
+
+ ~OnThreadExecutor()
+ {
+ _shutdown_request = true;
+ _task_cv.notify_one();
+ _worker_thread.join();
+ }
+
+ std::future submit(task_t task)
+ {
+ auto future = task.get_future();
+ std::lock_guard lock{ _task_mutex };
+ _task_queue.emplace(std::move(task));
+ _task_cv.notify_one();
+ return future;
+ }
+
+ void cancel()
+ {
+ std::lock_guard lock{ _task_mutex };
+ _task_queue = {};
+ _task_cv.notify_one();
+ }
+
+private:
+ void worker_thread()
+ {
+ while (!_shutdown_request)
+ {
+ task_t task;
+ {
+ std::unique_lock task_lock{ _task_mutex };
+ _task_cv.wait(task_lock, [this] { return !_task_queue.empty() || _shutdown_request; });
+ if (_shutdown_request)
+ {
+ return;
+ }
+ task = std::move(_task_queue.front());
+ _task_queue.pop();
+ }
+ task();
+ }
+ }
+
+ std::mutex _task_mutex;
+ std::condition_variable _task_cv;
+ std::atomic_bool _shutdown_request;
+ std::queue> _task_queue;
+ std::thread _worker_thread;
+};
diff --git a/src/common/utils/gpo.h b/src/common/utils/gpo.h
index cba300d8e5..8f71a21205 100644
--- a/src/common/utils/gpo.h
+++ b/src/common/utils/gpo.h
@@ -60,6 +60,7 @@ namespace powertoys_gpo {
const std::wstring POLICY_CONFIGURE_ENABLED_ENVIRONMENT_VARIABLES = L"ConfigureEnabledUtilityEnvironmentVariables";
const std::wstring POLICY_CONFIGURE_ENABLED_QOI_PREVIEW = L"ConfigureEnabledUtilityFileExplorerQOIPreview";
const std::wstring POLICY_CONFIGURE_ENABLED_QOI_THUMBNAILS = L"ConfigureEnabledUtilityFileExplorerQOIThumbnails";
+ const std::wstring POLICY_CONFIGURE_ENABLED_WORKSPACES = L"ConfigureEnabledUtilityWorkspaces";
// The registry value names for PowerToys installer and update policies.
const std::wstring POLICY_DISABLE_PER_USER_INSTALLATION = L"PerUserInstallationDisabled";
@@ -399,6 +400,11 @@ namespace powertoys_gpo {
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_ADVANCED_PASTE);
}
+ inline gpo_rule_configured_t getConfiguredWorkspacesEnabledValue()
+ {
+ return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_WORKSPACES);
+ }
+
inline gpo_rule_configured_t getConfiguredVideoConferenceMuteEnabledValue()
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_VIDEO_CONFERENCE_MUTE);
diff --git a/src/dsc/Microsoft.PowerToys.Configure/examples/disableAllModules.dsc.yaml b/src/dsc/Microsoft.PowerToys.Configure/examples/disableAllModules.dsc.yaml
index 8fa39a1050..0cfc689316 100644
--- a/src/dsc/Microsoft.PowerToys.Configure/examples/disableAllModules.dsc.yaml
+++ b/src/dsc/Microsoft.PowerToys.Configure/examples/disableAllModules.dsc.yaml
@@ -57,6 +57,8 @@ properties:
EnableQoiThumbnail: false
PowerOcr:
Enabled: false
+ Workspaces:
+ Enabled: false
ShortcutGuide:
Enabled: false
VideoConference:
diff --git a/src/dsc/Microsoft.PowerToys.Configure/examples/enableAllModules.dsc.yaml b/src/dsc/Microsoft.PowerToys.Configure/examples/enableAllModules.dsc.yaml
index a7b69fd423..c3bd636177 100644
--- a/src/dsc/Microsoft.PowerToys.Configure/examples/enableAllModules.dsc.yaml
+++ b/src/dsc/Microsoft.PowerToys.Configure/examples/enableAllModules.dsc.yaml
@@ -57,6 +57,8 @@ properties:
EnableQoiThumbnail: true
PowerOcr:
Enabled: true
+ Workspaces:
+ Enabled: true
ShortcutGuide:
Enabled: true
VideoConference:
diff --git a/src/gpo/assets/PowerToys.admx b/src/gpo/assets/PowerToys.admx
index a0f70baa30..a79e6f627c 100644
--- a/src/gpo/assets/PowerToys.admx
+++ b/src/gpo/assets/PowerToys.admx
@@ -1,11 +1,11 @@
-
+
-
+
@@ -20,6 +20,7 @@
+
@@ -364,6 +365,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/gpo/assets/en-US/PowerToys.adml b/src/gpo/assets/en-US/PowerToys.adml
index 761abde90f..37f8124bd1 100644
--- a/src/gpo/assets/en-US/PowerToys.adml
+++ b/src/gpo/assets/en-US/PowerToys.adml
@@ -10,6 +10,7 @@
Installer and Updates
PowerToys Run
Advanced Paste
+ Workspaces
Mouse Without Borders
General settings
@@ -25,6 +26,7 @@
PowerToys version 0.81.0 or later
PowerToys version 0.81.1 or later
PowerToys version 0.83.0 or later
+ PowerToys version 0.84.0 or later
This policy configures the enabled state for all PowerToys utilities.
@@ -110,6 +112,12 @@ If you don't configure this setting, users are able to enable or disable the plu
You can override this policy for individual plugins using the policy "Configure enabled state for individual plugins".
Note: Changes require a restart of PowerToys Run.
+
+ This policy configures the enabled disable state for the Workspaces utility.
+
+If you enable or don't configure this policy, the user takes control over the enabled state of the Workspaces utility.
+
+If you disable this policy, the user won't be able to enable Enable and use the Workspaces utility.
With this policy you can configure an individual enabled state for each PowerToys Run plugin that you add to the list.
@@ -219,6 +227,7 @@ If you disable or don't configure this policy, no predefined rules are applied.
Peek: Configure enabled state
Power Rename: Configure enabled state
PowerToys Run: Configure enabled state
+ PowerToys Workspaces: Configure enabled state
Quick Accent: Configure enabled state
Registry Preview: Configure enabled state
Screen Ruler: Configure enabled state
diff --git a/src/modules/Workspaces/Assets/Workspaces.ico b/src/modules/Workspaces/Assets/Workspaces.ico
new file mode 100644
index 0000000000..14292758fa
Binary files /dev/null and b/src/modules/Workspaces/Assets/Workspaces.ico differ
diff --git a/src/modules/Workspaces/WindowProperties/WorkspacesWindowPropertyUtils.h b/src/modules/Workspaces/WindowProperties/WorkspacesWindowPropertyUtils.h
new file mode 100644
index 0000000000..1c1ddeb6f4
--- /dev/null
+++ b/src/modules/Workspaces/WindowProperties/WorkspacesWindowPropertyUtils.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include
+
+namespace WorkspacesWindowProperties
+{
+ namespace Properties
+ {
+ const wchar_t LaunchedByWorkspacesID[] = L"PowerToys_LaunchedByWorkspaces";
+ }
+
+ inline void StampWorkspacesLaunchedProperty(HWND window)
+ {
+ ::SetPropW(window, Properties::LaunchedByWorkspacesID, reinterpret_cast(1));
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/App.config b/src/modules/Workspaces/WorkspacesEditor/App.config
new file mode 100644
index 0000000000..e31368d227
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/App.config
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/Workspaces/WorkspacesEditor/App.xaml b/src/modules/Workspaces/WorkspacesEditor/App.xaml
new file mode 100644
index 0000000000..567a577389
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/App.xaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/Workspaces/WorkspacesEditor/App.xaml.cs b/src/modules/Workspaces/WorkspacesEditor/App.xaml.cs
new file mode 100644
index 0000000000..ca29a95e68
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/App.xaml.cs
@@ -0,0 +1,120 @@
+// 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 System.Windows;
+using Common.UI;
+using ManagedCommon;
+using WorkspacesEditor.Utils;
+using WorkspacesEditor.ViewModels;
+
+namespace WorkspacesEditor
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : Application, IDisposable
+ {
+ private static Mutex _instanceMutex;
+
+ public static WorkspacesEditorIO WorkspacesEditorIO { get; private set; }
+
+ private MainWindow _mainWindow;
+
+ private MainViewModel _mainViewModel;
+
+ public static ThemeManager ThemeManager { get; set; }
+
+ private bool _isDisposed;
+
+ public App()
+ {
+ WorkspacesEditorIO = new WorkspacesEditorIO();
+ }
+
+ private void OnStartup(object sender, StartupEventArgs e)
+ {
+ Logger.InitializeLogger("\\Workspaces\\Logs");
+ AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
+
+ const string appName = "Local\\PowerToys_Workspaces_Editor_InstanceMutex";
+ bool createdNew;
+ _instanceMutex = new Mutex(true, appName, out createdNew);
+ if (!createdNew)
+ {
+ Logger.LogWarning("Another instance of Workspaces Editor 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;
+ }
+
+ ThemeManager = new ThemeManager(this);
+
+ if (_mainViewModel == null)
+ {
+ _mainViewModel = new MainViewModel(WorkspacesEditorIO);
+ }
+
+ var parseResult = WorkspacesEditorIO.ParseWorkspaces(_mainViewModel);
+
+ // normal start of editor
+ if (_mainWindow == null)
+ {
+ _mainWindow = new MainWindow(_mainViewModel);
+ }
+
+ // reset main window owner to keep it on the top
+ _mainWindow.ShowActivated = true;
+ _mainWindow.Topmost = true;
+ _mainWindow.Show();
+
+ // we can reset topmost flag after it's opened
+ _mainWindow.Topmost = false;
+ }
+
+ 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)
+ {
+ ThemeManager?.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);
+ }
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Controls/ResetIsEnabled.cs b/src/modules/Workspaces/WorkspacesEditor/Controls/ResetIsEnabled.cs
new file mode 100644
index 0000000000..6e7ff138da
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/Controls/ResetIsEnabled.cs
@@ -0,0 +1,22 @@
+// 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 System.Windows.Controls;
+
+namespace WorkspacesEditor.Controls
+{
+ public class ResetIsEnabled : ContentControl
+ {
+ static ResetIsEnabled()
+ {
+ IsEnabledProperty.OverrideMetadata(
+ typeof(ResetIsEnabled),
+ new UIPropertyMetadata(
+ defaultValue: true,
+ propertyChangedCallback: (_, __) => { },
+ coerceValueCallback: (_, x) => x));
+ }
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Converters/BooleanToInvertedVisibilityConverter.cs b/src/modules/Workspaces/WorkspacesEditor/Converters/BooleanToInvertedVisibilityConverter.cs
new file mode 100644
index 0000000000..c950209da2
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/Converters/BooleanToInvertedVisibilityConverter.cs
@@ -0,0 +1,29 @@
+// 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 WorkspacesEditor.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();
+ }
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Data/InvokePoint.cs b/src/modules/Workspaces/WorkspacesEditor/Data/InvokePoint.cs
new file mode 100644
index 0000000000..fe41a65bd7
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/Data/InvokePoint.cs
@@ -0,0 +1,14 @@
+// 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.
+
+namespace WorkspacesEditor.Data
+{
+ /* sync with workspaces-common */
+ public enum InvokePoint
+ {
+ EditorButton = 0,
+ Shortcut,
+ LaunchAndEdit,
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Data/ProjectData.cs b/src/modules/Workspaces/WorkspacesEditor/Data/ProjectData.cs
new file mode 100644
index 0000000000..52c771118c
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/Data/ProjectData.cs
@@ -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.Collections.Generic;
+using Workspaces.Data;
+using static WorkspacesEditor.Data.ProjectData;
+
+namespace WorkspacesEditor.Data
+{
+ public class ProjectData : WorkspacesEditorData
+ {
+ public struct ApplicationWrapper
+ {
+ public struct WindowPositionWrapper
+ {
+ public int X { get; set; }
+
+ public int Y { get; set; }
+
+ public int Width { get; set; }
+
+ public int Height { get; set; }
+ }
+
+ public string Application { get; set; }
+
+ public string ApplicationPath { get; set; }
+
+ public string Title { get; set; }
+
+ public string PackageFullName { get; set; }
+
+ public string AppUserModelId { get; set; }
+
+ public string CommandLineArguments { get; set; }
+
+ public bool IsElevated { get; set; }
+
+ public bool CanLaunchElevated { get; set; }
+
+ public bool Minimized { get; set; }
+
+ public bool Maximized { get; set; }
+
+ public WindowPositionWrapper Position { get; set; }
+
+ public int Monitor { get; set; }
+ }
+
+ public struct MonitorConfigurationWrapper
+ {
+ public struct MonitorRectWrapper
+ {
+ public int Top { get; set; }
+
+ public int Left { get; set; }
+
+ public int Width { get; set; }
+
+ public int Height { get; set; }
+ }
+
+ public string Id { get; set; }
+
+ public string InstanceId { get; set; }
+
+ public int MonitorNumber { get; set; }
+
+ public int Dpi { get; set; }
+
+ public MonitorRectWrapper MonitorRectDpiAware { get; set; }
+
+ public MonitorRectWrapper MonitorRectDpiUnaware { get; set; }
+ }
+
+ public struct ProjectWrapper
+ {
+ public string Id { get; set; }
+
+ public string Name { get; set; }
+
+ public long CreationTime { get; set; }
+
+ public long LastLaunchedTime { get; set; }
+
+ public bool IsShortcutNeeded { get; set; }
+
+ public bool MoveExistingWindows { get; set; }
+
+ public List MonitorConfiguration { get; set; }
+
+ public List Applications { get; set; }
+ }
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Data/TempProjectData.cs b/src/modules/Workspaces/WorkspacesEditor/Data/TempProjectData.cs
new file mode 100644
index 0000000000..b3de2f5b4d
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/Data/TempProjectData.cs
@@ -0,0 +1,27 @@
+// 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 WorkspacesEditor.Utils;
+
+namespace WorkspacesEditor.Data
+{
+ public class TempProjectData : ProjectData
+ {
+ public static string File
+ {
+ get
+ {
+ return FolderUtils.DataFolder() + "\\temp-workspaces.json";
+ }
+ }
+
+ public static void DeleteTempFile()
+ {
+ if (System.IO.File.Exists(File))
+ {
+ System.IO.File.Delete(File);
+ }
+ }
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesData.cs b/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesData.cs
new file mode 100644
index 0000000000..f0e2940366
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesData.cs
@@ -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 System.Collections.Generic;
+using Workspaces.Data;
+using WorkspacesEditor.Utils;
+using static WorkspacesEditor.Data.ProjectData;
+using static WorkspacesEditor.Data.WorkspacesData;
+
+namespace WorkspacesEditor.Data
+{
+ public class WorkspacesData : WorkspacesEditorData
+ {
+ public string File
+ {
+ get
+ {
+ return FolderUtils.DataFolder() + "\\workspaces.json";
+ }
+ }
+
+ public struct WorkspacesListWrapper
+ {
+ public List Workspaces { get; set; }
+ }
+
+ public enum OrderBy
+ {
+ LastViewed = 0,
+ Created = 1,
+ Name = 2,
+ Unknown = 3,
+ }
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesEditorData`1.cs b/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesEditorData`1.cs
new file mode 100644
index 0000000000..3c0c2d8fb1
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesEditorData`1.cs
@@ -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 System.Text.Json;
+using WorkspacesEditor.Utils;
+
+namespace Workspaces.Data
+{
+ public class WorkspacesEditorData
+ {
+ protected JsonSerializerOptions JsonOptions
+ {
+ get
+ {
+ return new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = new DashCaseNamingPolicy(),
+ WriteIndented = true,
+ };
+ }
+ }
+
+ public T Read(string file)
+ {
+ IOUtils ioUtils = new IOUtils();
+ string data = ioUtils.ReadFile(file);
+ return JsonSerializer.Deserialize(data, JsonOptions);
+ }
+
+ public string Serialize(T data)
+ {
+ return JsonSerializer.Serialize(data, JsonOptions);
+ }
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/HeadingTextBlock.cs b/src/modules/Workspaces/WorkspacesEditor/HeadingTextBlock.cs
new file mode 100644
index 0000000000..24b7cf4b59
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/HeadingTextBlock.cs
@@ -0,0 +1,31 @@
+// 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 System.Windows.Automation.Peers;
+using System.Windows.Controls;
+
+namespace WorkspacesEditor
+{
+ 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;
+ }
+ }
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/MainPage.xaml b/src/modules/Workspaces/WorkspacesEditor/MainPage.xaml
new file mode 100644
index 0000000000..dd1749143e
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/MainPage.xaml
@@ -0,0 +1,322 @@
+
+
+
+
+ 24,16,0,24
+ 0,24,24,0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/Workspaces/WorkspacesEditor/MainPage.xaml.cs b/src/modules/Workspaces/WorkspacesEditor/MainPage.xaml.cs
new file mode 100644
index 0000000000..30e9ed70bb
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/MainPage.xaml.cs
@@ -0,0 +1,63 @@
+// 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 System.Windows.Controls;
+using WorkspacesEditor.Models;
+using WorkspacesEditor.ViewModels;
+
+namespace WorkspacesEditor
+{
+ ///
+ /// Interaction logic for MainPage.xaml
+ ///
+ public partial class MainPage : Page
+ {
+ private MainViewModel _mainViewModel;
+
+ public MainPage(MainViewModel mainViewModel)
+ {
+ InitializeComponent();
+ _mainViewModel = mainViewModel;
+ this.DataContext = _mainViewModel;
+ }
+
+ private /*async*/ void NewProjectButton_Click(object sender, RoutedEventArgs e)
+ {
+ _mainViewModel.EnterSnapshotMode(false);
+ }
+
+ private void EditButtonClicked(object sender, RoutedEventArgs e)
+ {
+ _mainViewModel.CloseAllPopups();
+ Button button = sender as Button;
+ Project selectedProject = button.DataContext as Project;
+ _mainViewModel.EditProject(selectedProject);
+ }
+
+ private void DeleteButtonClicked(object sender, RoutedEventArgs e)
+ {
+ e.Handled = true;
+ Button button = sender as Button;
+ Project selectedProject = button.DataContext as Project;
+ _mainViewModel.DeleteProject(selectedProject);
+ }
+
+ private void MoreButton_Click(object sender, RoutedEventArgs e)
+ {
+ e.Handled = true;
+ Button button = sender as Button;
+ Project project = button.DataContext as Project;
+ project.IsPopupVisible = true;
+ }
+
+ private void LaunchButton_Click(object sender, RoutedEventArgs e)
+ {
+ e.Handled = true;
+ Button button = sender as Button;
+ Project project = button.DataContext as Project;
+ _mainViewModel.LaunchProject(project);
+ }
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/MainWindow.xaml b/src/modules/Workspaces/WorkspacesEditor/MainWindow.xaml
new file mode 100644
index 0000000000..0c5eb55ccc
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/MainWindow.xaml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
diff --git a/src/modules/Workspaces/WorkspacesEditor/MainWindow.xaml.cs b/src/modules/Workspaces/WorkspacesEditor/MainWindow.xaml.cs
new file mode 100644
index 0000000000..adaf9c9a52
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/MainWindow.xaml.cs
@@ -0,0 +1,71 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Windows;
+using System.Windows.Interop;
+using ManagedCommon;
+using WorkspacesEditor.Utils;
+using WorkspacesEditor.ViewModels;
+
+namespace WorkspacesEditor
+{
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class MainWindow : Window
+ {
+ public MainViewModel MainViewModel { get; set; }
+
+ private static MainPage _mainPage;
+
+ public MainWindow(MainViewModel mainViewModel)
+ {
+ MainViewModel = mainViewModel;
+ mainViewModel.SetMainWindow(this);
+
+ WindowInteropHelper windowInteropHelper = new WindowInteropHelper(this);
+ System.Windows.Forms.Screen screen = System.Windows.Forms.Screen.FromHandle(windowInteropHelper.Handle);
+ double dpi = MonitorHelper.GetScreenDpiFromScreen(screen);
+ this.Height = screen.WorkingArea.Height / dpi * 0.90;
+ this.Width = screen.WorkingArea.Width / dpi * 0.75;
+ this.Top = screen.WorkingArea.Top + (int)(screen.WorkingArea.Height / dpi * 0.05);
+ this.Left = screen.WorkingArea.Left + (int)(screen.WorkingArea.Width / dpi * 0.125);
+
+ InitializeComponent();
+
+ _mainPage = new MainPage(mainViewModel);
+
+ ContentFrame.Navigate(_mainPage);
+
+ MaxWidth = SystemParameters.PrimaryScreenWidth;
+ MaxHeight = SystemParameters.PrimaryScreenHeight;
+ }
+
+ private void OnClosing(object sender, EventArgs e)
+ {
+ App.Current.Shutdown();
+ }
+
+ // This is required to fix a WPF rendering bug when using custom chrome
+ private void OnContentRendered(object sender, EventArgs e)
+ {
+ // Get the window handle of the Workspaces Editor window
+ IntPtr handle = new WindowInteropHelper(this).Handle;
+ WindowHelpers.BringToForeground(handle);
+
+ InvalidateVisual();
+ }
+
+ public void ShowPage(ProjectEditor editPage)
+ {
+ ContentFrame.Navigate(editPage);
+ }
+
+ public void SwitchToMainView()
+ {
+ ContentFrame.GoBack();
+ }
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Models/AppListDataTemplateSelector.cs b/src/modules/Workspaces/WorkspacesEditor/Models/AppListDataTemplateSelector.cs
new file mode 100644
index 0000000000..660793c1c2
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/Models/AppListDataTemplateSelector.cs
@@ -0,0 +1,31 @@
+// 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.
+
+namespace WorkspacesEditor.Models
+{
+ public sealed class AppListDataTemplateSelector : System.Windows.Controls.DataTemplateSelector
+ {
+ public System.Windows.DataTemplate HeaderTemplate { get; set; }
+
+ public System.Windows.DataTemplate AppTemplate { get; set; }
+
+ public AppListDataTemplateSelector()
+ {
+ HeaderTemplate = new System.Windows.DataTemplate();
+ AppTemplate = new System.Windows.DataTemplate();
+ }
+
+ public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
+ {
+ if (item is MonitorHeaderRow)
+ {
+ return HeaderTemplate;
+ }
+ else
+ {
+ return AppTemplate;
+ }
+ }
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Models/Application.cs b/src/modules/Workspaces/WorkspacesEditor/Models/Application.cs
new file mode 100644
index 0000000000..92790aef43
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/Models/Application.cs
@@ -0,0 +1,474 @@
+// 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.Drawing2D;
+using System.Drawing.Imaging;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text.Json.Serialization;
+using System.Text.RegularExpressions;
+using System.Windows.Media.Imaging;
+using ManagedCommon;
+using Windows.Management.Deployment;
+
+namespace WorkspacesEditor.Models
+{
+ public class Application : INotifyPropertyChanged, IDisposable
+ {
+ private bool _isInitialized;
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public Application()
+ {
+ }
+
+ public Application(Application other)
+ {
+ AppName = other.AppName;
+ AppPath = other.AppPath;
+ AppTitle = other.AppTitle;
+ PackageFullName = other.PackageFullName;
+ AppUserModelId = other.AppUserModelId;
+ CommandLineArguments = other.CommandLineArguments;
+ IsElevated = other.IsElevated;
+ CanLaunchElevated = other.CanLaunchElevated;
+ Minimized = other.Minimized;
+ Maximized = other.Maximized;
+ Position = other.Position;
+ MonitorNumber = other.MonitorNumber;
+
+ Parent = other.Parent;
+ IsNotFound = other.IsNotFound;
+ IsHighlighted = other.IsHighlighted;
+ RepeatIndex = other.RepeatIndex;
+ PackagedId = other.PackagedId;
+ PackagedName = other.PackagedName;
+ PackagedPublisherID = other.PackagedPublisherID;
+ Aumid = other.Aumid;
+ IsExpanded = other.IsExpanded;
+ IsIncluded = other.IsIncluded;
+ }
+
+ public Project Parent { get; set; }
+
+ public struct WindowPosition
+ {
+ public int X { get; set; }
+
+ public int Y { get; set; }
+
+ public int Width { get; set; }
+
+ public int Height { get; set; }
+
+ public static bool operator ==(WindowPosition left, WindowPosition right)
+ {
+ return left.X == right.X && left.Y == right.Y && left.Width == right.Width && left.Height == right.Height;
+ }
+
+ public static bool operator !=(WindowPosition left, WindowPosition right)
+ {
+ return left.X != right.X || left.Y != right.Y || left.Width != right.Width || left.Height != right.Height;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj == null || GetType() != obj.GetType())
+ {
+ return false;
+ }
+
+ WindowPosition pos = (WindowPosition)obj;
+ return X == pos.X && Y == pos.Y && Width == pos.Width && Height == pos.Height;
+ }
+
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+ }
+
+ public string AppName { get; set; }
+
+ public string AppPath { get; set; }
+
+ public string AppTitle { get; set; }
+
+ public string PackageFullName { get; set; }
+
+ public string AppUserModelId { get; set; }
+
+ public string CommandLineArguments { get; set; }
+
+ private bool _isElevated;
+
+ public bool IsElevated
+ {
+ get => _isElevated;
+ set
+ {
+ _isElevated = value;
+ OnPropertyChanged(new PropertyChangedEventArgs(nameof(AppMainParams)));
+ }
+ }
+
+ public bool CanLaunchElevated { get; set; }
+
+ internal void SwitchDeletion()
+ {
+ IsIncluded = !IsIncluded;
+ RedrawPreviewImage();
+ }
+
+ private void RedrawPreviewImage()
+ {
+ if (_isInitialized)
+ {
+ Parent.Initialize(App.ThemeManager.GetCurrentTheme());
+ }
+ }
+
+ private bool _minimized;
+
+ public bool Minimized
+ {
+ get => _minimized;
+ set
+ {
+ _minimized = value;
+ OnPropertyChanged(new PropertyChangedEventArgs(nameof(Minimized)));
+ OnPropertyChanged(new PropertyChangedEventArgs(nameof(EditPositionEnabled)));
+ RedrawPreviewImage();
+ }
+ }
+
+ private bool _maximized;
+
+ public bool Maximized
+ {
+ get => _maximized;
+ set
+ {
+ _maximized = value;
+ OnPropertyChanged(new PropertyChangedEventArgs(nameof(Maximized)));
+ OnPropertyChanged(new PropertyChangedEventArgs(nameof(EditPositionEnabled)));
+ RedrawPreviewImage();
+ }
+ }
+
+ public bool EditPositionEnabled { get => !Minimized && !Maximized; }
+
+ private string _appMainParams;
+
+ public string AppMainParams
+ {
+ get
+ {
+ _appMainParams = _isElevated ? Properties.Resources.Admin : string.Empty;
+ if (!string.IsNullOrWhiteSpace(CommandLineArguments))
+ {
+ _appMainParams += (_appMainParams == string.Empty ? string.Empty : " | ") + Properties.Resources.Args + ": " + CommandLineArguments;
+ }
+
+ OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsAppMainParamVisible)));
+ return _appMainParams;
+ }
+ }
+
+ public bool IsAppMainParamVisible { get => !string.IsNullOrWhiteSpace(_appMainParams); }
+
+ private bool _isNotFound;
+
+ [JsonIgnore]
+ public bool IsNotFound
+ {
+ get
+ {
+ return _isNotFound;
+ }
+
+ set
+ {
+ if (_isNotFound != value)
+ {
+ _isNotFound = value;
+ OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsNotFound)));
+ }
+ }
+ }
+
+ [JsonIgnore]
+ public bool IsHighlighted { get; set; }
+
+ [JsonIgnore]
+ public int RepeatIndex { get; set; }
+
+ [JsonIgnore]
+ public string RepeatIndexString
+ {
+ get
+ {
+ return RepeatIndex <= 1 ? string.Empty : RepeatIndex.ToString(CultureInfo.InvariantCulture);
+ }
+ }
+
+ [JsonIgnore]
+ private Icon _icon = null;
+
+ [JsonIgnore]
+ public Icon Icon
+ {
+ get
+ {
+ if (_icon == null)
+ {
+ try
+ {
+ if (IsPackagedApp)
+ {
+ Uri uri = GetAppLogoByPackageFamilyName();
+ var bitmap = new Bitmap(uri.LocalPath);
+ var iconHandle = bitmap.GetHicon();
+ _icon = Icon.FromHandle(iconHandle);
+ }
+ else
+ {
+ _icon = Icon.ExtractAssociatedIcon(AppPath);
+ }
+ }
+ catch (Exception)
+ {
+ Logger.LogWarning($"Icon not found on app path: {AppPath}. Using default icon");
+ IsNotFound = true;
+ _icon = new Icon(@"images\DefaultIcon.ico");
+ }
+ }
+
+ return _icon;
+ }
+ }
+
+ public Uri GetAppLogoByPackageFamilyName()
+ {
+ var pkgManager = new PackageManager();
+ var pkg = pkgManager.FindPackagesForUser(string.Empty, PackagedId).FirstOrDefault();
+
+ if (pkg == null)
+ {
+ return null;
+ }
+
+ return pkg.Logo;
+ }
+
+ private BitmapImage _iconBitmapImage;
+
+ public BitmapImage IconBitmapImage
+ {
+ get
+ {
+ if (_iconBitmapImage == null)
+ {
+ try
+ {
+ Bitmap previewBitmap = new Bitmap(32, 32);
+ using (Graphics graphics = Graphics.FromImage(previewBitmap))
+ {
+ graphics.SmoothingMode = SmoothingMode.AntiAlias;
+ graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
+ graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
+
+ graphics.DrawIcon(Icon, new Rectangle(0, 0, 32, 32));
+ }
+
+ using (var memory = new MemoryStream())
+ {
+ previewBitmap.Save(memory, ImageFormat.Png);
+ memory.Position = 0;
+
+ var bitmapImage = new BitmapImage();
+ bitmapImage.BeginInit();
+ bitmapImage.StreamSource = memory;
+ bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
+ bitmapImage.EndInit();
+ bitmapImage.Freeze();
+
+ _iconBitmapImage = bitmapImage;
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.LogError($"Exception while drawing icon for app with path: {AppPath}. Exception message: {e.Message}");
+ }
+ }
+
+ return _iconBitmapImage;
+ }
+ }
+
+ private WindowPosition _position;
+
+ public WindowPosition Position
+ {
+ get => _position;
+ set
+ {
+ _position = value;
+ _scaledPosition = null;
+ }
+ }
+
+ private WindowPosition? _scaledPosition;
+
+ public WindowPosition ScaledPosition
+ {
+ get
+ {
+ if (_scaledPosition == null)
+ {
+ double scaleFactor = MonitorSetup.Dpi / 96.0;
+ _scaledPosition = new WindowPosition()
+ {
+ X = (int)(scaleFactor * Position.X),
+ Y = (int)(scaleFactor * Position.Y),
+ Height = (int)(scaleFactor * Position.Height),
+ Width = (int)(scaleFactor * Position.Width),
+ };
+ }
+
+ return _scaledPosition.Value;
+ }
+ }
+
+ public int MonitorNumber { get; set; }
+
+ private MonitorSetup _monitorSetup;
+
+ public MonitorSetup MonitorSetup
+ {
+ get
+ {
+ if (_monitorSetup == null)
+ {
+ _monitorSetup = Parent.Monitors.Where(x => x.MonitorNumber == MonitorNumber).FirstOrDefault();
+ }
+
+ return _monitorSetup;
+ }
+ }
+
+ public void OnPropertyChanged(PropertyChangedEventArgs e)
+ {
+ PropertyChanged?.Invoke(this, e);
+ }
+
+ public void InitializationFinished()
+ {
+ _isInitialized = true;
+ }
+
+ private bool? _isPackagedApp;
+
+ public string PackagedId { get; set; }
+
+ public string PackagedName { get; set; }
+
+ public string PackagedPublisherID { get; set; }
+
+ public string Aumid { get; set; }
+
+ public bool IsPackagedApp
+ {
+ get
+ {
+ if (_isPackagedApp == null)
+ {
+ if (!AppPath.StartsWith("C:\\Program Files\\WindowsApps\\", StringComparison.InvariantCultureIgnoreCase))
+ {
+ _isPackagedApp = false;
+ }
+ else
+ {
+ string appPath = AppPath.Replace("C:\\Program Files\\WindowsApps\\", string.Empty);
+ Regex packagedAppPathRegex = new Regex(@"(?[^_]*)_\d+.\d+.\d+.\d+_x64__(?[^\\]*)", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
+ Match match = packagedAppPathRegex.Match(appPath);
+ _isPackagedApp = match.Success;
+ if (match.Success)
+ {
+ PackagedName = match.Groups["APPID"].Value;
+ PackagedPublisherID = match.Groups["PublisherID"].Value;
+ PackagedId = $"{PackagedName}_{PackagedPublisherID}";
+ Aumid = $"{PackagedId}!App";
+ }
+ }
+ }
+
+ return _isPackagedApp.Value;
+ }
+ }
+
+ private bool _isExpanded;
+
+ public bool IsExpanded
+ {
+ get => _isExpanded;
+ set
+ {
+ if (_isExpanded != value)
+ {
+ _isExpanded = value;
+ OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsExpanded)));
+ }
+ }
+ }
+
+ public string DeleteButtonContent { get => _isIncluded ? Properties.Resources.Delete : Properties.Resources.AddBack; }
+
+ private bool _isIncluded = true;
+
+ public bool IsIncluded
+ {
+ get => _isIncluded;
+ set
+ {
+ if (_isIncluded != value)
+ {
+ _isIncluded = value;
+ OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsIncluded)));
+ OnPropertyChanged(new PropertyChangedEventArgs(nameof(DeleteButtonContent)));
+ if (!_isIncluded)
+ {
+ IsExpanded = false;
+ }
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ }
+
+ internal void CommandLineTextChanged(string newCommandLineValue)
+ {
+ CommandLineArguments = newCommandLineValue;
+ OnPropertyChanged(new PropertyChangedEventArgs(nameof(AppMainParams)));
+ }
+
+ internal void MaximizedChecked()
+ {
+ Minimized = false;
+ }
+
+ internal void MinimizedChecked()
+ {
+ Maximized = false;
+ }
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Models/Monitor.cs b/src/modules/Workspaces/WorkspacesEditor/Models/Monitor.cs
new file mode 100644
index 0000000000..2a911eb1fe
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/Models/Monitor.cs
@@ -0,0 +1,33 @@
+// 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;
+
+namespace WorkspacesEditor.Models
+{
+ public class Monitor
+ {
+ public string MonitorName { get; private set; }
+
+ public string MonitorInstanceId { get; private set; }
+
+ public int MonitorNumber { get; private set; }
+
+ public int Dpi { get; private set; }
+
+ public Rect MonitorDpiUnawareBounds { get; private set; }
+
+ public Rect MonitorDpiAwareBounds { get; private set; }
+
+ public Monitor(string monitorName, string monitorInstanceId, int number, int dpi, Rect dpiAwareBounds, Rect dpiUnawareBounds)
+ {
+ MonitorName = monitorName;
+ MonitorInstanceId = monitorInstanceId;
+ MonitorNumber = number;
+ Dpi = dpi;
+ MonitorDpiAwareBounds = dpiAwareBounds;
+ MonitorDpiUnawareBounds = dpiUnawareBounds;
+ }
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Models/MonitorHeaderRow.cs b/src/modules/Workspaces/WorkspacesEditor/Models/MonitorHeaderRow.cs
new file mode 100644
index 0000000000..5a29ea146f
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/Models/MonitorHeaderRow.cs
@@ -0,0 +1,13 @@
+// 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.
+
+namespace WorkspacesEditor.Models
+{
+ internal sealed class MonitorHeaderRow
+ {
+ public string MonitorName { get; set; }
+
+ public string SelectString { get; set; }
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Models/MonitorSetup.cs b/src/modules/Workspaces/WorkspacesEditor/Models/MonitorSetup.cs
new file mode 100644
index 0000000000..e44365a988
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/Models/MonitorSetup.cs
@@ -0,0 +1,33 @@
+// 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.ComponentModel;
+using System.Windows;
+
+namespace WorkspacesEditor.Models
+{
+ public class MonitorSetup : Monitor, INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public void OnPropertyChanged(PropertyChangedEventArgs e)
+ {
+ PropertyChanged?.Invoke(this, e);
+ }
+
+ public string MonitorInfo { get => MonitorName; }
+
+ public string MonitorInfoWithResolution { get => $"{MonitorName} {MonitorDpiAwareBounds.Width}x{MonitorDpiAwareBounds.Height}"; }
+
+ public MonitorSetup(string monitorName, string monitorInstanceId, int number, int dpi, Rect dpiAwareBounds, Rect dpiUnawareBounds)
+ : base(monitorName, monitorInstanceId, number, dpi, dpiAwareBounds, dpiUnawareBounds)
+ {
+ }
+
+ public MonitorSetup(MonitorSetup other)
+ : base(other.MonitorName, other.MonitorInstanceId, other.MonitorNumber, other.Dpi, other.MonitorDpiAwareBounds, other.MonitorDpiUnawareBounds)
+ {
+ }
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesEditor/Models/Project.cs b/src/modules/Workspaces/WorkspacesEditor/Models/Project.cs
new file mode 100644
index 0000000000..b0065dd2ee
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesEditor/Models/Project.cs
@@ -0,0 +1,376 @@
+// 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.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Globalization;
+using System.Linq;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+using System.Windows.Media.Imaging;
+using ManagedCommon;
+using WorkspacesEditor.Data;
+using WorkspacesEditor.Utils;
+
+namespace WorkspacesEditor.Models
+{
+ public class Project : INotifyPropertyChanged
+ {
+ [JsonIgnore]
+ public string EditorWindowTitle { get; set; }
+
+ public string Id { get; private set; }
+
+ private string _name;
+
+ public string Name
+ {
+ get
+ {
+ return _name;
+ }
+
+ set
+ {
+ _name = value;
+ OnPropertyChanged(new PropertyChangedEventArgs(nameof(Name)));
+ OnPropertyChanged(new PropertyChangedEventArgs(nameof(CanBeSaved)));
+ }
+ }
+
+ public long CreationTime { get; } // in seconds
+
+ public long LastLaunchedTime { get; } // in seconds
+
+ public bool IsShortcutNeeded { get; set; }
+
+ public bool MoveExistingWindows { get; set; }
+
+ public string LastLaunched
+ {
+ get
+ {
+ string lastLaunched = WorkspacesEditor.Properties.Resources.LastLaunched + ": ";
+ if (LastLaunchedTime == 0)
+ {
+ return lastLaunched + WorkspacesEditor.Properties.Resources.Never;
+ }
+
+ const int SECOND = 1;
+ const int MINUTE = 60 * SECOND;
+ const int HOUR = 60 * MINUTE;
+ const int DAY = 24 * HOUR;
+ const int MONTH = 30 * DAY;
+
+ DateTime lastLaunchDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(LastLaunchedTime);
+
+ var now = DateTime.UtcNow.Ticks;
+ var ts = DateTime.UtcNow - lastLaunchDateTime;
+ double delta = Math.Abs(ts.TotalSeconds);
+
+ if (delta < 1 * MINUTE)
+ {
+ return lastLaunched + WorkspacesEditor.Properties.Resources.Recently;
+ }
+
+ if (delta < 2 * MINUTE)
+ {
+ return lastLaunched + WorkspacesEditor.Properties.Resources.OneMinuteAgo;
+ }
+
+ if (delta < 45 * MINUTE)
+ {
+ return lastLaunched + ts.Minutes + " " + WorkspacesEditor.Properties.Resources.MinutesAgo;
+ }
+
+ if (delta < 90 * MINUTE)
+ {
+ return lastLaunched + WorkspacesEditor.Properties.Resources.OneHourAgo;
+ }
+
+ if (delta < 24 * HOUR)
+ {
+ return lastLaunched + ts.Hours + " " + WorkspacesEditor.Properties.Resources.HoursAgo;
+ }
+
+ if (delta < 48 * HOUR)
+ {
+ return lastLaunched + WorkspacesEditor.Properties.Resources.Yesterday;
+ }
+
+ if (delta < 30 * DAY)
+ {
+ return lastLaunched + ts.Days + " " + WorkspacesEditor.Properties.Resources.DaysAgo;
+ }
+
+ if (delta < 12 * MONTH)
+ {
+ int months = Convert.ToInt32(Math.Floor((double)ts.Days / 30));
+ return lastLaunched + (months <= 1 ? WorkspacesEditor.Properties.Resources.OneMonthAgo : months + " " + WorkspacesEditor.Properties.Resources.MonthsAgo);
+ }
+ else
+ {
+ int years = Convert.ToInt32(Math.Floor((double)ts.Days / 365));
+ return lastLaunched + (years <= 1 ? WorkspacesEditor.Properties.Resources.OneYearAgo : years + " " + WorkspacesEditor.Properties.Resources.YearsAgo);
+ }
+ }
+ }
+
+ public bool CanBeSaved
+ {
+ get => Name.Length > 0 && Applications.Count > 0;
+ }
+
+ private bool _isRevertEnabled;
+
+ public bool IsRevertEnabled
+ {
+ get => _isRevertEnabled;
+ set
+ {
+ if (_isRevertEnabled != value)
+ {
+ _isRevertEnabled = value;
+ OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsRevertEnabled)));
+ }
+ }
+ }
+
+ private bool _isPopupVisible;
+
+ [JsonIgnore]
+ public bool IsPopupVisible
+ {
+ get
+ {
+ return _isPopupVisible;
+ }
+
+ set
+ {
+ _isPopupVisible = value;
+ OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsPopupVisible)));
+ }
+ }
+
+ public List Applications { get; set; }
+
+ public List