[New Module] Workspaces (#34324)

* spell checker

* Adding OOBE Projects page

* changed the default hotkey

* module interface

* rename projects editor

* bug report tool

* installer

* gpo

* exit event constant

* extend search for projects by search over the containing apps' names

* [Projects] fix grammatical issue #43 (1 app - many apps)

* [Projects] Editor: Main page: fix layout if there are many apps, launch button not disappearing on the right side

* dsc

* github

* pipeline

* guid prefix

* [Projects] fixing general settings gpo handling in runner + minor changes

* arm build fix

* Do not allow saving project if name or applist is empty. Also minor UI changes

* version

* editor version

* spellcheck

* editor dll signing

* update projects names to filter them out

* shortcut saving fix

* [Projects] Editor: brining the highlighted app's icon into the foreground. + minor UI fixes

* spell checker

* spellcheck

* [Projects] re-implementing icon size calculation to have similar sized icons for every app.

* [projects] Adding info message for cases: there are no projects or no results for the search

* [Projects] Adding Edit button to the popup. + minor changes

* [Projects] Making popup having rounded corners

* changed "no projects" text color and position

* remove opening the first proj

* fix placing windows of the same app in the project

* [Projects] bringing back the breadcrumb on the editor page. Make it clickable.

* [Projects] optimizing click handlers

* [Projects] Removing not selected apps on save

* moved on thread executor to common

* moved display utils

* added convert rect

* unsigned monitor number

* set awareness

* app placement

* [Projects] Re-implementing preview drawing - one common image

* [Projects] fix boundary calculation, use DPI aware values

* fix launching with command line args

* Fix ARM64 CI build

* launch packaged apps using names when possible

* spell-check

* update packaged apps path

* projects editor single instance

* [Projects] Add Select all checkbox, Delete selected button

* Add Checkbox for per monitor selection

* modifying highlight in preview

* spell checker

* logs

* exclude help windows

https://github.com/JaneaSystems/PowerToys-DevProjects/issues/49

* Add intermediate step to project creation

* minor bugfix

* mutex fix

* modifying highlight for minimized apps

* Fixing bug: re-draw the preview on app deletion in the editor

* Adding helper class for getting the right bounds for screens

* spell checker

* spell checker

* Minor fixes in the capture dialog

* get dpi unaware screen bounds

* refactoring: added utils

* changed window filter

https://github.com/JaneaSystems/PowerToys-DevProjects/issues/2

* clean up

* refactoring

* projects common lib

* localizable default project prefix

* launcher resources

* clean up

* change snapshot project saving

https://github.com/JaneaSystems/PowerToys-DevProjects/issues/14

* changed project data

https://github.com/JaneaSystems/PowerToys-DevProjects/issues/14

* changed project creation save-cancel handles

https://github.com/JaneaSystems/PowerToys-DevProjects/issues/14

* spell-check

* Remove checkboxes, delete feature

* remove unused from the project

* get command line args in the snapshot

* minimized settings snap fix

* set window property after launching

* FZ: ignore projects launched windows

* Implementing major new features: remove button, position manipulation, arguments, admin, minimized, maximized

* modifying colors

* launcher project filters

* clean up

* Hide Admin checkbox

* hide WIP

* spell-check

* Revert "Hide Admin checkbox"

This reverts commit 3036df9d7f.

* get app elevated property

* Implementing Launch and Edit feature

* fixing: update of listed projects on the main page after hitting save in editor

* Fix for packaged app's icons

* fixing scroll speed issue

* change scroll speed to 15

* launch elevated apps

* minor fixes

* minor fix

* enhancing shortcut handling

* can-launch-elevated check

* projects module interface telemetry

* Implementing store of setting "order by".

* minor string correction

* moved projects data parsing

* telemetry

* add move apps checkbox

* notification about elevated apps

* restart unelevated

* move existing windows

* keep opened windows at the same positions

* handle powertoys settings

* use common theme

* fix corrupted data: project id and monitor id

* project launch on "launch and edit"

* clean up

* show screen numbers instead of monitor names

* launcher error messages

* fix default shortcut

* Adding launch button to projects settings, dashboard and flyout

* Adding new app which is launched when launching a project. It shows the status of the launch process

* spell checker

* Renaming Projects to App Layouts. Replacing only string values, not the variable names

* Re-ordering modules after Renaming Projects + spell checker

* setting window size according to the screen (making it bigger)

* commenting out feature "move apps if exist"

* spell checker

* Add ProjectsLauncherUI to signing

* opening apps in minimized state which are placed on a monitor, which is not found at the moment of launching

* consistent file name

* removed unused sln

* telemetry: create event

* WindowPosition comparison

* telemetry: edit event

* fix muted Launch as admin checkbox

* telemetry: delete event

* updated Edit telemetry event

* added invoke point to launcher args

* added utils

* parse invoke point

* replaced tuple with struct

* telemetry: launch event

* MonitorRect comparison

* resources

* rename: folders

* remove outdated

* rename: window property

* rename: files and folders

* rename: common data structures

* rename: telemetry namespace

* rename: workspaces data

* rename ProjectsLib -> WorkspacesLib

* rename: gpo

* rename: settings

* rename: launcher UI

* rename: other

* rename: pt run

* rename: fz

* rename: module interface

* rename: icon

* rename: snapshot tool

* rename: editor

* rename: common files

* rename: launcher

* rename: editor resources

* fix empty file crash

* rename: json

* rename: module interface

* fix custom actions build

* added launch editor event constant

* xaml formatting

* Add missing method defition to interop::Constants idl
Remove Any CPU config

* more .sln cleanup

* [Run][PowerToys] Fix Workspaces utility (#34336)

polished workspaces utility

* build fix - align CppWinRT version

* address PR comment: fix isdigit

* indentation

* address PR comment: rename function

* address PR comment: changed version for workspaces and revision

* added supported version definition

* addressPR comment: use BringToForeground

* address PR comments: updated projects

* address PR comment: uncomment gpo in settings

* address PR comment: rename oobe view

* update OOBE image with current module name

* moved AppUtils

* launching with AppUserModel.ID

* fixed module order in settings

* fix xaml formatting

* [Workspaces] Close launcher if there are failed launches. Plus adding new spinner gif

* fix topmost LauncherUI

* clean up

* UI closing

* BugReportTool - omit cmd arg data

* Delete icon on workspace removal

* Adding cancellation to launcher UI.

* reordered launching

* fix terminating UI

* Removing old shortcut on workspace renaming

* Sentence case labels

* get process path without waiting

* comment out unused

* remove unused argument

* logs

* New icon

* fix launch and edit for the new project

* fix launch and edit: save new project

* Update exe icons

---------

Co-authored-by: donlaci <laszlo@janeasystems.com>
Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
Co-authored-by: Stefan Markovic <stefan@janeasystems.com>
Co-authored-by: Davide Giacometti <davide.giacometti@outlook.it>
Co-authored-by: Niels Laute <niels.laute@live.nl>
This commit is contained in:
Seraphima Zykova 2024-08-23 09:28:13 +02:00 committed by GitHub
parent 2a8e211cfd
commit 579619952d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
221 changed files with 12909 additions and 12 deletions

View File

@ -76,6 +76,7 @@ body:
- System tray interaction
- TextExtractor
- Video Conference Mute
- Workspaces
- Welcome / PowerToys Tour window
validations:
required: true

View File

@ -50,6 +50,7 @@ body:
- System tray interaction
- TextExtractor
- Video Conference Mute
- Workspaces
- Welcome / PowerToys Tour window
validations:
required: true

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -18,6 +18,7 @@
<?define AdvancedPasteProjectName="AdvancedPaste"?>
<?define RegistryPreviewProjectName="RegistryPreview"?>
<?define PeekProjectName="Peek"?>
<?define WorkspacesProjectName="Workspaces"?>
<?define RepoDir="$(var.ProjectDir)..\..\" ?>
<?if $(var.Platform) = x64?>

View File

@ -449,6 +449,15 @@
</RegistryKey>
<File Id="PowerOCR_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\PowerToys.PowerOCR.resources.dll" />
</Component>
<Component
Id="WorkspacesEditor_$(var.IdSafeLanguage)_Component"
Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER"
Guid="$(var.CompGUIDPrefix)21">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="WorkspacesEditor_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes"/>
</RegistryKey>
<File Id="WorkspacesEditor_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\PowerToys.WorkspacesEditor.resources.dll" />
</Component>
<?undef IdSafeLanguage?>
<?undef CompGUIDPrefix?>
<?endforeach?>

View File

@ -1223,7 +1223,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
}
processes.resize(bytes / sizeof(processes[0]));
std::array<std::wstring_view, 32> processesToTerminate = {
std::array<std::wstring_view, 36> 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",
};

View File

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

View File

@ -24,15 +24,18 @@
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<AdditionalIncludeDirectories>..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>..\..\..\;..\..\common;.\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="DisplayUtils.h" />
<ClInclude Include="MonitorEnumerator.h" />
<ClInclude Include="monitors.h" />
<ClInclude Include="dpi_aware.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="DisplayUtils.cpp" />
<ClCompile Include="monitors.cpp" />
<ClCompile Include="dpi_aware.cpp" />
</ItemGroup>

View File

@ -0,0 +1,143 @@
#include "DisplayUtils.h"
#include <algorithm>
#include <cwctype>
#include <iterator>
#include <dpi_aware.h>
#include <MonitorEnumerator.h>
#include <utils/OnThreadExecutor.h>
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<std::wstring, std::wstring> 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<bool, std::vector<DisplayUtils::DisplayData>> GetDisplays()
{
bool success = true;
std::vector<DisplayUtils::DisplayData> 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 };
}
}

View File

@ -0,0 +1,21 @@
#pragma once
#include <Windows.h>
#include <string>
#include <vector>
namespace DisplayUtils
{
struct DisplayData
{
HMONITOR monitor{};
std::wstring id;
std::wstring instanceId;
unsigned int number{};
unsigned int dpi{};
RECT monitorRectDpiAware{};
RECT monitorRectDpiUnaware{};
};
std::pair<bool, std::vector<DisplayData>> GetDisplays();
};

View File

@ -0,0 +1,35 @@
#pragma once
#include <functional>
#include <vector>
#include <Windows.h>
class MonitorEnumerator
{
public:
static std::vector<std::pair<HMONITOR, MONITORINFOEX>> Enumerate()
{
MonitorEnumerator inst;
EnumDisplayMonitors(NULL, NULL, Callback, reinterpret_cast<LPARAM>(&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<MonitorEnumerator*>(param);
MONITORINFOEX mi;
mi.cbSize = sizeof(mi);
if (GetMonitorInfo(monitor, &mi))
{
inst->m_monitors.push_back({monitor, mi});
}
return TRUE;
}
std::vector<std::pair<HMONITOR, MONITORINFOEX>> m_monitors;
};

View File

@ -1,7 +1,9 @@
#include "dpi_aware.h"
#include "monitors.h"
#include <ShellScalingApi.h>
#include <array>
#include <cmath>
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<long>(std::round(rect.left * static_cast<float>(dpi_x) / DEFAULT_DPI));
rect.right = static_cast<long>(std::round(rect.right * static_cast<float>(dpi_x) / DEFAULT_DPI));
rect.top = static_cast<long>(std::round(rect.top * static_cast<float>(dpi_y) / DEFAULT_DPI));
rect.bottom = static_cast<long>(std::round(rect.bottom * static_cast<float>(dpi_y) / DEFAULT_DPI));
}
}
void ConvertByCursorPosition(float& width, float& height)
{
HMONITOR targetMonitor = nullptr;

View File

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

View File

@ -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<GpoRuleConfigured>(powertoys_gpo::getAllowedAdvancedPasteOnlineAIModelsValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredWorkspacesEnabledValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredWorkspacesEnabledValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredMwbClipboardSharingEnabledValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredMwbClipboardSharingEnabledValue());

View File

@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "GPOWrapper.g.h"
#include <common/utils/gpo.h>
@ -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();

View File

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

View File

@ -61,5 +61,10 @@ namespace PowerToys.GPOWrapperProjection
{
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetRunPluginEnabledValue(pluginID);
}
public static GpoRuleConfigured GetConfiguredWorkspacesEnabledValue()
{
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredWorkspacesEnabledValue();
}
}
}

View File

@ -30,5 +30,6 @@ namespace ManagedCommon
MeasureTool,
ShortcutGuide,
PowerOCR,
Workspaces,
}
}

View File

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

View File

@ -40,6 +40,7 @@ namespace winrt::PowerToys::Interop::implementation
static hstring CropAndLockReparentEvent();
static hstring ShowEnvironmentVariablesSharedEvent();
static hstring ShowEnvironmentVariablesAdminSharedEvent();
static hstring WorkspacesLaunchEditorEvent();
};
}

View File

@ -37,6 +37,7 @@ namespace PowerToys
static String CropAndLockReparentEvent();
static String ShowEnvironmentVariablesSharedEvent();
static String ShowEnvironmentVariablesAdminSharedEvent();
static String WorkspacesLaunchEditorEvent();
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,72 @@
#pragma once
#include <future>
#include <thread>
#include <functional>
#include <queue>
#include <atomic>
// 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<void()>;
OnThreadExecutor() :
_shutdown_request{ false },
_worker_thread{ [this] { worker_thread(); } }
{
}
~OnThreadExecutor()
{
_shutdown_request = true;
_task_cv.notify_one();
_worker_thread.join();
}
std::future<void> 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<std::packaged_task<void()>> _task_queue;
std::thread _worker_thread;
};

View File

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

View File

@ -57,6 +57,8 @@ properties:
EnableQoiThumbnail: false
PowerOcr:
Enabled: false
Workspaces:
Enabled: false
ShortcutGuide:
Enabled: false
VideoConference:

View File

@ -57,6 +57,8 @@ properties:
EnableQoiThumbnail: true
PowerOcr:
Enabled: true
Workspaces:
Enabled: true
ShortcutGuide:
Enabled: true
VideoConference:

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) Microsoft Corporation.
Licensed under the MIT License. -->
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.11" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.12" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<policyNamespaces>
<target prefix="powertoys" namespace="Microsoft.Policies.PowerToys" />
</policyNamespaces>
<resources minRequiredRevision="1.11"/><!-- Last changed with PowerToys v0.83.0 -->
<resources minRequiredRevision="1.12"/><!-- Last changed with PowerToys v0.84.0 -->
<supportedOn>
<definitions>
<definition name="SUPPORTED_POWERTOYS_0_64_0" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0)"/>
@ -20,6 +20,7 @@
<definition name="SUPPORTED_POWERTOYS_0_81_0" displayName="$(string.SUPPORTED_POWERTOYS_0_81_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_81_1" displayName="$(string.SUPPORTED_POWERTOYS_0_81_1)"/>
<definition name="SUPPORTED_POWERTOYS_0_83_0" displayName="$(string.SUPPORTED_POWERTOYS_0_83_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_84_0" displayName="$(string.SUPPORTED_POWERTOYS_0_84_0)"/>
</definitions>
</supportedOn>
<categories>
@ -364,6 +365,16 @@
<decimal value="0" />
</disabledValue>
</policy>
<policy name="ConfigureEnabledUtilityWorkspaces" class="Both" displayName="$(string.ConfigureEnabledUtilityWorkspaces)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityWorkspaces">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_84_0" />
<enabledValue>
<decimal value="1" />
</enabledValue>
<disabledValue>
<decimal value="0" />
</disabledValue>
</policy>
<policy name="ConfigureEnabledUtilityQuickAccent" class="Both" displayName="$(string.ConfigureEnabledUtilityQuickAccent)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityQuickAccent">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_64_0" />

View File

@ -10,6 +10,7 @@
<string id="InstallerUpdates">Installer and Updates</string>
<string id="PowerToysRun">PowerToys Run</string>
<string id="AdvancedPaste">Advanced Paste</string>
<string id="Workspaces">Workspaces</string>
<string id="MouseWithoutBorders">Mouse Without Borders</string>
<string id="GeneralSettings">General settings</string>
@ -25,6 +26,7 @@
<string id="SUPPORTED_POWERTOYS_0_81_0">PowerToys version 0.81.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_81_1">PowerToys version 0.81.1 or later</string>
<string id="SUPPORTED_POWERTOYS_0_83_0">PowerToys version 0.83.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_84_0">PowerToys version 0.84.0 or later</string>
<string id="ConfigureAllUtilityGlobalEnabledStateDescription">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.
</string>
<string id="ConfigureEnabledUtilityWorkspaces">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.
</string>
<string id="PowerToysRunIndividualPluginEnabledStateDescription">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.
<string id="ConfigureEnabledUtilityPeek">Peek: Configure enabled state</string>
<string id="ConfigureEnabledUtilityPowerRename">Power Rename: Configure enabled state</string>
<string id="ConfigureEnabledUtilityPowerLauncher">PowerToys Run: Configure enabled state</string>
<string id="ConfigureEnabledUtilityWorkspaces">PowerToys Workspaces: Configure enabled state</string>
<string id="ConfigureEnabledUtilityQuickAccent">Quick Accent: Configure enabled state</string>
<string id="ConfigureEnabledUtilityRegistryPreview">Registry Preview: Configure enabled state</string>
<string id="ConfigureEnabledUtilityScreenRuler">Screen Ruler: Configure enabled state</string>

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

View File

@ -0,0 +1,16 @@
#pragma once
#include <Windows.h>
namespace WorkspacesWindowProperties
{
namespace Properties
{
const wchar_t LaunchedByWorkspacesID[] = L"PowerToys_LaunchedByWorkspaces";
}
inline void StampWorkspacesLaunchedProperty(HWND window)
{
::SetPropW(window, Properties::LaunchedByWorkspacesID, reinterpret_cast<HANDLE>(1));
}
}

View File

@ -0,0 +1,9 @@
<?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

@ -0,0 +1,20 @@
<Application
x:Class="WorkspacesEditor.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WorkspacesEditor"
xmlns:ui="http://schemas.modernwpf.com/2019"
Exit="OnExit"
Startup="OnStartup">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ThemeResources />
<ui:XamlControlsResources />
<ResourceDictionary Source="pack://application:,,,/Styles/ButtonStyles.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style x:Key="HeadingTextBlock" TargetType="TextBlock" />
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@ -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
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
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);
}
}
}

View File

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

View File

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

View File

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

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.Collections.Generic;
using Workspaces.Data;
using static WorkspacesEditor.Data.ProjectData;
namespace WorkspacesEditor.Data
{
public class ProjectData : WorkspacesEditorData<ProjectWrapper>
{
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<MonitorConfigurationWrapper> MonitorConfiguration { get; set; }
public List<ApplicationWrapper> Applications { get; set; }
}
}
}

View File

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

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 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<WorkspacesListWrapper>
{
public string File
{
get
{
return FolderUtils.DataFolder() + "\\workspaces.json";
}
}
public struct WorkspacesListWrapper
{
public List<ProjectWrapper> Workspaces { get; set; }
}
public enum OrderBy
{
LastViewed = 0,
Created = 1,
Name = 2,
Unknown = 3,
}
}
}

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 System.Text.Json;
using WorkspacesEditor.Utils;
namespace Workspaces.Data
{
public class WorkspacesEditorData<T>
{
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<T>(data, JsonOptions);
}
public string Serialize(T data)
{
return JsonSerializer.Serialize(data, JsonOptions);
}
}
}

View File

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

View File

@ -0,0 +1,322 @@
<Page
x:Class="WorkspacesEditor.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:WorkspacesEditor.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WorkspacesEditor"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:props="clr-namespace:WorkspacesEditor.Properties"
Title="MainPage"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Page.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVis" />
<converters:BooleanToInvertedVisibilityConverter x:Key="BooleanToInvertedVisibilityConverter" />
<Thickness x:Key="ContentDialogPadding">24,16,0,24</Thickness>
<Thickness x:Key="ContentDialogCommandSpaceMargin">0,24,24,0</Thickness>
<Style x:Key="DeleteButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{DynamicResource TertiaryBackgroundBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border
x:Name="border"
Padding="26,6,26,6"
Background="{TemplateBinding Background}"
BorderBrush="Transparent">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="{DynamicResource TitleBarSecondaryForegroundBrush}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Background" Value="{DynamicResource TitleBarSecondaryForegroundBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Page.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<local:HeadingTextBlock
x:Name="WorkspacesHeaderBlock"
Grid.Row="0"
Margin="40,20,40,20"
AutomationProperties.HeadingLevel="Level1"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="{x:Static props:Resources.Workspaces}" />
<Button
x:Name="NewProjectButton"
Grid.Row="0"
Height="36"
Margin="0,20,40,20"
Padding="0"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
AutomationProperties.Name="{x:Static props:Resources.CreateWorkspace}"
Click="NewProjectButton_Click"
Style="{StaticResource AccentButtonStyle}"
TabIndex="3">
<StackPanel Margin="12,8,12,8" Orientation="Horizontal">
<TextBlock
AutomationProperties.Name="{x:Static props:Resources.CreateWorkspace}"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
Foreground="{DynamicResource AccentButtonForeground}"
Text="&#xE710;" />
<TextBlock
Margin="12,-3,0,0"
Foreground="{DynamicResource AccentButtonForeground}"
Text="{x:Static props:Resources.CreateWorkspace}" />
</StackPanel>
<Button.Effect>
<DropShadowEffect
BlurRadius="6"
Opacity="0.32"
ShadowDepth="1" />
</Button.Effect>
</Button>
<Border
Grid.Row="1"
Margin="40,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
BorderThickness="2"
CornerRadius="5">
<StackPanel Orientation="Horizontal">
<Grid>
<TextBox
x:Name="SearchTextBox"
Width="320"
Background="{DynamicResource SecondaryBackgroundBrush}"
BorderBrush="{DynamicResource PrimaryBorderBrush}"
Text="{Binding SearchTerm, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ToolTip="{x:Static props:Resources.SearchExplanation}" />
<TextBlock
Margin="10,0,0,0"
VerticalAlignment="Center"
Foreground="{DynamicResource SecondaryForegroundBrush}"
IsHitTestVisible="False"
Text="{x:Static props:Resources.Search}"
ToolTip="{x:Static props:Resources.SearchExplanation}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=SearchTextBox}" Value="">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
<TextBlock
Margin="-50,0,34,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
AutomationProperties.Name="{x:Static props:Resources.Search}"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
Foreground="{DynamicResource SecondaryForegroundBrush}"
Text="&#xE71E;" />
</StackPanel>
</Border>
<StackPanel
Grid.Row="1"
Margin="0,0,40,0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<TextBlock
Margin="10,0,10,0"
VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="{x:Static props:Resources.SortBy}" />
<ComboBox
Width="140"
Background="{DynamicResource SecondaryBackgroundBrush}"
BorderBrush="{DynamicResource PrimaryBorderBrush}"
SelectedIndex="{Binding OrderByIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ComboBoxItem Content="{x:Static props:Resources.LastLaunched}" />
<ComboBoxItem Content="{x:Static props:Resources.Created}" />
<ComboBoxItem Content="{x:Static props:Resources.Name}" />
</ComboBox>
</StackPanel>
<TextBlock
Grid.Row="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="20"
Foreground="{DynamicResource SecondaryForegroundBrush}"
Text="{Binding EmptyWorkspacesViewMessage, UpdateSourceTrigger=PropertyChanged}"
TextAlignment="Center"
Visibility="{Binding IsWorkspacesViewEmpty, Mode=OneWay, Converter={StaticResource BoolToVis}, UpdateSourceTrigger=PropertyChanged}" />
<ScrollViewer
Grid.Row="2"
Margin="40,15,40,40"
VerticalContentAlignment="Stretch"
VerticalScrollBarVisibility="Auto"
Visibility="{Binding IsWorkspacesViewEmpty, Mode=OneWay, Converter={StaticResource BooleanToInvertedVisibilityConverter}, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl ItemsSource="{Binding WorkspacesView, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel
HorizontalAlignment="Stretch"
IsItemsHost="True"
Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="models:Project">
<Button
x:Name="EditButton"
Margin="0,12,0,0"
Padding="1"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
AutomationProperties.Name="{x:Static props:Resources.Edit}"
Background="{DynamicResource SecondaryBackgroundBrush}"
Click="EditButtonClicked">
<Border
HorizontalAlignment="Stretch"
Background="{DynamicResource SecondaryBackgroundBrush}"
CornerRadius="5">
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="110" />
</Grid.ColumnDefinitions>
<StackPanel
Margin="12,14,10,10"
HorizontalAlignment="Left"
Orientation="Vertical">
<TextBlock
Margin="0,0,0,8"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="16"
FontWeight="SemiBold"
Text="{Binding Name, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
<StackPanel
Margin="0,0,0,8"
VerticalAlignment="Center"
Orientation="Horizontal">
<Image Height="20" Source="{Binding PreviewIcons, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock
Margin="6,0,4,0"
VerticalAlignment="Center"
Text="{Binding AppsCountString}" />
</StackPanel>
<StackPanel VerticalAlignment="Center" Orientation="Horizontal">
<TextBlock
Margin="0,3,10,0"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="&#xE81C;" />
<TextBlock Text="{Binding LastLaunched, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</StackPanel>
<StackPanel
Grid.Column="1"
Margin="12,12,12,12"
Orientation="Vertical">
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<Button
x:Name="MoreButton"
HorizontalAlignment="Right"
Click="MoreButton_Click"
Style="{StaticResource IconOnlyButtonStyle}">
<TextBlock
FontFamily="{DynamicResource SymbolThemeFontFamily}"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="&#xE712;" />
</Button>
<Popup
AllowsTransparency="True"
IsOpen="{Binding IsPopupVisible, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Placement="Left"
PlacementTarget="{Binding ElementName=MoreButton}"
StaysOpen="False">
<Grid Background="{DynamicResource PrimaryBackgroundBrush}">
<Grid.OpacityMask>
<VisualBrush Visual="{Binding ElementName=OpacityBorder}" />
</Grid.OpacityMask>
<Border
x:Name="OpacityBorder"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Black"
CornerRadius="5" />
<StackPanel Background="{DynamicResource PrimaryBackgroundBrush}" Orientation="Vertical">
<Button
AutomationProperties.Name="{x:Static props:Resources.Edit}"
Click="EditButtonClicked"
Style="{StaticResource DeleteButtonStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock
AutomationProperties.Name="{x:Static props:Resources.Edit}"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="&#xE70F;" />
<TextBlock
Margin="10,0,0,0"
AutomationProperties.Name="{x:Static props:Resources.Edit}"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="{x:Static props:Resources.Edit}" />
</StackPanel>
</Button>
<Button
AutomationProperties.Name="{x:Static props:Resources.Delete}"
Click="DeleteButtonClicked"
Style="{StaticResource DeleteButtonStyle}">
<StackPanel Orientation="Horizontal">
<TextBlock
AutomationProperties.Name="{x:Static props:Resources.Delete}"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="&#xE74D;" />
<TextBlock
Margin="10,0,0,0"
AutomationProperties.Name="{x:Static props:Resources.Delete}"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="{x:Static props:Resources.Delete}" />
</StackPanel>
</Button>
</StackPanel>
</Grid>
</Popup>
</StackPanel>
<Button
Margin="0,6,0,0"
Padding="20,4,20,4"
HorizontalAlignment="Right"
AutomationProperties.Name="{x:Static props:Resources.Launch}"
Background="{DynamicResource TertiaryBackgroundBrush}"
BorderBrush="{DynamicResource SecondaryBorderBrush}"
BorderThickness="1"
Click="LaunchButton_Click"
Content="{x:Static props:Resources.Launch}" />
</StackPanel>
</Grid>
</Border>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</Page>

View File

@ -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
{
/// <summary>
/// Interaction logic for MainPage.xaml
/// </summary>
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);
}
}
}

View File

@ -0,0 +1,29 @@
<Window
x:Class="WorkspacesEditor.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:props="clr-namespace:WorkspacesEditor.Properties"
xmlns:ui="http://schemas.modernwpf.com/2019"
x:Name="WorkspacesMainWindow"
Title="{x:Static props:Resources.MainTitle}"
MinWidth="700"
MinHeight="680"
ui:TitleBar.Background="{DynamicResource PrimaryBackgroundBrush}"
ui:TitleBar.InactiveBackground="{DynamicResource TertiaryBackgroundBrush}"
ui:TitleBar.IsIconVisible="True"
ui:WindowHelper.UseModernWindowStyle="True"
AutomationProperties.Name="Workspaces Editor"
Background="{DynamicResource PrimaryBackgroundBrush}"
Closing="OnClosing"
ContentRendered="OnContentRendered"
ResizeMode="CanResize"
WindowStartupLocation="CenterOwner"
mc:Ignorable="d">
<Border BorderThickness="1" CornerRadius="20">
<Grid Margin="0,10,0,0">
<Frame x:Name="ContentFrame" NavigationUIVisibility="Hidden" />
</Grid>
</Border>
</Window>

View File

@ -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
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
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();
}
}
}

View File

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

View File

@ -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(@"(?<APPID>[^_]*)_\d+.\d+.\d+.\d+_x64__(?<PublisherID>[^\\]*)", 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;
}
}
}

View File

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

View File

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

View File

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

View File

@ -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<Application> Applications { get; set; }
public List<object> ApplicationsListed
{
get
{
List<object> applicationsListed = new List<object>();
ILookup<MonitorSetup, Application> apps = Applications.Where(x => !x.Minimized).ToLookup(x => x.MonitorSetup);
foreach (var appItem in apps.OrderBy(x => x.Key.MonitorDpiUnawareBounds.Left).ThenBy(x => x.Key.MonitorDpiUnawareBounds.Top))
{
MonitorHeaderRow headerRow = new MonitorHeaderRow { MonitorName = "Screen " + appItem.Key.MonitorNumber, SelectString = Properties.Resources.SelectAllAppsOnMonitor + " " + appItem.Key.MonitorInfo };
applicationsListed.Add(headerRow);
foreach (Application app in appItem)
{
applicationsListed.Add(app);
}
}
var minimizedApps = Applications.Where(x => x.Minimized);
if (minimizedApps.Any())
{
MonitorHeaderRow headerRow = new MonitorHeaderRow { MonitorName = Properties.Resources.Minimized_Apps, SelectString = Properties.Resources.SelectAllMinimizedApps };
applicationsListed.Add(headerRow);
foreach (Application app in minimizedApps)
{
applicationsListed.Add(app);
}
}
return applicationsListed;
}
}
[JsonIgnore]
public string AppsCountString
{
get
{
int count = Applications.Count;
return count.ToString(CultureInfo.InvariantCulture) + " " + (count == 1 ? Properties.Resources.App : Properties.Resources.Apps);
}
}
public List<MonitorSetup> Monitors { get; }
public bool IsPositionChangedManually { get; set; } // telemetry
private BitmapImage _previewIcons;
private BitmapImage _previewImage;
private double _previewImageWidth;
public Project(Project selectedProject)
{
Id = selectedProject.Id;
Name = selectedProject.Name;
PreviewIcons = selectedProject.PreviewIcons;
PreviewImage = selectedProject.PreviewImage;
IsShortcutNeeded = selectedProject.IsShortcutNeeded;
MoveExistingWindows = selectedProject.MoveExistingWindows;
int screenIndex = 1;
Monitors = new List<MonitorSetup>();
foreach (var item in selectedProject.Monitors.OrderBy(x => x.MonitorDpiAwareBounds.Left).ThenBy(x => x.MonitorDpiAwareBounds.Top))
{
Monitors.Add(item);
screenIndex++;
}
Applications = new List<Application>();
foreach (var item in selectedProject.Applications)
{
Application newApp = new Application(item);
newApp.Parent = this;
newApp.InitializationFinished();
Applications.Add(newApp);
}
}
public Project(ProjectData.ProjectWrapper project)
{
Id = project.Id;
Name = project.Name;
CreationTime = project.CreationTime;
LastLaunchedTime = project.LastLaunchedTime;
IsShortcutNeeded = project.IsShortcutNeeded;
MoveExistingWindows = project.MoveExistingWindows;
Monitors = new List<MonitorSetup>() { };
Applications = new List<Models.Application> { };
foreach (var app in project.Applications)
{
Models.Application newApp = new Models.Application()
{
AppName = app.Application,
AppPath = app.ApplicationPath,
AppTitle = app.Title,
PackageFullName = app.PackageFullName,
AppUserModelId = app.AppUserModelId,
Parent = this,
CommandLineArguments = app.CommandLineArguments,
IsElevated = app.IsElevated,
CanLaunchElevated = app.CanLaunchElevated,
Maximized = app.Maximized,
Minimized = app.Minimized,
IsNotFound = false,
Position = new Models.Application.WindowPosition()
{
Height = app.Position.Height,
Width = app.Position.Width,
X = app.Position.X,
Y = app.Position.Y,
},
MonitorNumber = app.Monitor,
};
newApp.InitializationFinished();
Applications.Add(newApp);
}
foreach (var monitor in project.MonitorConfiguration)
{
System.Windows.Rect dpiAware = new System.Windows.Rect(monitor.MonitorRectDpiAware.Left, monitor.MonitorRectDpiAware.Top, monitor.MonitorRectDpiAware.Width, monitor.MonitorRectDpiAware.Height);
System.Windows.Rect dpiUnaware = new System.Windows.Rect(monitor.MonitorRectDpiUnaware.Left, monitor.MonitorRectDpiUnaware.Top, monitor.MonitorRectDpiUnaware.Width, monitor.MonitorRectDpiUnaware.Height);
Monitors.Add(new MonitorSetup(monitor.Id, monitor.InstanceId, monitor.MonitorNumber, monitor.Dpi, dpiAware, dpiUnaware));
}
}
public BitmapImage PreviewIcons
{
get
{
return _previewIcons;
}
set
{
_previewIcons = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(PreviewIcons)));
}
}
public BitmapImage PreviewImage
{
get
{
return _previewImage;
}
set
{
_previewImage = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(PreviewImage)));
}
}
public double PreviewImageWidth
{
get
{
return _previewImageWidth;
}
set
{
_previewImageWidth = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(PreviewImageWidth)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChanged?.Invoke(this, e);
}
public async void Initialize(Theme currentTheme)
{
PreviewIcons = await Task.Run(() => DrawHelper.DrawPreviewIcons(this));
Rectangle commonBounds = GetCommonBounds();
PreviewImage = await Task.Run(() => DrawHelper.DrawPreview(this, commonBounds, currentTheme));
PreviewImageWidth = commonBounds.Width / (commonBounds.Height * 1.2 / 200);
}
private Rectangle GetCommonBounds()
{
double minX = Monitors.First().MonitorDpiAwareBounds.Left;
double minY = Monitors.First().MonitorDpiAwareBounds.Top;
double maxX = Monitors.First().MonitorDpiAwareBounds.Right;
double maxY = Monitors.First().MonitorDpiAwareBounds.Bottom;
for (int monitorIndex = 1; monitorIndex < Monitors.Count; monitorIndex++)
{
Monitor monitor = Monitors[monitorIndex];
minX = Math.Min(minX, monitor.MonitorDpiAwareBounds.Left);
minY = Math.Min(minY, monitor.MonitorDpiAwareBounds.Top);
maxX = Math.Max(maxX, monitor.MonitorDpiAwareBounds.Right);
maxY = Math.Max(maxY, monitor.MonitorDpiAwareBounds.Bottom);
}
return new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY));
}
public void UpdateAfterLaunchAndEdit(Project other)
{
Id = other.Id;
Name = other.Name;
IsRevertEnabled = true;
}
internal void CloseExpanders()
{
foreach (Application app in Applications)
{
app.IsExpanded = false;
}
}
}
}

View File

@ -0,0 +1,16 @@
<Window
x:Class="WorkspacesEditor.OverlayWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WorkspacesEditor"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
AllowsTransparency="True"
Background="Transparent"
WindowStyle="None"
mc:Ignorable="d">
<Border
Background="Transparent"
BorderBrush="Red"
BorderThickness="3" />
</Window>

View File

@ -0,0 +1,19 @@
// 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
{
/// <summary>
/// Interaction logic for OverlayWindow.xaml
/// </summary>
public partial class OverlayWindow : Window
{
public OverlayWindow()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,693 @@
//------------------------------------------------------------------------------
// <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 WorkspacesEditor.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", "17.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("WorkspacesEditor.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 Add Back.
/// </summary>
public static string AddBack {
get {
return ResourceManager.GetString("AddBack", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Admin.
/// </summary>
public static string Admin {
get {
return ResourceManager.GetString("Admin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Launch new app instances.
/// </summary>
public static string AlwaysLaunch {
get {
return ResourceManager.GetString("AlwaysLaunch", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to app.
/// </summary>
public static string App {
get {
return ResourceManager.GetString("App", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to App name.
/// </summary>
public static string App_name {
get {
return ResourceManager.GetString("App_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to apps.
/// </summary>
public static string Apps {
get {
return ResourceManager.GetString("Apps", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Are you sure?.
/// </summary>
public static string Are_You_Sure {
get {
return ResourceManager.GetString("Are_You_Sure", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Are you sure you want to delete this Workspace?.
/// </summary>
public static string Are_You_Sure_Description {
get {
return ResourceManager.GetString("Are_You_Sure_Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Args.
/// </summary>
public static string Args {
get {
return ResourceManager.GetString("Args", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Cancel.
/// </summary>
public static string Cancel {
get {
return ResourceManager.GetString("Cancel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to CLI arguments.
/// </summary>
public static string CliArguments {
get {
return ResourceManager.GetString("CliArguments", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Created.
/// </summary>
public static string Created {
get {
return ResourceManager.GetString("Created", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Create Desktop Shortcut.
/// </summary>
public static string CreateShortcut {
get {
return ResourceManager.GetString("CreateShortcut", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Create Workspace.
/// </summary>
public static string CreateWorkspace {
get {
return ResourceManager.GetString("CreateWorkspace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to days ago.
/// </summary>
public static string DaysAgo {
get {
return ResourceManager.GetString("DaysAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Workspace.
/// </summary>
public static string DefaultWorkspaceNamePrefix {
get {
return ResourceManager.GetString("DefaultWorkspaceNamePrefix", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Remove.
/// </summary>
public static string Delete {
get {
return ResourceManager.GetString("Delete", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Delete Workspace dialog..
/// </summary>
public static string Delete_Workspace_Dialog_Announce {
get {
return ResourceManager.GetString("Delete_Workspace_Dialog_Announce", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Remove Selected Apps.
/// </summary>
public static string DeleteSelected {
get {
return ResourceManager.GetString("DeleteSelected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Edit.
/// </summary>
public static string Edit {
get {
return ResourceManager.GetString("Edit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to opened.
/// </summary>
public static string Edit_Project_Open_Announce {
get {
return ResourceManager.GetString("Edit_Project_Open_Announce", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Edit Workspace.
/// </summary>
public static string EditWorkspace {
get {
return ResourceManager.GetString("EditWorkspace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Error parsing Workspaces data..
/// </summary>
public static string Error_Parsing_Message {
get {
return ResourceManager.GetString("Error_Parsing_Message", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Height.
/// </summary>
public static string Height {
get {
return ResourceManager.GetString("Height", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to hours ago.
/// </summary>
public static string HoursAgo {
get {
return ResourceManager.GetString("HoursAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Last launched.
/// </summary>
public static string LastLaunched {
get {
return ResourceManager.GetString("LastLaunched", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Launch.
/// </summary>
public static string Launch {
get {
return ResourceManager.GetString("Launch", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Launch args.
/// </summary>
public static string Launch_args {
get {
return ResourceManager.GetString("Launch_args", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Launch as Admin.
/// </summary>
public static string LaunchAsAdmin {
get {
return ResourceManager.GetString("LaunchAsAdmin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Launch &amp; Edit.
/// </summary>
public static string LaunchEdit {
get {
return ResourceManager.GetString("LaunchEdit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Left.
/// </summary>
public static string Left {
get {
return ResourceManager.GetString("Left", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Workspaces Editor.
/// </summary>
public static string MainTitle {
get {
return ResourceManager.GetString("MainTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Maximized.
/// </summary>
public static string Maximized {
get {
return ResourceManager.GetString("Maximized", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Minimized.
/// </summary>
public static string Minimized {
get {
return ResourceManager.GetString("Minimized", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Minimized Apps.
/// </summary>
public static string Minimized_Apps {
get {
return ResourceManager.GetString("Minimized_Apps", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to minutes ago.
/// </summary>
public static string MinutesAgo {
get {
return ResourceManager.GetString("MinutesAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to months ago.
/// </summary>
public static string MonthsAgo {
get {
return ResourceManager.GetString("MonthsAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Move apps if present.
/// </summary>
public static string MoveIfExist {
get {
return ResourceManager.GetString("MoveIfExist", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name.
/// </summary>
public static string Name {
get {
return ResourceManager.GetString("Name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to never.
/// </summary>
public static string Never {
get {
return ResourceManager.GetString("Never", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to New Workspace.
/// </summary>
public static string New_Workspace {
get {
return ResourceManager.GetString("New_Workspace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to There are no saved Workspaces..
/// </summary>
public static string No_Workspaces_Message {
get {
return ResourceManager.GetString("No_Workspaces_Message", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The application cannot be found.
/// </summary>
public static string NotFoundTooltip {
get {
return ResourceManager.GetString("NotFoundTooltip", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No Workspaces match the current search..
/// </summary>
public static string NoWorkspacesMatch {
get {
return ResourceManager.GetString("NoWorkspacesMatch", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to an hour ago.
/// </summary>
public static string OneHourAgo {
get {
return ResourceManager.GetString("OneHourAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to a minute ago.
/// </summary>
public static string OneMinuteAgo {
get {
return ResourceManager.GetString("OneMinuteAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to one month ago.
/// </summary>
public static string OneMonthAgo {
get {
return ResourceManager.GetString("OneMonthAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to one second ago.
/// </summary>
public static string OneSecondAgo {
get {
return ResourceManager.GetString("OneSecondAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to one year ago.
/// </summary>
public static string OneYearAgo {
get {
return ResourceManager.GetString("OneYearAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Pin Workspaces to Taskbar.
/// </summary>
public static string PinToTaskbar {
get {
return ResourceManager.GetString("PinToTaskbar", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to recently.
/// </summary>
public static string Recently {
get {
return ResourceManager.GetString("Recently", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Revert.
/// </summary>
public static string Revert {
get {
return ResourceManager.GetString("Revert", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Save Workspace.
/// </summary>
public static string Save_Workspace {
get {
return ResourceManager.GetString("Save_Workspace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search.
/// </summary>
public static string Search {
get {
return ResourceManager.GetString("Search", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search for Workspaces or apps.
/// </summary>
public static string SearchExplanation {
get {
return ResourceManager.GetString("SearchExplanation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to seconds ago.
/// </summary>
public static string SecondsAgo {
get {
return ResourceManager.GetString("SecondsAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Select All Apps on.
/// </summary>
public static string SelectAllAppsOnMonitor {
get {
return ResourceManager.GetString("SelectAllAppsOnMonitor", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Select All Minimized Apps.
/// </summary>
public static string SelectAllMinimizedApps {
get {
return ResourceManager.GetString("SelectAllMinimizedApps", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Select All Apps in Workspace.
/// </summary>
public static string SelectedAllInWorkspace {
get {
return ResourceManager.GetString("SelectedAllInWorkspace", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Edit your layout and click &quot;Capture&quot; when finished..
/// </summary>
public static string SnapshotDescription {
get {
return ResourceManager.GetString("SnapshotDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Snapshot Creator.
/// </summary>
public static string SnapshotWindowTitle {
get {
return ResourceManager.GetString("SnapshotWindowTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Sort by.
/// </summary>
public static string SortBy {
get {
return ResourceManager.GetString("SortBy", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Capture.
/// </summary>
public static string Take_Snapshot {
get {
return ResourceManager.GetString("Take_Snapshot", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Top.
/// </summary>
public static string Top {
get {
return ResourceManager.GetString("Top", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Width.
/// </summary>
public static string Width {
get {
return ResourceManager.GetString("Width", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Workspace name.
/// </summary>
public static string WorkspaceName {
get {
return ResourceManager.GetString("WorkspaceName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Workspaces.
/// </summary>
public static string Workspaces {
get {
return ResourceManager.GetString("Workspaces", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Write arguments here.
/// </summary>
public static string WriteArgs {
get {
return ResourceManager.GetString("WriteArgs", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to years ago.
/// </summary>
public static string YearsAgo {
get {
return ResourceManager.GetString("YearsAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to yesterday.
/// </summary>
public static string Yesterday {
get {
return ResourceManager.GetString("Yesterday", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,333 @@
<?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="AddBack" xml:space="preserve">
<value>Add back</value>
</data>
<data name="Admin" xml:space="preserve">
<value>Admin</value>
</data>
<data name="AlwaysLaunch" xml:space="preserve">
<value>Launch new app instances</value>
</data>
<data name="App" xml:space="preserve">
<value>app</value>
</data>
<data name="Apps" xml:space="preserve">
<value>apps</value>
</data>
<data name="App_name" xml:space="preserve">
<value>App name</value>
</data>
<data name="Are_You_Sure" xml:space="preserve">
<value>Are you sure?</value>
</data>
<data name="Are_You_Sure_Description" xml:space="preserve">
<value>Are you sure you want to delete this Workspace?</value>
</data>
<data name="Args" xml:space="preserve">
<value>Args</value>
<comment>Arguments</comment>
</data>
<data name="Cancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="CliArguments" xml:space="preserve">
<value>CLI arguments</value>
</data>
<data name="Created" xml:space="preserve">
<value>Created</value>
</data>
<data name="CreateWorkspace" xml:space="preserve">
<value>Create Workspace</value>
</data>
<data name="CreateShortcut" xml:space="preserve">
<value>Create desktop shortcut</value>
</data>
<data name="DaysAgo" xml:space="preserve">
<value>days ago</value>
</data>
<data name="DefaultWorkspaceNamePrefix" xml:space="preserve">
<value>Workspace</value>
</data>
<data name="Delete" xml:space="preserve">
<value>Remove</value>
</data>
<data name="DeleteSelected" xml:space="preserve">
<value>Remove selected apps</value>
</data>
<data name="Delete_Workspace_Dialog_Announce" xml:space="preserve">
<value>Delete Workspace dialog.</value>
</data>
<data name="Edit" xml:space="preserve">
<value>Edit</value>
</data>
<data name="EditWorkspace" xml:space="preserve">
<value>Edit Workspace</value>
</data>
<data name="Edit_Project_Open_Announce" xml:space="preserve">
<value>opened</value>
</data>
<data name="Error_Parsing_Message" xml:space="preserve">
<value>Error parsing Workspaces data.</value>
</data>
<data name="Height" xml:space="preserve">
<value>Height</value>
</data>
<data name="HoursAgo" xml:space="preserve">
<value>hours ago</value>
</data>
<data name="LastLaunched" xml:space="preserve">
<value>Last launched</value>
</data>
<data name="Launch" xml:space="preserve">
<value>Launch</value>
</data>
<data name="LaunchAsAdmin" xml:space="preserve">
<value>Launch as Admin</value>
</data>
<data name="LaunchEdit" xml:space="preserve">
<value>Launch &amp; edit</value>
</data>
<data name="Launch_args" xml:space="preserve">
<value>Launch args</value>
</data>
<data name="Left" xml:space="preserve">
<value>Left</value>
<comment>the left x coordinate</comment>
</data>
<data name="MainTitle" xml:space="preserve">
<value>Workspaces Editor</value>
</data>
<data name="Maximized" xml:space="preserve">
<value>Maximized</value>
</data>
<data name="Minimized" xml:space="preserve">
<value>Minimized</value>
</data>
<data name="Minimized_Apps" xml:space="preserve">
<value>Minimized apps</value>
</data>
<data name="MinutesAgo" xml:space="preserve">
<value>minutes ago</value>
</data>
<data name="MonthsAgo" xml:space="preserve">
<value>months ago</value>
</data>
<data name="MoveIfExist" xml:space="preserve">
<value>Move apps if present</value>
</data>
<data name="Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="Never" xml:space="preserve">
<value>never</value>
</data>
<data name="New_Workspace" xml:space="preserve">
<value>New Workspace</value>
</data>
<data name="NoWorkspacesMatch" xml:space="preserve">
<value>No Workspaces match the current search.</value>
</data>
<data name="NotFoundTooltip" xml:space="preserve">
<value>The application cannot be found</value>
</data>
<data name="No_Workspaces_Message" xml:space="preserve">
<value>There are no saved Workspaces.</value>
</data>
<data name="OneHourAgo" xml:space="preserve">
<value>an hour ago</value>
</data>
<data name="OneMinuteAgo" xml:space="preserve">
<value>a minute ago</value>
</data>
<data name="OneMonthAgo" xml:space="preserve">
<value>one month ago</value>
</data>
<data name="OneSecondAgo" xml:space="preserve">
<value>one second ago</value>
</data>
<data name="OneYearAgo" xml:space="preserve">
<value>one year ago</value>
</data>
<data name="PinToTaskbar" xml:space="preserve">
<value>Pin Workspaces to taskbar</value>
</data>
<data name="WorkspaceName" xml:space="preserve">
<value>Workspace name</value>
</data>
<data name="Workspaces" xml:space="preserve">
<value>Workspaces</value>
</data>
<data name="Recently" xml:space="preserve">
<value>recently</value>
</data>
<data name="Revert" xml:space="preserve">
<value>Revert</value>
</data>
<data name="Save_Workspace" xml:space="preserve">
<value>Save Workspace</value>
</data>
<data name="Search" xml:space="preserve">
<value>Search</value>
</data>
<data name="SearchExplanation" xml:space="preserve">
<value>Search for Workspaces or apps</value>
</data>
<data name="SecondsAgo" xml:space="preserve">
<value>seconds ago</value>
</data>
<data name="SelectAllAppsOnMonitor" xml:space="preserve">
<value>Select all apps on</value>
</data>
<data name="SelectAllMinimizedApps" xml:space="preserve">
<value>Select all minimized apps</value>
</data>
<data name="SelectedAllInWorkspace" xml:space="preserve">
<value>Select all apps in Workspace</value>
</data>
<data name="SnapshotDescription" xml:space="preserve">
<value>Edit your layout and click "Capture" when finished.</value>
</data>
<data name="SnapshotWindowTitle" xml:space="preserve">
<value>Snapshot Creator</value>
</data>
<data name="SortBy" xml:space="preserve">
<value>Sort by</value>
</data>
<data name="Take_Snapshot" xml:space="preserve">
<value>Capture</value>
</data>
<data name="Top" xml:space="preserve">
<value>Top</value>
<comment>the top y coordinate</comment>
</data>
<data name="Width" xml:space="preserve">
<value>Width</value>
</data>
<data name="WriteArgs" xml:space="preserve">
<value>Write arguments here</value>
</data>
<data name="YearsAgo" xml:space="preserve">
<value>years ago</value>
</data>
<data name="Yesterday" xml:space="preserve">
<value>yesterday</value>
</data>
</root>

View File

@ -0,0 +1,26 @@
//------------------------------------------------------------------------------
// <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 WorkspacesEditor.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

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

View File

@ -0,0 +1,59 @@
<Window
x:Class="WorkspacesEditor.SnapshotWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WorkspacesEditor"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:props="clr-namespace:WorkspacesEditor.Properties"
xmlns:ui="http://schemas.modernwpf.com/2019"
Title="{x:Static props:Resources.SnapshotWindowTitle}"
Width="350"
Height="140"
ui:TitleBar.Background="{DynamicResource PrimaryBackgroundBrush}"
ui:TitleBar.InactiveBackground="{DynamicResource TertiaryBackgroundBrush}"
ui:WindowHelper.UseModernWindowStyle="True"
Background="{DynamicResource PrimaryBackgroundBrush}"
BorderBrush="Red"
BorderThickness="5"
Closing="Window_Closing"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Grid Margin="5" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.ColumnSpan="2"
Margin="5"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="{x:Static props:Resources.SnapshotDescription}" />
<Button
x:Name="CancelButton"
Grid.Row="1"
Height="36"
Margin="5,5,5,5"
HorizontalAlignment="Stretch"
AutomationProperties.Name="{x:Static props:Resources.Cancel}"
Background="{DynamicResource SecondaryBackgroundBrush}"
Click="CancelButtonClicked"
Content="{x:Static props:Resources.Cancel}" />
<Button
x:Name="SnapshotButton"
Grid.Row="1"
Grid.Column="1"
Height="36"
Margin="5,5,5,5"
HorizontalAlignment="Stretch"
AutomationProperties.Name="{x:Static props:Resources.Take_Snapshot}"
Click="SnapshotButtonClicked"
Content="{x:Static props:Resources.Take_Snapshot}"
Style="{StaticResource AccentButtonStyle}" />
</Grid>
</Window>

View File

@ -0,0 +1,40 @@
// 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 WorkspacesEditor.ViewModels;
namespace WorkspacesEditor
{
/// <summary>
/// Interaction logic for SnapshotWindow.xaml
/// </summary>
public partial class SnapshotWindow : Window
{
private MainViewModel _mainViewModel;
public SnapshotWindow(MainViewModel mainViewModel)
{
_mainViewModel = mainViewModel;
InitializeComponent();
}
private void CancelButtonClicked(object sender, RoutedEventArgs e)
{
Close();
_mainViewModel.CancelSnapshot();
}
private void SnapshotButtonClicked(object sender, RoutedEventArgs e)
{
Close();
_mainViewModel.SnapWorkspace();
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
_mainViewModel.CancelSnapshot();
}
}
}

View File

@ -0,0 +1,55 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.modernwpf.com/2019">
<Style
x:Key="IconOnlyButtonStyle"
BasedOn="{StaticResource DefaultButtonStyle}"
TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border
x:Name="Background"
Background="Transparent"
CornerRadius="{TemplateBinding ui:ControlHelper.CornerRadius}"
SnapsToDevicePixels="True">
<Border
x:Name="Border"
Padding="{TemplateBinding Padding}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding ui:ControlHelper.CornerRadius}">
<ContentPresenter
x:Name="ContentPresenter"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Focusable="False"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Background" Property="Background" Value="{DynamicResource ButtonBackgroundPointerOver}" />
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPointerOver}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Background" Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPressed}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Background" Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@ -0,0 +1,39 @@
// 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.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace WorkspacesEditor.Telemetry
{
[EventData]
public class CreateEvent : EventBase, IEvent
{
public CreateEvent()
{
EventName = "Workspaces_CreateEvent";
}
// True if operation successfully completely. False if failed
public bool Successful { get; set; }
// Number of screens present in the project
public int NumScreens { get; set; }
// Total number of apps in the project
public int AppCount { get; set; }
// Number of apps with CLI args
public int CliCount { get; set; }
// Number of apps with "Launch as admin" set
public int AdminCount { get; set; }
// True of user checked "Create Shortcut". False if not.
public bool ShortcutCreated { get; set; }
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@ -0,0 +1,23 @@
// 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.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace WorkspacesEditor.Telemetry
{
[EventData]
public class DeleteEvent : EventBase, IEvent
{
public DeleteEvent()
{
EventName = "Workspaces_DeleteEvent";
}
public bool Successful { get; set; }
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@ -0,0 +1,48 @@
// 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.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace WorkspacesEditor.Telemetry
{
[EventData]
public class EditEvent : EventBase, IEvent
{
public EditEvent()
{
EventName = "Workspaces_EditEvent";
}
// True if operation successfully completely. False if failed.
public bool Successful { get; set; }
// Change in number of screens in project
public int ScreenCountDelta { get; set; }
// Number of apps added to project through editing
public int AppsAdded { get; set; }
// Number of apps removed from project through editing
public int AppsRemoved { get; set; }
// Number of apps with CLI args added
public int CliAdded { get; set; }
// Number of apps with CLI args removed
public int CliRemoved { get; set; }
// Number of apps with admin added
public int AdminAdded { get; set; }
// Number of apps with admin removed
public int AdminRemoved { get; set; }
// True if used window pixel sizing boxes to adjust size
public bool PixelAdjustmentsUsed { get; set; }
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

View File

@ -0,0 +1,26 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<!-- Metadata -->
<system:String x:Key="Theme.Name">Dark.Accent1</system:String>
<system:String x:Key="Theme.Origin">Origin</system:String>
<system:String x:Key="Theme.DisplayName">Accent1 (Dark)</system:String>
<system:String x:Key="Theme.BaseColorScheme">Dark</system:String>
<system:String x:Key="Theme.ColorScheme">Accent1</system:String>
<Color x:Key="Theme.PrimaryAccentColor">Black</Color>
<SolidColorBrush x:Key="PrimaryBackgroundBrush" Color="#FF242424" />
<SolidColorBrush x:Key="SecondaryBackgroundBrush" Color="#FF2b2b2b" />
<SolidColorBrush x:Key="TertiaryBackgroundBrush" Color="#FF373737" />
<SolidColorBrush x:Key="QuaternaryBackgroundBrush" Color="#FF272727" />
<SolidColorBrush x:Key="MonitorViewBackgroundBrush" Color="#FF161616" />
<SolidColorBrush x:Key="PrimaryForegroundBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="SecondaryForegroundBrush" Color="#FF9a9a9a" />
<SolidColorBrush x:Key="PrimaryBorderBrush" Color="#FF373737" />
<SolidColorBrush x:Key="SecondaryBorderBrush" Color="#FF494949" />
<SolidColorBrush x:Key="TertiaryBorderBrush" Color="#FF202020" />
<SolidColorBrush x:Key="BackdropBrush" Color="#40F0F0F0" />
<SolidColorBrush x:Key="TitleBarSecondaryForegroundBrush" Color="#FF9a9a9a" />
</ResourceDictionary>

View File

@ -0,0 +1,26 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<!-- Metadata -->
<system:String x:Key="Theme.Name">HighContrast.Accent2</system:String>
<system:String x:Key="Theme.Origin">Origin</system:String>
<system:String x:Key="Theme.DisplayName">Accent2 (HighContrast)</system:String>
<system:String x:Key="Theme.BaseColorScheme">HighContrast</system:String>
<system:String x:Key="Theme.ColorScheme">Accent2</system:String>
<Color x:Key="Theme.PrimaryAccentColor">White</Color>
<SolidColorBrush x:Key="PrimaryBackgroundBrush" Color="#FF242424" />
<SolidColorBrush x:Key="SecondaryBackgroundBrush" Color="#FF1c1c1c" />
<SolidColorBrush x:Key="TertiaryBackgroundBrush" Color="#FF202020" />
<SolidColorBrush x:Key="QuaternaryBackgroundBrush" Color="#FF272727" />
<SolidColorBrush x:Key="MonitorViewBackgroundBrush" Color="#FF161616" />
<SolidColorBrush x:Key="PrimaryForegroundBrush" Color="#FFffff00" />
<SolidColorBrush x:Key="SecondaryForegroundBrush" Color="#FF00ff00" />
<SolidColorBrush x:Key="PrimaryBorderBrush" Color="#FF373737" />
<SolidColorBrush x:Key="SecondaryBorderBrush" Color="#FF494949" />
<SolidColorBrush x:Key="TertiaryBorderBrush" Color="#FF202020" />
<SolidColorBrush x:Key="BackdropBrush" Color="#E55B5B5B" />
<SolidColorBrush x:Key="TitleBarSecondaryForegroundBrush" Color="#FF9a9a9a" />
</ResourceDictionary>

View File

@ -0,0 +1,26 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<!-- Metadata -->
<system:String x:Key="Theme.Name">HighContrast.Accent3</system:String>
<system:String x:Key="Theme.Origin">Origin</system:String>
<system:String x:Key="Theme.DisplayName">Accent3 (HighContrast)</system:String>
<system:String x:Key="Theme.BaseColorScheme">HighContrast</system:String>
<system:String x:Key="Theme.ColorScheme">Accent3</system:String>
<Color x:Key="Theme.PrimaryAccentColor">White</Color>
<SolidColorBrush x:Key="PrimaryBackgroundBrush" Color="#FF242424" />
<SolidColorBrush x:Key="SecondaryBackgroundBrush" Color="#FF1c1c1c" />
<SolidColorBrush x:Key="TertiaryBackgroundBrush" Color="#FF202020" />
<SolidColorBrush x:Key="QuaternaryBackgroundBrush" Color="#FF272727" />
<SolidColorBrush x:Key="MonitorViewBackgroundBrush" Color="#FF161616" />
<SolidColorBrush x:Key="PrimaryForegroundBrush" Color="#FFffff00" />
<SolidColorBrush x:Key="SecondaryForegroundBrush" Color="#FFc0c0c0" />
<SolidColorBrush x:Key="PrimaryBorderBrush" Color="#FF373737" />
<SolidColorBrush x:Key="SecondaryBorderBrush" Color="#FF494949" />
<SolidColorBrush x:Key="TertiaryBorderBrush" Color="#FF202020" />
<SolidColorBrush x:Key="BackdropBrush" Color="#E55B5B5B" />
<SolidColorBrush x:Key="TitleBarSecondaryForegroundBrush" Color="#FF9a9a9a" />
</ResourceDictionary>

View File

@ -0,0 +1,26 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<!-- Metadata -->
<system:String x:Key="Theme.Name">HighContrast.Accent4</system:String>
<system:String x:Key="Theme.Origin">Origin</system:String>
<system:String x:Key="Theme.DisplayName">Accent4 (HighContrast)</system:String>
<system:String x:Key="Theme.BaseColorScheme">HighContrast</system:String>
<system:String x:Key="Theme.ColorScheme">Accent4</system:String>
<Color x:Key="Theme.PrimaryAccentColor">White</Color>
<SolidColorBrush x:Key="PrimaryBackgroundBrush" Color="#FF242424" />
<SolidColorBrush x:Key="SecondaryBackgroundBrush" Color="#FF1c1c1c" />
<SolidColorBrush x:Key="TertiaryBackgroundBrush" Color="#FF202020" />
<SolidColorBrush x:Key="QuaternaryBackgroundBrush" Color="#FF272727" />
<SolidColorBrush x:Key="MonitorViewBackgroundBrush" Color="#FF161616" />
<SolidColorBrush x:Key="PrimaryForegroundBrush" Color="#FFffffff" />
<SolidColorBrush x:Key="SecondaryForegroundBrush" Color="#FF1aebff" />
<SolidColorBrush x:Key="PrimaryBorderBrush" Color="#FF373737" />
<SolidColorBrush x:Key="SecondaryBorderBrush" Color="#FF494949" />
<SolidColorBrush x:Key="TertiaryBorderBrush" Color="#FF202020" />
<SolidColorBrush x:Key="BackdropBrush" Color="#E55B5B5B" />
<SolidColorBrush x:Key="TitleBarSecondaryForegroundBrush" Color="#FF9a9a9a" />
</ResourceDictionary>

View File

@ -0,0 +1,26 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<!-- Metadata -->
<system:String x:Key="Theme.Name">HighContrast.Accent5</system:String>
<system:String x:Key="Theme.Origin">Origin</system:String>
<system:String x:Key="Theme.DisplayName">Accent5 (HighContrast)</system:String>
<system:String x:Key="Theme.BaseColorScheme">HighContrast</system:String>
<system:String x:Key="Theme.ColorScheme">Accent5</system:String>
<Color x:Key="Theme.PrimaryAccentColor">White</Color>
<SolidColorBrush x:Key="PrimaryBackgroundBrush" Color="#FFf9f9f9" />
<SolidColorBrush x:Key="SecondaryBackgroundBrush" Color="#FFeeeeee" />
<SolidColorBrush x:Key="TertiaryBackgroundBrush" Color="#FFF3F3F3" />
<SolidColorBrush x:Key="QuaternaryBackgroundBrush" Color="#FFf6f6f6" />
<SolidColorBrush x:Key="MonitorViewBackgroundBrush" Color="#FF161616" />
<SolidColorBrush x:Key="PrimaryForegroundBrush" Color="#FF000000" />
<SolidColorBrush x:Key="SecondaryForegroundBrush" Color="#FF37006e" />
<SolidColorBrush x:Key="PrimaryBorderBrush" Color="#FF373737" />
<SolidColorBrush x:Key="SecondaryBorderBrush" Color="#FF494949" />
<SolidColorBrush x:Key="TertiaryBorderBrush" Color="#FF202020" />
<SolidColorBrush x:Key="BackdropBrush" Color="#E5949494" />
<SolidColorBrush x:Key="TitleBarSecondaryForegroundBrush" Color="#FF949494" />
</ResourceDictionary>

View File

@ -0,0 +1,26 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<!-- Metadata -->
<system:String x:Key="Theme.Name">Light.Accent1</system:String>
<system:String x:Key="Theme.Origin">Origin</system:String>
<system:String x:Key="Theme.DisplayName">Accent1 (Light)</system:String>
<system:String x:Key="Theme.BaseColorScheme">Light</system:String>
<system:String x:Key="Theme.ColorScheme">Accent1</system:String>
<Color x:Key="Theme.PrimaryAccentColor">White</Color>
<SolidColorBrush x:Key="PrimaryBackgroundBrush" Color="#FFf3f3f3" />
<SolidColorBrush x:Key="SecondaryBackgroundBrush" Color="#FFfbfbfb" />
<SolidColorBrush x:Key="TertiaryBackgroundBrush" Color="#FFfefefe" />
<SolidColorBrush x:Key="QuaternaryBackgroundBrush" Color="#FFf6f6f6" />
<SolidColorBrush x:Key="MonitorViewBackgroundBrush" Color="#FFF9F9F9" />
<SolidColorBrush x:Key="PrimaryForegroundBrush" Color="#FF191919" />
<SolidColorBrush x:Key="SecondaryForegroundBrush" Color="#FF6A6A6A" />
<SolidColorBrush x:Key="PrimaryBorderBrush" Color="#FFe5e5e5" />
<SolidColorBrush x:Key="SecondaryBorderBrush" Color="#FFe5e5e5" />
<SolidColorBrush x:Key="TertiaryBorderBrush" Color="#FFe5e5e5" />
<SolidColorBrush x:Key="BackdropBrush" Color="#85F0F0F0" />
<SolidColorBrush x:Key="TitleBarSecondaryForegroundBrush" Color="#FF949494" />
</ResourceDictionary>

View File

@ -0,0 +1,18 @@
// 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;
namespace WorkspacesEditor.Utils
{
public class DashCaseNamingPolicy : JsonNamingPolicy
{
public static DashCaseNamingPolicy Instance { get; } = new DashCaseNamingPolicy();
public override string ConvertName(string name)
{
return name.UpperCamelCaseToDashCase();
}
}
}

View File

@ -0,0 +1,358 @@
// 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.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Windows.Media.Imaging;
using ManagedCommon;
using WorkspacesEditor.Models;
namespace WorkspacesEditor.Utils
{
public class DrawHelper
{
private static Font font = new("Tahoma", 24);
private static double scale = 0.1;
private static double gapWidth;
private static double gapHeight;
public static BitmapImage DrawPreview(Project project, Rectangle bounds, Theme currentTheme)
{
List<double> horizontalGaps = new List<double>();
List<double> verticalGaps = new List<double>();
gapWidth = bounds.Width * 0.01;
gapHeight = bounds.Height * 0.01;
int Scaled(double value)
{
return (int)(value * scale);
}
int TransformX(double posX)
{
double gapTransform = verticalGaps.Where(x => x <= posX).Count() * gapWidth;
return Scaled(posX - bounds.Left + gapTransform);
}
int TransformY(double posY)
{
double gapTransform = horizontalGaps.Where(x => x <= posY).Count() * gapHeight;
return Scaled(posY - bounds.Top + gapTransform);
}
Rectangle GetAppRect(Application app)
{
if (app.Maximized)
{
Project project = app.Parent;
var monitor = project.Monitors.Where(x => x.MonitorNumber == app.MonitorNumber).FirstOrDefault();
return new Rectangle(TransformX(monitor.MonitorDpiAwareBounds.Left), TransformY(monitor.MonitorDpiAwareBounds.Top), Scaled(monitor.MonitorDpiAwareBounds.Width), Scaled(monitor.MonitorDpiAwareBounds.Height));
}
else
{
return new Rectangle(TransformX(app.ScaledPosition.X), TransformY(app.ScaledPosition.Y), Scaled(app.ScaledPosition.Width), Scaled(app.ScaledPosition.Height));
}
}
Dictionary<string, int> repeatCounter = new Dictionary<string, int>();
var appsIncluded = project.Applications.Where(x => x.IsIncluded);
foreach (Application app in appsIncluded)
{
if (repeatCounter.TryGetValue(app.AppPath, out int value))
{
repeatCounter[app.AppPath] = ++value;
}
else
{
repeatCounter.Add(app.AppPath, 1);
}
app.RepeatIndex = repeatCounter[app.AppPath];
}
foreach (Application app in project.Applications.Where(x => !x.IsIncluded))
{
app.RepeatIndex = 0;
}
// now that all repeat index values are set, update the repeat index strings on UI
foreach (Application app in project.Applications)
{
app.OnPropertyChanged(new PropertyChangedEventArgs("RepeatIndexString"));
}
foreach (MonitorSetup monitor in project.Monitors)
{
// check for vertical gap
if (monitor.MonitorDpiAwareBounds.Left > bounds.Left && project.Monitors.Any(x => x.MonitorDpiAwareBounds.Right <= monitor.MonitorDpiAwareBounds.Left))
{
verticalGaps.Add(monitor.MonitorDpiAwareBounds.Left);
}
// check for horizontal gap
if (monitor.MonitorDpiAwareBounds.Top > bounds.Top && project.Monitors.Any(x => x.MonitorDpiAwareBounds.Bottom <= monitor.MonitorDpiAwareBounds.Top))
{
horizontalGaps.Add(monitor.MonitorDpiAwareBounds.Top);
}
}
Bitmap previewBitmap = new Bitmap(Scaled(bounds.Width + (verticalGaps.Count * gapWidth)), Scaled((bounds.Height * 1.2) + (horizontalGaps.Count * gapHeight)));
double desiredIconSize = Scaled(Math.Min(bounds.Width, bounds.Height)) * 0.25;
using (Graphics g = Graphics.FromImage(previewBitmap))
{
g.SmoothingMode = SmoothingMode.AntiAlias;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
Brush brush = new SolidBrush(currentTheme == Theme.Dark ? Color.FromArgb(10, 255, 255, 255) : Color.FromArgb(10, 0, 0, 0));
Brush brushForHighlight = new SolidBrush(currentTheme == Theme.Dark ? Color.FromArgb(192, 255, 255, 255) : Color.FromArgb(192, 0, 0, 0));
// draw the monitors
foreach (MonitorSetup monitor in project.Monitors)
{
Brush monitorBrush = new SolidBrush(currentTheme == Theme.Dark ? Color.FromArgb(32, 7, 91, 155) : Color.FromArgb(32, 7, 91, 155));
g.FillRectangle(monitorBrush, new Rectangle(TransformX(monitor.MonitorDpiAwareBounds.Left), TransformY(monitor.MonitorDpiAwareBounds.Top), Scaled(monitor.MonitorDpiAwareBounds.Width), Scaled(monitor.MonitorDpiAwareBounds.Height)));
}
var appsToDraw = appsIncluded.Where(x => !x.Minimized);
// draw the highlighted app at the end to have its icon in the foreground for the case there are overlapping icons
foreach (Application app in appsToDraw.Where(x => !x.IsHighlighted))
{
Rectangle rect = GetAppRect(app);
DrawWindow(g, brush, rect, app, desiredIconSize, currentTheme);
}
foreach (Application app in appsToDraw.Where(x => x.IsHighlighted))
{
Rectangle rect = GetAppRect(app);
DrawWindow(g, brushForHighlight, rect, app, desiredIconSize, currentTheme);
}
// draw the minimized windows
Rectangle rectMinimized = new Rectangle(0, Scaled((bounds.Height * 1.02) + (horizontalGaps.Count * gapHeight)), Scaled(bounds.Width + (verticalGaps.Count * gapWidth)), Scaled(bounds.Height * 0.18));
DrawWindow(g, brush, brushForHighlight, rectMinimized, appsIncluded.Where(x => x.Minimized), currentTheme);
}
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();
return bitmapImage;
}
}
public static void DrawWindow(Graphics graphics, Brush brush, Rectangle bounds, Application app, double desiredIconSize, Theme currentTheme)
{
if (graphics == null)
{
return;
}
if (brush == null)
{
return;
}
using (GraphicsPath path = RoundedRect(bounds))
{
if (app.IsHighlighted)
{
graphics.DrawPath(new Pen(currentTheme == Theme.Dark ? Color.White : Color.DarkGray, graphics.VisibleClipBounds.Height / 50), path);
}
else
{
graphics.DrawPath(new Pen(currentTheme == Theme.Dark ? Color.FromArgb(128, 82, 82, 82) : Color.FromArgb(128, 160, 160, 160), graphics.VisibleClipBounds.Height / 200), path);
}
graphics.FillPath(brush, path);
}
double iconSize = Math.Min(Math.Min(bounds.Width - 4, bounds.Height - 4), desiredIconSize);
Rectangle iconBounds = new Rectangle((int)(bounds.Left + (bounds.Width / 2) - (iconSize / 2)), (int)(bounds.Top + (bounds.Height / 2) - (iconSize / 2)), (int)iconSize, (int)iconSize);
try
{
graphics.DrawIcon(app.Icon, iconBounds);
if (app.RepeatIndex > 1)
{
string indexString = app.RepeatIndex.ToString(CultureInfo.InvariantCulture);
int indexSize = (int)(iconBounds.Width * 0.5);
Rectangle indexBounds = new Rectangle(iconBounds.Right - indexSize, iconBounds.Bottom - indexSize, indexSize, indexSize);
var textSize = graphics.MeasureString(indexString, font);
var state = graphics.Save();
graphics.TranslateTransform(indexBounds.Left, indexBounds.Top);
graphics.ScaleTransform(indexBounds.Width / textSize.Width, indexBounds.Height / textSize.Height);
graphics.DrawString(indexString, font, Brushes.Black, PointF.Empty);
graphics.Restore(state);
}
}
catch (Exception)
{
// sometimes drawing an icon throws an exception despite that the icon seems to be ok
}
}
public static void DrawWindow(Graphics graphics, Brush brush, Brush brushForHighlight, Rectangle bounds, IEnumerable<Application> apps, Theme currentTheme)
{
int appsCount = apps.Count();
if (appsCount == 0)
{
return;
}
if (graphics == null)
{
return;
}
if (brush == null)
{
return;
}
using (GraphicsPath path = RoundedRect(bounds))
{
if (apps.Where(x => x.IsHighlighted).Any())
{
graphics.DrawPath(new Pen(currentTheme == Theme.Dark ? Color.White : Color.DarkGray, graphics.VisibleClipBounds.Height / 50), path);
graphics.FillPath(brushForHighlight, path);
}
else
{
graphics.DrawPath(new Pen(currentTheme == Theme.Dark ? Color.FromArgb(128, 82, 82, 82) : Color.FromArgb(128, 160, 160, 160), graphics.VisibleClipBounds.Height / 200), path);
graphics.FillPath(brush, path);
}
}
double iconSize = Math.Min(bounds.Width, bounds.Height) * 0.5;
for (int iconCounter = 0; iconCounter < appsCount; iconCounter++)
{
Application app = apps.ElementAt(iconCounter);
Rectangle iconBounds = new Rectangle((int)(bounds.Left + (bounds.Width / 2) - (iconSize * ((appsCount / 2) - iconCounter))), (int)(bounds.Top + (bounds.Height / 2) - (iconSize / 2)), (int)iconSize, (int)iconSize);
try
{
graphics.DrawIcon(app.Icon, iconBounds);
if (app.RepeatIndex > 0)
{
string indexString = app.RepeatIndex.ToString(CultureInfo.InvariantCulture);
int indexSize = (int)(iconBounds.Width * 0.5);
Rectangle indexBounds = new Rectangle(iconBounds.Right - indexSize, iconBounds.Bottom - indexSize, indexSize, indexSize);
var textSize = graphics.MeasureString(indexString, font);
var state = graphics.Save();
graphics.TranslateTransform(indexBounds.Left, indexBounds.Top);
graphics.ScaleTransform(indexBounds.Width / textSize.Width, indexBounds.Height / textSize.Height);
graphics.DrawString(indexString, font, Brushes.Black, PointF.Empty);
graphics.Restore(state);
}
}
catch (Exception)
{
// sometimes drawing an icon throws an exception despite that the icon seems to be ok
}
}
}
public static BitmapImage DrawPreviewIcons(Project project)
{
int appsCount = project.Applications.Count;
if (appsCount == 0)
{
return null;
}
Bitmap previewBitmap = new Bitmap(32 * appsCount, 24);
using (Graphics graphics = Graphics.FromImage(previewBitmap))
{
graphics.SmoothingMode = SmoothingMode.AntiAlias;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
int appIndex = 0;
foreach (var app in project.Applications)
{
try
{
graphics.DrawIcon(app.Icon, new Rectangle(32 * appIndex, 0, 24, 24));
}
catch (Exception e)
{
Logger.LogError($"Exception while drawing the icon for app {app.AppName}. Exception message: {e.Message}");
}
appIndex++;
}
}
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();
return bitmapImage;
}
}
private static GraphicsPath RoundedRect(Rectangle bounds)
{
int minorSize = Math.Min(bounds.Width, bounds.Height);
int radius = (int)(minorSize / 8);
int diameter = radius * 2;
Size size = new Size(diameter, diameter);
Rectangle arc = new Rectangle(bounds.Location, size);
GraphicsPath path = new GraphicsPath();
if (radius == 0)
{
path.AddRectangle(bounds);
return path;
}
// top left arc
path.AddArc(arc, 180, 90);
// top right arc
arc.X = bounds.Right - diameter;
path.AddArc(arc, 270, 90);
// bottom right arc
arc.Y = bounds.Bottom - diameter;
path.AddArc(arc, 0, 90);
// bottom left arc
arc.X = bounds.Left;
path.AddArc(arc, 90, 90);
path.CloseFigure();
return path;
}
}
}

View File

@ -0,0 +1,28 @@
// 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.IO;
namespace WorkspacesEditor.Utils
{
public class FolderUtils
{
public static string Desktop()
{
return Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
}
public static string Temp()
{
return Path.GetTempPath();
}
// Note: the same path should be used in SnapshotTool and Launcher
public static string DataFolder()
{
return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\Microsoft\\PowerToys\\Workspaces";
}
}
}

View File

@ -0,0 +1,54 @@
// 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.IO;
using System.IO.Abstractions;
using System.Threading.Tasks;
namespace WorkspacesEditor.Utils
{
public class IOUtils
{
private readonly IFileSystem _fileSystem = new FileSystem();
public IOUtils()
{
}
public void WriteFile(string fileName, string data)
{
_fileSystem.File.WriteAllText(fileName, data);
}
public string ReadFile(string fileName)
{
if (_fileSystem.File.Exists(fileName))
{
var attempts = 0;
while (attempts < 10)
{
try
{
using (Stream inputStream = _fileSystem.File.Open(fileName, FileMode.Open))
using (StreamReader reader = new StreamReader(inputStream))
{
string data = reader.ReadToEnd();
inputStream.Close();
return data;
}
}
catch (Exception)
{
Task.Delay(10).Wait();
}
attempts++;
}
}
return string.Empty;
}
}
}

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.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
namespace WorkspacesEditor.Utils
{
public class MonitorHelper
{
private const int DpiAwarenessContextUnaware = -1;
private Screen[] screens;
[DllImport("user32.dll")]
private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
private void SaveDpiUnawareScreens()
{
SetThreadDpiAwarenessContext(DpiAwarenessContextUnaware);
screens = Screen.AllScreens;
}
private Screen[] GetDpiUnawareScreenBounds()
{
Thread dpiUnawareThread = new Thread(new ThreadStart(SaveDpiUnawareScreens));
dpiUnawareThread.Start();
dpiUnawareThread.Join();
return screens;
}
public static Screen[] GetDpiUnawareScreens()
{
MonitorHelper monitorHelper = new MonitorHelper();
return monitorHelper.GetDpiUnawareScreenBounds();
}
internal static double GetScreenDpiFromScreen(Screen screen)
{
var point = new System.Drawing.Point(screen.Bounds.Left + 1, screen.Bounds.Top + 1);
var monitor = NativeMethods.MonitorFromPoint(point, NativeMethods._MONITOR_DEFAULTTONEAREST);
NativeMethods.GetDpiForMonitor(monitor, NativeMethods.DpiType.EFFECTIVE, out uint dpiX, out uint dpiY);
return dpiX / 96.0;
}
}
}

View File

@ -0,0 +1,58 @@
// 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.Runtime.InteropServices;
namespace WorkspacesEditor.Utils
{
internal sealed class NativeMethods
{
public const int SW_RESTORE = 9;
public const int SW_NORMAL = 1;
public const int SW_MINIMIZE = 6;
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
[DllImport("USER32.DLL")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
public static extern void SwitchToThisWindow(IntPtr hWnd, bool fAltTab);
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr processId);
[DllImport("kernel32.dll")]
public static extern uint GetCurrentThreadId();
[DllImport("user32.dll")]
public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
public enum DpiType
{
EFFECTIVE = 0,
ANGULAR = 1,
RAW = 2,
}
[DllImport("User32.dll")]
public static extern IntPtr MonitorFromPoint([In] System.Drawing.Point pt, [In] uint dwFlags);
[DllImport("Shcore.dll")]
public static extern IntPtr GetDpiForMonitor([In] IntPtr hmonitor, [In] DpiType dpiType, [Out] out uint dpiX, [Out] out uint dpiY);
public const int _S_OK = 0;
public const int _MONITOR_DEFAULTTONEAREST = 2;
public const int _E_INVALIDARG = -2147024809;
}
}

View File

@ -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.
namespace WorkspacesEditor.Utils
{
public struct ParsingResult
{
public bool Result { get; }
public string Message { get; }
public string MalformedData { get; }
public ParsingResult(bool result, string message = "", string data = "")
{
Result = result;
Message = message;
MalformedData = data;
}
}
}

View File

@ -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 Microsoft.PowerToys.Settings.UI.Library;
namespace WorkspacesEditor.Utils
{
public class Settings
{
private const string WorkspacesModuleName = "Workspaces";
private static SettingsUtils _settingsUtils = new SettingsUtils();
public static WorkspacesSettings ReadSettings()
{
if (!_settingsUtils.SettingsExists(WorkspacesModuleName))
{
var defaultWorkspacesSettings = new WorkspacesSettings();
defaultWorkspacesSettings.Save(_settingsUtils);
return defaultWorkspacesSettings;
}
WorkspacesSettings settings = _settingsUtils.GetSettingsOrDefault<WorkspacesSettings>(WorkspacesModuleName);
return settings;
}
}
}

View File

@ -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.Linq;
namespace WorkspacesEditor.Utils
{
public static class StringUtils
{
public static string UpperCamelCaseToDashCase(this string str)
{
// If it's single letter variable, leave it as it is
if (str.Length == 1)
{
return str;
}
return string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "-" + x.ToString() : x.ToString())).ToLowerInvariant();
}
}
}

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 System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ManagedCommon;
using WorkspacesEditor.Data;
using WorkspacesEditor.Models;
using WorkspacesEditor.ViewModels;
namespace WorkspacesEditor.Utils
{
public class WorkspacesEditorIO
{
public WorkspacesEditorIO()
{
}
public ParsingResult ParseWorkspaces(MainViewModel mainViewModel)
{
try
{
WorkspacesData parser = new WorkspacesData();
if (!File.Exists(parser.File))
{
Logger.LogWarning($"Workspaces storage file not found: {parser.File}");
return new ParsingResult(true);
}
WorkspacesData.WorkspacesListWrapper workspaces = parser.Read(parser.File);
if (workspaces.Workspaces == null)
{
return new ParsingResult(true);
}
if (!SetWorkspaces(mainViewModel, workspaces))
{
Logger.LogWarning($"Workspaces storage file content could not be set. Reason: {Properties.Resources.Error_Parsing_Message}");
return new ParsingResult(false, WorkspacesEditor.Properties.Resources.Error_Parsing_Message);
}
return new ParsingResult(true);
}
catch (Exception e)
{
Logger.LogError($"Exception while parsing storage file: {e.Message}");
return new ParsingResult(false, e.Message);
}
}
public Project ParseTempProject()
{
try
{
ProjectData parser = new ProjectData();
if (!File.Exists(TempProjectData.File))
{
Logger.LogWarning($"ParseProject method. Workspaces storage file not found: {TempProjectData.File}");
return null;
}
Project project = new Project(parser.Read(TempProjectData.File));
return project;
}
catch (Exception e)
{
Logger.LogError($"ParseProject method. Exception while parsing storage file: {e.Message}");
return null;
}
}
public void SerializeWorkspaces(List<Project> workspaces, bool useTempFile = false)
{
WorkspacesData serializer = new WorkspacesData();
WorkspacesData.WorkspacesListWrapper workspacesWrapper = new WorkspacesData.WorkspacesListWrapper { };
workspacesWrapper.Workspaces = new List<ProjectData.ProjectWrapper>();
foreach (Project project in workspaces)
{
ProjectData.ProjectWrapper wrapper = new ProjectData.ProjectWrapper
{
Id = project.Id,
Name = project.Name,
CreationTime = project.CreationTime,
IsShortcutNeeded = project.IsShortcutNeeded,
MoveExistingWindows = project.MoveExistingWindows,
LastLaunchedTime = project.LastLaunchedTime,
Applications = new List<ProjectData.ApplicationWrapper> { },
MonitorConfiguration = new List<ProjectData.MonitorConfigurationWrapper> { },
};
foreach (var app in project.Applications.Where(x => x.IsIncluded))
{
wrapper.Applications.Add(new ProjectData.ApplicationWrapper
{
Application = app.AppName,
ApplicationPath = app.AppPath,
Title = app.AppTitle,
PackageFullName = app.PackageFullName,
AppUserModelId = app.AppUserModelId,
CommandLineArguments = app.CommandLineArguments,
IsElevated = app.IsElevated,
CanLaunchElevated = app.CanLaunchElevated,
Maximized = app.Maximized,
Minimized = app.Minimized,
Position = new ProjectData.ApplicationWrapper.WindowPositionWrapper
{
X = app.Position.X,
Y = app.Position.Y,
Height = app.Position.Height,
Width = app.Position.Width,
},
Monitor = app.MonitorNumber,
});
}
foreach (var monitor in project.Monitors)
{
wrapper.MonitorConfiguration.Add(new ProjectData.MonitorConfigurationWrapper
{
Id = monitor.MonitorName,
InstanceId = monitor.MonitorInstanceId,
MonitorNumber = monitor.MonitorNumber,
Dpi = monitor.Dpi,
MonitorRectDpiAware = new ProjectData.MonitorConfigurationWrapper.MonitorRectWrapper
{
Left = (int)monitor.MonitorDpiAwareBounds.Left,
Top = (int)monitor.MonitorDpiAwareBounds.Top,
Width = (int)monitor.MonitorDpiAwareBounds.Width,
Height = (int)monitor.MonitorDpiAwareBounds.Height,
},
MonitorRectDpiUnaware = new ProjectData.MonitorConfigurationWrapper.MonitorRectWrapper
{
Left = (int)monitor.MonitorDpiUnawareBounds.Left,
Top = (int)monitor.MonitorDpiUnawareBounds.Top,
Width = (int)monitor.MonitorDpiUnawareBounds.Width,
Height = (int)monitor.MonitorDpiUnawareBounds.Height,
},
});
}
workspacesWrapper.Workspaces.Add(wrapper);
}
try
{
IOUtils ioUtils = new IOUtils();
ioUtils.WriteFile(useTempFile ? TempProjectData.File : serializer.File, serializer.Serialize(workspacesWrapper));
}
catch (Exception e)
{
// TODO: show error
Logger.LogError($"Exception while writing storage file: {e.Message}");
}
}
private bool AddWorkspaces(MainViewModel mainViewModel, WorkspacesData.WorkspacesListWrapper workspaces)
{
foreach (var project in workspaces.Workspaces)
{
mainViewModel.Workspaces.Add(new Project(project));
}
mainViewModel.Initialize();
return true;
}
private bool SetWorkspaces(MainViewModel mainViewModel, WorkspacesData.WorkspacesListWrapper workspaces)
{
mainViewModel.Workspaces = new System.Collections.ObjectModel.ObservableCollection<Project> { };
return AddWorkspaces(mainViewModel, workspaces);
}
internal void SerializeTempProject(Project project)
{
SerializeWorkspaces(new List<Project>() { project }, true);
}
}
}

View File

@ -0,0 +1,153 @@
// 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.Linq;
using ManagedCommon;
namespace WorkspacesEditor.Utils
{
public class WorkspacesIcon : IDisposable
{
private const int IconSize = 128;
public static readonly Brush LightThemeIconBackground = new SolidBrush(Color.FromArgb(255, 239, 243, 251));
public static readonly Brush LightThemeIconForeground = new SolidBrush(Color.FromArgb(255, 47, 50, 56));
public static readonly Brush DarkThemeIconBackground = new SolidBrush(Color.FromArgb(255, 55, 55, 55));
public static readonly Brush DarkThemeIconForeground = new SolidBrush(Color.FromArgb(255, 228, 228, 228));
public static readonly Font IconFont = new("Aptos", 24, FontStyle.Bold);
public static string IconTextFromProjectName(string projectName)
{
string result = string.Empty;
char[] delimiterChars = { ' ', ',', '.', ':', '-', '\t' };
string[] words = projectName.Split(delimiterChars);
foreach (string word in words)
{
if (string.IsNullOrEmpty(word))
{
continue;
}
if (word.All(char.IsDigit))
{
result += word;
}
else
{
result += word.ToUpper(System.Globalization.CultureInfo.CurrentCulture).ToCharArray()[0];
}
}
return result;
}
public static Bitmap DrawIcon(string text, Theme currentTheme)
{
Brush background = currentTheme == Theme.Dark ? DarkThemeIconBackground : LightThemeIconBackground;
Brush foreground = currentTheme == Theme.Dark ? DarkThemeIconForeground : LightThemeIconForeground;
Bitmap bitmap = new Bitmap(IconSize, IconSize);
using (Graphics graphics = Graphics.FromImage(bitmap))
{
graphics.SmoothingMode = SmoothingMode.AntiAlias;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
graphics.FillEllipse(background, 0, 0, IconSize, IconSize);
var textSize = graphics.MeasureString(text, IconFont);
var state = graphics.Save();
// Calculate scaling factors
float scaleX = (float)IconSize / textSize.Width;
float scaleY = (float)IconSize / textSize.Height;
float scale = Math.Min(scaleX, scaleY) * 0.8f; // Use the smaller scale factor to maintain aspect ratio
// Calculate the position to center the text
float textX = (IconSize - (textSize.Width * scale)) / 2;
float textY = ((IconSize - (textSize.Height * scale)) / 2) + 6;
graphics.TranslateTransform(textX, textY);
graphics.ScaleTransform(scale, scale);
graphics.DrawString(text, IconFont, foreground, 0, 0);
graphics.Restore(state);
}
return bitmap;
}
public static void SaveIcon(Bitmap icon, string path)
{
if (Path.Exists(path))
{
File.Delete(path);
}
FileStream fileStream = new FileStream(path, FileMode.CreateNew);
using (var memoryStream = new MemoryStream())
{
icon.Save(memoryStream, ImageFormat.Png);
BinaryWriter iconWriter = new BinaryWriter(fileStream);
if (fileStream != null && iconWriter != null)
{
// 0-1 reserved, 0
iconWriter.Write((byte)0);
iconWriter.Write((byte)0);
// 2-3 image type, 1 = icon, 2 = cursor
iconWriter.Write((short)1);
// 4-5 number of images
iconWriter.Write((short)1);
// image entry 1
// 0 image width
iconWriter.Write((byte)IconSize);
// 1 image height
iconWriter.Write((byte)IconSize);
// 2 number of colors
iconWriter.Write((byte)0);
// 3 reserved
iconWriter.Write((byte)0);
// 4-5 color planes
iconWriter.Write((short)0);
// 6-7 bits per pixel
iconWriter.Write((short)32);
// 8-11 size of image data
iconWriter.Write((int)memoryStream.Length);
// 12-15 offset of image data
iconWriter.Write((int)(6 + 16));
// write image data
// png data must contain the whole png data file
iconWriter.Write(memoryStream.ToArray());
iconWriter.Flush();
}
}
fileStream.Flush();
fileStream.Close();
}
public void Dispose()
{
GC.SuppressFinalize(this);
}
}
}

View File

@ -0,0 +1,598 @@
// 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.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Timers;
using System.Windows;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Telemetry;
using WorkspacesEditor.Data;
using WorkspacesEditor.Models;
using WorkspacesEditor.Telemetry;
using WorkspacesEditor.Utils;
using static WorkspacesEditor.Data.WorkspacesData;
namespace WorkspacesEditor.ViewModels
{
public class MainViewModel : INotifyPropertyChanged, IDisposable
{
private WorkspacesEditorIO _workspacesEditorIO;
private ProjectEditor editPage;
private SnapshotWindow _snapshotWindow;
private List<OverlayWindow> _overlayWindows = new List<OverlayWindow>();
private Project editedProject;
private Project projectBeforeLaunch;
private string projectNameBeingEdited;
private MainWindow _mainWindow;
private Timer lastUpdatedTimer;
private WorkspacesSettings settings;
public ObservableCollection<Project> Workspaces { get; set; } = new ObservableCollection<Project>();
public IEnumerable<Project> WorkspacesView
{
get
{
IEnumerable<Project> workspaces = GetFilteredWorkspaces();
IsWorkspacesViewEmpty = !(workspaces != null && workspaces.Any());
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsWorkspacesViewEmpty)));
if (IsWorkspacesViewEmpty)
{
if (Workspaces != null && Workspaces.Any())
{
EmptyWorkspacesViewMessage = Properties.Resources.NoWorkspacesMatch;
}
else
{
EmptyWorkspacesViewMessage = Properties.Resources.No_Workspaces_Message;
}
OnPropertyChanged(new PropertyChangedEventArgs(nameof(EmptyWorkspacesViewMessage)));
return Enumerable.Empty<Project>();
}
OrderBy orderBy = (OrderBy)_orderByIndex;
if (orderBy == OrderBy.LastViewed)
{
return workspaces.OrderByDescending(x => x.LastLaunchedTime);
}
else if (orderBy == OrderBy.Created)
{
return workspaces.OrderByDescending(x => x.CreationTime);
}
else
{
return workspaces.OrderBy(x => x.Name);
}
}
}
public bool IsWorkspacesViewEmpty { get; set; }
public string EmptyWorkspacesViewMessage { get; set; }
// return those workspaces where the project name or any of the selected apps' name contains the search term
private IEnumerable<Project> GetFilteredWorkspaces()
{
if (string.IsNullOrEmpty(_searchTerm))
{
return Workspaces;
}
return Workspaces.Where(x =>
{
if (x.Name.Contains(_searchTerm, StringComparison.InvariantCultureIgnoreCase))
{
return true;
}
if (x.Applications == null)
{
return false;
}
return x.Applications.Any(app => app.AppName.Contains(_searchTerm, StringComparison.InvariantCultureIgnoreCase));
});
}
private string _searchTerm;
public string SearchTerm
{
get => _searchTerm;
set
{
_searchTerm = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView)));
}
}
private int _orderByIndex;
public int OrderByIndex
{
get => _orderByIndex;
set
{
_orderByIndex = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView)));
settings.Properties.SortBy = (WorkspacesProperties.SortByProperty)value;
settings.Save(new SettingsUtils());
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChanged?.Invoke(this, e);
}
public MainViewModel(WorkspacesEditorIO workspacesEditorIO)
{
settings = Utils.Settings.ReadSettings();
_orderByIndex = (int)settings.Properties.SortBy;
_workspacesEditorIO = workspacesEditorIO;
lastUpdatedTimer = new System.Timers.Timer();
lastUpdatedTimer.Interval = 1000;
lastUpdatedTimer.Elapsed += LastUpdatedTimerElapsed;
lastUpdatedTimer.Start();
}
public void Initialize()
{
foreach (Project project in Workspaces)
{
project.Initialize(App.ThemeManager.GetCurrentTheme());
}
OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView)));
}
public void SetEditedProject(Project editedProject)
{
this.editedProject = editedProject;
}
public void SaveProject(Project projectToSave)
{
SendEditTelemetryEvent(projectToSave, editedProject);
if (editedProject.Name != projectToSave.Name)
{
RemoveShortcut(editedProject);
}
editedProject.Name = projectToSave.Name;
editedProject.IsShortcutNeeded = projectToSave.IsShortcutNeeded;
editedProject.MoveExistingWindows = projectToSave.MoveExistingWindows;
editedProject.PreviewIcons = projectToSave.PreviewIcons;
editedProject.PreviewImage = projectToSave.PreviewImage;
editedProject.Applications = projectToSave.Applications.Where(x => x.IsIncluded).ToList();
editedProject.OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs("AppsCountString"));
editedProject.Initialize(App.ThemeManager.GetCurrentTheme());
_workspacesEditorIO.SerializeWorkspaces(Workspaces.ToList());
ApplyShortcut(editedProject);
}
private void ApplyShortcut(Project project)
{
string basePath = AppDomain.CurrentDomain.BaseDirectory;
string shortcutAddress = Path.Combine(FolderUtils.Desktop(), project.Name + ".lnk");
string shortcutIconFilename = Path.Combine(FolderUtils.Temp(), project.Id + ".ico");
if (!project.IsShortcutNeeded)
{
if (File.Exists(shortcutIconFilename))
{
File.Delete(shortcutIconFilename);
}
if (File.Exists(shortcutAddress))
{
File.Delete(shortcutAddress);
}
return;
}
Bitmap icon = WorkspacesIcon.DrawIcon(WorkspacesIcon.IconTextFromProjectName(project.Name), App.ThemeManager.GetCurrentTheme());
WorkspacesIcon.SaveIcon(icon, shortcutIconFilename);
try
{
// Workaround to be able to create a shortcut with unicode filename
File.WriteAllBytes(shortcutAddress, Array.Empty<byte>());
// Create a ShellLinkObject that references the .lnk file
Shell32.Shell shell = new Shell32.Shell();
Shell32.Folder dir = shell.NameSpace(FolderUtils.Desktop());
Shell32.FolderItem folderItem = dir.Items().Item($"{project.Name}.lnk");
Shell32.ShellLinkObject link = (Shell32.ShellLinkObject)folderItem.GetLink;
// Set the .lnk file properties
link.Description = $"Project Launcher {project.Id}";
link.Path = Path.Combine(basePath, "PowerToys.WorkspacesLauncher.exe");
link.Arguments = $"{project.Id.ToString()} {(int)InvokePoint.Shortcut}";
link.WorkingDirectory = basePath;
link.SetIconLocation(shortcutIconFilename, 0);
link.Save(shortcutAddress);
}
catch (Exception ex)
{
Logger.LogError($"Shortcut creation error: {ex.Message}");
}
}
public void SaveProjectName(Project project)
{
projectNameBeingEdited = project.Name;
}
public void CancelProjectName(Project project)
{
project.Name = projectNameBeingEdited;
}
public async void SnapWorkspace()
{
CancelSnapshot();
await Task.Run(() => RunSnapshotTool());
Project project = _workspacesEditorIO.ParseTempProject();
if (project != null)
{
if (editedProject != null)
{
project.UpdateAfterLaunchAndEdit(projectBeforeLaunch);
project.EditorWindowTitle = Properties.Resources.EditWorkspace;
editPage.DataContext = project;
CheckShortcutPresence(project);
project.Initialize(App.ThemeManager.GetCurrentTheme());
}
else
{
EditProject(project, true);
}
}
}
internal void RevertLaunch()
{
CheckShortcutPresence(projectBeforeLaunch);
editPage.DataContext = projectBeforeLaunch;
projectBeforeLaunch.Initialize(App.ThemeManager.GetCurrentTheme());
}
public void EditProject(Project selectedProject, bool isNewlyCreated = false)
{
editPage = new ProjectEditor(this);
SetEditedProject(selectedProject);
if (!isNewlyCreated)
{
selectedProject = new Project(selectedProject);
}
if (isNewlyCreated)
{
// generate a default name for the new project
string defaultNamePrefix = Properties.Resources.DefaultWorkspaceNamePrefix;
int nextProjectIndex = 0;
foreach (var proj in Workspaces)
{
if (proj.Name.StartsWith(defaultNamePrefix, StringComparison.CurrentCulture))
{
try
{
int index = int.Parse(proj.Name[(defaultNamePrefix.Length + 1)..], CultureInfo.CurrentCulture);
if (nextProjectIndex < index)
{
nextProjectIndex = index;
}
}
catch (Exception)
{
}
}
}
selectedProject.Name = defaultNamePrefix + " " + (nextProjectIndex + 1).ToString(CultureInfo.CurrentCulture);
}
selectedProject.EditorWindowTitle = isNewlyCreated ? Properties.Resources.CreateWorkspace : Properties.Resources.EditWorkspace;
selectedProject.Initialize(App.ThemeManager.GetCurrentTheme());
CheckShortcutPresence(selectedProject);
editPage.DataContext = selectedProject;
_mainWindow.ShowPage(editPage);
lastUpdatedTimer.Stop();
}
private void CheckShortcutPresence(Project project)
{
string basePath = AppDomain.CurrentDomain.BaseDirectory;
string shortcutAddress = Path.Combine(FolderUtils.Desktop(), project.Name + ".lnk");
project.IsShortcutNeeded = File.Exists(shortcutAddress);
}
public void AddNewProject(Project project)
{
project.Applications.RemoveAll(app => !app.IsIncluded);
project.Initialize(App.ThemeManager.GetCurrentTheme());
Workspaces.Add(project);
_workspacesEditorIO.SerializeWorkspaces(Workspaces.ToList());
TempProjectData.DeleteTempFile();
OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView)));
ApplyShortcut(project);
SendCreateTelemetryEvent(project);
}
public void DeleteProject(Project selectedProject)
{
Workspaces.Remove(selectedProject);
_workspacesEditorIO.SerializeWorkspaces(Workspaces.ToList());
RemoveShortcut(selectedProject);
OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView)));
SendDeleteTelemetryEvent();
}
private void RemoveShortcut(Project selectedProject)
{
string shortcutAddress = Path.Combine(FolderUtils.Desktop(), selectedProject.Name + ".lnk");
string shortcutIconFilename = Path.Combine(FolderUtils.Temp(), selectedProject.Id + ".ico");
if (File.Exists(shortcutIconFilename))
{
File.Delete(shortcutIconFilename);
}
if (File.Exists(shortcutAddress))
{
File.Delete(shortcutAddress);
}
}
public void SetMainWindow(MainWindow mainWindow)
{
_mainWindow = mainWindow;
}
public void SwitchToMainView()
{
_mainWindow.SwitchToMainView();
SearchTerm = string.Empty;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(SearchTerm)));
lastUpdatedTimer.Start();
editedProject = null;
}
public void LaunchProject(string projectId)
{
if (!Workspaces.Where(x => x.Id == projectId).Any())
{
Logger.LogWarning($"Workspace to launch not found. Id: {projectId}");
return;
}
LaunchProject(Workspaces.Where(x => x.Id == projectId).First(), true);
}
public async void LaunchProject(Project project, bool exitAfterLaunch = false)
{
await Task.Run(() => RunLauncher(project.Id, InvokePoint.EditorButton));
if (_workspacesEditorIO.ParseWorkspaces(this).Result == true)
{
OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView)));
}
if (exitAfterLaunch)
{
Logger.LogInfo($"Launched the Workspace {project.Name}. Exiting.");
Environment.Exit(0);
}
}
private void LastUpdatedTimerElapsed(object sender, ElapsedEventArgs e)
{
if (Workspaces == null)
{
return;
}
foreach (Project project in Workspaces)
{
project.OnPropertyChanged(new PropertyChangedEventArgs("LastLaunched"));
}
}
private void RunSnapshotTool(string filename = null)
{
Process process = new Process();
process.StartInfo = new ProcessStartInfo(@".\PowerToys.WorkspacesSnapshotTool.exe");
process.StartInfo.CreateNoWindow = true;
if (!string.IsNullOrEmpty(filename))
{
process.StartInfo.Arguments = filename;
}
try
{
process.Start();
process.WaitForExit();
}
catch (Exception ex)
{
MessageBox.Show($"An error occurred: {ex.Message}");
}
}
private void RunLauncher(string projectId, InvokePoint invokePoint)
{
Process process = new Process();
process.StartInfo = new ProcessStartInfo(@".\PowerToys.WorkspacesLauncher.exe", $"{projectId} {(int)invokePoint}");
process.StartInfo.CreateNoWindow = true;
try
{
process.Start();
process.WaitForExit();
}
catch (Exception ex)
{
MessageBox.Show($"An error occurred: {ex.Message}");
}
}
public void Dispose()
{
GC.SuppressFinalize(this);
}
internal void CloseAllPopups()
{
foreach (Project project in Workspaces)
{
project.IsPopupVisible = false;
}
}
internal void EnterSnapshotMode(bool isExistingProjectLaunched)
{
_mainWindow.WindowState = System.Windows.WindowState.Minimized;
_overlayWindows.Clear();
foreach (var screen in MonitorHelper.GetDpiUnawareScreens())
{
var bounds = screen.Bounds;
OverlayWindow overlayWindow = new OverlayWindow();
overlayWindow.Top = bounds.Top;
overlayWindow.Left = bounds.Left;
overlayWindow.Width = bounds.Width;
overlayWindow.Height = bounds.Height;
overlayWindow.ShowActivated = true;
overlayWindow.Topmost = true;
overlayWindow.Show();
_overlayWindows.Add(overlayWindow);
}
_snapshotWindow = new SnapshotWindow(this);
_snapshotWindow.ShowActivated = true;
_snapshotWindow.Topmost = true;
_snapshotWindow.Show();
}
internal void CancelSnapshot()
{
foreach (OverlayWindow overlayWindow in _overlayWindows)
{
overlayWindow.Close();
}
_mainWindow.WindowState = System.Windows.WindowState.Normal;
}
internal async void LaunchAndEdit(Project project)
{
await Task.Run(() => RunLauncher(project.Id, InvokePoint.LaunchAndEdit));
projectBeforeLaunch = new Project(project);
EnterSnapshotMode(true);
}
private void SendCreateTelemetryEvent(Project project)
{
var telemetryEvent = new CreateEvent();
telemetryEvent.Successful = true;
telemetryEvent.NumScreens = project.Monitors.Count;
telemetryEvent.AppCount = project.Applications.Count;
telemetryEvent.CliCount = project.Applications.FindAll(app => app.CommandLineArguments.Length > 0).Count;
telemetryEvent.ShortcutCreated = project.IsShortcutNeeded;
telemetryEvent.AdminCount = project.Applications.FindAll(app => app.IsElevated).Count;
PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
}
private void SendEditTelemetryEvent(Project updatedProject, Project prevProject)
{
int appsRemovedCount = updatedProject.Applications.FindAll(val => !val.IsIncluded).Count;
foreach (var app in prevProject.Applications)
{
var updatedApp = updatedProject.Applications.Find(val => app.AppName == val.AppName && app.Position == val.Position);
if (updatedApp == null)
{
appsRemovedCount++;
}
}
int appsAddedCount = 0;
int cliAdded = 0, cliRemoved = 0;
int adminAdded = 0, adminRemoved = 0;
foreach (var app in updatedProject.Applications)
{
var prevApp = prevProject.Applications.Find(val => app.AppName == val.AppName && app.Position == val.Position);
if (prevApp == null)
{
if (app.IsIncluded)
{
appsAddedCount++;
}
continue;
}
if (app.CommandLineArguments.Length > 0 && prevApp.CommandLineArguments.Length == 0)
{
cliAdded++;
}
if (prevApp.CommandLineArguments.Length > 0 && app.CommandLineArguments.Length == 0)
{
cliRemoved++;
}
if (app.IsElevated && !prevApp.IsElevated)
{
adminAdded++;
}
if (!app.IsElevated && prevApp.IsElevated)
{
adminRemoved++;
}
}
var telemetryEvent = new EditEvent();
telemetryEvent.Successful = true;
telemetryEvent.ScreenCountDelta = updatedProject.Monitors.Count - prevProject.Monitors.Count;
telemetryEvent.AppsAdded = appsAddedCount;
telemetryEvent.AppsRemoved = appsRemovedCount;
telemetryEvent.CliAdded = cliAdded;
telemetryEvent.CliRemoved = cliRemoved;
telemetryEvent.AdminAdded = adminAdded;
telemetryEvent.AdminRemoved = adminRemoved;
telemetryEvent.PixelAdjustmentsUsed = updatedProject.IsPositionChangedManually;
PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
}
private void SendDeleteTelemetryEvent()
{
var telemetryEvent = new EditEvent();
telemetryEvent.Successful = true;
PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
}
}
}

View File

@ -0,0 +1,106 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<PropertyGroup>
<AssemblyTitle>PowerToys.WorkspacesEditor</AssemblyTitle>
<AssemblyDescription>PowerToys Workspaces Editor</AssemblyDescription>
<Description>PowerToys Workspaces Editor</Description>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
</PropertyGroup>
<PropertyGroup>
<ProjectGuid>{367D7543-7DBA-4381-99F1-BF6142A996C4}</ProjectGuid>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>images\Workspaces.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AssemblyName>PowerToys.WorkspacesEditor</AssemblyName>
</PropertyGroup>
<ItemGroup>
<None Remove="images\DefaultIcon.ico" />
<None Remove="images\Workspaces.ico" />
</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>
<Content Include="images\DefaultIcon.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="images\Workspaces.ico" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="app.manifest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ControlzEx" />
<PackageReference Include="ModernWpfUI" />
<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" />
</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>
<ItemGroup>
<_DeploymentManifestIconFile Remove="images\Workspaces.ico" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,420 @@
<Page
x:Class="WorkspacesEditor.ProjectEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:WorkspacesEditor.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WorkspacesEditor"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="clr-namespace:WorkspacesEditor.Models"
xmlns:props="clr-namespace:WorkspacesEditor.Properties"
Title="Workspaces Editor"
Background="{DynamicResource PrimaryBackgroundBrush}"
mc:Ignorable="d">
<Page.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVis" />
<Style x:Key="TextBlockEnabledStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource PrimaryForegroundBrush}" />
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{DynamicResource SecondaryForegroundBrush}" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="ButtonEnabledStyle" TargetType="Button">
<Setter Property="Foreground" Value="{DynamicResource PrimaryForegroundBrush}" />
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{DynamicResource SecondaryForegroundBrush}" />
</Trigger>
</Style.Triggers>
</Style>
<DataTemplate x:Key="headerTemplate">
<Border HorizontalAlignment="Stretch">
<TextBlock
Margin="0,0,20,0"
VerticalAlignment="Center"
DockPanel.Dock="Left"
FontSize="14"
FontWeight="Normal"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="{Binding MonitorName}" />
</Border>
</DataTemplate>
<DataTemplate x:Key="appTemplate">
<Border
Margin="1"
Background="{DynamicResource SecondaryBackgroundBrush}"
MouseEnter="AppBorder_MouseEnter"
MouseLeave="AppBorder_MouseLeave">
<Expander
Margin="0,0,20,0"
FlowDirection="RightToLeft"
IsEnabled="{Binding IsIncluded, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
IsExpanded="{Binding IsExpanded, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<Expander.Header>
<Grid HorizontalAlignment="{Binding HorizontalAlignment, RelativeSource={RelativeSource AncestorType=ContentPresenter}, Mode=OneWayToSource}" FlowDirection="LeftToRight">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" />
<ColumnDefinition Width="60" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Margin="5,0,0,0"
VerticalAlignment="Center"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
FontSize="14"
FontWeight="Normal"
Foreground="#EED202"
Text="&#xE7BA;"
ToolTip="{x:Static props:Resources.NotFoundTooltip}"
Visibility="{Binding IsNotFound, Converter={StaticResource BoolToVis}, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
<Image
Grid.Column="1"
Width="20"
Height="20"
Margin="10"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Source="{Binding IconBitmapImage}" />
<TextBlock
Grid.Column="2"
Width="20"
VerticalAlignment="Center"
FontSize="14"
FontWeight="Normal"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="{Binding RepeatIndexString, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
<StackPanel Grid.Column="3" VerticalAlignment="Center">
<TextBlock
FontSize="14"
FontWeight="Normal"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="{Binding AppName}" />
<TextBlock
FontSize="12"
FontWeight="Normal"
Foreground="{DynamicResource SecondaryForegroundBrush}"
Text="{Binding AppMainParams, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Visibility="{Binding IsAppMainParamVisible, Converter={StaticResource BoolToVis}, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
<controls:ResetIsEnabled Grid.Column="4">
<Button
Width="120"
Margin="10,5"
Padding="24,6"
AutomationProperties.Name="{x:Static props:Resources.Delete}"
Background="{DynamicResource TertiaryBackgroundBrush}"
Click="DeleteButtonClicked"
Content="{Binding DeleteButtonContent, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="True" />
</controls:ResetIsEnabled>
</Grid>
</Expander.Header>
<Grid
Margin="-20,0,0,0"
HorizontalAlignment="{Binding HorizontalAlignment, RelativeSource={RelativeSource AncestorType=ContentPresenter}, Mode=OneWayToSource}"
Background="{DynamicResource QuaternaryBackgroundBrush}"
FlowDirection="LeftToRight">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<DockPanel Margin="100,5,0,0">
<TextBlock
VerticalAlignment="Center"
FontSize="14"
FontWeight="Normal"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="{x:Static props:Resources.CliArguments}" />
<TextBox
x:Name="CommandLineTextBox"
Margin="15,0,50,0"
HorizontalAlignment="Stretch"
VerticalContentAlignment="Center"
Background="{DynamicResource TertiaryBackgroundBrush}"
BorderThickness="0"
FontSize="14"
FontWeight="Normal"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="{Binding CommandLineArguments, Mode=TwoWay}"
TextChanged="CommandLineTextBox_TextChanged" />
</DockPanel>
<StackPanel
Grid.Row="1"
Margin="100,5,0,0"
Orientation="Horizontal">
<CheckBox
MinWidth="10"
Content="{x:Static props:Resources.LaunchAsAdmin}"
IsChecked="{Binding IsElevated, Mode=TwoWay}"
IsEnabled="{Binding CanLaunchElevated, Mode=OneWay}" />
<CheckBox
MinWidth="10"
Margin="15,0,0,0"
Checked="MaximizedChecked"
Content="{x:Static props:Resources.Maximized}"
IsChecked="{Binding Maximized, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<CheckBox
MinWidth="10"
Margin="15,0,0,0"
Checked="MinimizedChecked"
Content="{x:Static props:Resources.Minimized}"
IsChecked="{Binding Minimized, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
<StackPanel
Grid.Row="2"
Margin="100,5,0,0"
Orientation="Horizontal">
<TextBlock
VerticalAlignment="Center"
FontSize="14"
FontWeight="Normal"
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource TextBlockEnabledStyle}"
Text="{x:Static props:Resources.Left}" />
<TextBox
x:Name="LeftTextBox"
Margin="15,0,0,0"
HorizontalAlignment="Stretch"
VerticalContentAlignment="Center"
Background="{DynamicResource TertiaryBackgroundBrush}"
BorderThickness="0"
FontSize="14"
FontWeight="Normal"
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Text="{Binding Position.X, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
TextChanged="LeftTextBox_TextChanged" />
<TextBlock
Margin="15,0,0,0"
VerticalAlignment="Center"
FontSize="14"
FontWeight="Normal"
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource TextBlockEnabledStyle}"
Text="{x:Static props:Resources.Top}" />
<TextBox
x:Name="TopTextBox"
Margin="15,0,0,0"
HorizontalAlignment="Stretch"
VerticalContentAlignment="Center"
Background="{DynamicResource TertiaryBackgroundBrush}"
BorderThickness="0"
FontSize="14"
FontWeight="Normal"
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Text="{Binding Position.Y, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
TextChanged="TopTextBox_TextChanged" />
<TextBlock
Margin="15,0,0,0"
VerticalAlignment="Center"
FontSize="14"
FontWeight="Normal"
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource TextBlockEnabledStyle}"
Text="{x:Static props:Resources.Width}" />
<TextBox
x:Name="WidthTextBox"
Margin="15,0,0,0"
HorizontalAlignment="Stretch"
VerticalContentAlignment="Center"
Background="{DynamicResource TertiaryBackgroundBrush}"
BorderThickness="0"
FontSize="14"
FontWeight="Normal"
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Text="{Binding Position.Width, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
TextChanged="WidthTextBox_TextChanged" />
<TextBlock
Margin="15,0,0,0"
VerticalAlignment="Center"
FontSize="14"
FontWeight="Normal"
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource TextBlockEnabledStyle}"
Text="{x:Static props:Resources.Height}" />
<TextBox
x:Name="HeightTextBox"
Margin="15,0,0,0"
HorizontalAlignment="Stretch"
VerticalContentAlignment="Center"
Background="{DynamicResource TertiaryBackgroundBrush}"
BorderThickness="0"
FontSize="14"
FontWeight="Normal"
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Text="{Binding Position.Height, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
TextChanged="HeightTextBox_TextChanged" />
</StackPanel>
</Grid>
</Expander>
</Border>
</DataTemplate>
<models:AppListDataTemplateSelector
x:Key="AppListDataTemplateSelector"
AppTemplate="{StaticResource appTemplate}"
HeaderTemplate="{StaticResource headerTemplate}" />
</Page.Resources>
<Grid Margin="40,0,40,40">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<Button
Margin="0,20,0,20"
VerticalAlignment="Center"
Background="Transparent"
Click="CancelButtonClicked">
<TextBlock
VerticalAlignment="Center"
FontSize="24"
FontWeight="Normal"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="{x:Static props:Resources.Workspaces}" />
</Button>
<TextBlock
Margin="10,0,0,0"
VerticalAlignment="Center"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
FontSize="16"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="&#xE76C;" />
<TextBlock
Margin="10,0,0,0"
VerticalAlignment="Center"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="{Binding EditorWindowTitle}" />
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Vertical">
<TextBlock
FontSize="14"
FontWeight="Normal"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="{x:Static props:Resources.WorkspaceName}" />
<TextBox
x:Name="EditNameTextBox"
Width="320"
Margin="0,6,0,6"
HorizontalAlignment="Left"
Background="{DynamicResource SecondaryBackgroundBrush}"
BorderBrush="{DynamicResource PrimaryBorderBrush}"
BorderThickness="2"
GotFocus="EditNameTextBox_GotFocus"
KeyDown="EditNameTextBoxKeyDown"
Text="{Binding Name, Mode=TwoWay}"
TextChanged="EditNameTextBox_TextChanged" />
</StackPanel>
<Border
Grid.Row="2"
HorizontalAlignment="Stretch"
Background="{DynamicResource MonitorViewBackgroundBrush}"
CornerRadius="5">
<DockPanel>
<Image
Width="{Binding PreviewImageWidth, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Height="200"
Margin="2"
DockPanel.Dock="Top"
Source="{Binding PreviewImage, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Stretch="Fill" />
<Button
x:Name="RevertButton"
Height="36"
Margin="0,0,20,10"
Padding="24,0,24,0"
HorizontalAlignment="Right"
AutomationProperties.Name="{x:Static props:Resources.Revert}"
Background="{DynamicResource SecondaryBackgroundBrush}"
Click="RevertButtonClicked"
Content="{x:Static props:Resources.Revert}"
DockPanel.Dock="Right"
IsEnabled="{Binding IsRevertEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
<Button
x:Name="LaunchEditButton"
Height="36"
Margin="0,0,10,10"
Padding="24,0,24,0"
HorizontalAlignment="Right"
AutomationProperties.Name="{x:Static props:Resources.LaunchEdit}"
Background="{DynamicResource SecondaryBackgroundBrush}"
Click="LaunchEditButtonClicked"
Content="{x:Static props:Resources.LaunchEdit}"
DockPanel.Dock="Right" />
</DockPanel>
</Border>
<ScrollViewer
Grid.Row="4"
Margin="0,10,0,0"
PreviewMouseWheel="ScrollViewer_PreviewMouseWheel"
VerticalScrollBarVisibility="Auto">
<StackPanel Orientation="Vertical">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ItemsControl ItemTemplateSelector="{StaticResource AppListDataTemplateSelector}" ItemsSource="{Binding ApplicationsListed, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</StackPanel>
</ScrollViewer>
<StackPanel
Grid.Row="5"
Margin="0,5,0,0"
Orientation="Horizontal"
Visibility="Collapsed">
<CheckBox
Margin="0,0,0,0"
VerticalAlignment="Center"
Content="{x:Static props:Resources.MoveIfExist}"
FontSize="14"
FontWeight="Normal"
Foreground="{DynamicResource PrimaryForegroundBrush}"
IsChecked="{Binding MoveExistingWindows, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
<DockPanel Grid.Row="6" Margin="0,20,0,20">
<CheckBox
Content="{x:Static props:Resources.CreateShortcut}"
DockPanel.Dock="Left"
FontSize="14"
IsChecked="{Binding IsShortcutNeeded, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<StackPanel
Margin="40,0,0,0"
HorizontalAlignment="Right"
DockPanel.Dock="Right"
Orientation="Horizontal">
<Button
x:Name="CancelButton"
Height="36"
Margin="20,0,0,0"
Padding="24,0,24,0"
AutomationProperties.Name="{x:Static props:Resources.Cancel}"
Background="{DynamicResource SecondaryBackgroundBrush}"
Click="CancelButtonClicked"
Content="{x:Static props:Resources.Cancel}" />
<Button
x:Name="SaveButton"
Height="36"
Margin="20,0,0,0"
Padding="24,0,24,0"
AutomationProperties.Name="{x:Static props:Resources.Save_Workspace}"
Click="SaveButtonClicked"
Content="{x:Static props:Resources.Save_Workspace}"
IsEnabled="{Binding CanBeSaved, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource AccentButtonStyle}" />
</StackPanel>
</DockPanel>
</Grid>
</Page>

View File

@ -0,0 +1,221 @@
// 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.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using WorkspacesEditor.Data;
using WorkspacesEditor.Models;
using WorkspacesEditor.ViewModels;
namespace WorkspacesEditor
{
/// <summary>
/// Interaction logic for ProjectEditor.xaml
/// </summary>
public partial class ProjectEditor : Page
{
private const double ScrollSpeed = 15;
private MainViewModel _mainViewModel;
public ProjectEditor(MainViewModel mainViewModel)
{
_mainViewModel = mainViewModel;
InitializeComponent();
}
private void SaveButtonClicked(object sender, RoutedEventArgs e)
{
Project projectToSave = this.DataContext as Project;
projectToSave.CloseExpanders();
if (_mainViewModel.Workspaces.Any(x => x.Id == projectToSave.Id))
{
_mainViewModel.SaveProject(projectToSave);
}
else
{
_mainViewModel.AddNewProject(projectToSave);
}
_mainViewModel.SwitchToMainView();
}
private void CancelButtonClicked(object sender, RoutedEventArgs e)
{
// delete the temp file created by the snapshot tool
TempProjectData.DeleteTempFile();
_mainViewModel.SwitchToMainView();
}
private void DeleteButtonClicked(object sender, RoutedEventArgs e)
{
Button button = sender as Button;
Models.Application app = button.DataContext as Models.Application;
app.SwitchDeletion();
}
private void EditNameTextBoxKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
e.Handled = true;
Project project = this.DataContext as Project;
TextBox textBox = sender as TextBox;
project.Name = textBox.Text;
}
else if (e.Key == Key.Escape)
{
e.Handled = true;
Project project = this.DataContext as Project;
_mainViewModel.CancelProjectName(project);
}
}
private void EditNameTextBox_GotFocus(object sender, RoutedEventArgs e)
{
_mainViewModel.SaveProjectName(DataContext as Project);
}
private void AppBorder_MouseEnter(object sender, MouseEventArgs e)
{
Border border = sender as Border;
Models.Application app = border.DataContext as Models.Application;
app.IsHighlighted = true;
Project project = app.Parent;
project.Initialize(App.ThemeManager.GetCurrentTheme());
}
private void AppBorder_MouseLeave(object sender, MouseEventArgs e)
{
Border border = sender as Border;
Models.Application app = border.DataContext as Models.Application;
if (app == null)
{
return;
}
app.IsHighlighted = false;
Project project = app.Parent;
project.Initialize(App.ThemeManager.GetCurrentTheme());
}
private void EditNameTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
Project project = this.DataContext as Project;
TextBox textBox = sender as TextBox;
project.Name = textBox.Text;
project.OnPropertyChanged(new PropertyChangedEventArgs(nameof(Project.CanBeSaved)));
}
private void LeftTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = sender as TextBox;
Models.Application application = textBox.DataContext as Models.Application;
int newPos;
if (!int.TryParse(textBox.Text, out newPos))
{
newPos = 0;
}
application.Position = new Models.Application.WindowPosition() { X = newPos, Y = application.Position.Y, Width = application.Position.Width, Height = application.Position.Height };
Project project = application.Parent;
project.IsPositionChangedManually = true;
project.Initialize(App.ThemeManager.GetCurrentTheme());
}
private void TopTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = sender as TextBox;
Models.Application application = textBox.DataContext as Models.Application;
int newPos;
if (!int.TryParse(textBox.Text, out newPos))
{
newPos = 0;
}
application.Position = new Models.Application.WindowPosition() { X = application.Position.X, Y = newPos, Width = application.Position.Width, Height = application.Position.Height };
Project project = application.Parent;
project.IsPositionChangedManually = true;
project.Initialize(App.ThemeManager.GetCurrentTheme());
}
private void WidthTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = sender as TextBox;
Models.Application application = textBox.DataContext as Models.Application;
int newPos;
if (!int.TryParse(textBox.Text, out newPos))
{
newPos = 0;
}
application.Position = new Models.Application.WindowPosition() { X = application.Position.X, Y = application.Position.Y, Width = newPos, Height = application.Position.Height };
Project project = application.Parent;
project.IsPositionChangedManually = true;
project.Initialize(App.ThemeManager.GetCurrentTheme());
}
private void HeightTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = sender as TextBox;
Models.Application application = textBox.DataContext as Models.Application;
int newPos;
if (!int.TryParse(textBox.Text, out newPos))
{
newPos = 0;
}
application.Position = new Models.Application.WindowPosition() { X = application.Position.X, Y = application.Position.Y, Width = application.Position.Width, Height = newPos };
Project project = application.Parent;
project.IsPositionChangedManually = true;
project.Initialize(App.ThemeManager.GetCurrentTheme());
}
private void CommandLineTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = sender as TextBox;
Models.Application application = textBox.DataContext as Models.Application;
application.CommandLineTextChanged(textBox.Text);
}
private void MaximizedChecked(object sender, RoutedEventArgs e)
{
CheckBox checkBox = sender as CheckBox;
Models.Application application = checkBox.DataContext as Models.Application;
application.MaximizedChecked();
}
private void MinimizedChecked(object sender, RoutedEventArgs e)
{
CheckBox checkBox = sender as CheckBox;
Models.Application application = checkBox.DataContext as Models.Application;
application.MinimizedChecked();
}
private void LaunchEditButtonClicked(object sender, RoutedEventArgs e)
{
Button button = sender as Button;
Project project = button.DataContext as Project;
_mainViewModel.LaunchAndEdit(project);
}
private void RevertButtonClicked(object sender, RoutedEventArgs e)
{
_mainViewModel.RevertLaunch();
}
private void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
ScrollViewer scrollViewer = sender as ScrollViewer;
double scrollAmount = Math.Sign(e.Delta) * ScrollSpeed;
scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - scrollAmount);
e.Handled = true;
}
}
}

View File

@ -0,0 +1,74 @@
<?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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

View File

@ -0,0 +1,479 @@
#include "pch.h"
#include "AppLauncher.h"
#include <winrt/Windows.Management.Deployment.h>
#include <winrt/Windows.ApplicationModel.Core.h>
#include <shellapi.h>
#include <ShellScalingApi.h>
#include <filesystem>
#include <workspaces-common/MonitorEnumerator.h>
#include <workspaces-common/WindowEnumerator.h>
#include <workspaces-common/WindowFilter.h>
#include <WorkspacesLib/AppUtils.h>
#include <common/Display/dpi_aware.h>
#include <common/utils/winapi_error.h>
#include <LaunchingApp.h>
#include <LauncherUIHelper.h>
#include <RegistryUtils.h>
#include <WindowProperties/WorkspacesWindowPropertyUtils.h>
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Management::Deployment;
namespace FancyZones
{
inline bool allMonitorsHaveSameDpiScaling()
{
auto monitors = MonitorEnumerator::Enumerate();
if (monitors.size() < 2)
{
return true;
}
UINT firstMonitorDpiX;
UINT firstMonitorDpiY;
if (S_OK != GetDpiForMonitor(monitors[0].first, MDT_EFFECTIVE_DPI, &firstMonitorDpiX, &firstMonitorDpiY))
{
return false;
}
for (int i = 1; i < monitors.size(); i++)
{
UINT iteratedMonitorDpiX;
UINT iteratedMonitorDpiY;
if (S_OK != GetDpiForMonitor(monitors[i].first, MDT_EFFECTIVE_DPI, &iteratedMonitorDpiX, &iteratedMonitorDpiY) ||
iteratedMonitorDpiX != firstMonitorDpiX)
{
return false;
}
}
return true;
}
inline void ScreenToWorkAreaCoords(HWND window, HMONITOR monitor, RECT& rect)
{
MONITORINFOEXW monitorInfo{ sizeof(MONITORINFOEXW) };
GetMonitorInfoW(monitor, &monitorInfo);
auto xOffset = monitorInfo.rcWork.left - monitorInfo.rcMonitor.left;
auto yOffset = monitorInfo.rcWork.top - monitorInfo.rcMonitor.top;
DPIAware::Convert(monitor, rect);
auto referenceRect = RECT(rect.left - xOffset, rect.top - yOffset, rect.right - xOffset, rect.bottom - yOffset);
// Now, this rect should be used to determine the monitor and thus taskbar size. This fixes
// scenarios where the zone lies approximately between two monitors, and the taskbar is on the left.
monitor = MonitorFromRect(&referenceRect, MONITOR_DEFAULTTOPRIMARY);
GetMonitorInfoW(monitor, &monitorInfo);
xOffset = monitorInfo.rcWork.left - monitorInfo.rcMonitor.left;
yOffset = monitorInfo.rcWork.top - monitorInfo.rcMonitor.top;
rect.left -= xOffset;
rect.right -= xOffset;
rect.top -= yOffset;
rect.bottom -= yOffset;
}
inline bool SizeWindowToRect(HWND window, HMONITOR monitor, bool isMinimized, bool isMaximized, RECT rect) noexcept
{
WINDOWPLACEMENT placement{};
::GetWindowPlacement(window, &placement);
if (isMinimized)
{
placement.showCmd = SW_MINIMIZE;
}
else
{
if ((placement.showCmd != SW_SHOWMINIMIZED) &&
(placement.showCmd != SW_MINIMIZE))
{
if (placement.showCmd == SW_SHOWMAXIMIZED)
placement.flags &= ~WPF_RESTORETOMAXIMIZED;
placement.showCmd = SW_RESTORE;
}
ScreenToWorkAreaCoords(window, monitor, rect);
placement.rcNormalPosition = rect;
}
placement.flags |= WPF_ASYNCWINDOWPLACEMENT;
auto result = ::SetWindowPlacement(window, &placement);
if (!result)
{
Logger::error(L"SetWindowPlacement failed, {}", get_last_error_or_default(GetLastError()));
return false;
}
// make sure window is moved to the correct monitor before maximize.
if (isMaximized)
{
placement.showCmd = SW_SHOWMAXIMIZED;
}
// Do it again, allowing Windows to resize the window and set correct scaling
// This fixes Issue #365
result = ::SetWindowPlacement(window, &placement);
if (!result)
{
Logger::error(L"SetWindowPlacement failed, {}", get_last_error_or_default(GetLastError()));
return false;
}
return true;
}
}
namespace
{
LaunchingApps Prepare(std::vector<WorkspacesData::WorkspacesProject::Application>& apps, const Utils::Apps::AppList& installedApps)
{
LaunchingApps launchedApps{};
launchedApps.reserve(apps.size());
for (auto& app : apps)
{
// Packaged apps have version in the path, it will be outdated after update.
// We need make sure the current package is up to date.
if (!app.packageFullName.empty())
{
auto installedApp = std::find_if(installedApps.begin(), installedApps.end(), [&](const Utils::Apps::AppData& val) { return val.name == app.name; });
if (installedApp != installedApps.end() && app.packageFullName != installedApp->packageFullName)
{
std::wstring exeFileName = app.path.substr(app.path.find_last_of(L"\\") + 1);
app.packageFullName = installedApp->packageFullName;
app.path = installedApp->installPath + L"\\" + exeFileName;
Logger::trace(L"Updated package full name for {}: {}", app.name, app.packageFullName);
}
}
launchedApps.push_back({ app, nullptr, L"waiting" });
}
return launchedApps;
}
bool AllWindowsFound(const LaunchingApps& launchedApps)
{
return std::find_if(launchedApps.begin(), launchedApps.end(), [&](const LaunchingApp& val) {
return val.window == nullptr;
}) == launchedApps.end();
};
bool AddOpenedWindows(LaunchingApps& launchedApps, const std::vector<HWND>& windows, const Utils::Apps::AppList& installedApps)
{
bool statusChanged = false;
for (HWND window : windows)
{
auto installedAppData = Utils::Apps::GetApp(window, installedApps);
if (!installedAppData.has_value())
{
continue;
}
auto insertionIter = launchedApps.end();
for (auto iter = launchedApps.begin(); iter != launchedApps.end(); ++iter)
{
if (iter->window == nullptr && installedAppData.value().name == iter->application.name)
{
insertionIter = iter;
}
// keep the window at the same position if it's already opened
WINDOWPLACEMENT placement{};
::GetWindowPlacement(window, &placement);
HMONITOR monitor = MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY);
UINT dpi = DPIAware::DEFAULT_DPI;
DPIAware::GetScreenDPIForMonitor(monitor, dpi);
float x = static_cast<float>(placement.rcNormalPosition.left);
float y = static_cast<float>(placement.rcNormalPosition.top);
float width = static_cast<float>(placement.rcNormalPosition.right - placement.rcNormalPosition.left);
float height = static_cast<float>(placement.rcNormalPosition.bottom - placement.rcNormalPosition.top);
DPIAware::InverseConvert(monitor, x, y);
DPIAware::InverseConvert(monitor, width, height);
WorkspacesData::WorkspacesProject::Application::Position windowPosition{
.x = static_cast<int>(std::round(x)),
.y = static_cast<int>(std::round(y)),
.width = static_cast<int>(std::round(width)),
.height = static_cast<int>(std::round(height)),
};
if (iter->application.position == windowPosition)
{
Logger::debug(L"{} window already found at {} {}.", iter->application.name, iter->application.position.x, iter->application.position.y);
insertionIter = iter;
break;
}
}
if (insertionIter != launchedApps.end())
{
insertionIter->window = window;
insertionIter->state = L"launched";
statusChanged = true;
}
if (AllWindowsFound(launchedApps))
{
break;
}
}
return statusChanged;
}
}
bool LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated, ErrorList& launchErrors)
{
SHELLEXECUTEINFO sei = { 0 };
sei.cbSize = sizeof(SHELLEXECUTEINFO);
sei.hwnd = nullptr;
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;
sei.lpVerb = elevated ? L"runas" : L"open";
sei.lpFile = appPath.c_str();
sei.lpParameters = commandLineArgs.c_str();
sei.lpDirectory = nullptr;
sei.nShow = SW_SHOWNORMAL;
if (!ShellExecuteEx(&sei))
{
auto error = GetLastError();
Logger::error(L"Failed to launch process. {}", get_last_error_or_default(error));
launchErrors.push_back({ std::filesystem::path(appPath).filename(), get_last_error_or_default(error) });
return false;
}
return true;
}
bool LaunchPackagedApp(const std::wstring& packageFullName, ErrorList& launchErrors)
{
try
{
PackageManager packageManager;
for (const auto& package : packageManager.FindPackagesForUser({}))
{
if (package.Id().FullName() == packageFullName)
{
auto getAppListEntriesOperation = package.GetAppListEntriesAsync();
auto appEntries = getAppListEntriesOperation.get();
if (appEntries.Size() > 0)
{
IAsyncOperation<bool> launchOperation = appEntries.GetAt(0).LaunchAsync();
bool launchResult = launchOperation.get();
return launchResult;
}
else
{
Logger::error(L"No app entries found for the package.");
launchErrors.push_back({ packageFullName, L"No app entries found for the package." });
}
}
}
}
catch (const hresult_error& ex)
{
Logger::error(L"Packaged app launching error: {}", ex.message());
launchErrors.push_back({ packageFullName, ex.message().c_str() });
}
return false;
}
bool Launch(const WorkspacesData::WorkspacesProject::Application& app, ErrorList& launchErrors)
{
bool launched{ false };
// packaged apps: check protocol in registry
// usage example: Settings with cmd args
if (!app.packageFullName.empty())
{
auto names = RegistryUtils::GetUriProtocolNames(app.packageFullName);
if (!names.empty())
{
Logger::trace(L"Launching packaged by protocol with command line args {}", app.name);
std::wstring uriProtocolName = names[0];
std::wstring command = std::wstring(uriProtocolName + (app.commandLineArgs.starts_with(L":") ? L"" : L":") + app.commandLineArgs);
launched = LaunchApp(command, L"", app.isElevated, launchErrors);
}
else
{
Logger::info(L"Uri protocol names not found for {}", app.packageFullName);
}
}
// packaged apps: try launching first by AppUserModel.ID
// usage example: elevated Terminal
if (!launched && !app.appUserModelId.empty() && !app.packageFullName.empty())
{
Logger::trace(L"Launching {} as {}", app.name, app.appUserModelId);
launched = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated, launchErrors);
}
// packaged apps: try launching by package full name
// doesn't work for elevated apps or apps with command line args
if (!launched && !app.packageFullName.empty() && app.commandLineArgs.empty() && !app.isElevated)
{
Logger::trace(L"Launching packaged app {}", app.name);
launched = LaunchPackagedApp(app.packageFullName, launchErrors);
}
if (!launched)
{
Logger::trace(L"Launching {} at {}", app.name, app.path);
DWORD dwAttrib = GetFileAttributesW(app.path.c_str());
if (dwAttrib == INVALID_FILE_ATTRIBUTES)
{
Logger::error(L"File not found at {}", app.path);
launchErrors.push_back({ std::filesystem::path(app.path).filename(), L"File not found" });
return false;
}
launched = LaunchApp(app.path, app.commandLineArgs, app.isElevated, launchErrors);
}
Logger::trace(L"{} {} at {}", app.name, (launched ? L"launched" : L"not launched"), app.path);
return launched;
}
bool Launch(WorkspacesData::WorkspacesProject& project, const std::vector<WorkspacesData::WorkspacesProject::Monitor>& monitors, ErrorList& launchErrors)
{
bool launchedSuccessfully{ true };
LauncherUIHelper uiHelper;
uiHelper.LaunchUI();
// Get the set of windows before launching the app
std::vector<HWND> windowsBefore = WindowEnumerator::Enumerate(WindowFilter::Filter);
auto installedApps = Utils::Apps::GetAppsList();
auto launchedApps = Prepare(project.apps, installedApps);
uiHelper.UpdateLaunchStatus(launchedApps);
// Launch apps
for (auto& app : launchedApps)
{
if (!app.window)
{
if (!Launch(app.application, launchErrors))
{
Logger::error(L"Failed to launch {}", app.application.name);
app.state = L"failed";
uiHelper.UpdateLaunchStatus(launchedApps);
launchedSuccessfully = false;
}
}
}
// Get newly opened windows after launching apps, keep retrying for 5 seconds
Logger::trace(L"Find new windows");
for (int attempt = 0; attempt < 50 && !AllWindowsFound(launchedApps); attempt++)
{
std::vector<HWND> windowsAfter = WindowEnumerator::Enumerate(WindowFilter::Filter);
std::vector<HWND> windowsDiff{};
std::copy_if(windowsAfter.begin(), windowsAfter.end(), std::back_inserter(windowsDiff), [&](HWND window) { return std::find(windowsBefore.begin(), windowsBefore.end(), window) == windowsBefore.end(); });
if (AddOpenedWindows(launchedApps, windowsDiff, installedApps))
{
uiHelper.UpdateLaunchStatus(launchedApps);
}
// check if all windows were found
if (AllWindowsFound(launchedApps))
{
Logger::trace(L"All windows found.");
break;
}
else
{
Logger::trace(L"Not all windows found, retry.");
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
// Check single-instance app windows
Logger::trace(L"Find single-instance app windows");
if (!AllWindowsFound(launchedApps))
{
if (AddOpenedWindows(launchedApps, WindowEnumerator::Enumerate(WindowFilter::Filter), installedApps))
{
uiHelper.UpdateLaunchStatus(launchedApps);
}
}
// Place windows
for (const auto& [app, window, status] : launchedApps)
{
if (window == nullptr)
{
Logger::warn(L"{} window not found.", app.name);
launchedSuccessfully = false;
continue;
}
auto snapMonitorIter = std::find_if(project.monitors.begin(), project.monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.number == app.monitor; });
if (snapMonitorIter == project.monitors.end())
{
Logger::error(L"No monitor saved for launching the app");
continue;
}
bool launchMinimized = app.isMinimized;
bool launchMaximized = app.isMaximized;
HMONITOR currentMonitor{};
UINT currentDpi = DPIAware::DEFAULT_DPI;
auto currentMonitorIter = std::find_if(monitors.begin(), monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.number == app.monitor; });
if (currentMonitorIter != monitors.end())
{
currentMonitor = currentMonitorIter->monitor;
currentDpi = currentMonitorIter->dpi;
}
else
{
currentMonitor = MonitorFromPoint(POINT{ 0, 0 }, MONITOR_DEFAULTTOPRIMARY);
DPIAware::GetScreenDPIForMonitor(currentMonitor, currentDpi);
launchMinimized = true;
launchMaximized = false;
}
RECT rect = app.position.toRect();
float mult = static_cast<float>(snapMonitorIter->dpi) / currentDpi;
rect.left = static_cast<long>(std::round(rect.left * mult));
rect.right = static_cast<long>(std::round(rect.right * mult));
rect.top = static_cast<long>(std::round(rect.top * mult));
rect.bottom = static_cast<long>(std::round(rect.bottom * mult));
if (FancyZones::SizeWindowToRect(window, currentMonitor, launchMinimized, launchMaximized, rect))
{
WorkspacesWindowProperties::StampWorkspacesLaunchedProperty(window);
Logger::trace(L"Placed {} to ({},{}) [{}x{}]", app.name, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
}
else
{
Logger::error(L"Failed placing {}", app.name);
launchedSuccessfully = false;
}
}
return launchedSuccessfully;
}

View File

@ -0,0 +1,7 @@
#pragma once
#include <WorkspacesLib/WorkspacesData.h>
using ErrorList = std::vector<std::pair<std::wstring, std::wstring>>;
bool Launch(WorkspacesData::WorkspacesProject& project, const std::vector<WorkspacesData::WorkspacesProject::Monitor>& monitors, ErrorList& launchErrors);

View File

@ -0,0 +1,78 @@
#include "pch.h"
#include "LauncherUIHelper.h"
#include <filesystem>
#include <shellapi.h>
#include <common/utils/OnThreadExecutor.h>
#include <common/utils/winapi_error.h>
LauncherUIHelper::~LauncherUIHelper()
{
OnThreadExecutor().submit(OnThreadExecutor::task_t{ [&] {
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
HANDLE uiProcess = OpenProcess(PROCESS_ALL_ACCESS, false, uiProcessId);
if (uiProcess)
{
bool res = TerminateProcess(uiProcess, 0);
if (!res)
{
Logger::error(L"Unable to terminate UI process: {}", get_last_error_or_default(GetLastError()));
}
}
else
{
Logger::error(L"Unable to find UI process: {}", get_last_error_or_default(GetLastError()));
}
std::filesystem::remove(WorkspacesData::LaunchWorkspacesFile());
} }).wait();
}
void LauncherUIHelper::LaunchUI()
{
Logger::trace(L"Starting WorkspacesLauncherUI");
STARTUPINFO info = { sizeof(info) };
PROCESS_INFORMATION pi = { 0 };
TCHAR buffer[MAX_PATH] = { 0 };
GetModuleFileName(NULL, buffer, MAX_PATH);
std::wstring path = std::filesystem::path(buffer).parent_path();
path.append(L"\\PowerToys.WorkspacesLauncherUI.exe");
auto succeeded = CreateProcessW(path.c_str(), nullptr, nullptr, nullptr, FALSE, 0, nullptr, nullptr, &info, &pi);
if (succeeded)
{
if (pi.hProcess)
{
uiProcessId = pi.dwProcessId;
CloseHandle(pi.hProcess);
}
if (pi.hThread)
{
CloseHandle(pi.hThread);
}
}
else
{
Logger::error(L"CreateProcessW() failed. {}", get_last_error_or_default(GetLastError()));
}
}
void LauncherUIHelper::UpdateLaunchStatus(LaunchingApps launchedApps)
{
WorkspacesData::AppLaunchData appData = WorkspacesData::AppLaunchData();
appData.appLaunchInfoList.reserve(launchedApps.size());
appData.launcherProcessID = GetCurrentProcessId();
for (auto& app : launchedApps)
{
WorkspacesData::AppLaunchInfo appLaunchInfo = WorkspacesData::AppLaunchInfo();
appLaunchInfo.name = app.application.name;
appLaunchInfo.path = app.application.path;
appLaunchInfo.state = app.state;
appData.appLaunchInfoList.push_back(appLaunchInfo);
}
json::to_file(WorkspacesData::LaunchWorkspacesFile(), WorkspacesData::AppLaunchDataJSON::ToJson(appData));
}

View File

@ -0,0 +1,16 @@
#pragma once
#include <LaunchingApp.h>
class LauncherUIHelper
{
public:
LauncherUIHelper() = default;
~LauncherUIHelper();
void LaunchUI();
void UpdateLaunchStatus(LaunchingApps launchedApps);
private:
DWORD uiProcessId;
};

View File

@ -0,0 +1,13 @@
#pragma once
#include <Windows.h>
#include <WorkspacesLib/WorkspacesData.h>
struct LaunchingApp
{
WorkspacesData::WorkspacesProject::Application application;
HWND window;
std::wstring state;
};
using LaunchingApps = std::vector<LaunchingApp>;

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Label="PropertySheets" />
<PropertyGroup Label="UserMacros" />
<!--
To customize common C++/WinRT project properties:
* right-click the project node
* expand the Common Properties item
* select the C++/WinRT property page
For more advanced scenarios, and complete documentation, please see:
https://github.com/Microsoft/cppwinrt/tree/master/nuget
-->
<PropertyGroup />
<ItemDefinitionGroup />
</Project>

View File

@ -0,0 +1,84 @@
#include "pch.h"
#include "RegistryUtils.h"
#include <strsafe.h>
#include <common/utils/winapi_error.h>
namespace RegistryUtils
{
namespace NonLocalizable
{
const wchar_t RegKeyPackageId[] = L"Extensions\\ContractId\\Windows.Protocol\\PackageId\\";
const wchar_t RegKeyPackageActivatableClassId[] = L"\\ActivatableClassId";
const wchar_t RegKeyPackageCustomProperties[] = L"\\CustomProperties";
const wchar_t RegValueName[] = L"Name";
}
HKEY OpenRootRegKey(const wchar_t* key)
{
HKEY hKey{ nullptr };
if (RegOpenKeyEx(HKEY_CLASSES_ROOT, key, 0, KEY_READ, &hKey) == ERROR_SUCCESS)
{
return hKey;
}
return nullptr;
}
std::vector<std::wstring> GetUriProtocolNames(const std::wstring& packageFullPath)
{
std::vector<std::wstring> names{};
std::wstring keyPath = std::wstring(NonLocalizable::RegKeyPackageId) + packageFullPath + std::wstring(NonLocalizable::RegKeyPackageActivatableClassId);
HKEY key = OpenRootRegKey(keyPath.c_str());
if (key != nullptr)
{
LSTATUS result;
// iterate over all the subkeys to get the protocol names
DWORD index = 0;
wchar_t keyName[256];
DWORD keyNameSize = sizeof(keyName) / sizeof(keyName[0]);
FILETIME lastWriteTime;
while ((result = RegEnumKeyEx(key, index, keyName, &keyNameSize, NULL, NULL, NULL, &lastWriteTime)) != ERROR_NO_MORE_ITEMS)
{
if (result == ERROR_SUCCESS)
{
std::wstring subkeyPath = std::wstring(keyPath) + L"\\" + std::wstring(keyName, keyNameSize) + std::wstring(NonLocalizable::RegKeyPackageCustomProperties);
HKEY subkey = OpenRootRegKey(subkeyPath.c_str());
if (subkey != nullptr)
{
DWORD dataSize;
wchar_t value[256];
result = RegGetValueW(subkey, nullptr, NonLocalizable::RegValueName, RRF_RT_REG_SZ, nullptr, value, &dataSize);
if (result == ERROR_SUCCESS)
{
names.emplace_back(std::wstring(value, dataSize / sizeof(wchar_t) - 1));
}
else
{
Logger::error(L"Failed to query registry value. Error: {}", get_last_error_or_default(result));
}
RegCloseKey(subkey);
}
}
else
{
Logger::error(L"Failed to enumerate subkey. Error: {}", get_last_error_or_default(result));
break;
}
keyNameSize = sizeof(keyName) / sizeof(keyName[0]); // Reset the buffer size
++index;
}
// Close the registry key
RegCloseKey(key);
}
return names;
}
}

View File

@ -0,0 +1,6 @@
#pragma once
namespace RegistryUtils
{
std::vector<std::wstring> GetUriProtocolNames(const std::wstring& packageFullPath);
};

View File

@ -0,0 +1,139 @@
<?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="Empty_file" xml:space="preserve">
<value>File {0} is empty.</value>
</data>
<data name="File_reading_error" xml:space="preserve">
<value>Error reading file {0}.</value>
</data>
<data name="Incorrect_args" xml:space="preserve">
<value>Incorrect command line arguments</value>
</data>
<data name="Incorrect_file_error" xml:space="preserve">
<value>Incorrect {0} file.</value>
</data>
<data name="Workspaces" xml:space="preserve">
<value>Workspaces</value>
<comment>Name of the module</comment>
</data>
<data name="Project_not_found" xml:space="preserve">
<value>Workspace {0} not found.</value>
</data>
</root>

Some files were not shown because too many files have changed in this diff Show More