From d87dde132df50206d8ff90b1fb9168f79364c03f Mon Sep 17 00:00:00 2001 From: Kai Tao <69313318+vanzue@users.noreply.github.com> Date: Tue, 23 Dec 2025 21:07:44 +0800 Subject: [PATCH] Cmdpal extension: Powertoys extension for cmdpal (#44006) ## Summary of the Pull Request ## PR Checklist - [ ] Closes: #xxx - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed Installer built, and every command works as expected, Now use sparse app deployment, so we don't need an extra msix --------- Co-authored-by: kaitao-ms --- .github/actions/spell-check/expect.txt | 19 +- .gitignore | 2 +- .pipelines/ESRPSigning_core.json | 14 +- .pipelines/v2/templates/job-build-project.yml | 2 +- .pipelines/versionAndSignCheck.ps1 | 2 +- Directory.Packages.props | 4 +- NOTICE.md | 1 + PowerToys.slnx | 21 + .../PowerToysSetupVNext/BaseApplications.wxs | 7 + .../PowerToysInstallerVNext.wixproj | 2 +- src/PackageIdentity/AppxManifest.xml | 43 +- src/common/Common.UI/SettingsDeepLink.cs | 35 +- .../ManagedCommon/PowerToysPathResolver.cs | 168 ++++ .../IModuleService.cs | 47 ++ .../OperationResult.cs | 30 + .../PowerToys.ModuleContracts.csproj | 16 + src/common/interop/Constants.cpp | 52 ++ src/common/interop/Constants.h | 15 +- src/common/interop/Constants.idl | 15 +- src/common/interop/shared_constants.h | 17 + src/common/utils/EventWaiter.h | 134 +++- src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj | 1 + .../ViewModels/OptionsViewModel.cs | 2 +- .../AdvancedPasteModuleInterface/dllmain.cpp | 19 + .../dllmain.cpp | 4 +- src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs | 2 +- .../Hosts/HostsModuleInterface/dllmain.cpp | 4 +- .../LightSwitchModuleInterface.vcxproj | 5 +- .../LightSwitchModuleInterface/dllmain.cpp | 114 ++- .../MeasureToolModuleInterface/dllmain.cpp | 2 +- src/modules/MouseUtils/CursorWrap/dllmain.cpp | 102 ++- .../MouseUtils/FindMyMouse/dllmain.cpp | 18 +- .../MouseUtils/MouseHighlighter/dllmain.cpp | 14 + .../MousePointerCrosshairs/dllmain.cpp | 21 +- .../PowerOCR/PowerOCR/OCROverlay.xaml.cs | 2 +- .../ShortcutGuide/ShortcutGuide/main.cpp | 2 +- .../ShortcutGuideModuleInterface/dllmain.cpp | 2 +- .../IWorkspaceService.cs | 22 + .../WorkspaceService.cs | 96 +++ .../Workspaces.ModuleServices.csproj | 20 + .../Data/ApplicationWrapper.cs | 70 ++ .../Data/InvokePoint.cs | 15 +- .../Data/MonitorConfigurationWrapper.cs | 43 ++ .../Data/ProjectData.cs | 11 + .../Data/ProjectWrapper.cs | 26 + .../Data/TempProjectData.cs | 5 +- .../Data/WorkspacesData.cs | 27 + .../Data/WorkspacesEditorData`1.cs | 38 + .../Data/WorkspacesJsonOptions.cs | 17 + .../Data/WorkspacesStorage.cs | 96 +++ .../Data/WorkspacesStorageJsonContext.cs | 18 + .../Models/BaseApplication.cs | 2 +- .../Utils/DashCaseNamingPolicy.cs | 18 + .../Utils/FolderUtils.cs | 27 + .../WorkspacesCsharpLibrary/Utils/IOUtils.cs | 47 ++ .../Utils/StringUtils.cs | 18 + .../WorkspacesCsharpLibrary.csproj | 8 +- .../WorkspacesEditor/Data/ProjectData.cs | 101 --- .../WorkspacesEditor/Data/WorkspacesData.cs | 30 - .../Data/WorkspacesEditorData`1.cs | 33 - .../WorkspacesEditor/Models/Project.cs | 8 +- .../Utils/DashCaseNamingPolicy.cs | 18 - .../WorkspacesEditor/Utils/FolderUtils.cs | 28 - .../WorkspacesEditor/Utils/IOUtils.cs | 52 -- .../Utils/WorkspacesEditorIO.cs | 18 +- .../ViewModels/MainViewModel.cs | 6 +- .../WorkspacesEditor/WorkspacesEditor.csproj | 11 +- .../WorkspacesEditorPage.xaml.cs | 2 +- .../Data/AppLaunchData.cs | 5 +- .../Data/AppLaunchInfoData.cs | 5 +- .../Data/AppLaunchInfosData.cs | 5 +- .../Data/WorkspacesUIData`1.cs | 35 - .../ViewModels/MainViewModel.cs | 2 - .../WorkspacesLauncherUI.csproj | 192 ++--- .../WorkspacesModuleInterface/dllmain.cpp | 2 +- src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj | 2 +- src/modules/ZoomIt/ZoomIt/Zoomit.cpp | 136 +++- .../ZoomIt/ZoomItModuleInterface/dllmain.cpp | 1 + .../Awake.ModuleServices.csproj | 20 + .../Awake.ModuleServices/AwakeService.cs | 151 ++++ .../AwakeServiceJsonContext.cs | 13 + .../awake/Awake.ModuleServices/AwakeState.cs | 26 + .../Awake.ModuleServices/IAwakeService.cs | 21 + .../PublishProfiles/win-arm64.pubxml | 2 +- .../Properties/PublishProfiles/win-x64.pubxml | 2 +- .../AdvancedPaste/OpenAdvancedPasteCommand.cs | 36 + .../Awake/RefreshAwakeStatusCommand.cs | 26 + .../Commands/Awake/StartAwakeCommand.cs | 59 ++ .../Commands/Awake/StopAwakeCommand.cs | 48 ++ .../ColorPicker/CopySavedColorCommand.cs | 39 + .../ColorPicker/OpenColorPickerCommand.cs | 38 + .../CropAndLock/CropAndLockReparentCommand.cs | 35 + .../CropAndLockThumbnailCommand.cs | 35 + .../OpenEnvironmentVariablesAdminCommand.cs | 35 + .../OpenEnvironmentVariablesCommand.cs | 35 + .../ApplyFancyZonesLayoutCommand.cs | 36 + .../FancyZones/FancyZonesLayoutListItem.cs | 70 ++ .../FancyZones/FancyZonesMonitorListItem.cs | 75 ++ .../IdentifyFancyZonesMonitorCommand.cs | 46 ++ .../FancyZones/OpenFancyZonesEditorCommand.cs | 35 + .../Hosts/OpenHostsEditorAdminCommand.cs | 35 + .../Commands/Hosts/OpenHostsEditorCommand.cs | 35 + .../Commands/LaunchModuleCommand.cs | 121 +++ .../LightSwitch/ToggleLightSwitchCommand.cs | 35 + .../MouseUtils/ShowMouseJumpPreviewCommand.cs | 35 + .../MouseUtils/ToggleCursorWrapCommand.cs | 35 + .../MouseUtils/ToggleFindMyMouseCommand.cs | 42 + .../ToggleMouseCrosshairsCommand.cs | 35 + .../ToggleMouseHighlighterCommand.cs | 35 + .../Commands/OpenInSettingsCommand.cs | 28 + .../Commands/OpenPowerToysSettingsCommand.cs | 61 ++ .../OpenRegistryPreviewCommand.cs | 35 + .../ScreenRuler/ToggleScreenRulerCommand.cs | 32 + .../ToggleShortcutGuideCommand.cs | 35 + .../ToggleTextExtractorCommand.cs | 35 + .../Workspaces/LaunchWorkspaceCommand.cs | 35 + .../Workspaces/OpenWorkspaceEditorCommand.cs | 22 + .../Commands/Workspaces/WorkspaceListItem.cs | 117 +++ .../Commands/ZoomIt/ZoomItActionCommand.cs | 84 ++ .../Helpers/AwakeStatusService.cs | 60 ++ .../Helpers/ColorSwatchIconFactory.cs | 45 ++ .../Helpers/FancyZonesDataService.cs | 517 +++++++++++++ .../Helpers/FancyZonesLayoutDescriptor.cs | 24 + .../Helpers/FancyZonesLayoutDescriptor1.cs | 5 + .../Helpers/FancyZonesLayoutSource.cs | 11 + .../Helpers/FancyZonesMonitorDescriptor.cs | 27 + .../Helpers/FancyZonesMonitorIdentifier.cs | 461 +++++++++++ .../FancyZonesMonitorPreviewRenderer.cs | 399 ++++++++++ .../Helpers/FancyZonesNotifier.cs | 25 + .../Helpers/FancyZonesThumbnailRenderer.cs | 716 ++++++++++++++++++ .../Helpers/FancyZonesVirtualDesktop.cs | 103 +++ .../Helpers/GpoEnablementService.cs | 86 +++ .../Helpers/ModuleCommandCatalog.cs | 54 ++ .../Helpers/ModuleEnablementService.cs | 162 ++++ .../Helpers/PowerToysFallbackCommandItem.cs | 88 +++ .../Helpers/PowerToysPathResolver.cs | 161 ++++ .../Helpers/PowerToysResourcesHelper.cs | 99 +++ .../Helpers/SettingsChangeNotifier.cs | 72 ++ .../Microsoft.CmdPal.Ext.PowerToys.csproj | 72 ++ .../AdvancedPasteModuleCommandProvider.cs | 38 + .../AlwaysOnTopModuleCommandProvider.cs | 27 + .../Modules/AwakeModuleCommandProvider.cs | 91 +++ .../ColorPickerModuleCommandProvider.cs | 53 ++ .../CommandNotFoundModuleCommandProvider.cs | 27 + .../CropAndLockModuleCommandProvider.cs | 45 ++ ...vironmentVariablesModuleCommandProvider.cs | 45 ++ .../FancyZonesModuleCommandProvider.cs | 53 ++ ...FileExplorerAddonsModuleCommandProvider.cs | 27 + .../FileLocksmithModuleCommandProvider.cs | 27 + .../Modules/HostsModuleCommandProvider.cs | 45 ++ .../ImageResizerModuleCommandProvider.cs | 27 + .../KeyboardManagerModuleCommandProvider.cs | 27 + .../LightSwitchModuleCommandProvider.cs | 42 + .../Modules/ModuleCommandProvider.cs | 16 + .../MouseUtilsModuleCommandProvider.cs | 78 ++ ...ouseWithoutBordersModuleCommandProvider.cs | 27 + .../Modules/NewPlusModuleCommandProvider.cs | 27 + .../Modules/PeekModuleCommandProvider.cs | 27 + .../PowerRenameModuleCommandProvider.cs | 27 + .../PowerToysRunModuleCommandProvider.cs | 27 + .../QuickAccentModuleCommandProvider.cs | 27 + .../RegistryPreviewModuleCommandProvider.cs | 38 + .../ScreenRulerModuleCommandProvider.cs | 38 + .../ShortcutGuideModuleCommandProvider.cs | 38 + .../TextExtractorModuleCommandProvider.cs | 38 + .../WorkspacesModuleCommandProvider.cs | 64 ++ .../Modules/ZoomItModuleCommandProvider.cs | 69 ++ .../Pages/ColorPickerSavedColorsPage.cs | 96 +++ .../Pages/FancyZonesLayoutsPage.cs | 103 +++ .../FancyZonesMonitorLayoutPickerPage.cs | 68 ++ .../Pages/FancyZonesMonitorsPage.cs | 67 ++ .../Pages/PowerToysExtensionPage.cs | 46 ++ .../Pages/PowerToysListPage.cs | 36 + .../PowerToysCommandsProvider.cs | 46 ++ .../PowerToysExtension.cs | 41 + .../PowerToysExtensionCommandsProvider.cs | 50 ++ .../Microsoft.CmdPal.Ext.PowerToys/Program.cs | 70 ++ .../Public/README.md | 6 + .../app.manifest | 21 + .../ColorFormatValue.cs | 7 + .../ColorPicker.ModuleServices.csproj | 29 + .../ColorPickerService.cs | 157 ++++ .../ColorPickerServiceJsonContext.cs | 19 + .../IColorPickerService.cs | 14 + .../ColorPicker.ModuleServices/SavedColor.cs | 7 + .../ColorPickerUI/Helpers/AppStateHandler.cs | 2 +- .../FancyZones.FuzzTests.csproj | 8 +- .../fancyzones/FancyZones/FancyZonesApp.cpp | 2 +- .../Utils/AppZoneHistory.cs | 8 +- .../Data/CustomLayouts.cs | 8 +- .../Data/EditorData`1.cs | 19 +- .../Data/FancyZonesJsonContext.cs | 38 + .../fancyzones/FancyZonesLib/FancyZones.cpp | 2 +- .../editor/FancyZonesEditor/EditorWindow.cs | 1 - .../FancyZonesEditor/MainWindow.xaml.cs | 2 +- .../ui/ViewModels/InputViewModel.cs | 2 +- .../KeyboardManagerEngine/main.cpp | 3 +- .../KeyboardManager.cpp | 2 +- .../Components/Utility.cs | 2 +- .../RegistryPreviewExt/dllmain.cpp | 2 +- .../Settings.UI.XamlIndexBuilder.csproj | 2 +- .../Settings.UI/PowerToys.Settings.csproj | 2 +- .../Settings.UI/SettingsXAML/App.xaml.cs | 2 +- .../OOBE/Views/OobeWhatsNew.xaml.cs | 2 +- .../Views/PowerLauncherPage.xaml.cs | 2 +- tools/build/build-installer.ps1 | 144 ++-- 206 files changed, 8800 insertions(+), 691 deletions(-) create mode 100644 src/common/ManagedCommon/PowerToysPathResolver.cs create mode 100644 src/common/PowerToys.ModuleContracts/IModuleService.cs create mode 100644 src/common/PowerToys.ModuleContracts/OperationResult.cs create mode 100644 src/common/PowerToys.ModuleContracts/PowerToys.ModuleContracts.csproj create mode 100644 src/modules/Workspaces/Workspaces.ModuleServices/IWorkspaceService.cs create mode 100644 src/modules/Workspaces/Workspaces.ModuleServices/WorkspaceService.cs create mode 100644 src/modules/Workspaces/Workspaces.ModuleServices/Workspaces.ModuleServices.csproj create mode 100644 src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ApplicationWrapper.cs rename src/modules/Workspaces/{WorkspacesEditor => WorkspacesCsharpLibrary}/Data/InvokePoint.cs (51%) create mode 100644 src/modules/Workspaces/WorkspacesCsharpLibrary/Data/MonitorConfigurationWrapper.cs create mode 100644 src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ProjectData.cs create mode 100644 src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ProjectWrapper.cs rename src/modules/Workspaces/{WorkspacesEditor => WorkspacesCsharpLibrary}/Data/TempProjectData.cs (82%) create mode 100644 src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesData.cs create mode 100644 src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesEditorData`1.cs create mode 100644 src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesJsonOptions.cs create mode 100644 src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesStorage.cs create mode 100644 src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesStorageJsonContext.cs create mode 100644 src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/DashCaseNamingPolicy.cs create mode 100644 src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/FolderUtils.cs create mode 100644 src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/IOUtils.cs create mode 100644 src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/StringUtils.cs delete mode 100644 src/modules/Workspaces/WorkspacesEditor/Data/ProjectData.cs delete mode 100644 src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesData.cs delete mode 100644 src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesEditorData`1.cs delete mode 100644 src/modules/Workspaces/WorkspacesEditor/Utils/DashCaseNamingPolicy.cs delete mode 100644 src/modules/Workspaces/WorkspacesEditor/Utils/FolderUtils.cs delete mode 100644 src/modules/Workspaces/WorkspacesEditor/Utils/IOUtils.cs delete mode 100644 src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesUIData`1.cs create mode 100644 src/modules/awake/Awake.ModuleServices/Awake.ModuleServices.csproj create mode 100644 src/modules/awake/Awake.ModuleServices/AwakeService.cs create mode 100644 src/modules/awake/Awake.ModuleServices/AwakeServiceJsonContext.cs create mode 100644 src/modules/awake/Awake.ModuleServices/AwakeState.cs create mode 100644 src/modules/awake/Awake.ModuleServices/IAwakeService.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/AdvancedPaste/OpenAdvancedPasteCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/RefreshAwakeStatusCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/StartAwakeCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/StopAwakeCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ColorPicker/CopySavedColorCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ColorPicker/OpenColorPickerCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockReparentCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockThumbnailCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/EnvironmentVariables/OpenEnvironmentVariablesAdminCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/EnvironmentVariables/OpenEnvironmentVariablesCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/ApplyFancyZonesLayoutCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/FancyZonesLayoutListItem.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/FancyZonesMonitorListItem.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/IdentifyFancyZonesMonitorCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/OpenFancyZonesEditorCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Hosts/OpenHostsEditorAdminCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Hosts/OpenHostsEditorCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/LaunchModuleCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/LightSwitch/ToggleLightSwitchCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ShowMouseJumpPreviewCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleCursorWrapCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleFindMyMouseCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleMouseCrosshairsCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleMouseHighlighterCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/OpenInSettingsCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/OpenPowerToysSettingsCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/RegistryPreview/OpenRegistryPreviewCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ScreenRuler/ToggleScreenRulerCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ShortcutGuide/ToggleShortcutGuideCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/TextExtractor/ToggleTextExtractorCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/LaunchWorkspaceCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/OpenWorkspaceEditorCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/WorkspaceListItem.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ZoomIt/ZoomItActionCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/AwakeStatusService.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ColorSwatchIconFactory.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesDataService.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutDescriptor.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutDescriptor1.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutSource.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorDescriptor.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorIdentifier.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorPreviewRenderer.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesNotifier.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesThumbnailRenderer.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesVirtualDesktop.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/GpoEnablementService.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ModuleCommandCatalog.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ModuleEnablementService.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysFallbackCommandItem.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysPathResolver.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysResourcesHelper.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/SettingsChangeNotifier.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AdvancedPasteModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AlwaysOnTopModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AwakeModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ColorPickerModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CommandNotFoundModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CropAndLockModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/EnvironmentVariablesModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FancyZonesModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FileExplorerAddonsModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FileLocksmithModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/HostsModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ImageResizerModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/KeyboardManagerModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/LightSwitchModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseUtilsModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseWithoutBordersModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/NewPlusModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PeekModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PowerRenameModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PowerToysRunModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/QuickAccentModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/RegistryPreviewModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ScreenRulerModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ShortcutGuideModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/TextExtractorModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/WorkspacesModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ZoomItModuleCommandProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/ColorPickerSavedColorsPage.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesLayoutsPage.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesMonitorLayoutPickerPage.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesMonitorsPage.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysExtensionPage.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysListPage.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysCommandsProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtension.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtensionCommandsProvider.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Program.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Public/README.md create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/app.manifest create mode 100644 src/modules/colorPicker/ColorPicker.ModuleServices/ColorFormatValue.cs create mode 100644 src/modules/colorPicker/ColorPicker.ModuleServices/ColorPicker.ModuleServices.csproj create mode 100644 src/modules/colorPicker/ColorPicker.ModuleServices/ColorPickerService.cs create mode 100644 src/modules/colorPicker/ColorPicker.ModuleServices/ColorPickerServiceJsonContext.cs create mode 100644 src/modules/colorPicker/ColorPicker.ModuleServices/IColorPickerService.cs create mode 100644 src/modules/colorPicker/ColorPicker.ModuleServices/SavedColor.cs create mode 100644 src/modules/fancyzones/FancyZonesEditorCommon/Data/FancyZonesJsonContext.cs diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 7876e37cd9..ff7a8fdf02 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -216,6 +216,7 @@ CImage cla CLASSDC CLASSNOTAVAILABLE +CLEARTYPE clickable clickonce CLIENTEDGE @@ -253,6 +254,7 @@ colorhistory colorhistorylimit COLORKEY colorref +Convs comctl comdlg comexp @@ -529,9 +531,12 @@ eyetracker FANCYZONESDRAWLAYOUTTEST FANCYZONESEDITOR FARPROC +fdw fdx +FErase fesf FFFF +FInc Figma FILEEXPLORER fileexploreraddons @@ -573,6 +578,7 @@ formatetc FORPARSING foundrylocal FRAMECHANGED +FRestore frm FROMTOUCH fsanitize @@ -607,6 +613,7 @@ GETSCREENSAVERRUNNING GETSECKEY GETSTICKYKEYS GETTEXTLENGTH +gfx GHND gitmodules GMEM @@ -657,6 +664,7 @@ hdwwiz Helpline helptext HGFE +hgdiobj hglobal hhk HHmmssfff @@ -704,7 +712,7 @@ hotlight hotspot HPAINTBUFFER HRAWINPUT -HREDRAW +hredraw hres hresult hrgn @@ -882,7 +890,7 @@ LINKOVERLAY LINQTo listview LIVEDRAW -LIVEZOOM +livezoom LLKH llkhf LMEM @@ -911,6 +919,7 @@ LPBITMAPINFOHEADER LPCFHOOKPROC LPCITEMIDLIST LPCLSID +lpch lpcmi LPCMINVOKECOMMANDINFO LPCREATESTRUCT @@ -935,6 +944,7 @@ lptpm LPTR LPTSTR lpv +LPrivate LPW lpwcx lpwndpl @@ -1349,6 +1359,7 @@ ppv ppwsz prc Prefixer +Premul prependpath prepopulate prevhost @@ -1904,7 +1915,7 @@ valuegenerator variantassignment VARTYPE vcamp -VCENTER +vcenter vcgtq VCINSTALLDIR Vcpkg @@ -1936,7 +1947,7 @@ vorrq VOS vpaddlq vqsubq -VREDRAW +vredraw vreinterpretq VSC VSCBD diff --git a/.gitignore b/.gitignore index 1318abc22c..1ed1bbcbc1 100644 --- a/.gitignore +++ b/.gitignore @@ -358,4 +358,4 @@ src/common/Telemetry/*.etl /src/settings-ui/Settings.UI/Assets/Settings/search.index.json # PowerToysInstaller Build Temp Files -installer/*/*.wxs.bk \ No newline at end of file +installer/*/*.wxs.bk diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index e3ebffc20c..1b4325ba1a 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -235,6 +235,14 @@ "PowerToys.CmdPalModuleInterface.dll", "CmdPalKeyboardService.dll", + "PowerToys.ModuleContracts.dll", + "Awake.ModuleServices.dll", + "ColorPicker.ModuleServices.dll", + "Workspaces.ModuleServices.dll", + "Microsoft.CommandPalette.Extensions.dll", + "Microsoft.CommandPalette.Extensions.Toolkit.dll", + "Microsoft.CmdPal.Ext.PowerToys.dll", + "Microsoft.CmdPal.Ext.PowerToys.exe", "*Microsoft.CmdPal.UI_*.msix", "PowerToys.DSC.dll", @@ -358,9 +366,13 @@ "boost_regex-vc143-mt-x32-1_87.dll", "boost_regex-vc143-mt-x64-1_87.dll", + "Microsoft.ML.OnnxRuntime.dll", + "UnitsNet.dll", "UtfUnknown.dll", - "Wpf.Ui.dll" + "Wpf.Ui.dll", + "Shmuelie.WinRTServer.dll", + "ToolGood.Words.Pinyin.dll" ], "SigningInfo": { "Operations": [ diff --git a/.pipelines/v2/templates/job-build-project.yml b/.pipelines/v2/templates/job-build-project.yml index 6a4df0c720..57fdba6397 100644 --- a/.pipelines/v2/templates/job-build-project.yml +++ b/.pipelines/v2/templates/job-build-project.yml @@ -624,4 +624,4 @@ jobs: - publish: $(JobOutputDirectory) artifact: $(JobOutputArtifactName)-failure-$(System.JobAttempt) displayName: Publish failure logs - condition: or(failed(), canceled()) + condition: or(failed(), canceled()) \ No newline at end of file diff --git a/.pipelines/versionAndSignCheck.ps1 b/.pipelines/versionAndSignCheck.ps1 index cf1f515e78..5b03250dd6 100644 --- a/.pipelines/versionAndSignCheck.ps1 +++ b/.pipelines/versionAndSignCheck.ps1 @@ -104,4 +104,4 @@ if ($totalFailure -gt 0) { exit 1 } -exit 0 +exit 0 \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index eb04903b7e..28bd723f93 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -37,6 +37,7 @@ + @@ -94,6 +95,7 @@ + @@ -145,4 +147,4 @@ - \ No newline at end of file + diff --git a/NOTICE.md b/NOTICE.md index 23efb64864..4273edbb18 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1560,6 +1560,7 @@ SOFTWARE. - ReverseMarkdown - ScipBe.Common.Office.OneNote - SharpCompress +- Shmuelie.WinRTServer - SkiaSharp.Views.WinUI - StreamJsonRpc - StyleCop.Analyzers diff --git a/PowerToys.slnx b/PowerToys.slnx index 1884b2d58b..bc481ce526 100644 --- a/PowerToys.slnx +++ b/PowerToys.slnx @@ -44,6 +44,10 @@ + + + + @@ -156,6 +160,10 @@ + + + + @@ -166,6 +174,10 @@ + + + + @@ -206,6 +218,11 @@ + + + + + @@ -932,6 +949,10 @@ + + + + diff --git a/installer/PowerToysSetupVNext/BaseApplications.wxs b/installer/PowerToysSetupVNext/BaseApplications.wxs index 1947cbf1f2..57a9c71637 100644 --- a/installer/PowerToysSetupVNext/BaseApplications.wxs +++ b/installer/PowerToysSetupVNext/BaseApplications.wxs @@ -7,11 +7,18 @@ + + + + + + + diff --git a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj index 18d6232140..a7a9744e87 100644 --- a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj +++ b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj @@ -173,4 +173,4 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil - \ No newline at end of file + diff --git a/src/PackageIdentity/AppxManifest.xml b/src/PackageIdentity/AppxManifest.xml index 822daae8bc..502cc33ff0 100644 --- a/src/PackageIdentity/AppxManifest.xml +++ b/src/PackageIdentity/AppxManifest.xml @@ -10,7 +10,8 @@ xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10" xmlns:systemai="http://schemas.microsoft.com/appx/manifest/systemai/windows10" - IgnorableNamespaces="uap uap2 uap3 rescap desktop uap10 systemai"> + xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10" + IgnorableNamespaces="uap uap2 uap3 rescap desktop uap10 systemai com"> + @@ -66,5 +68,42 @@ AppListEntry="none"> + + + + + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/src/common/Common.UI/SettingsDeepLink.cs b/src/common/Common.UI/SettingsDeepLink.cs index 1891532d16..5233c0d668 100644 --- a/src/common/Common.UI/SettingsDeepLink.cs +++ b/src/common/Common.UI/SettingsDeepLink.cs @@ -2,8 +2,10 @@ // 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.Diagnostics; using System.IO; +using ManagedCommon; namespace Common.UI { @@ -120,28 +122,33 @@ namespace Common.UI } } - public static void OpenSettings(SettingsWindow window, bool mainExecutableIsOnTheParentFolder) + // What about debug build? Should also consider debug build, maybe tray window message? + public static void OpenSettings(SettingsWindow window) { try { - var directoryPath = System.AppContext.BaseDirectory; - if (mainExecutableIsOnTheParentFolder) + var exePath = Path.Combine( + PowerToysPathResolver.GetPowerToysInstallPath(), + "PowerToys.exe"); + + if (exePath == null || !File.Exists(exePath)) { - // Need to go into parent folder for PowerToys.exe. Likely a WinUI3 App SDK application. - directoryPath = Path.Combine(directoryPath, ".."); - directoryPath = Path.Combine(directoryPath, "PowerToys.exe"); - } - else - { - // PowerToys.exe is in the same path as the application. - directoryPath = Path.Combine(directoryPath, "PowerToys.exe"); + Logger.LogError($"Failed to find powertoys exe path, {exePath}"); + return; } - Process.Start(new ProcessStartInfo(directoryPath) { Arguments = "--open-settings=" + SettingsWindowNameToString(window) }); + var args = "--open-settings=" + SettingsWindowNameToString(window); + + Process.Start(new ProcessStartInfo + { + FileName = exePath, + Arguments = args, + UseShellExecute = false, + }); } - catch + catch (Exception ex) { - // TODO(stefan): Log exception once unified logging is implemented + Logger.LogError(ex.Message); } } } diff --git a/src/common/ManagedCommon/PowerToysPathResolver.cs b/src/common/ManagedCommon/PowerToysPathResolver.cs new file mode 100644 index 0000000000..fc6afee818 --- /dev/null +++ b/src/common/ManagedCommon/PowerToysPathResolver.cs @@ -0,0 +1,168 @@ +// 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.Diagnostics; +using System.IO; +using System.Runtime.Versioning; +using Microsoft.Win32; + +namespace ManagedCommon +{ + [SupportedOSPlatform("windows")] + public class PowerToysPathResolver + { + private const string PowerToysRegistryKey = @"Software\Classes\powertoys"; + private const string PowerToysExe = "PowerToys.exe"; + + /// + /// Gets the PowerToys installation path by checking registry entries + /// + /// The path to PowerToys installation directory, or null if not found + public static string GetPowerToysInstallPath() + { +#if DEBUG + // In debug builds, resolve directly from the running process (no installer/registry involved). + return GetPathFromCurrentProcess(); +#else + // Try to get path from Per-User installation first + string path = GetPathFromRegistry(RegistryHive.CurrentUser); + if (!string.IsNullOrEmpty(path)) + { + return path; + } + + // Fall back to Per-Machine installation + path = GetPathFromRegistry(RegistryHive.LocalMachine); + if (!string.IsNullOrEmpty(path)) + { + return path; + } + + return null; +#endif + } + + private static string GetPathFromRegistry(RegistryHive hive) + { + try + { + using var baseKey = RegistryKey.OpenBaseKey(hive, RegistryView.Registry64); + + // First try to get path from the powertoys protocol registration + string path = GetPathFromProtocolRegistration(baseKey); + if (!string.IsNullOrEmpty(path)) + { + return path; + } + } + catch (Exception) + { + // Ignore registry access errors + } + + return null; + } + + private static string GetPathFromProtocolRegistration(RegistryKey baseKey) + { + try + { + using var key = baseKey.OpenSubKey($@"{PowerToysRegistryKey}\shell\open\command"); + + if (key != null) + { + string command = key.GetValue(string.Empty)?.ToString(); + if (!string.IsNullOrEmpty(command)) + { + // Parse command like: "C:\Program Files\PowerToys\PowerToys.exe" "%1" + return ExtractPathFromCommand(command); + } + } + } + catch (Exception) + { + // Ignore registry access errors + } + + return null; + } + + private static string GetPathFromCurrentProcess() + { + try + { + // If we're running inside PowerToys.exe (dev/debug builds), use the executable location. + var processPath = Process.GetCurrentProcess().MainModule?.FileName; + if (!string.IsNullOrEmpty(processPath)) + { + var processDir = Path.GetDirectoryName(processPath); + if (!string.IsNullOrEmpty(processDir) && File.Exists(Path.Combine(processDir, PowerToysExe))) + { + return processDir; + } + } + + // As a fallback, walk up from AppContext.BaseDirectory to find PowerToys.exe. + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, PowerToysExe); + if (File.Exists(candidate)) + { + return directory.FullName; + } + + directory = directory.Parent; + } + } + catch + { + // Ignore reflection/process permission errors; caller will see null and handle accordingly. + } + + return null; + } + + private static string ExtractPathFromCommand(string command) + { + if (string.IsNullOrEmpty(command)) + { + return null; + } + + try + { + // Handle quoted paths: "C:\Program Files\PowerToys\PowerToys.exe" "%1" + if (command.StartsWith('\"')) + { + int endQuote = command.IndexOf('\"', 1); + if (endQuote > 1) + { + string exePath = command.Substring(1, endQuote - 1); + if (File.Exists(exePath)) + { + return Path.GetDirectoryName(exePath); + } + } + } + else + { + // Handle unquoted paths (less common) + string[] parts = command.Split(' '); + if (parts.Length > 0 && File.Exists(parts[0])) + { + return Path.GetDirectoryName(parts[0]); + } + } + } + catch (Exception) + { + // Ignore path parsing errors + } + + return null; + } + } +} diff --git a/src/common/PowerToys.ModuleContracts/IModuleService.cs b/src/common/PowerToys.ModuleContracts/IModuleService.cs new file mode 100644 index 0000000000..845d40e656 --- /dev/null +++ b/src/common/PowerToys.ModuleContracts/IModuleService.cs @@ -0,0 +1,47 @@ +// 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 Common.UI; + +namespace PowerToys.ModuleContracts; + +/// +/// Base contract for PowerToys modules exposed to the Command Palette. +/// +public interface IModuleService +{ + /// + /// Gets module identifier (e.g., Workspaces, Awake). + /// + string Key { get; } + + Task LaunchAsync(CancellationToken cancellationToken = default); + + Task OpenSettingsAsync(CancellationToken cancellationToken = default); +} + +/// +/// Helper base to reduce duplication for simple modules. +/// +public abstract class ModuleServiceBase : IModuleService +{ + public abstract string Key { get; } + + protected abstract SettingsDeepLink.SettingsWindow SettingsWindow { get; } + + public abstract Task LaunchAsync(CancellationToken cancellationToken = default); + + public virtual Task OpenSettingsAsync(CancellationToken cancellationToken = default) + { + try + { + SettingsDeepLink.OpenSettings(SettingsWindow); + return Task.FromResult(OperationResult.Ok()); + } + catch (Exception ex) + { + return Task.FromResult(OperationResult.Fail($"Failed to open settings for {Key}: {ex.Message}")); + } + } +} diff --git a/src/common/PowerToys.ModuleContracts/OperationResult.cs b/src/common/PowerToys.ModuleContracts/OperationResult.cs new file mode 100644 index 0000000000..a20aa26a3f --- /dev/null +++ b/src/common/PowerToys.ModuleContracts/OperationResult.cs @@ -0,0 +1,30 @@ +// 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 PowerToys.ModuleContracts; + +/// +/// Lightweight result type for module operations. +/// +public readonly record struct OperationResult(bool Success, string? Error = null) +{ + public static OperationResult Ok() => new(true, null); + + public static OperationResult Fail(string error) => new(false, error); +} + +/// +/// Result type with a payload. +/// +public readonly record struct OperationResult(bool Success, T? Value, string? Error = null); + +/// +/// Factory helpers for creating operation results. +/// +public static class OperationResults +{ + public static OperationResult Ok(T value) => new(true, value, null); + + public static OperationResult Fail(string error) => new(false, default, error); +} diff --git a/src/common/PowerToys.ModuleContracts/PowerToys.ModuleContracts.csproj b/src/common/PowerToys.ModuleContracts/PowerToys.ModuleContracts.csproj new file mode 100644 index 0000000000..aa80bb05fb --- /dev/null +++ b/src/common/PowerToys.ModuleContracts/PowerToys.ModuleContracts.csproj @@ -0,0 +1,16 @@ + + + + + + + enable + enable + false + false + + + + + + diff --git a/src/common/interop/Constants.cpp b/src/common/interop/Constants.cpp index 4fc9fca6f6..3e1c339233 100644 --- a/src/common/interop/Constants.cpp +++ b/src/common/interop/Constants.cpp @@ -75,10 +75,62 @@ namespace winrt::PowerToys::Interop::implementation { return CommonSharedConstants::ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE; } + hstring Constants::AdvancedPasteShowUIEvent() + { + return CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_EVENT; + } hstring Constants::AdvancedPasteTerminateAppMessage() { return CommonSharedConstants::ADVANCED_PASTE_TERMINATE_APP_MESSAGE; } + hstring Constants::AlwaysOnTopPinEvent() + { + return CommonSharedConstants::ALWAYS_ON_TOP_PIN_EVENT; + } + hstring Constants::FindMyMouseTriggerEvent() + { + return CommonSharedConstants::FIND_MY_MOUSE_TRIGGER_EVENT; + } + hstring Constants::MouseHighlighterTriggerEvent() + { + return CommonSharedConstants::MOUSE_HIGHLIGHTER_TRIGGER_EVENT; + } + hstring Constants::MouseCrosshairsTriggerEvent() + { + return CommonSharedConstants::MOUSE_CROSSHAIRS_TRIGGER_EVENT; + } + hstring Constants::CursorWrapTriggerEvent() + { + return CommonSharedConstants::CURSOR_WRAP_TRIGGER_EVENT; + } + hstring Constants::LightSwitchToggleEvent() + { + return CommonSharedConstants::LIGHTSWITCH_TOGGLE_EVENT; + } + hstring Constants::ZoomItZoomEvent() + { + return CommonSharedConstants::ZOOMIT_ZOOM_EVENT; + } + hstring Constants::ZoomItDrawEvent() + { + return CommonSharedConstants::ZOOMIT_DRAW_EVENT; + } + hstring Constants::ZoomItBreakEvent() + { + return CommonSharedConstants::ZOOMIT_BREAK_EVENT; + } + hstring Constants::ZoomItLiveZoomEvent() + { + return CommonSharedConstants::ZOOMIT_LIVEZOOM_EVENT; + } + hstring Constants::ZoomItSnipEvent() + { + return CommonSharedConstants::ZOOMIT_SNIP_EVENT; + } + hstring Constants::ZoomItRecordEvent() + { + return CommonSharedConstants::ZOOMIT_RECORD_EVENT; + } hstring Constants::ShowPowerOCRSharedEvent() { return CommonSharedConstants::SHOW_POWEROCR_SHARED_EVENT; diff --git a/src/common/interop/Constants.h b/src/common/interop/Constants.h index 80834442c5..8c95a09f99 100644 --- a/src/common/interop/Constants.h +++ b/src/common/interop/Constants.h @@ -23,6 +23,20 @@ namespace winrt::PowerToys::Interop::implementation static hstring AdvancedPasteAdditionalActionMessage(); static hstring AdvancedPasteCustomActionMessage(); static hstring AdvancedPasteTerminateAppMessage(); + static hstring AdvancedPasteShowUIEvent(); + static hstring AlwaysOnTopPinEvent(); + static hstring MeasureToolTriggerEvent(); + static hstring FindMyMouseTriggerEvent(); + static hstring MouseHighlighterTriggerEvent(); + static hstring MouseCrosshairsTriggerEvent(); + static hstring CursorWrapTriggerEvent(); + static hstring LightSwitchToggleEvent(); + static hstring ZoomItZoomEvent(); + static hstring ZoomItDrawEvent(); + static hstring ZoomItBreakEvent(); + static hstring ZoomItLiveZoomEvent(); + static hstring ZoomItSnipEvent(); + static hstring ZoomItRecordEvent(); static hstring ShowPowerOCRSharedEvent(); static hstring TerminatePowerOCRSharedEvent(); static hstring MouseJumpShowPreviewEvent(); @@ -33,7 +47,6 @@ namespace winrt::PowerToys::Interop::implementation static hstring PowerAccentExitEvent(); static hstring ShortcutGuideTriggerEvent(); static hstring RegistryPreviewTriggerEvent(); - static hstring MeasureToolTriggerEvent(); static hstring GcodePreviewResizeEvent(); static hstring BgcodePreviewResizeEvent(); static hstring QoiPreviewResizeEvent(); diff --git a/src/common/interop/Constants.idl b/src/common/interop/Constants.idl index 6833b0d417..97be6c8b7e 100644 --- a/src/common/interop/Constants.idl +++ b/src/common/interop/Constants.idl @@ -20,6 +20,19 @@ namespace PowerToys static String AdvancedPasteAdditionalActionMessage(); static String AdvancedPasteCustomActionMessage(); static String AdvancedPasteTerminateAppMessage(); + static String AdvancedPasteShowUIEvent(); + static String AlwaysOnTopPinEvent(); + static String FindMyMouseTriggerEvent(); + static String MouseHighlighterTriggerEvent(); + static String MouseCrosshairsTriggerEvent(); + static String CursorWrapTriggerEvent(); + static String LightSwitchToggleEvent(); + static String ZoomItZoomEvent(); + static String ZoomItDrawEvent(); + static String ZoomItBreakEvent(); + static String ZoomItLiveZoomEvent(); + static String ZoomItSnipEvent(); + static String ZoomItRecordEvent(); static String ShowPowerOCRSharedEvent(); static String TerminatePowerOCRSharedEvent(); static String MouseJumpShowPreviewEvent(); @@ -51,4 +64,4 @@ namespace PowerToys static String ShowCmdPalEvent(); } } -} \ No newline at end of file +} diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h index aa6306d7ba..73c4fb7006 100644 --- a/src/common/interop/shared_constants.h +++ b/src/common/interop/shared_constants.h @@ -40,6 +40,8 @@ namespace CommonSharedConstants const wchar_t ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE[] = L"CustomAction"; const wchar_t ADVANCED_PASTE_TERMINATE_APP_MESSAGE[] = L"TerminateApp"; + + const wchar_t ADVANCED_PASTE_SHOW_UI_EVENT[] = L"Local\\PowerToys_AdvancedPaste_ShowUI"; // Path to the event used to show Color Picker const wchar_t SHOW_COLOR_PICKER_SHARED_EVENT[] = L"Local\\ShowColorPickerEvent-8c46be2a-3e05-4186-b56b-4ae986ef2525"; @@ -83,12 +85,21 @@ namespace CommonSharedConstants const wchar_t TERMINATE_MOUSE_JUMP_SHARED_EVENT[] = L"Local\\TerminateMouseJumpEvent-252fa337-317f-4c37-a61f-99464c3f9728"; + // Paths to the events used by other Mouse Utilities + const wchar_t FIND_MY_MOUSE_TRIGGER_EVENT[] = L"Local\\FindMyMouseTriggerEvent-5a9dc5f4-1c74-4f2f-a66f-1b9b6a2f9b23"; + const wchar_t MOUSE_HIGHLIGHTER_TRIGGER_EVENT[] = L"Local\\MouseHighlighterTriggerEvent-1e3c9c3d-3fdf-4f9a-9a52-31c9b3c3a8f4"; + const wchar_t MOUSE_CROSSHAIRS_TRIGGER_EVENT[] = L"Local\\MouseCrosshairsTriggerEvent-0d4c7f92-0a5c-4f5c-b64b-8a2a2f7e0b21"; + const wchar_t CURSOR_WRAP_TRIGGER_EVENT[] = L"Local\\CursorWrapTriggerEvent-1f8452b5-4e6e-45b3-8b09-13f14a5900c9"; + // Path to the event used by RegistryPreview const wchar_t REGISTRY_PREVIEW_TRIGGER_EVENT[] = L"Local\\RegistryPreviewEvent-4C559468-F75A-4E7F-BC4F-9C9688316687"; // Path to the event used by MeasureTool const wchar_t MEASURE_TOOL_TRIGGER_EVENT[] = L"Local\\MeasureToolEvent-3d46745f-09b3-4671-a577-236be7abd199"; + // Path to the event used by LightSwitch + const wchar_t LIGHTSWITCH_TOGGLE_EVENT[] = L"Local\\PowerToys-LightSwitch-ToggleEvent-d8dc2f29-8c94-4ca1-8c5f-3e2b1e3c4f5a"; + // Path to the event used by GcodePreviewHandler const wchar_t GCODE_PREVIEW_RESIZE_EVENT[] = L"Local\\PowerToysGcodePreviewResizeEvent-6ff1f9bd-ccbd-4b24-a79f-40a34fb0317d"; @@ -130,6 +141,12 @@ namespace CommonSharedConstants // Path to the events used by ZoomIt const wchar_t ZOOMIT_REFRESH_SETTINGS_EVENT[] = L"Local\\PowerToysZoomIt-RefreshSettingsEvent-f053a563-d519-4b0d-8152-a54489c13324"; const wchar_t ZOOMIT_EXIT_EVENT[] = L"Local\\PowerToysZoomIt-ExitEvent-36641ce6-df02-4eac-abea-a3fbf9138220"; + const wchar_t ZOOMIT_ZOOM_EVENT[] = L"Local\\PowerToysZoomIt-ZoomEvent-1e4190d7-94bc-4ad5-adc0-9a8fd07cb393"; + const wchar_t ZOOMIT_DRAW_EVENT[] = L"Local\\PowerToysZoomIt-DrawEvent-56338997-404d-4549-bd9a-d132b6766975"; + const wchar_t ZOOMIT_BREAK_EVENT[] = L"Local\\PowerToysZoomIt-BreakEvent-17f2e63c-4c56-41dd-90a0-2d12f9f50c6b"; + const wchar_t ZOOMIT_LIVEZOOM_EVENT[] = L"Local\\PowerToysZoomIt-LiveZoomEvent-390bf0c7-616f-47dc-bafe-a2d228add20d"; + const wchar_t ZOOMIT_SNIP_EVENT[] = L"Local\\PowerToysZoomIt-SnipEvent-2fd9c211-436d-4f17-a902-2528aaae3e30"; + const wchar_t ZOOMIT_RECORD_EVENT[] = L"Local\\PowerToysZoomIt-RecordEvent-74539344-eaad-4711-8e83-23946e424512"; // used from quick access window const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a"; diff --git a/src/common/utils/EventWaiter.h b/src/common/utils/EventWaiter.h index b9f420c81d..c2db880530 100644 --- a/src/common/utils/EventWaiter.h +++ b/src/common/utils/EventWaiter.h @@ -3,78 +3,128 @@ #include #include #include +#include #include +/// +/// A reusable utility class that listens for a named Windows event and invokes a callback when triggered. +/// Provides RAII-based resource management for event handles and the listener thread. +/// The thread is properly joined on destruction to ensure clean shutdown. +/// class EventWaiter { public: - EventWaiter() {} - EventWaiter(const std::wstring& name, std::function callback) + EventWaiter() = default; + + EventWaiter(const EventWaiter&) = delete; + EventWaiter& operator=(const EventWaiter&) = delete; + EventWaiter(EventWaiter&&) = delete; + EventWaiter& operator=(EventWaiter&&) = delete; + + ~EventWaiter() { - // Create localExitThreadEvent and localWaitingEvent for capturing. We cannot capture 'this' as we implement move constructor. - auto localExitThreadEvent = exitThreadEvent = CreateEvent(nullptr, false, false, nullptr); - HANDLE localWaitingEvent = waitingEvent = CreateEvent(nullptr, false, false, name.c_str()); - std::thread([=]() { - HANDLE events[2] = { localWaitingEvent, localExitThreadEvent }; - while (true) + stop(); + } + + /// + /// Starts listening for the specified named event. When the event is signaled, the callback is invoked. + /// + /// The name of the Windows event to listen for. + /// The callback function to invoke when the event is triggered. Receives ERROR_SUCCESS on success. + /// true if listening started successfully, false otherwise. + bool start(const std::wstring& name, std::function callback) + { + if (m_listening) + { + return false; + } + + m_exitThreadEvent = CreateEventW(nullptr, false, false, nullptr); + m_waitingEvent = CreateEventW(nullptr, false, false, name.c_str()); + + if (!m_exitThreadEvent || !m_waitingEvent) + { + cleanup(); + return false; + } + + m_listening = true; + m_eventThread = std::thread([this, cb = std::move(callback)]() { + HANDLE events[2] = { m_waitingEvent, m_exitThreadEvent }; + while (m_listening) { auto waitResult = WaitForMultipleObjects(2, events, false, INFINITE); + if (!m_listening) + { + break; + } + if (waitResult == WAIT_OBJECT_0 + 1) { + // Exit event signaled break; } if (waitResult == WAIT_FAILED) { - callback(GetLastError()); + cb(GetLastError()); continue; } if (waitResult == WAIT_OBJECT_0) { - callback(ERROR_SUCCESS); + cb(ERROR_SUCCESS); } } - }).detach(); + }); + + return true; } - EventWaiter(EventWaiter&) = delete; - EventWaiter& operator=(EventWaiter&) = delete; - - EventWaiter(EventWaiter&& a) noexcept + /// + /// Stops listening for the event and cleans up resources. + /// Waits for the listener thread to finish before returning. + /// Safe to call multiple times. + /// + void stop() { - this->exitThreadEvent = a.exitThreadEvent; - this->waitingEvent = a.waitingEvent; - - a.exitThreadEvent = nullptr; - a.waitingEvent = nullptr; - } - - EventWaiter& operator=(EventWaiter&& a) noexcept - { - this->exitThreadEvent = a.exitThreadEvent; - this->waitingEvent = a.waitingEvent; - - a.exitThreadEvent = nullptr; - a.waitingEvent = nullptr; - return *this; - } - - ~EventWaiter() - { - if (exitThreadEvent) + m_listening = false; + if (m_exitThreadEvent) { - SetEvent(exitThreadEvent); - CloseHandle(exitThreadEvent); + SetEvent(m_exitThreadEvent); } - - if (waitingEvent) + if (m_eventThread.joinable()) { - CloseHandle(waitingEvent); + m_eventThread.join(); } + cleanup(); + } + + /// + /// Returns whether the listener is currently active. + /// + bool is_listening() const + { + return m_listening; } private: - HANDLE exitThreadEvent = nullptr; - HANDLE waitingEvent = nullptr; + void cleanup() + { + if (m_exitThreadEvent) + { + CloseHandle(m_exitThreadEvent); + m_exitThreadEvent = nullptr; + } + if (m_waitingEvent) + { + CloseHandle(m_waitingEvent); + m_waitingEvent = nullptr; + } + } + + HANDLE m_exitThreadEvent = nullptr; + HANDLE m_waitingEvent = nullptr; + std::thread m_eventThread; + std::atomic_bool m_listening{ false }; }; \ No newline at end of file diff --git a/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj b/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj index 9dc11a0a8a..a87508604f 100644 --- a/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj +++ b/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj @@ -45,6 +45,7 @@ + \ No newline at end of file diff --git a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs index b055d46457..b474b8215a 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs @@ -661,7 +661,7 @@ namespace AdvancedPaste.ViewModels [RelayCommand] public void OpenSettings() { - SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.AdvancedPaste, true); + SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.AdvancedPaste); GetMainWindow()?.Close(); } diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp index 64caaa115f..6cf2e8d9a9 100644 --- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp +++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp @@ -15,9 +15,11 @@ #include #include #include +#include #include #include +#include #include BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/) @@ -101,6 +103,9 @@ private: bool m_is_advanced_ai_enabled = false; bool m_preview_custom_format_output = true; + // Event listening for external triggers (e.g., from CmdPal extension) + EventWaiter m_triggerEventWaiter; + Hotkey parse_single_hotkey(const wchar_t* keyName, const winrt::Windows::Data::Json::JsonObject& settingsObject) { try @@ -779,6 +784,17 @@ public: Trace::AdvancedPaste_Enable(true); m_enabled = true; m_process_manager.start(); + + // Start listening for external trigger event so we can invoke the same logic as the hotkey. + // Note: Use start() directly instead of constructor + move assignment to avoid dangling this pointer in the thread. + m_triggerEventWaiter.start(CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_EVENT, [this](DWORD) { + // Same logic as hotkeyId == 1 (m_advanced_paste_ui_hotkey) + Logger::trace(L"AdvancedPaste ShowUI event triggered"); + m_process_manager.start(); + m_process_manager.bring_to_front(); + m_process_manager.send_message(CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_MESSAGE); + Trace::AdvancedPaste_Invoked(L"AdvancedPasteUIEvent"); + }); }; void Disable(bool traceEvent) @@ -787,6 +803,9 @@ public: { m_process_manager.stop(); + // Stop event listening + m_triggerEventWaiter.stop(); + if (traceEvent) { Trace::AdvancedPaste_Enable(false); diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/dllmain.cpp b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/dllmain.cpp index a4158d1c66..89db922ddd 100644 --- a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/dllmain.cpp +++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/dllmain.cpp @@ -146,7 +146,7 @@ public: } } - m_showEventWaiter = EventWaiter(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT, [&](int err) { + m_showEventWaiter.start(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT, [&](DWORD err) { if (m_enabled && err == ERROR_SUCCESS) { Logger::trace(L"{} event was signaled", CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT); @@ -164,7 +164,7 @@ public: } }); - m_showAdminEventWaiter = EventWaiter(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT, [&](int err) { + m_showAdminEventWaiter.start(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT, [&](DWORD err) { if (m_enabled && err == ERROR_SUCCESS) { Logger::trace(L"{} event was signaled", CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT); diff --git a/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs b/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs index 8dbec70de8..0b2739ebe1 100644 --- a/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs +++ b/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs @@ -67,7 +67,7 @@ namespace Hosts services.AddSingleton(); services.AddSingleton(() => { - SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.Hosts, true); + SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.Hosts); }); services.AddSingleton(); diff --git a/src/modules/Hosts/HostsModuleInterface/dllmain.cpp b/src/modules/Hosts/HostsModuleInterface/dllmain.cpp index 993226ac2b..fb8dd40011 100644 --- a/src/modules/Hosts/HostsModuleInterface/dllmain.cpp +++ b/src/modules/Hosts/HostsModuleInterface/dllmain.cpp @@ -155,7 +155,7 @@ public: } } - m_showEventWaiter = EventWaiter(CommonSharedConstants::SHOW_HOSTS_EVENT, [&](int err) + m_showEventWaiter.start(CommonSharedConstants::SHOW_HOSTS_EVENT, [&](DWORD err) { if (m_enabled && err == ERROR_SUCCESS) { @@ -174,7 +174,7 @@ public: } }); - m_showAdminEventWaiter = EventWaiter(CommonSharedConstants::SHOW_HOSTS_ADMIN_EVENT, [&](int err) + m_showAdminEventWaiter.start(CommonSharedConstants::SHOW_HOSTS_ADMIN_EVENT, [&](DWORD err) { if (m_enabled && err == ERROR_SUCCESS) { diff --git a/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj b/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj index 261cfab1e6..b86b25a4d1 100644 --- a/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj +++ b/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj @@ -168,9 +168,6 @@ ..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories) - - $(CoreLibraryDependencies);%(AdditionalDependencies);advapi32.lib - @@ -222,4 +219,4 @@ - \ No newline at end of file + diff --git a/src/modules/LightSwitch/LightSwitchModuleInterface/dllmain.cpp b/src/modules/LightSwitch/LightSwitchModuleInterface/dllmain.cpp index a5973a396f..11cfd412b0 100644 --- a/src/modules/LightSwitch/LightSwitchModuleInterface/dllmain.cpp +++ b/src/modules/LightSwitch/LightSwitchModuleInterface/dllmain.cpp @@ -8,6 +8,8 @@ #include #include #include "ThemeHelper.h" +#include +#include extern "C" IMAGE_DOS_HEADER __ImageBase; @@ -103,12 +105,18 @@ private: HANDLE m_force_light_event_handle; HANDLE m_force_dark_event_handle; HANDLE m_manual_override_event_handle; + HANDLE m_toggle_event_handle{ nullptr }; + std::thread m_toggle_thread; + std::atomic m_toggle_thread_running{ false }; static const constexpr int NUM_DEFAULT_HOTKEYS = 4; Hotkey m_toggle_theme_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'D' }; void init_settings(); + void ToggleTheme(); + void StartToggleListener(); + void StopToggleListener(); public: LightSwitchInterface() @@ -118,6 +126,7 @@ public: m_force_light_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_LIGHT"); m_force_dark_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_DARK"); m_manual_override_event_handle = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); + m_toggle_event_handle = CreateDefaultEvent(L"Local\\PowerToys-LightSwitch-ToggleEvent-d8dc2f29-8c94-4ca1-8c5f-3e2b1e3c4f5a"); init_settings(); }; @@ -130,6 +139,8 @@ public: // Destroy the powertoy and free memory virtual void destroy() override { + // Ensure worker threads/process handles are cleaned up before destruction + disable(); delete this; } @@ -444,6 +455,8 @@ public: Logger::info(L"Light Switch process launched successfully (PID: {}).", pi.dwProcessId); m_process = pi.hProcess; CloseHandle(pi.hThread); + + StartToggleListener(); } // Disable the powertoy @@ -469,6 +482,8 @@ public: CloseHandle(m_process); m_process = nullptr; } + + StopToggleListener(); } // Returns if the powertoys is enabled @@ -530,31 +545,8 @@ public: } else if (hotkeyId == 0) { - // get current will return true if in light mode; otherwise false Logger::info(L"[Light Switch] Hotkey triggered: Toggle Theme"); - if (g_settings.m_changeSystem) - { - SetSystemTheme(!GetCurrentSystemTheme()); - } - if (g_settings.m_changeApps) - { - SetAppsTheme(!GetCurrentAppsTheme()); - } - - if (!m_manual_override_event_handle) - { - m_manual_override_event_handle = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); - if (!m_manual_override_event_handle) - { - m_manual_override_event_handle = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); - } - } - - if (m_manual_override_event_handle) - { - SetEvent(m_manual_override_event_handle); - Logger::debug(L"[Light Switch] Manual override event set"); - } + ToggleTheme(); } return true; @@ -567,8 +559,80 @@ public: { return WaitForSingleObject(m_process, 0) == WAIT_TIMEOUT; } + }; +void LightSwitchInterface::ToggleTheme() +{ + if (g_settings.m_changeSystem) + { + SetSystemTheme(!GetCurrentSystemTheme()); + } + if (g_settings.m_changeApps) + { + SetAppsTheme(!GetCurrentAppsTheme()); + } + + if (!m_manual_override_event_handle) + { + m_manual_override_event_handle = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); + if (!m_manual_override_event_handle) + { + m_manual_override_event_handle = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); + } + } + + if (m_manual_override_event_handle) + { + SetEvent(m_manual_override_event_handle); + Logger::debug(L"[Light Switch] Manual override event set"); + } +} + +void LightSwitchInterface::StartToggleListener() +{ + if (m_toggle_thread_running || !m_toggle_event_handle) + { + return; + } + + m_toggle_thread_running = true; + m_toggle_thread = std::thread([this]() { + while (m_toggle_thread_running) + { + const DWORD wait_result = WaitForSingleObject(m_toggle_event_handle, 500); + if (!m_toggle_thread_running) + { + break; + } + + if (wait_result == WAIT_OBJECT_0) + { + ToggleTheme(); + ResetEvent(m_toggle_event_handle); + } + } + }); +} + +void LightSwitchInterface::StopToggleListener() +{ + if (!m_toggle_thread_running) + { + return; + } + + m_toggle_thread_running = false; + if (m_toggle_event_handle) + { + SetEvent(m_toggle_event_handle); + } + if (m_toggle_thread.joinable()) + { + m_toggle_thread.join(); + } +} + std::wstring utf8_to_wstring(const std::string& str) { if (str.empty()) @@ -646,4 +710,4 @@ void LightSwitchInterface::init_settings() extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() { return new LightSwitchInterface(); -} \ No newline at end of file +} diff --git a/src/modules/MeasureTool/MeasureToolModuleInterface/dllmain.cpp b/src/modules/MeasureTool/MeasureToolModuleInterface/dllmain.cpp index cfac5ce640..0c31e7f9ef 100644 --- a/src/modules/MeasureTool/MeasureToolModuleInterface/dllmain.cpp +++ b/src/modules/MeasureTool/MeasureToolModuleInterface/dllmain.cpp @@ -149,7 +149,7 @@ public: init_settings(); triggerEvent = CreateEvent(nullptr, false, false, CommonSharedConstants::MEASURE_TOOL_TRIGGER_EVENT); - triggerEventWaiter = EventWaiter(CommonSharedConstants::MEASURE_TOOL_TRIGGER_EVENT, [this](int) { + triggerEventWaiter.start(CommonSharedConstants::MEASURE_TOOL_TRIGGER_EVENT, [this](DWORD) { on_hotkey(0); }); } diff --git a/src/modules/MouseUtils/CursorWrap/dllmain.cpp b/src/modules/MouseUtils/CursorWrap/dllmain.cpp index 74524ed9f9..09342d3a88 100644 --- a/src/modules/MouseUtils/CursorWrap/dllmain.cpp +++ b/src/modules/MouseUtils/CursorWrap/dllmain.cpp @@ -6,6 +6,7 @@ #include "../../../common/utils/resources.h" #include "../../../common/logger/logger.h" #include "../../../common/utils/logger_helper.h" +#include "../../../common/interop/shared_constants.h" #include #include #include @@ -108,6 +109,12 @@ private: // Hotkey Hotkey m_activationHotkey{}; + // Event-driven trigger support (for CmdPal/automation) + HANDLE m_triggerEventHandle = nullptr; + HANDLE m_terminateEventHandle = nullptr; + std::thread m_eventThread; + std::atomic_bool m_listening{ false }; + public: // Constructor CursorWrap() @@ -121,7 +128,8 @@ public: // Destroy the powertoy and free memory virtual void destroy() override { - StopMouseHook(); + // Ensure hooks/threads/handles are torn down before deletion + disable(); g_cursorWrapInstance = nullptr; // Clear global instance pointer delete this; } @@ -195,11 +203,54 @@ public: { m_enabled = true; Trace::EnableCursorWrap(true); - - // Always start the mouse hook when the module is enabled - // This ensures cursor wrapping is active immediately after enabling - StartMouseHook(); - Logger::info("CursorWrap enabled - mouse hook started"); + + // Start listening for external trigger event so we can invoke the same logic as the activation hotkey. + m_triggerEventHandle = CreateEventW(nullptr, false, false, CommonSharedConstants::CURSOR_WRAP_TRIGGER_EVENT); + m_terminateEventHandle = CreateEventW(nullptr, false, false, nullptr); + if (m_triggerEventHandle && m_terminateEventHandle) + { + m_listening = true; + m_eventThread = std::thread([this]() { + HANDLE handles[2] = { m_triggerEventHandle, m_terminateEventHandle }; + + // WH_MOUSE_LL callbacks are delivered to the thread that installed the hook. + // Ensure this thread has a message queue and pumps messages while the hook is active. + MSG msg; + PeekMessage(&msg, nullptr, WM_USER, WM_USER, PM_NOREMOVE); + + StartMouseHook(); + Logger::info("CursorWrap enabled - mouse hook started"); + + while (m_listening) + { + auto res = MsgWaitForMultipleObjects(2, handles, false, INFINITE, QS_ALLINPUT); + if (!m_listening) + { + break; + } + + if (res == WAIT_OBJECT_0) + { + ToggleMouseHook(); + } + else if (res == WAIT_OBJECT_0 + 1) + { + break; + } + else + { + while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + } + + StopMouseHook(); + Logger::info("CursorWrap event listener stopped"); + }); + } } // Disable the powertoy @@ -207,8 +258,26 @@ public: { m_enabled = false; Trace::EnableCursorWrap(false); - StopMouseHook(); - Logger::info("CursorWrap disabled - mouse hook stopped"); + + m_listening = false; + if (m_terminateEventHandle) + { + SetEvent(m_terminateEventHandle); + } + if (m_eventThread.joinable()) + { + m_eventThread.join(); + } + if (m_triggerEventHandle) + { + CloseHandle(m_triggerEventHandle); + m_triggerEventHandle = nullptr; + } + if (m_terminateEventHandle) + { + CloseHandle(m_terminateEventHandle); + m_terminateEventHandle = nullptr; + } } // Returns if the powertoys is enabled @@ -240,7 +309,19 @@ public: return false; } - // Toggle cursor wrapping + // Toggle on the thread that owns the WH_MOUSE_LL hook (the event listener thread). + if (m_triggerEventHandle) + { + return SetEvent(m_triggerEventHandle); + } + + return false; + } + + private: + void ToggleMouseHook() + { + // Toggle cursor wrapping. if (m_hookActive) { StopMouseHook(); @@ -253,11 +334,8 @@ public: RunComprehensiveTests(); #endif } - - return true; } -private: // Load the settings file. void init_settings() { diff --git a/src/modules/MouseUtils/FindMyMouse/dllmain.cpp b/src/modules/MouseUtils/FindMyMouse/dllmain.cpp index b7ffb6177a..af99f45136 100644 --- a/src/modules/MouseUtils/FindMyMouse/dllmain.cpp +++ b/src/modules/MouseUtils/FindMyMouse/dllmain.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include namespace { @@ -69,6 +71,9 @@ private: // Find My Mouse specific settings FindMyMouseSettings m_findMyMouseSettings; + // Event-driven trigger support + EventWaiter m_triggerEventWaiter; + // Load initial settings from the persisted values. void init_settings(); @@ -86,6 +91,8 @@ public: // Destroy the powertoy and free memory virtual void destroy() override { + // Ensure threads/handles are cleaned up before destruction + disable(); delete this; } @@ -150,6 +157,11 @@ public: m_enabled = true; Trace::EnableFindMyMouse(true); std::thread([=]() { FindMyMouseMain(m_hModule, m_findMyMouseSettings); }).detach(); + + // Start listening for external trigger event so we can invoke the same logic as the hotkey. + m_triggerEventWaiter.start(CommonSharedConstants::FIND_MY_MOUSE_TRIGGER_EVENT, [this](DWORD) { + OnHotkeyEx(); + }); } // Disable the powertoy @@ -158,6 +170,8 @@ public: m_enabled = false; Trace::EnableFindMyMouse(false); FindMyMouseDisable(); + + m_triggerEventWaiter.stop(); } // Returns if the powertoys is enabled @@ -216,7 +230,7 @@ inline static uint8_t LegacyOpacityToAlpha(int overlayOpacityPercent) overlayOpacityPercent = 100; } - // Round to nearest integer (0–255) + // Round to nearest integer (0–255) return static_cast((overlayOpacityPercent * 255 + 50) / 100); } @@ -532,4 +546,4 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings) extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() { return new FindMyMouse(); -} \ No newline at end of file +} diff --git a/src/modules/MouseUtils/MouseHighlighter/dllmain.cpp b/src/modules/MouseUtils/MouseHighlighter/dllmain.cpp index 45c62ae9ca..83a8837409 100644 --- a/src/modules/MouseUtils/MouseHighlighter/dllmain.cpp +++ b/src/modules/MouseUtils/MouseHighlighter/dllmain.cpp @@ -4,6 +4,8 @@ #include "trace.h" #include "MouseHighlighter.h" #include "common/utils/color.h" +#include +#include namespace { @@ -61,6 +63,9 @@ private: // Mouse Highlighter specific settings MouseHighlighterSettings m_highlightSettings; + // Event-driven trigger support + EventWaiter m_triggerEventWaiter; + public: // Constructor MouseHighlighter() @@ -72,6 +77,8 @@ public: // Destroy the powertoy and free memory virtual void destroy() override { + // Tear down threads/handles before deletion to avoid abort() on joinable threads during shutdown + disable(); delete this; } @@ -132,6 +139,11 @@ public: m_enabled = true; Trace::EnableMouseHighlighter(true); std::thread([=]() { MouseHighlighterMain(m_hModule, m_highlightSettings); }).detach(); + + // Start listening for external trigger event so we can invoke the same logic as the hotkey. + m_triggerEventWaiter.start(CommonSharedConstants::MOUSE_HIGHLIGHTER_TRIGGER_EVENT, [this](DWORD) { + OnHotkeyEx(); + }); } // Disable the powertoy @@ -140,6 +152,8 @@ public: m_enabled = false; Trace::EnableMouseHighlighter(false); MouseHighlighterDisable(); + + m_triggerEventWaiter.stop(); } // Returns if the powertoys is enabled diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp index b460e29643..5697d83d30 100644 --- a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp +++ b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp @@ -4,7 +4,8 @@ #include "trace.h" #include "InclusiveCrosshairs.h" #include "common/utils/color.h" -#include +#include +#include #include #include #include @@ -124,6 +125,9 @@ private: // Mouse Pointer Crosshairs specific settings InclusiveCrosshairsSettings m_inclusiveCrosshairsSettings; + // Event-driven trigger support + EventWaiter m_triggerEventWaiter; + public: // Constructor MousePointerCrosshairs() @@ -137,11 +141,9 @@ public: // Destroy the powertoy and free memory virtual void destroy() override { - UninstallKeyboardHook(); - StopXTimer(); - StopYTimer(); + // Ensure all background threads/handles are torn down before destruction to avoid std::terminate/abort on joinable threads + disable(); g_instance.store(nullptr, std::memory_order_release); - // Release shared state so worker threads (if any) exit when weak_ptr lock fails m_state.reset(); delete this; } @@ -203,6 +205,11 @@ public: m_enabled = true; Trace::EnableMousePointerCrosshairs(true); std::thread([=]() { InclusiveCrosshairsMain(m_hModule, m_inclusiveCrosshairsSettings); }).detach(); + + // Start listening for external trigger event so we can invoke the same logic as the activation hotkey. + m_triggerEventWaiter.start(CommonSharedConstants::MOUSE_CROSSHAIRS_TRIGGER_EVENT, [this](DWORD) { + on_hotkey(0); // activation hotkey + }); } // Disable the powertoy @@ -215,6 +222,8 @@ public: StopYTimer(); m_glideState = 0; InclusiveCrosshairsDisable(); + + m_triggerEventWaiter.stop(); } // Returns if the powertoys is enabled @@ -901,4 +910,4 @@ private: extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() { return new MousePointerCrosshairs(); -} \ No newline at end of file +} diff --git a/src/modules/PowerOCR/PowerOCR/OCROverlay.xaml.cs b/src/modules/PowerOCR/PowerOCR/OCROverlay.xaml.cs index 2664b8f03b..88219a4110 100644 --- a/src/modules/PowerOCR/PowerOCR/OCROverlay.xaml.cs +++ b/src/modules/PowerOCR/PowerOCR/OCROverlay.xaml.cs @@ -426,7 +426,7 @@ public partial class OCROverlay : Window private void SettingsMenuItem_Click(object sender, RoutedEventArgs e) { WindowUtilities.CloseAllOCROverlays(); - SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.PowerOCR, false); + SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.PowerOCR); } private static bool CheckIfCheckingOrUnchecking(object? sender) diff --git a/src/modules/ShortcutGuide/ShortcutGuide/main.cpp b/src/modules/ShortcutGuide/ShortcutGuide/main.cpp index 713446403b..57a4491d41 100644 --- a/src/modules/ShortcutGuide/ShortcutGuide/main.cpp +++ b/src/modules/ShortcutGuide/ShortcutGuide/main.cpp @@ -121,7 +121,7 @@ int WINAPI wWinMain(_In_ HINSTANCE /*hInstance*/, _In_opt_ HINSTANCE /*hPrevInst else { auto mainThreadId = GetCurrentThreadId(); - exitEventWaiter = EventWaiter(CommonSharedConstants::SHORTCUT_GUIDE_EXIT_EVENT, [mainThreadId, &window](int err) { + exitEventWaiter.start(CommonSharedConstants::SHORTCUT_GUIDE_EXIT_EVENT, [mainThreadId, &window](DWORD err) { if (err != ERROR_SUCCESS) { Logger::error(L"Failed to wait for {} event. {}", CommonSharedConstants::SHORTCUT_GUIDE_EXIT_EVENT, get_last_error_or_default(err)); diff --git a/src/modules/ShortcutGuide/ShortcutGuideModuleInterface/dllmain.cpp b/src/modules/ShortcutGuide/ShortcutGuideModuleInterface/dllmain.cpp index 5e8fe9aa1b..a870fb9ad8 100644 --- a/src/modules/ShortcutGuide/ShortcutGuideModuleInterface/dllmain.cpp +++ b/src/modules/ShortcutGuide/ShortcutGuideModuleInterface/dllmain.cpp @@ -37,7 +37,7 @@ public: } triggerEvent = CreateEvent(nullptr, false, false, CommonSharedConstants::SHORTCUT_GUIDE_TRIGGER_EVENT); - triggerEventWaiter = EventWaiter(CommonSharedConstants::SHORTCUT_GUIDE_TRIGGER_EVENT, [this](int) { + triggerEventWaiter.start(CommonSharedConstants::SHORTCUT_GUIDE_TRIGGER_EVENT, [this](DWORD) { OnHotkeyEx(); }); diff --git a/src/modules/Workspaces/Workspaces.ModuleServices/IWorkspaceService.cs b/src/modules/Workspaces/Workspaces.ModuleServices/IWorkspaceService.cs new file mode 100644 index 0000000000..00300fea9f --- /dev/null +++ b/src/modules/Workspaces/Workspaces.ModuleServices/IWorkspaceService.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using PowerToys.ModuleContracts; +using WorkspacesCsharpLibrary.Data; + +namespace Workspaces.ModuleServices; + +/// +/// Workspaces-specific operations. +/// +public interface IWorkspaceService : IModuleService +{ + Task LaunchWorkspaceAsync(string workspaceId, CancellationToken cancellationToken = default); + + Task LaunchEditorAsync(CancellationToken cancellationToken = default); + + Task SnapshotAsync(string? targetPath = null, CancellationToken cancellationToken = default); + + Task>> GetWorkspacesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/modules/Workspaces/Workspaces.ModuleServices/WorkspaceService.cs b/src/modules/Workspaces/Workspaces.ModuleServices/WorkspaceService.cs new file mode 100644 index 0000000000..eb916e24df --- /dev/null +++ b/src/modules/Workspaces/Workspaces.ModuleServices/WorkspaceService.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.IO; +using Common.UI; +using ManagedCommon; +using PowerToys.Interop; +using PowerToys.ModuleContracts; +using WorkspacesCsharpLibrary.Data; + +namespace Workspaces.ModuleServices; + +/// +/// Implementation of workspace actions for reuse across hosts. +/// +public sealed class WorkspaceService : ModuleServiceBase, IWorkspaceService +{ + public static WorkspaceService Instance { get; } = new(); + + public override string Key => SettingsDeepLink.SettingsWindow.Workspaces.ToString(); + + protected override SettingsDeepLink.SettingsWindow SettingsWindow => SettingsDeepLink.SettingsWindow.Workspaces; + + public override Task LaunchAsync(CancellationToken cancellationToken = default) + { + // Treat launch as invoking the Workspaces editor. + return LaunchEditorAsync(cancellationToken); + } + + public Task LaunchEditorAsync(CancellationToken cancellationToken = default) + { + try + { + using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.WorkspacesLaunchEditorEvent()); + eventHandle.Set(); + return Task.FromResult(OperationResult.Ok()); + } + catch (Exception ex) + { + return Task.FromResult(OperationResult.Fail($"Failed to launch Workspaces editor: {ex.Message}")); + } + } + + public Task LaunchWorkspaceAsync(string workspaceId, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(workspaceId)) + { + return Task.FromResult(OperationResult.Fail("Workspace id is required.")); + } + + try + { + var powertoysBaseDir = PowerToysPathResolver.GetPowerToysInstallPath(); + if (string.IsNullOrEmpty(powertoysBaseDir)) + { + return Task.FromResult(OperationResult.Fail("PowerToys installation path not found.")); + } + + var launcherPath = Path.Combine(powertoysBaseDir, "PowerToys.WorkspacesLauncher.exe"); + var startInfo = new ProcessStartInfo(launcherPath) + { + Arguments = workspaceId, + UseShellExecute = true, + }; + + Process.Start(startInfo); + return Task.FromResult(OperationResult.Ok()); + } + catch (Exception ex) + { + return Task.FromResult(OperationResult.Fail($"Failed to launch workspace: {ex.Message}")); + } + } + + public Task SnapshotAsync(string? targetPath = null, CancellationToken cancellationToken = default) + { + // Snapshot orchestration is not yet exposed via events; provide a clear failure for now. + return Task.FromResult(OperationResult.Fail("Snapshot is not implemented for Workspaces.")); + } + + public Task>> GetWorkspacesAsync(CancellationToken cancellationToken = default) + { + try + { + var items = WorkspacesStorage.Load(); + + return Task.FromResult(OperationResults.Ok>(items)); + } + catch (Exception ex) + { + return Task.FromResult(OperationResults.Fail>($"Failed to read workspaces: {ex.Message}")); + } + } +} diff --git a/src/modules/Workspaces/Workspaces.ModuleServices/Workspaces.ModuleServices.csproj b/src/modules/Workspaces/Workspaces.ModuleServices/Workspaces.ModuleServices.csproj new file mode 100644 index 0000000000..f835138a27 --- /dev/null +++ b/src/modules/Workspaces/Workspaces.ModuleServices/Workspaces.ModuleServices.csproj @@ -0,0 +1,20 @@ + + + + + + + enable + enable + false + false + + + + + + + + + + diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ApplicationWrapper.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ApplicationWrapper.cs new file mode 100644 index 0000000000..5bf07b9fc1 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ApplicationWrapper.cs @@ -0,0 +1,70 @@ +// 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.Serialization; + +namespace WorkspacesCsharpLibrary.Data; + +public struct ApplicationWrapper +{ + public struct WindowPositionWrapper + { + [JsonPropertyName("x")] + public int X { get; set; } + + [JsonPropertyName("y")] + public int Y { get; set; } + + [JsonPropertyName("width")] + public int Width { get; set; } + + [JsonPropertyName("height")] + public int Height { get; set; } + } + + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("application")] + public string Application { get; set; } + + [JsonPropertyName("application-path")] + public string ApplicationPath { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("package-full-name")] + public string PackageFullName { get; set; } + + [JsonPropertyName("app-user-model-id")] + public string AppUserModelId { get; set; } + + [JsonPropertyName("pwa-app-id")] + public string PwaAppId { get; set; } + + [JsonPropertyName("command-line-arguments")] + public string CommandLineArguments { get; set; } + + [JsonPropertyName("is-elevated")] + public bool IsElevated { get; set; } + + [JsonPropertyName("can-launch-elevated")] + public bool CanLaunchElevated { get; set; } + + [JsonPropertyName("minimized")] + public bool Minimized { get; set; } + + [JsonPropertyName("maximized")] + public bool Maximized { get; set; } + + [JsonPropertyName("position")] + public WindowPositionWrapper Position { get; set; } + + [JsonPropertyName("monitor")] + public int Monitor { get; set; } + + [JsonPropertyName("version")] + public string Version { get; set; } +} diff --git a/src/modules/Workspaces/WorkspacesEditor/Data/InvokePoint.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/InvokePoint.cs similarity index 51% rename from src/modules/Workspaces/WorkspacesEditor/Data/InvokePoint.cs rename to src/modules/Workspaces/WorkspacesCsharpLibrary/Data/InvokePoint.cs index fe41a65bd7..3f24d51f28 100644 --- a/src/modules/Workspaces/WorkspacesEditor/Data/InvokePoint.cs +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/InvokePoint.cs @@ -2,13 +2,12 @@ // 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 +namespace WorkspacesCsharpLibrary.Data; + +public enum InvokePoint { - /* sync with workspaces-common */ - public enum InvokePoint - { - EditorButton = 0, - Shortcut, - LaunchAndEdit, - } + EditorButton = 0, + Shortcut, + LaunchAndEdit, + CommandPaletteExtension, } diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/MonitorConfigurationWrapper.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/MonitorConfigurationWrapper.cs new file mode 100644 index 0000000000..1c48dee1ab --- /dev/null +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/MonitorConfigurationWrapper.cs @@ -0,0 +1,43 @@ +// 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.Serialization; + +namespace WorkspacesCsharpLibrary.Data; + +public struct MonitorConfigurationWrapper +{ + public struct MonitorRectWrapper + { + [JsonPropertyName("top")] + public int Top { get; set; } + + [JsonPropertyName("left")] + public int Left { get; set; } + + [JsonPropertyName("width")] + public int Width { get; set; } + + [JsonPropertyName("height")] + public int Height { get; set; } + } + + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("instance-id")] + public string InstanceId { get; set; } + + [JsonPropertyName("monitor-number")] + public int MonitorNumber { get; set; } + + [JsonPropertyName("dpi")] + public int Dpi { get; set; } + + [JsonPropertyName("monitor-rect-dpi-aware")] + public MonitorRectWrapper MonitorRectDpiAware { get; set; } + + [JsonPropertyName("monitor-rect-dpi-unaware")] + public MonitorRectWrapper MonitorRectDpiUnaware { get; set; } +} diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ProjectData.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ProjectData.cs new file mode 100644 index 0000000000..04006cb2c5 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ProjectData.cs @@ -0,0 +1,11 @@ +// 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 WorkspacesCsharpLibrary.Data; + +namespace WorkspacesCsharpLibrary.Data; + +public class ProjectData : WorkspacesEditorData +{ +} diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ProjectWrapper.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ProjectWrapper.cs new file mode 100644 index 0000000000..3f0f4dbc58 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/ProjectWrapper.cs @@ -0,0 +1,26 @@ +// 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; + +namespace WorkspacesCsharpLibrary.Data; + +public struct ProjectWrapper +{ + public string Id { get; set; } + + public string Name { get; set; } + + public long CreationTime { get; set; } + + public long LastLaunchedTime { get; set; } + + public bool IsShortcutNeeded { get; set; } + + public bool MoveExistingWindows { get; set; } + + public List MonitorConfiguration { get; set; } + + public List Applications { get; set; } +} diff --git a/src/modules/Workspaces/WorkspacesEditor/Data/TempProjectData.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/TempProjectData.cs similarity index 82% rename from src/modules/Workspaces/WorkspacesEditor/Data/TempProjectData.cs rename to src/modules/Workspaces/WorkspacesCsharpLibrary/Data/TempProjectData.cs index a1600885b9..c5e4a3ce25 100644 --- a/src/modules/Workspaces/WorkspacesEditor/Data/TempProjectData.cs +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/TempProjectData.cs @@ -2,9 +2,10 @@ // 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; +using WorkspacesCsharpLibrary.Data; +using WorkspacesCsharpLibrary.Utils; -namespace WorkspacesEditor.Data +namespace WorkspacesCsharpLibrary.Data { public class TempProjectData : ProjectData { diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesData.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesData.cs new file mode 100644 index 0000000000..6395bffdba --- /dev/null +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesData.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using WorkspacesCsharpLibrary.Utils; +using static WorkspacesCsharpLibrary.Data.WorkspacesData; + +namespace WorkspacesCsharpLibrary.Data; + +public class WorkspacesData : WorkspacesEditorData +{ + public string File => FolderUtils.DataFolder() + "\\workspaces.json"; + + public struct WorkspacesListWrapper + { + public List Workspaces { get; set; } + } + + public enum OrderBy + { + LastViewed = 0, + Created = 1, + Name = 2, + Unknown = 3, + } +} diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesEditorData`1.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesEditorData`1.cs new file mode 100644 index 0000000000..eed73af224 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesEditorData`1.cs @@ -0,0 +1,38 @@ +// 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.CodeAnalysis; +using System.Text.Json; +using WorkspacesCsharpLibrary.Utils; + +namespace WorkspacesCsharpLibrary.Data; + +/// +/// Shared JSON serializer helper for Workspaces payloads. +/// +public class WorkspacesEditorData +{ + [RequiresUnreferencedCode("JSON serialization uses reflection-based serializer.")] + [RequiresDynamicCode("JSON serialization uses reflection-based serializer.")] + public T Read(string file) + { + IOUtils ioUtils = new(); + string data = ioUtils.ReadFile(file); + return JsonSerializer.Deserialize(data, WorkspacesJsonOptions.EditorOptions)!; + } + + [RequiresUnreferencedCode("JSON serialization uses reflection-based serializer.")] + [RequiresDynamicCode("JSON serialization uses reflection-based serializer.")] + public string Serialize(T data) + { + return JsonSerializer.Serialize(data, WorkspacesJsonOptions.EditorOptions); + } + + [RequiresUnreferencedCode("JSON serialization uses reflection-based serializer.")] + [RequiresDynamicCode("JSON serialization uses reflection-based serializer.")] + public T Deserialize(string json) + { + return JsonSerializer.Deserialize(json, WorkspacesJsonOptions.EditorOptions)!; + } +} diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesJsonOptions.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesJsonOptions.cs new file mode 100644 index 0000000000..d9d00152b3 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesJsonOptions.cs @@ -0,0 +1,17 @@ +// 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 WorkspacesCsharpLibrary.Utils; + +namespace WorkspacesCsharpLibrary.Data; + +internal static class WorkspacesJsonOptions +{ + internal static readonly JsonSerializerOptions EditorOptions = new() + { + PropertyNamingPolicy = new DashCaseNamingPolicy(), + WriteIndented = true, + }; +} diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesStorage.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesStorage.cs new file mode 100644 index 0000000000..ea33884577 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesStorage.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace WorkspacesCsharpLibrary.Data; + +/// +/// Lightweight reader for persisted workspaces. +/// +public static class WorkspacesStorage +{ + public static IReadOnlyList Load() + { + var filePath = GetDefaultFilePath(); + if (!File.Exists(filePath)) + { + return []; + } + + try + { + var json = File.ReadAllText(filePath); + var data = JsonSerializer.Deserialize(json, WorkspacesStorageJsonContext.Default.WorkspacesFile); + + if (data?.Workspaces == null) + { + return []; + } + + return data.Workspaces + .Where(ws => !string.IsNullOrWhiteSpace(ws.Id) && !string.IsNullOrWhiteSpace(ws.Name)) + .Select(ws => new ProjectWrapper + { + Id = ws.Id!, + Name = ws.Name!, + Applications = ws.Applications ?? new List(), + CreationTime = ws.CreationTime, + LastLaunchedTime = ws.LastLaunchedTime, + IsShortcutNeeded = ws.IsShortcutNeeded, + MoveExistingWindows = ws.MoveExistingWindows, + MonitorConfiguration = ws.MonitorConfiguration ?? new List(), + }) + .ToList() + .AsReadOnly(); + } + catch + { + return Array.Empty(); + } + } + + public static string GetDefaultFilePath() + { + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + return Path.Combine(localAppData, "Microsoft", "PowerToys", "Workspaces", "workspaces.json"); + } + + internal sealed class WorkspacesFile + { + public List Workspaces { get; set; } = new(); + } + + internal sealed class WorkspaceProject + { + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("applications")] + public List Applications { get; set; } = new(); + + [JsonPropertyName("monitor-configuration")] + public List MonitorConfiguration { get; set; } = new(); + + [JsonPropertyName("creation-time")] + public long CreationTime { get; set; } + + [JsonPropertyName("last-launched-time")] + public long LastLaunchedTime { get; set; } + + [JsonPropertyName("is-shortcut-needed")] + public bool IsShortcutNeeded { get; set; } + + [JsonPropertyName("move-existing-windows")] + public bool MoveExistingWindows { get; set; } + } +} diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesStorageJsonContext.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesStorageJsonContext.cs new file mode 100644 index 0000000000..45ba31a03d --- /dev/null +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesStorageJsonContext.cs @@ -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.Serialization; + +namespace WorkspacesCsharpLibrary.Data; + +[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)] +[JsonSerializable(typeof(WorkspacesStorage.WorkspacesFile))] +[JsonSerializable(typeof(WorkspacesStorage.WorkspaceProject))] +[JsonSerializable(typeof(ApplicationWrapper))] +[JsonSerializable(typeof(ApplicationWrapper.WindowPositionWrapper))] +[JsonSerializable(typeof(MonitorConfigurationWrapper))] +[JsonSerializable(typeof(MonitorConfigurationWrapper.MonitorRectWrapper))] +internal sealed partial class WorkspacesStorageJsonContext : JsonSerializerContext +{ +} diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Models/BaseApplication.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Models/BaseApplication.cs index e3c0bff508..897bd97de5 100644 --- a/src/modules/Workspaces/WorkspacesCsharpLibrary/Models/BaseApplication.cs +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Models/BaseApplication.cs @@ -16,7 +16,7 @@ using Windows.Management.Deployment; namespace WorkspacesCsharpLibrary.Models { - public class BaseApplication : INotifyPropertyChanged, IDisposable + public partial class BaseApplication : INotifyPropertyChanged, IDisposable { public event PropertyChangedEventHandler PropertyChanged; diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/DashCaseNamingPolicy.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/DashCaseNamingPolicy.cs new file mode 100644 index 0000000000..cb1a0f1377 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/DashCaseNamingPolicy.cs @@ -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; +using WorkspacesCsharpLibrary.Utils; + +namespace WorkspacesCsharpLibrary.Utils; + +public class DashCaseNamingPolicy : JsonNamingPolicy +{ + public static DashCaseNamingPolicy Instance { get; } = new DashCaseNamingPolicy(); + + public override string ConvertName(string name) + { + return name.UpperCamelCaseToDashCase(); + } +} diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/FolderUtils.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/FolderUtils.cs new file mode 100644 index 0000000000..cef2aae957 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/FolderUtils.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; + +namespace WorkspacesCsharpLibrary.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"; + } +} diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/IOUtils.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/IOUtils.cs new file mode 100644 index 0000000000..8ceb703cc4 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/IOUtils.cs @@ -0,0 +1,47 @@ +// 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 WorkspacesCsharpLibrary.Utils; + +public class IOUtils +{ + private readonly IFileSystem _fileSystem = new FileSystem(); + + public void WriteFile(string fileName, string data) + { + _fileSystem.File.WriteAllText(fileName, data); + } + + public string ReadFile(string fileName) + { + if (_fileSystem.File.Exists(fileName)) + { + int attempts = 0; + while (attempts < 10) + { + try + { + using FileSystemStream inputStream = _fileSystem.File.Open(fileName, FileMode.Open); + using StreamReader reader = new(inputStream); + string data = reader.ReadToEnd(); + inputStream.Close(); + return data; + } + catch (Exception) + { + Task.Delay(10).Wait(); + } + + attempts++; + } + } + + return string.Empty; + } +} diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/StringUtils.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/StringUtils.cs new file mode 100644 index 0000000000..0b85911fa8 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/StringUtils.cs @@ -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.Linq; + +namespace WorkspacesCsharpLibrary.Utils; + +public static class StringUtils +{ + public static string UpperCamelCaseToDashCase(this string str) + { + // If it's a single letter variable, leave it as it is + return str.Length == 1 + ? str + : string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "-" + x : x.ToString())).ToLowerInvariant(); + } +} diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/WorkspacesCsharpLibrary.csproj b/src/modules/Workspaces/WorkspacesCsharpLibrary/WorkspacesCsharpLibrary.csproj index eea9001b12..501f6b4f0c 100644 --- a/src/modules/Workspaces/WorkspacesCsharpLibrary/WorkspacesCsharpLibrary.csproj +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/WorkspacesCsharpLibrary.csproj @@ -1,11 +1,13 @@  + PowerToys.WorkspacesCsharpLibrary PowerToys Workspaces Csharp Library PowerToys Workspaces Csharp Library + enable true true false @@ -15,4 +17,8 @@ PowerToys.WorkspacesCsharpLibrary - \ No newline at end of file + + + + + diff --git a/src/modules/Workspaces/WorkspacesEditor/Data/ProjectData.cs b/src/modules/Workspaces/WorkspacesEditor/Data/ProjectData.cs deleted file mode 100644 index 27c4290909..0000000000 --- a/src/modules/Workspaces/WorkspacesEditor/Data/ProjectData.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.Generic; -using static WorkspacesEditor.Data.ProjectData; - -namespace WorkspacesEditor.Data -{ - public class ProjectData : WorkspacesEditorData - { - public struct ApplicationWrapper - { - public struct WindowPositionWrapper - { - public int X { get; set; } - - public int Y { get; set; } - - public int Width { get; set; } - - public int Height { get; set; } - } - - public string Id { 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 PwaAppId { 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 string Version { get; set; } - } - - public struct MonitorConfigurationWrapper - { - public struct MonitorRectWrapper - { - public int Top { get; set; } - - public int Left { get; set; } - - public int Width { get; set; } - - public int Height { get; set; } - } - - public string Id { get; set; } - - public string InstanceId { get; set; } - - public int MonitorNumber { get; set; } - - public int Dpi { get; set; } - - public MonitorRectWrapper MonitorRectDpiAware { get; set; } - - public MonitorRectWrapper MonitorRectDpiUnaware { get; set; } - } - - public struct ProjectWrapper - { - public string Id { get; set; } - - public string Name { get; set; } - - public long CreationTime { get; set; } - - public long LastLaunchedTime { get; set; } - - public bool IsShortcutNeeded { get; set; } - - public bool MoveExistingWindows { get; set; } - - public List MonitorConfiguration { get; set; } - - public List Applications { get; set; } - } - } -} diff --git a/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesData.cs b/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesData.cs deleted file mode 100644 index 6e0d015905..0000000000 --- a/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesData.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.Generic; -using WorkspacesEditor.Utils; - -using static WorkspacesEditor.Data.ProjectData; -using static WorkspacesEditor.Data.WorkspacesData; - -namespace WorkspacesEditor.Data -{ - public class WorkspacesData : WorkspacesEditorData - { - public string File => FolderUtils.DataFolder() + "\\workspaces.json"; - - public struct WorkspacesListWrapper - { - public List Workspaces { get; set; } - } - - public enum OrderBy - { - LastViewed = 0, - Created = 1, - Name = 2, - Unknown = 3, - } - } -} diff --git a/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesEditorData`1.cs b/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesEditorData`1.cs deleted file mode 100644 index c2ad0a70a4..0000000000 --- a/src/modules/Workspaces/WorkspacesEditor/Data/WorkspacesEditorData`1.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Text.Json; -using WorkspacesEditor.Utils; - -namespace WorkspacesEditor.Data -{ - public class WorkspacesEditorData - { - protected JsonSerializerOptions JsonOptions - { - get => new() - { - PropertyNamingPolicy = new DashCaseNamingPolicy(), - WriteIndented = true, - }; - } - - public T Read(string file) - { - IOUtils ioUtils = new(); - string data = ioUtils.ReadFile(file); - return JsonSerializer.Deserialize(data, JsonOptions); - } - - public string Serialize(T data) - { - return JsonSerializer.Serialize(data, JsonOptions); - } - } -} diff --git a/src/modules/Workspaces/WorkspacesEditor/Models/Project.cs b/src/modules/Workspaces/WorkspacesEditor/Models/Project.cs index a3b82355b7..bb5fd1c93e 100644 --- a/src/modules/Workspaces/WorkspacesEditor/Models/Project.cs +++ b/src/modules/Workspaces/WorkspacesEditor/Models/Project.cs @@ -13,7 +13,7 @@ using System.Threading.Tasks; using System.Windows.Media.Imaging; using ManagedCommon; -using WorkspacesEditor.Data; +using WorkspacesCsharpLibrary.Data; using WorkspacesEditor.Utils; namespace WorkspacesEditor.Models @@ -226,7 +226,7 @@ namespace WorkspacesEditor.Models } } - public Project(ProjectData.ProjectWrapper project) + public Project(ProjectWrapper project) { Id = project.Id; Name = project.Name; @@ -237,7 +237,7 @@ namespace WorkspacesEditor.Models Monitors = []; Applications = []; - foreach (ProjectData.ApplicationWrapper app in project.Applications) + foreach (ApplicationWrapper app in project.Applications) { Models.Application newApp = new() { @@ -269,7 +269,7 @@ namespace WorkspacesEditor.Models Applications.Add(newApp); } - foreach (ProjectData.MonitorConfigurationWrapper monitor in project.MonitorConfiguration) + foreach (MonitorConfigurationWrapper monitor in project.MonitorConfiguration) { System.Windows.Rect dpiAware = new(monitor.MonitorRectDpiAware.Left, monitor.MonitorRectDpiAware.Top, monitor.MonitorRectDpiAware.Width, monitor.MonitorRectDpiAware.Height); System.Windows.Rect dpiUnaware = new(monitor.MonitorRectDpiUnaware.Left, monitor.MonitorRectDpiUnaware.Top, monitor.MonitorRectDpiUnaware.Width, monitor.MonitorRectDpiUnaware.Height); diff --git a/src/modules/Workspaces/WorkspacesEditor/Utils/DashCaseNamingPolicy.cs b/src/modules/Workspaces/WorkspacesEditor/Utils/DashCaseNamingPolicy.cs deleted file mode 100644 index 3e8857a076..0000000000 --- a/src/modules/Workspaces/WorkspacesEditor/Utils/DashCaseNamingPolicy.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.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(); - } - } -} diff --git a/src/modules/Workspaces/WorkspacesEditor/Utils/FolderUtils.cs b/src/modules/Workspaces/WorkspacesEditor/Utils/FolderUtils.cs deleted file mode 100644 index fc12593e09..0000000000 --- a/src/modules/Workspaces/WorkspacesEditor/Utils/FolderUtils.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.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"; - } - } -} diff --git a/src/modules/Workspaces/WorkspacesEditor/Utils/IOUtils.cs b/src/modules/Workspaces/WorkspacesEditor/Utils/IOUtils.cs deleted file mode 100644 index fe69777593..0000000000 --- a/src/modules/Workspaces/WorkspacesEditor/Utils/IOUtils.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.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)) - { - int attempts = 0; - while (attempts < 10) - { - try - { - using FileSystemStream inputStream = _fileSystem.File.Open(fileName, FileMode.Open); - using StreamReader reader = new(inputStream); - string data = reader.ReadToEnd(); - inputStream.Close(); - return data; - } - catch (Exception) - { - Task.Delay(10).Wait(); - } - - attempts++; - } - } - - return string.Empty; - } - } -} diff --git a/src/modules/Workspaces/WorkspacesEditor/Utils/WorkspacesEditorIO.cs b/src/modules/Workspaces/WorkspacesEditor/Utils/WorkspacesEditorIO.cs index a66145b484..b0b3dc9a50 100644 --- a/src/modules/Workspaces/WorkspacesEditor/Utils/WorkspacesEditorIO.cs +++ b/src/modules/Workspaces/WorkspacesEditor/Utils/WorkspacesEditorIO.cs @@ -6,9 +6,9 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; - using ManagedCommon; -using WorkspacesEditor.Data; +using WorkspacesCsharpLibrary.Data; +using WorkspacesCsharpLibrary.Utils; using WorkspacesEditor.Models; using WorkspacesEditor.ViewModels; @@ -81,7 +81,7 @@ namespace WorkspacesEditor.Utils foreach (Project project in workspaces) { - ProjectData.ProjectWrapper wrapper = new() + ProjectWrapper wrapper = new() { Id = project.Id, Name = project.Name, @@ -95,7 +95,7 @@ namespace WorkspacesEditor.Utils foreach (Application app in project.Applications.Where(x => x.IsIncluded)) { - wrapper.Applications.Add(new ProjectData.ApplicationWrapper + wrapper.Applications.Add(new ApplicationWrapper { Id = app.Id, Application = app.AppName, @@ -110,7 +110,7 @@ namespace WorkspacesEditor.Utils Version = app.Version, Maximized = app.Maximized, Minimized = app.Minimized, - Position = new ProjectData.ApplicationWrapper.WindowPositionWrapper + Position = new ApplicationWrapper.WindowPositionWrapper { X = app.Position.X, Y = app.Position.Y, @@ -123,20 +123,20 @@ namespace WorkspacesEditor.Utils foreach (MonitorSetup monitor in project.Monitors) { - wrapper.MonitorConfiguration.Add(new ProjectData.MonitorConfigurationWrapper + wrapper.MonitorConfiguration.Add(new MonitorConfigurationWrapper { Id = monitor.MonitorName, InstanceId = monitor.MonitorInstanceId, MonitorNumber = monitor.MonitorNumber, Dpi = monitor.Dpi, - MonitorRectDpiAware = new ProjectData.MonitorConfigurationWrapper.MonitorRectWrapper + MonitorRectDpiAware = new 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 + MonitorRectDpiUnaware = new MonitorConfigurationWrapper.MonitorRectWrapper { Left = (int)monitor.MonitorDpiUnawareBounds.Left, Top = (int)monitor.MonitorDpiUnawareBounds.Top, @@ -163,7 +163,7 @@ namespace WorkspacesEditor.Utils private bool AddWorkspaces(MainViewModel mainViewModel, WorkspacesData.WorkspacesListWrapper workspaces) { - foreach (ProjectData.ProjectWrapper project in workspaces.Workspaces) + foreach (ProjectWrapper project in workspaces.Workspaces) { mainViewModel.Workspaces.Add(new Project(project)); } diff --git a/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs b/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs index 30b28f4cd8..9c76c26fa0 100644 --- a/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs +++ b/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs @@ -18,12 +18,12 @@ using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Telemetry; using WorkspacesCsharpLibrary; -using WorkspacesEditor.Data; +using WorkspacesCsharpLibrary.Data; +using WorkspacesCsharpLibrary.Utils; using WorkspacesEditor.Models; using WorkspacesEditor.Telemetry; using WorkspacesEditor.Utils; - -using static WorkspacesEditor.Data.WorkspacesData; +using static WorkspacesCsharpLibrary.Data.WorkspacesData; namespace WorkspacesEditor.ViewModels { diff --git a/src/modules/Workspaces/WorkspacesEditor/WorkspacesEditor.csproj b/src/modules/Workspaces/WorkspacesEditor/WorkspacesEditor.csproj index 3f7d153e56..71e91979bd 100644 --- a/src/modules/Workspaces/WorkspacesEditor/WorkspacesEditor.csproj +++ b/src/modules/Workspaces/WorkspacesEditor/WorkspacesEditor.csproj @@ -78,6 +78,15 @@ + + + + + + + + + True @@ -96,4 +105,4 @@ Settings.Designer.cs - \ No newline at end of file + diff --git a/src/modules/Workspaces/WorkspacesEditor/WorkspacesEditorPage.xaml.cs b/src/modules/Workspaces/WorkspacesEditor/WorkspacesEditorPage.xaml.cs index 1dde4d1114..4a573584ad 100644 --- a/src/modules/Workspaces/WorkspacesEditor/WorkspacesEditorPage.xaml.cs +++ b/src/modules/Workspaces/WorkspacesEditor/WorkspacesEditorPage.xaml.cs @@ -9,7 +9,7 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Input; -using WorkspacesEditor.Data; +using WorkspacesCsharpLibrary.Data; using WorkspacesEditor.Models; using WorkspacesEditor.ViewModels; diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchData.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchData.cs index 6e9ad24379..1e9bf665c5 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchData.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchData.cs @@ -3,15 +3,12 @@ // See the LICENSE file in the project root for more information. using System.Text.Json.Serialization; - -using Workspaces.Data; - using static WorkspacesLauncherUI.Data.AppLaunchData; using static WorkspacesLauncherUI.Data.AppLaunchInfosData; namespace WorkspacesLauncherUI.Data { - public class AppLaunchData : WorkspacesUIData + public class AppLaunchData : WorkspacesCsharpLibrary.Data.WorkspacesEditorData { public struct AppLaunchDataWrapper { diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfoData.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfoData.cs index c01ffaba8c..5a19ccde15 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfoData.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfoData.cs @@ -3,14 +3,11 @@ // See the LICENSE file in the project root for more information. using System.Text.Json.Serialization; - -using Workspaces.Data; - using static WorkspacesLauncherUI.Data.AppLaunchInfoData; namespace WorkspacesLauncherUI.Data { - public class AppLaunchInfoData : WorkspacesUIData + public class AppLaunchInfoData : WorkspacesCsharpLibrary.Data.WorkspacesEditorData { public struct AppLaunchInfoWrapper { diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfosData.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfosData.cs index cb00cb4478..a656712d9a 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfosData.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfosData.cs @@ -4,15 +4,12 @@ using System.Collections.Generic; using System.Text.Json.Serialization; - -using Workspaces.Data; - using static WorkspacesLauncherUI.Data.AppLaunchInfoData; using static WorkspacesLauncherUI.Data.AppLaunchInfosData; namespace WorkspacesLauncherUI.Data { - public class AppLaunchInfosData : WorkspacesUIData + public class AppLaunchInfosData : WorkspacesCsharpLibrary.Data.WorkspacesEditorData { public struct AppLaunchInfoListWrapper { diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesUIData`1.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesUIData`1.cs deleted file mode 100644 index 5e9b88a728..0000000000 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesUIData`1.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Text.Json; - -using WorkspacesLauncherUI.Utils; - -namespace Workspaces.Data -{ - public class WorkspacesUIData - { - protected JsonSerializerOptions JsonOptions - { - get - { - return new JsonSerializerOptions - { - PropertyNamingPolicy = new DashCaseNamingPolicy(), - WriteIndented = true, - }; - } - } - - public T Deserialize(string data) - { - return JsonSerializer.Deserialize(data, JsonOptions); - } - - public string Serialize(T data) - { - return JsonSerializer.Serialize(data, JsonOptions); - } - } -} diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/ViewModels/MainViewModel.cs b/src/modules/Workspaces/WorkspacesLauncherUI/ViewModels/MainViewModel.cs index aa029d7ea2..5b358686de 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/ViewModels/MainViewModel.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/ViewModels/MainViewModel.cs @@ -6,13 +6,11 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; -using System.Diagnostics; using ManagedCommon; using WorkspacesCsharpLibrary; using WorkspacesLauncherUI.Data; using WorkspacesLauncherUI.Models; -using WorkspacesLauncherUI.Utils; namespace WorkspacesLauncherUI.ViewModels { diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/WorkspacesLauncherUI.csproj b/src/modules/Workspaces/WorkspacesLauncherUI/WorkspacesLauncherUI.csproj index 839c08f90d..f55d30205f 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/WorkspacesLauncherUI.csproj +++ b/src/modules/Workspaces/WorkspacesLauncherUI/WorkspacesLauncherUI.csproj @@ -1,102 +1,102 @@  - - - - - - PowerToys.WorkspacesLauncherUI - PowerToys Workspaces Editor - PowerToys Workspaces Editor - WinExe - true - true - false - false - true - ..\..\..\..\$(Platform)\$(Configuration) - - - - {9C53CC25-0623-4569-95BC-B05410675EE3} - {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - true - + + + - - ..\Assets\Workspaces\Workspaces.ico - - - app.manifest - PowerToys.WorkspacesLauncherUI - - - - - Assets\Workspaces\%(Filename)%(Extension) - PreserveNewest - - + + PowerToys.WorkspacesLauncherUI + PowerToys Workspaces Launcher UI + PowerToys Workspaces Launcher UI + WinExe + true + true + false + false + true + ..\..\..\..\$(Platform)\$(Configuration) + - - - tlbimp - 0 - 1 - f935dc20-1cf0-11d0-adb9-00c04fd58a0b - 0 - false - true - - - tlbimp - 0 - 1 - 50a7e9b0-70ef-11d1-b75a-00a0c90564fe - 0 - false - true - - - - - - PublicResXFileCodeGenerator - Resources.Designer.cs - - - + + {9C53CC25-0623-4569-95BC-B05410675EE3} + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + true + - - - - - + + ..\Assets\Workspaces\Workspaces.ico + + + app.manifest + PowerToys.WorkspacesLauncherUI + - - - - - - - - - - - - True - True - Resources.resx - - - True - True - Settings.settings - - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - + + + Assets\Workspaces\%(Filename)%(Extension) + PreserveNewest + + + + + + tlbimp + 0 + 1 + f935dc20-1cf0-11d0-adb9-00c04fd58a0b + 0 + false + true + + + tlbimp + 0 + 1 + 50a7e9b0-70ef-11d1-b75a-00a0c90564fe + 0 + false + true + + + + + + PublicResXFileCodeGenerator + Resources.Designer.cs + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + True + True + Settings.settings + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesModuleInterface/dllmain.cpp b/src/modules/Workspaces/WorkspacesModuleInterface/dllmain.cpp index 90c898fc5f..a4caf01649 100644 --- a/src/modules/Workspaces/WorkspacesModuleInterface/dllmain.cpp +++ b/src/modules/Workspaces/WorkspacesModuleInterface/dllmain.cpp @@ -201,7 +201,7 @@ public: Logger::error(message.value()); } } - m_toggleEditorEventWaiter = EventWaiter(CommonSharedConstants::WORKSPACES_LAUNCH_EDITOR_EVENT, [&](int err) { + m_toggleEditorEventWaiter.start(CommonSharedConstants::WORKSPACES_LAUNCH_EDITOR_EVENT, [&](DWORD err) { if (err == ERROR_SUCCESS) { Logger::trace(L"{} event was signaled", CommonSharedConstants::WORKSPACES_LAUNCH_EDITOR_EVENT); diff --git a/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj b/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj index b1f91a5228..77c299f303 100644 --- a/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj +++ b/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj @@ -369,4 +369,4 @@ - \ No newline at end of file + diff --git a/src/modules/ZoomIt/ZoomIt/Zoomit.cpp b/src/modules/ZoomIt/ZoomIt/Zoomit.cpp index 71f09fc578..e38ca07f66 100644 --- a/src/modules/ZoomIt/ZoomIt/Zoomit.cpp +++ b/src/modules/ZoomIt/ZoomIt/Zoomit.cpp @@ -28,6 +28,20 @@ #include #include #include +#include +#include +#endif // __ZOOMIT_POWERTOYS__ + +#ifdef __ZOOMIT_POWERTOYS__ +enum class ZoomItCommand +{ + Zoom, + Draw, + Break, + LiveZoom, + Snip, + Record, +}; #endif // __ZOOMIT_POWERTOYS__ namespace winrt @@ -172,7 +186,6 @@ std::wstring g_RecordingSaveLocationGIF; winrt::IDirect3DDevice g_RecordDevice{ nullptr }; std::shared_ptr g_RecordingSession = nullptr; std::shared_ptr g_GifRecordingSession = nullptr; - type_pGetMonitorInfo pGetMonitorInfo; type_MonitorFromPoint pMonitorFromPoint; type_pSHAutoComplete pSHAutoComplete; @@ -7712,6 +7725,53 @@ HWND InitInstance( HINSTANCE hInstance, int nCmdShow ) } +// Dispatch commands coming from the PowerToys IPC channel. +#ifdef __ZOOMIT_POWERTOYS__ +void ZoomIt_DispatchCommand(ZoomItCommand cmd) +{ + auto post_hotkey = [](WPARAM id) + { + if (g_hWndMain != nullptr) + { + PostMessage(g_hWndMain, WM_HOTKEY, id, 0); + } + }; + + switch (cmd) + { + case ZoomItCommand::Zoom: + if (g_hWndMain != nullptr) + { + PostMessage(g_hWndMain, WM_COMMAND, IDC_ZOOM, 0); + } + Trace::ZoomItActivateZoom(); + break; + case ZoomItCommand::Draw: + post_hotkey(DRAW_HOTKEY); + Trace::ZoomItActivateDraw(); + break; + case ZoomItCommand::Break: + post_hotkey(BREAK_HOTKEY); + Trace::ZoomItActivateBreak(); + break; + case ZoomItCommand::LiveZoom: + post_hotkey(LIVE_HOTKEY); + Trace::ZoomItActivateLiveZoom(); + break; + case ZoomItCommand::Snip: + post_hotkey(SNIP_HOTKEY); + Trace::ZoomItActivateSnip(); + break; + case ZoomItCommand::Record: + post_hotkey(RECORD_HOTKEY); + Trace::ZoomItActivateRecord(); + break; + default: + break; + } +} +#endif + //---------------------------------------------------------------------------- // // WinMain @@ -7746,7 +7806,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance // Initialize logger LoggerHelpers::init_logger(L"ZoomIt", L"", LogSettings::zoomItLoggerName); - ProcessWaiter::OnProcessTerminate(pid, [mainThreadId](int err) { if (err != ERROR_SUCCESS) { @@ -7905,27 +7964,63 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance #ifdef __ZOOMIT_POWERTOYS__ HANDLE m_reload_settings_event_handle = NULL; HANDLE m_exit_event_handle = NULL; + HANDLE m_zoom_event_handle = NULL; + HANDLE m_draw_event_handle = NULL; + HANDLE m_break_event_handle = NULL; + HANDLE m_live_zoom_event_handle = NULL; + HANDLE m_snip_event_handle = NULL; + HANDLE m_record_event_handle = NULL; std::thread m_event_triggers_thread; if( g_StartedByPowerToys ) { // Start a thread to listen to PowerToys Events. m_reload_settings_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_REFRESH_SETTINGS_EVENT); m_exit_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_EXIT_EVENT); - if (!m_reload_settings_event_handle || !m_exit_event_handle) + m_zoom_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_ZOOM_EVENT); + m_draw_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_DRAW_EVENT); + m_break_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_BREAK_EVENT); + m_live_zoom_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_LIVEZOOM_EVENT); + m_snip_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_SNIP_EVENT); + m_record_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::ZOOMIT_RECORD_EVENT); + if (!m_reload_settings_event_handle || !m_exit_event_handle || !m_zoom_event_handle || !m_draw_event_handle || !m_break_event_handle || !m_live_zoom_event_handle || !m_snip_event_handle || !m_record_event_handle) { Logger::warn(L"Failed to create events. {}", get_last_error_or_default(GetLastError())); return 1; } - m_event_triggers_thread = std::thread([&]() { + const std::array event_handles{ + m_reload_settings_event_handle, + m_exit_event_handle, + m_zoom_event_handle, + m_draw_event_handle, + m_break_event_handle, + m_live_zoom_event_handle, + m_snip_event_handle, + m_record_event_handle, + }; + const DWORD handle_count = static_cast(event_handles.size()); + m_event_triggers_thread = std::thread([event_handles, handle_count]() { MSG msg; - HANDLE event_handles[2] = {m_reload_settings_event_handle, m_exit_event_handle}; while (g_running) { - DWORD dwEvt = MsgWaitForMultipleObjects(2, event_handles, false, INFINITE, QS_ALLINPUT); + DWORD dwEvt = MsgWaitForMultipleObjects(handle_count, event_handles.data(), false, INFINITE, QS_ALLINPUT); + if (dwEvt == WAIT_FAILED) + { + Logger::error(L"ZoomIt event wait failed. {}", get_last_error_or_default(GetLastError())); + break; + } if (!g_running) { break; } + if (dwEvt == WAIT_OBJECT_0 + handle_count) + { + if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) + { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + continue; + } switch (dwEvt) { case WAIT_OBJECT_0: @@ -7938,19 +8033,28 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance case WAIT_OBJECT_0 + 1: { // Exit Event - Logger::trace(L"Received an exit event."); PostMessage(g_hWndMain, WM_QUIT, 0, 0); break; } case WAIT_OBJECT_0 + 2: - if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) - { - TranslateMessage(&msg); - DispatchMessageW(&msg); - } + ZoomIt_DispatchCommand(ZoomItCommand::Zoom); break; - default: + case WAIT_OBJECT_0 + 3: + ZoomIt_DispatchCommand(ZoomItCommand::Draw); break; + case WAIT_OBJECT_0 + 4: + ZoomIt_DispatchCommand(ZoomItCommand::Break); + break; + case WAIT_OBJECT_0 + 5: + ZoomIt_DispatchCommand(ZoomItCommand::LiveZoom); + break; + case WAIT_OBJECT_0 + 6: + ZoomIt_DispatchCommand(ZoomItCommand::Snip); + break; + case WAIT_OBJECT_0 + 7: + ZoomIt_DispatchCommand(ZoomItCommand::Record); + break; + default: break; } } }); @@ -7980,6 +8084,12 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance SetEvent(m_reload_settings_event_handle); CloseHandle(m_reload_settings_event_handle); CloseHandle(m_exit_event_handle); + CloseHandle(m_zoom_event_handle); + CloseHandle(m_draw_event_handle); + CloseHandle(m_break_event_handle); + CloseHandle(m_live_zoom_event_handle); + CloseHandle(m_snip_event_handle); + CloseHandle(m_record_event_handle); m_event_triggers_thread.join(); } #endif // __ZOOMIT_POWERTOYS__ diff --git a/src/modules/ZoomIt/ZoomItModuleInterface/dllmain.cpp b/src/modules/ZoomIt/ZoomItModuleInterface/dllmain.cpp index eea809a0a2..40158ed68f 100644 --- a/src/modules/ZoomIt/ZoomItModuleInterface/dllmain.cpp +++ b/src/modules/ZoomIt/ZoomItModuleInterface/dllmain.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include diff --git a/src/modules/awake/Awake.ModuleServices/Awake.ModuleServices.csproj b/src/modules/awake/Awake.ModuleServices/Awake.ModuleServices.csproj new file mode 100644 index 0000000000..52588938e4 --- /dev/null +++ b/src/modules/awake/Awake.ModuleServices/Awake.ModuleServices.csproj @@ -0,0 +1,20 @@ + + + + + + + enable + enable + false + false + + + + + + + + + + diff --git a/src/modules/awake/Awake.ModuleServices/AwakeService.cs b/src/modules/awake/Awake.ModuleServices/AwakeService.cs new file mode 100644 index 0000000000..f783cdc3db --- /dev/null +++ b/src/modules/awake/Awake.ModuleServices/AwakeService.cs @@ -0,0 +1,151 @@ +// 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; +using System.Text.Json; +using Common.UI; +using Microsoft.PowerToys.Settings.UI.Library; +using PowerToys.ModuleContracts; + +namespace Awake.ModuleServices; + +/// +/// Provides CLI-based Awake control for reuse across hosts. +/// +public sealed class AwakeService : ModuleServiceBase, IAwakeService +{ + public static AwakeService Instance { get; } = new(); + + public override string Key => SettingsDeepLink.SettingsWindow.Awake.ToString(); + + protected override SettingsDeepLink.SettingsWindow SettingsWindow => SettingsDeepLink.SettingsWindow.Awake; + + public override Task LaunchAsync(CancellationToken cancellationToken = default) + { + // Default launch -> indefinite, honoring Awake's own settings for display behavior. + return SetIndefiniteAsync(cancellationToken); + } + + public AwakeState GetCurrentState() + { + var isRunning = IsAwakeProcessRunning(); + var settings = ReadSettings(); + + if (settings is null) + { + return new AwakeState(isRunning, AwakeStateMode.Passive, false, null, null); + } + + var mode = settings.Properties.Mode switch + { + AwakeMode.PASSIVE => AwakeStateMode.Passive, + AwakeMode.INDEFINITE => AwakeStateMode.Indefinite, + AwakeMode.TIMED => AwakeStateMode.Timed, + AwakeMode.EXPIRABLE => AwakeStateMode.Expirable, + _ => AwakeStateMode.Passive, + }; + + TimeSpan? duration = null; + DateTimeOffset? expiration = null; + + switch (mode) + { + case AwakeStateMode.Timed: + duration = TimeSpan.FromHours(settings.Properties.IntervalHours) + TimeSpan.FromMinutes(settings.Properties.IntervalMinutes); + break; + case AwakeStateMode.Expirable: + expiration = settings.Properties.ExpirationDateTime; + break; + } + + return new AwakeState(isRunning, mode, settings.Properties.KeepDisplayOn, duration, expiration); + } + + public Task SetIndefiniteAsync(CancellationToken cancellationToken = default) + { + return UpdateSettingsAsync( + settings => + { + settings.Properties.Mode = AwakeMode.INDEFINITE; + }, + cancellationToken); + } + + public Task SetTimedAsync(int minutes, CancellationToken cancellationToken = default) + { + if (minutes <= 0) + { + return Task.FromResult(OperationResult.Fail("Minutes must be greater than zero.")); + } + + return UpdateSettingsAsync( + settings => + { + var totalMinutes = Math.Min(minutes, int.MaxValue); + settings.Properties.Mode = AwakeMode.TIMED; + settings.Properties.IntervalHours = (uint)(totalMinutes / 60); + settings.Properties.IntervalMinutes = (uint)(totalMinutes % 60); + }, + cancellationToken); + } + + public Task SetOffAsync(CancellationToken cancellationToken = default) + { + return UpdateSettingsAsync( + settings => + { + settings.Properties.Mode = AwakeMode.PASSIVE; + }, + cancellationToken); + } + + private static Task UpdateSettingsAsync(Action mutateSettings, CancellationToken cancellationToken) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + + var settingsUtils = SettingsUtils.Default; + var settings = settingsUtils.GetSettingsOrDefault(AwakeSettings.ModuleName); + + mutateSettings(settings); + + settingsUtils.SaveSettings(JsonSerializer.Serialize(settings, AwakeServiceJsonContext.Default.AwakeSettings), AwakeSettings.ModuleName); + return Task.FromResult(OperationResult.Ok()); + } + catch (OperationCanceledException) + { + return Task.FromResult(OperationResult.Fail("Awake update was cancelled.")); + } + catch (Exception ex) + { + return Task.FromResult(OperationResult.Fail($"Failed to update Awake settings: {ex.Message}")); + } + } + + private static bool IsAwakeProcessRunning() + { + try + { + return Process.GetProcessesByName("PowerToys.Awake").Length > 0; + } + catch + { + return false; + } + } + + private static AwakeSettings? ReadSettings() + { + try + { + var settingsUtils = SettingsUtils.Default; + return settingsUtils.GetSettingsOrDefault(AwakeSettings.ModuleName); + } + catch + { + return null; + } + } +} diff --git a/src/modules/awake/Awake.ModuleServices/AwakeServiceJsonContext.cs b/src/modules/awake/Awake.ModuleServices/AwakeServiceJsonContext.cs new file mode 100644 index 0000000000..cfaeff5bed --- /dev/null +++ b/src/modules/awake/Awake.ModuleServices/AwakeServiceJsonContext.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Serialization; +using Microsoft.PowerToys.Settings.UI.Library; + +namespace Awake.ModuleServices; + +[JsonSerializable(typeof(AwakeSettings))] +internal sealed partial class AwakeServiceJsonContext : JsonSerializerContext +{ +} diff --git a/src/modules/awake/Awake.ModuleServices/AwakeState.cs b/src/modules/awake/Awake.ModuleServices/AwakeState.cs new file mode 100644 index 0000000000..4a59291732 --- /dev/null +++ b/src/modules/awake/Awake.ModuleServices/AwakeState.cs @@ -0,0 +1,26 @@ +// 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 Awake.ModuleServices; + +/// +/// Represents the current state of the Awake module. +/// +/// Whether the Awake process is currently running. +/// The current Awake mode (Passive, Indefinite, Timed, Expirable). +/// Whether the display is kept on. +/// For timed mode, the configured duration. +/// For expirable mode, the expiration date/time. +public readonly record struct AwakeState(bool IsRunning, AwakeStateMode Mode, bool KeepDisplayOn, TimeSpan? Duration, DateTimeOffset? Expiration); + +/// +/// The mode of the Awake module. +/// +public enum AwakeStateMode +{ + Passive = 0, + Indefinite = 1, + Timed = 2, + Expirable = 3, +} diff --git a/src/modules/awake/Awake.ModuleServices/IAwakeService.cs b/src/modules/awake/Awake.ModuleServices/IAwakeService.cs new file mode 100644 index 0000000000..1e600f52fe --- /dev/null +++ b/src/modules/awake/Awake.ModuleServices/IAwakeService.cs @@ -0,0 +1,21 @@ +// 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 PowerToys.ModuleContracts; + +namespace Awake.ModuleServices; + +public interface IAwakeService : IModuleService +{ + Task SetIndefiniteAsync(CancellationToken cancellationToken = default); + + Task SetTimedAsync(int minutes, CancellationToken cancellationToken = default); + + Task SetOffAsync(CancellationToken cancellationToken = default); + + /// + /// Gets the current state of the Awake module. + /// + AwakeState GetCurrentState(); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml index 882c64e3e7..a7bece87c1 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml @@ -12,4 +12,4 @@ https://go.microsoft.com/fwlink/?LinkID=208121. true False - \ No newline at end of file + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml index c686bf808b..73aa1ac98f 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml @@ -12,4 +12,4 @@ https://go.microsoft.com/fwlink/?LinkID=208121. true False - \ No newline at end of file + diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/AdvancedPaste/OpenAdvancedPasteCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/AdvancedPaste/OpenAdvancedPasteCommand.cs new file mode 100644 index 0000000000..4e738a7342 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/AdvancedPaste/OpenAdvancedPasteCommand.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Opens Advanced Paste UI by signaling the module's show event. +/// The DLL interface handles starting the process if it's not running. +/// +internal sealed partial class OpenAdvancedPasteCommand : InvokableCommand +{ + public OpenAdvancedPasteCommand() + { + Name = "Open Advanced Paste"; + } + + public override CommandResult Invoke() + { + try + { + using var showEvent = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.AdvancedPasteShowUIEvent()); + showEvent.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to open Advanced Paste: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/RefreshAwakeStatusCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/RefreshAwakeStatusCommand.cs new file mode 100644 index 0000000000..7327090fd3 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/RefreshAwakeStatusCommand.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace PowerToysExtension.Commands; + +internal sealed partial class RefreshAwakeStatusCommand : InvokableCommand +{ + private readonly Action _refreshAction; + + internal RefreshAwakeStatusCommand(Action refreshAction) + { + ArgumentNullException.ThrowIfNull(refreshAction); + _refreshAction = refreshAction; + Name = "Refresh Awake status"; + } + + public override CommandResult Invoke() + { + _refreshAction(); + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/StartAwakeCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/StartAwakeCommand.cs new file mode 100644 index 0000000000..d4695535ff --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/StartAwakeCommand.cs @@ -0,0 +1,59 @@ +// 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.Tasks; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.ModuleContracts; + +namespace PowerToysExtension.Commands; + +internal sealed partial class StartAwakeCommand : InvokableCommand +{ + private readonly Func> _action; + private readonly string _successToast; + private readonly Action? _onSuccess; + + internal StartAwakeCommand(string title, Func> action, string successToast = "", Action? onSuccess = null) + { + ArgumentNullException.ThrowIfNull(action); + ArgumentException.ThrowIfNullOrWhiteSpace(title); + + _action = action; + _successToast = successToast ?? string.Empty; + _onSuccess = onSuccess; + Name = title; + } + + public override CommandResult Invoke() + { + try + { + var result = _action().GetAwaiter().GetResult(); + if (!result.Success) + { + return ShowToastKeepOpen(result.Error ?? "Failed to start Awake."); + } + + _onSuccess?.Invoke(); + + return string.IsNullOrWhiteSpace(_successToast) + ? CommandResult.KeepOpen() + : ShowToastKeepOpen(_successToast); + } + catch (Exception ex) + { + return ShowToastKeepOpen($"Launching Awake failed: {ex.Message}"); + } + } + + private static CommandResult ShowToastKeepOpen(string message) + { + return CommandResult.ShowToast(new ToastArgs() + { + Message = message, + Result = CommandResult.KeepOpen(), + }); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/StopAwakeCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/StopAwakeCommand.cs new file mode 100644 index 0000000000..426c039437 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Awake/StopAwakeCommand.cs @@ -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; +using Awake.ModuleServices; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace PowerToysExtension.Commands; + +internal sealed partial class StopAwakeCommand : InvokableCommand +{ + private readonly Action? _onSuccess; + + internal StopAwakeCommand(Action? onSuccess = null) + { + _onSuccess = onSuccess; + Name = "Set Awake to Off"; + } + + public override CommandResult Invoke() + { + try + { + var result = AwakeService.Instance.SetOffAsync().GetAwaiter().GetResult(); + if (result.Success) + { + _onSuccess?.Invoke(); + return ShowToastKeepOpen("Awake switched to Off."); + } + + return ShowToastKeepOpen(result.Error ?? "Awake does not appear to be running."); + } + catch (Exception ex) + { + return ShowToastKeepOpen($"Failed to switch Awake off: {ex.Message}"); + } + } + + private static CommandResult ShowToastKeepOpen(string message) + { + return CommandResult.ShowToast(new ToastArgs() + { + Message = message, + Result = CommandResult.KeepOpen(), + }); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ColorPicker/CopySavedColorCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ColorPicker/CopySavedColorCommand.cs new file mode 100644 index 0000000000..96b43a9a17 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ColorPicker/CopySavedColorCommand.cs @@ -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; +using ColorPicker.ModuleServices; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace PowerToysExtension.Commands; + +/// +/// Copies a saved color in a chosen format. +/// +internal sealed partial class CopySavedColorCommand : InvokableCommand +{ + private readonly SavedColor _color; + private readonly string _copyValue; + + public CopySavedColorCommand(SavedColor color, string copyValue) + { + _color = color; + _copyValue = copyValue; + Name = $"Copy {_color.Hex}"; + } + + public override CommandResult Invoke() + { + try + { + ClipboardHelper.SetText(_copyValue); + + return CommandResult.ShowToast($"Copied {_copyValue}"); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to copy color: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ColorPicker/OpenColorPickerCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ColorPicker/OpenColorPickerCommand.cs new file mode 100644 index 0000000000..6982c5dffe --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ColorPicker/OpenColorPickerCommand.cs @@ -0,0 +1,38 @@ +// 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 ColorPicker.ModuleServices; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace PowerToysExtension.Commands; + +/// +/// Opens the Color Picker picker session via shared event. +/// +internal sealed partial class OpenColorPickerCommand : InvokableCommand +{ + public OpenColorPickerCommand() + { + Name = "Open Color Picker"; + } + + public override CommandResult Invoke() + { + try + { + var result = ColorPickerService.Instance.OpenPickerAsync().GetAwaiter().GetResult(); + if (!result.Success) + { + return CommandResult.ShowToast(result.Error ?? "Failed to open Color Picker."); + } + + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to open Color Picker: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockReparentCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockReparentCommand.cs new file mode 100644 index 0000000000..417ab34a5d --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockReparentCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Triggers Crop and Lock reparent mode via the shared event. +/// +internal sealed partial class CropAndLockReparentCommand : InvokableCommand +{ + public CropAndLockReparentCommand() + { + Name = "Crop and Lock (Reparent)"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.CropAndLockReparentEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to start Crop and Lock (Reparent): {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockThumbnailCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockThumbnailCommand.cs new file mode 100644 index 0000000000..b9996f7835 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockThumbnailCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Triggers Crop and Lock thumbnail mode via the shared event. +/// +internal sealed partial class CropAndLockThumbnailCommand : InvokableCommand +{ + public CropAndLockThumbnailCommand() + { + Name = "Crop and Lock (Thumbnail)"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.CropAndLockThumbnailEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to start Crop and Lock (Thumbnail): {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/EnvironmentVariables/OpenEnvironmentVariablesAdminCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/EnvironmentVariables/OpenEnvironmentVariablesAdminCommand.cs new file mode 100644 index 0000000000..6961783325 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/EnvironmentVariables/OpenEnvironmentVariablesAdminCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Launches Environment Variables (admin) via the shared event. +/// +internal sealed partial class OpenEnvironmentVariablesAdminCommand : InvokableCommand +{ + public OpenEnvironmentVariablesAdminCommand() + { + Name = "Open Environment Variables (Admin)"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowEnvironmentVariablesAdminSharedEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to open Environment Variables (Admin): {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/EnvironmentVariables/OpenEnvironmentVariablesCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/EnvironmentVariables/OpenEnvironmentVariablesCommand.cs new file mode 100644 index 0000000000..71bb81068d --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/EnvironmentVariables/OpenEnvironmentVariablesCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Launches Environment Variables (user) via the shared event. +/// +internal sealed partial class OpenEnvironmentVariablesCommand : InvokableCommand +{ + public OpenEnvironmentVariablesCommand() + { + Name = "Open Environment Variables"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowEnvironmentVariablesSharedEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to open Environment Variables: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/ApplyFancyZonesLayoutCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/ApplyFancyZonesLayoutCommand.cs new file mode 100644 index 0000000000..b4ef1e55c9 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/ApplyFancyZonesLayoutCommand.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Helpers; + +namespace PowerToysExtension.Commands; + +internal sealed partial class ApplyFancyZonesLayoutCommand : InvokableCommand +{ + private readonly FancyZonesLayoutDescriptor _layout; + private readonly FancyZonesMonitorDescriptor? _targetMonitor; + + public ApplyFancyZonesLayoutCommand(FancyZonesLayoutDescriptor layout, FancyZonesMonitorDescriptor? monitor) + { + _layout = layout; + _targetMonitor = monitor; + + Name = monitor is null ? "Apply to all monitors" : $"Apply to Monitor {monitor.Value.Title}"; + + Icon = new IconInfo("\uF78C"); + } + + public override CommandResult Invoke() + { + var monitor = _targetMonitor; + var (success, message) = monitor is null + ? FancyZonesDataService.ApplyLayoutToAllMonitors(_layout) + : FancyZonesDataService.ApplyLayoutToMonitor(_layout, monitor.Value); + + return success + ? CommandResult.Dismiss() + : CommandResult.ShowToast(message); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/FancyZonesLayoutListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/FancyZonesLayoutListItem.cs new file mode 100644 index 0000000000..b2e7077fee --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/FancyZonesLayoutListItem.cs @@ -0,0 +1,70 @@ +// 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.Threading.Tasks; +using ManagedCommon; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Helpers; + +namespace PowerToysExtension.Pages; + +internal sealed partial class FancyZonesLayoutListItem : ListItem +{ + private readonly Lazy> _iconLoadTask; + private readonly string _layoutId; + private readonly string _layoutTitle; + + private int _isLoadingIcon; + + public override IIconInfo? Icon + { + get + { + if (Interlocked.Exchange(ref _isLoadingIcon, 1) == 0) + { + _ = LoadIconAsync(); + } + + return base.Icon; + } + set => base.Icon = value; + } + + public FancyZonesLayoutListItem(ICommand command, FancyZonesLayoutDescriptor layout, IconInfo fallbackIcon) + : base(command) + { + Title = layout.Title; + Subtitle = layout.Subtitle; + Icon = fallbackIcon; + _layoutId = layout.Id; + _layoutTitle = layout.Title; + + _iconLoadTask = new Lazy>(async () => await FancyZonesThumbnailRenderer.RenderLayoutIconAsync(layout)); + } + + private async Task LoadIconAsync() + { + try + { + Logger.LogDebug($"FancyZones layout icon load starting. LayoutId={_layoutId} Title=\"{_layoutTitle}\""); + var icon = await _iconLoadTask.Value; + if (icon is not null) + { + Icon = icon; + Logger.LogDebug($"FancyZones layout icon load succeeded. LayoutId={_layoutId} Title=\"{_layoutTitle}\""); + } + else + { + Logger.LogDebug($"FancyZones layout icon load returned null. LayoutId={_layoutId} Title=\"{_layoutTitle}\""); + } + } + catch (Exception ex) + { + Logger.LogWarning($"FancyZones layout icon load failed. LayoutId={_layoutId} Title=\"{_layoutTitle}\" Exception={ex}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/FancyZonesMonitorListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/FancyZonesMonitorListItem.cs new file mode 100644 index 0000000000..c65db779df --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/FancyZonesMonitorListItem.cs @@ -0,0 +1,75 @@ +// 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 System.Globalization; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; + +namespace PowerToysExtension.Pages; + +internal sealed partial class FancyZonesMonitorListItem : ListItem +{ + public FancyZonesMonitorListItem(FancyZonesMonitorDescriptor monitor, string subtitle, IconInfo icon) + : base(new IdentifyFancyZonesMonitorCommand(monitor)) + { + Title = monitor.Title; + Subtitle = subtitle; + Icon = icon; + + Details = BuildMonitorDetails(monitor); + + var pickerPage = new FancyZonesMonitorLayoutPickerPage(monitor) + { + Name = "Set active layout", + }; + + MoreCommands = + [ + new CommandContextItem(pickerPage) + { + Title = "Set active layout", + Subtitle = "Pick a layout for this monitor", + }, + ]; + } + + public static Details BuildMonitorDetails(FancyZonesMonitorDescriptor monitor) + { + var currentVirtualDesktop = FancyZonesVirtualDesktop.GetCurrentVirtualDesktopIdString(); + var tags = new List + { + DetailTag("Monitor", monitor.Data.Monitor), + DetailTag("Instance", monitor.Data.MonitorInstanceId), + DetailTag("Serial", monitor.Data.MonitorSerialNumber), + DetailTag("Number", monitor.Data.MonitorNumber.ToString(CultureInfo.InvariantCulture)), + DetailTag("Virtual desktop", currentVirtualDesktop), + DetailTag("Work area", $"{monitor.Data.LeftCoordinate},{monitor.Data.TopCoordinate} {monitor.Data.WorkAreaWidth}\u00D7{monitor.Data.WorkAreaHeight}"), + DetailTag("Resolution", $"{monitor.Data.MonitorWidth}\u00D7{monitor.Data.MonitorHeight}"), + DetailTag("DPI", monitor.Data.Dpi.ToString(CultureInfo.InvariantCulture)), + }; + + return new Details + { + Title = monitor.Title, + HeroImage = FancyZonesMonitorPreviewRenderer.TryRenderMonitorHeroImage(monitor) ?? + PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png"), + Metadata = tags.ToArray(), + }; + } + + private static DetailsElement DetailTag(string key, string? value) + { + return new DetailsElement + { + Key = key, + Data = new DetailsTags + { + Tags = [new Tag(string.IsNullOrWhiteSpace(value) ? "n/a" : value)], + }, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/IdentifyFancyZonesMonitorCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/IdentifyFancyZonesMonitorCommand.cs new file mode 100644 index 0000000000..6e4b8b45c5 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/IdentifyFancyZonesMonitorCommand.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Helpers; + +namespace PowerToysExtension.Commands; + +internal sealed partial class IdentifyFancyZonesMonitorCommand : InvokableCommand +{ + private readonly FancyZonesMonitorDescriptor _monitor; + + public IdentifyFancyZonesMonitorCommand(FancyZonesMonitorDescriptor monitor) + { + _monitor = monitor; + Name = $"Identify {_monitor.Title}"; + Icon = new IconInfo("\uE773"); + } + + public override CommandResult Invoke() + { + if (!FancyZonesDataService.TryGetMonitors(out var monitors, out var error)) + { + return CommandResult.ShowToast(error); + } + + var monitor = monitors.FirstOrDefault(m => m.Data.MonitorInstanceId == _monitor.Data.MonitorInstanceId); + + if (monitor == null) + { + return CommandResult.ShowToast("Monitor not found."); + } + + FancyZonesMonitorIdentifier.Show( + monitor.Data.LeftCoordinate, + monitor.Data.TopCoordinate, + monitor.Data.WorkAreaWidth, + monitor.Data.WorkAreaHeight, + _monitor.Title, + durationMs: 1200); + + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/OpenFancyZonesEditorCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/OpenFancyZonesEditorCommand.cs new file mode 100644 index 0000000000..9376fba709 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/FancyZones/OpenFancyZonesEditorCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Launches the FancyZones layout editor via the shared event. +/// +internal sealed partial class OpenFancyZonesEditorCommand : InvokableCommand +{ + public OpenFancyZonesEditorCommand() + { + Name = "Open FancyZones Editor"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.FZEToggleEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to open FancyZones editor: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Hosts/OpenHostsEditorAdminCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Hosts/OpenHostsEditorAdminCommand.cs new file mode 100644 index 0000000000..63bd74d62a --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Hosts/OpenHostsEditorAdminCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Launches Hosts File Editor (Admin) via the shared event. +/// +internal sealed partial class OpenHostsEditorAdminCommand : InvokableCommand +{ + public OpenHostsEditorAdminCommand() + { + Name = "Open Hosts File Editor (Admin)"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowHostsAdminSharedEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to open Hosts File Editor (Admin): {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Hosts/OpenHostsEditorCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Hosts/OpenHostsEditorCommand.cs new file mode 100644 index 0000000000..fdf5c807d0 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Hosts/OpenHostsEditorCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Launches Hosts File Editor via the shared event. +/// +internal sealed partial class OpenHostsEditorCommand : InvokableCommand +{ + public OpenHostsEditorCommand() + { + Name = "Open Hosts File Editor"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowHostsSharedEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to open Hosts File Editor: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/LaunchModuleCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/LaunchModuleCommand.cs new file mode 100644 index 0000000000..3101a73030 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/LaunchModuleCommand.cs @@ -0,0 +1,121 @@ +// 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.Diagnostics; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Helpers; + +namespace PowerToysExtension.Commands; + +/// +/// Launches a PowerToys module either by raising its shared event or starting the backing executable. +/// +internal sealed partial class LaunchModuleCommand : InvokableCommand +{ + private readonly string _moduleName; + private readonly string _eventName; + private readonly string _executableName; + private readonly string _arguments; + + internal LaunchModuleCommand( + string moduleName, + string eventName = "", + string executableName = "", + string arguments = "", + string displayName = "") + { + if (string.IsNullOrWhiteSpace(moduleName)) + { + throw new ArgumentException("Module name is required", nameof(moduleName)); + } + + _moduleName = moduleName; + _eventName = eventName ?? string.Empty; + _executableName = executableName ?? string.Empty; + _arguments = arguments ?? string.Empty; + Name = string.IsNullOrWhiteSpace(displayName) ? $"Launch {moduleName}" : displayName; + } + + public override CommandResult Invoke() + { + try + { + if (TrySignalEvent()) + { + return CommandResult.Hide(); + } + + if (TryLaunchExecutable()) + { + return CommandResult.Hide(); + } + + return CommandResult.ShowToast($"Unable to launch {_moduleName}."); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Launching {_moduleName} failed: {ex.Message}"); + } + } + + private bool TrySignalEvent() + { + if (string.IsNullOrEmpty(_eventName)) + { + return false; + } + + try + { + using var existingHandle = EventWaitHandle.OpenExisting(_eventName); + return existingHandle.Set(); + } + catch (WaitHandleCannotBeOpenedException) + { + try + { + using var newHandle = new EventWaitHandle(false, EventResetMode.AutoReset, _eventName, out _); + return newHandle.Set(); + } + catch + { + return false; + } + } + catch + { + return false; + } + } + + private bool TryLaunchExecutable() + { + if (string.IsNullOrEmpty(_executableName)) + { + return false; + } + + var executablePath = PowerToysPathResolver.TryResolveExecutable(_executableName); + if (string.IsNullOrEmpty(executablePath)) + { + return false; + } + + var startInfo = new ProcessStartInfo(executablePath) + { + UseShellExecute = true, + }; + + if (!string.IsNullOrWhiteSpace(_arguments)) + { + startInfo.Arguments = _arguments; + startInfo.UseShellExecute = false; + } + + Process.Start(startInfo); + return true; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/LightSwitch/ToggleLightSwitchCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/LightSwitch/ToggleLightSwitchCommand.cs new file mode 100644 index 0000000000..8702d7630a --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/LightSwitch/ToggleLightSwitchCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Toggles Light Switch via the shared event. +/// +internal sealed partial class ToggleLightSwitchCommand : InvokableCommand +{ + public ToggleLightSwitchCommand() + { + Name = "Toggle Light Switch"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.LightSwitchToggleEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to toggle Light Switch: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ShowMouseJumpPreviewCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ShowMouseJumpPreviewCommand.cs new file mode 100644 index 0000000000..f0c3b9af32 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ShowMouseJumpPreviewCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Shows Mouse Jump preview via the shared event. +/// +internal sealed partial class ShowMouseJumpPreviewCommand : InvokableCommand +{ + public ShowMouseJumpPreviewCommand() + { + Name = "Show Mouse Jump Preview"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.MouseJumpShowPreviewEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to show Mouse Jump preview: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleCursorWrapCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleCursorWrapCommand.cs new file mode 100644 index 0000000000..9e6d9e6817 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleCursorWrapCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Toggles Cursor Wrap via the shared trigger event. +/// +internal sealed partial class ToggleCursorWrapCommand : InvokableCommand +{ + public ToggleCursorWrapCommand() + { + Name = "Toggle Cursor Wrap"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.CursorWrapTriggerEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to toggle Cursor Wrap: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleFindMyMouseCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleFindMyMouseCommand.cs new file mode 100644 index 0000000000..f8bb115789 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleFindMyMouseCommand.cs @@ -0,0 +1,42 @@ +// 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.Threading.Tasks; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Triggers Find My Mouse via the shared event. +/// +internal sealed partial class ToggleFindMyMouseCommand : InvokableCommand +{ + public ToggleFindMyMouseCommand() + { + Name = "Trigger Find My Mouse"; + } + + public override CommandResult Invoke() + { + // Delay the trigger so the Command Palette dismisses first + _ = Task.Run(async () => + { + await Task.Delay(200).ConfigureAwait(false); + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.FindMyMouseTriggerEvent()); + evt.Set(); + } + catch + { + // Ignore errors in background task + } + }); + + return CommandResult.Dismiss(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleMouseCrosshairsCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleMouseCrosshairsCommand.cs new file mode 100644 index 0000000000..2209a60d58 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleMouseCrosshairsCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Toggles Mouse Pointer Crosshairs via the shared event. +/// +internal sealed partial class ToggleMouseCrosshairsCommand : InvokableCommand +{ + public ToggleMouseCrosshairsCommand() + { + Name = "Toggle Mouse Crosshairs"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.MouseCrosshairsTriggerEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to toggle Mouse Crosshairs: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleMouseHighlighterCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleMouseHighlighterCommand.cs new file mode 100644 index 0000000000..1485885723 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseUtils/ToggleMouseHighlighterCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Toggles Mouse Highlighter via the shared event. +/// +internal sealed partial class ToggleMouseHighlighterCommand : InvokableCommand +{ + public ToggleMouseHighlighterCommand() + { + Name = "Toggle Mouse Highlighter"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.MouseHighlighterTriggerEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to toggle Mouse Highlighter: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/OpenInSettingsCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/OpenInSettingsCommand.cs new file mode 100644 index 0000000000..65860c249a --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/OpenInSettingsCommand.cs @@ -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 Common.UI; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace PowerToysExtension.Commands; + +/// +/// Opens the PowerToys settings page for the given module via SettingsDeepLink. +/// +internal sealed partial class OpenInSettingsCommand : InvokableCommand +{ + private readonly SettingsDeepLink.SettingsWindow _module; + + internal OpenInSettingsCommand(SettingsDeepLink.SettingsWindow module, string title = "") + { + _module = module; + Name = string.IsNullOrWhiteSpace(title) ? $"Open {_module} settings" : title; + } + + public override CommandResult Invoke() + { + SettingsDeepLink.OpenSettings(_module); + return CommandResult.Hide(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/OpenPowerToysSettingsCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/OpenPowerToysSettingsCommand.cs new file mode 100644 index 0000000000..bd865fcab1 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/OpenPowerToysSettingsCommand.cs @@ -0,0 +1,61 @@ +// 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.Diagnostics; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Helpers; + +namespace PowerToysExtension.Commands; + +/// +/// Opens the PowerToys settings application deep linked to a specific module. +/// +internal sealed partial class OpenPowerToysSettingsCommand : InvokableCommand +{ + private readonly string _moduleName; + private readonly string _settingsKey; + + internal OpenPowerToysSettingsCommand(string moduleName, string settingsKey) + { + if (string.IsNullOrWhiteSpace(moduleName)) + { + throw new ArgumentException("Module name is required", nameof(moduleName)); + } + + if (string.IsNullOrWhiteSpace(settingsKey)) + { + throw new ArgumentException("Settings key is required", nameof(settingsKey)); + } + + _moduleName = moduleName; + _settingsKey = settingsKey; + Name = $"Open {_moduleName} settings"; + } + + public override CommandResult Invoke() + { + try + { + var powerToysPath = PowerToysPathResolver.TryResolveExecutable("PowerToys.exe"); + if (string.IsNullOrEmpty(powerToysPath)) + { + return CommandResult.ShowToast("Unable to locate PowerToys."); + } + + var startInfo = new ProcessStartInfo(powerToysPath) + { + Arguments = $"--open-settings={_settingsKey}", + UseShellExecute = false, + }; + + Process.Start(startInfo); + return CommandResult.Hide(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Opening {_moduleName} settings failed: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/RegistryPreview/OpenRegistryPreviewCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/RegistryPreview/OpenRegistryPreviewCommand.cs new file mode 100644 index 0000000000..6df382256f --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/RegistryPreview/OpenRegistryPreviewCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Launches Registry Preview via the shared event. +/// +internal sealed partial class OpenRegistryPreviewCommand : InvokableCommand +{ + public OpenRegistryPreviewCommand() + { + Name = "Open Registry Preview"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.RegistryPreviewTriggerEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to open Registry Preview: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ScreenRuler/ToggleScreenRulerCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ScreenRuler/ToggleScreenRulerCommand.cs new file mode 100644 index 0000000000..889cb5d7b9 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ScreenRuler/ToggleScreenRulerCommand.cs @@ -0,0 +1,32 @@ +// 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 Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +internal sealed partial class ToggleScreenRulerCommand : InvokableCommand +{ + public ToggleScreenRulerCommand() + { + Name = "Toggle Screen Ruler"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.MeasureToolTriggerEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to toggle Screen Ruler: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ShortcutGuide/ToggleShortcutGuideCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ShortcutGuide/ToggleShortcutGuideCommand.cs new file mode 100644 index 0000000000..4c6d056eaf --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ShortcutGuide/ToggleShortcutGuideCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Toggles the Shortcut Guide UI via the shared trigger event. +/// +internal sealed partial class ToggleShortcutGuideCommand : InvokableCommand +{ + public ToggleShortcutGuideCommand() + { + Name = "Toggle Shortcut Guide"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShortcutGuideTriggerEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to toggle Shortcut Guide: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/TextExtractor/ToggleTextExtractorCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/TextExtractor/ToggleTextExtractorCommand.cs new file mode 100644 index 0000000000..615fb0e395 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/TextExtractor/ToggleTextExtractorCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Triggers the Text Extractor UI via the existing show event. +/// +internal sealed partial class ToggleTextExtractorCommand : InvokableCommand +{ + public ToggleTextExtractorCommand() + { + Name = "Toggle Text Extractor"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowPowerOCRSharedEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to toggle Text Extractor: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/LaunchWorkspaceCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/LaunchWorkspaceCommand.cs new file mode 100644 index 0000000000..4372e5b7ff --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/LaunchWorkspaceCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions.Toolkit; +using Workspaces.ModuleServices; + +namespace PowerToysExtension.Commands; + +internal sealed partial class LaunchWorkspaceCommand : InvokableCommand +{ + private readonly string _workspaceId; + + internal LaunchWorkspaceCommand(string workspaceId) + { + _workspaceId = workspaceId; + Name = "Launch workspace"; + } + + public override CommandResult Invoke() + { + if (string.IsNullOrEmpty(_workspaceId)) + { + return CommandResult.KeepOpen(); + } + + var result = WorkspaceService.Instance.LaunchWorkspaceAsync(_workspaceId).GetAwaiter().GetResult(); + if (!result.Success) + { + return CommandResult.ShowToast(result.Error ?? "Launching workspace failed."); + } + + return CommandResult.Hide(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/OpenWorkspaceEditorCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/OpenWorkspaceEditorCommand.cs new file mode 100644 index 0000000000..63150902a6 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/OpenWorkspaceEditorCommand.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions.Toolkit; +using Workspaces.ModuleServices; + +namespace PowerToysExtension.Commands; + +internal sealed partial class OpenWorkspaceEditorCommand : InvokableCommand +{ + public override CommandResult Invoke() + { + var result = WorkspaceService.Instance.LaunchEditorAsync().GetAwaiter().GetResult(); + if (!result.Success) + { + return CommandResult.ShowToast(result.Error ?? "Unable to launch the Workspaces editor."); + } + + return CommandResult.Hide(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/WorkspaceListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/WorkspaceListItem.cs new file mode 100644 index 0000000000..1b2024e759 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/Workspaces/WorkspaceListItem.cs @@ -0,0 +1,117 @@ +// 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.Globalization; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Helpers; +using WorkspacesCsharpLibrary.Data; + +namespace PowerToysExtension.Commands; + +internal sealed partial class WorkspaceListItem : ListItem +{ + public WorkspaceListItem(ProjectWrapper workspace, IconInfo icon) + : base(new LaunchWorkspaceCommand(workspace.Id)) + { + Title = workspace.Name; + Subtitle = BuildSubtitle(workspace); + Icon = icon; + Details = BuildDetails(workspace, icon); + } + + private static string BuildSubtitle(ProjectWrapper workspace) + { + var appCount = workspace.Applications?.Count ?? 0; + var appsText = appCount switch + { + 0 => "No applications", + _ => string.Format(CultureInfo.CurrentCulture, "{0} applications", appCount), + }; + + var lastLaunched = workspace.LastLaunchedTime > 0 + ? $"Last launched {FormatRelativeTime(workspace.LastLaunchedTime)}" + : "Never launched"; + + return $"{appsText} \u2022 {lastLaunched}"; + } + + private static Details BuildDetails(ProjectWrapper workspace, IconInfo icon) + { + var appCount = workspace.Applications?.Count ?? 0; + var body = appCount switch + { + 0 => "No applications in this workspace", + 1 => "1 application", + _ => $"{appCount} applications", + }; + + return new Details + { + HeroImage = icon, + Title = workspace.Name ?? "Workspace", + Body = body, + Metadata = BuildAppMetadata(workspace), + }; + } + + private static IDetailsElement[] BuildAppMetadata(ProjectWrapper workspace) + { + if (workspace.Applications is null || workspace.Applications.Count == 0) + { + return Array.Empty(); + } + + var elements = new List(); + foreach (var app in workspace.Applications) + { + var appName = string.IsNullOrWhiteSpace(app.Application) ? "App" : app.Application; + var title = string.IsNullOrWhiteSpace(app.Title) ? appName : app.Title; + + var tags = new List(); + + if (!string.IsNullOrWhiteSpace(app.ApplicationPath)) + { + tags.Add(new Tag(app.ApplicationPath)); + } + else + { + tags.Add(new Tag(appName)); + } + + elements.Add(new DetailsElement + { + Key = title, + Data = new DetailsTags { Tags = tags.ToArray() }, + }); + } + + return elements.ToArray(); + } + + private static string FormatRelativeTime(long unixSeconds) + { + var lastLaunch = DateTimeOffset.FromUnixTimeSeconds(unixSeconds).UtcDateTime; + var delta = DateTime.UtcNow - lastLaunch; + + if (delta.TotalMinutes < 1) + { + return "just now"; + } + + if (delta.TotalMinutes < 60) + { + return string.Format(CultureInfo.CurrentCulture, "{0} min ago", (int)delta.TotalMinutes); + } + + if (delta.TotalHours < 24) + { + return string.Format(CultureInfo.CurrentCulture, "{0} hr ago", (int)delta.TotalHours); + } + + return string.Format(CultureInfo.CurrentCulture, "{0} days ago", (int)delta.TotalDays); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ZoomIt/ZoomItActionCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ZoomIt/ZoomItActionCommand.cs new file mode 100644 index 0000000000..44b5ea447a --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/ZoomIt/ZoomItActionCommand.cs @@ -0,0 +1,84 @@ +// 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.Threading.Tasks; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +internal sealed partial class ZoomItActionCommand : InvokableCommand +{ + private readonly string _action; + private readonly string _title; + + public ZoomItActionCommand(string action, string title) + { + _action = action; + _title = title; + Name = title; + } + + public override CommandResult Invoke() + { + try + { + if (!TryGetEventName(_action, out var eventName)) + { + return CommandResult.ShowToast($"Unknown ZoomIt action: {_action}."); + } + + var evt = EventWaitHandle.OpenExisting(eventName); + _ = Task.Run(async () => + { + using (evt) + { + // Hide CmdPal first, then signal shortly after so UI like snip/zoom won't capture it. + await Task.Delay(50).ConfigureAwait(false); + evt.Set(); + } + }); + + return CommandResult.Hide(); + } + catch (WaitHandleCannotBeOpenedException) + { + return CommandResult.ShowToast("ZoomIt is not running. Please start it from PowerToys and try again."); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to invoke ZoomIt ({_title}): {ex.Message}"); + } + } + + private static bool TryGetEventName(string action, out string eventName) + { + switch (action.ToLowerInvariant()) + { + case "zoom": + eventName = Constants.ZoomItZoomEvent(); + return true; + case "draw": + eventName = Constants.ZoomItDrawEvent(); + return true; + case "break": + eventName = Constants.ZoomItBreakEvent(); + return true; + case "livezoom": + eventName = Constants.ZoomItLiveZoomEvent(); + return true; + case "snip": + eventName = Constants.ZoomItSnipEvent(); + return true; + case "record": + eventName = Constants.ZoomItRecordEvent(); + return true; + default: + eventName = string.Empty; + return false; + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/AwakeStatusService.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/AwakeStatusService.cs new file mode 100644 index 0000000000..102c9e90fc --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/AwakeStatusService.cs @@ -0,0 +1,60 @@ +// 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 Awake.ModuleServices; +using Common.UI; + +namespace PowerToysExtension.Helpers; + +internal static class AwakeStatusService +{ + internal static string GetStatusSubtitle() + { + var state = AwakeService.Instance.GetCurrentState(); + if (!state.IsRunning) + { + return "Awake is idle"; + } + + if (state.Mode == AwakeStateMode.Passive) + { + // When the PowerToys Awake module is enabled, the Awake process stays resident + // even in passive mode. In that case "idle" is correct. If the module is disabled, + // a running process implies a standalone/session keep-awake, so report as active. + return ModuleEnablementService.IsModuleEnabled(SettingsDeepLink.SettingsWindow.Awake) + ? "Awake is idle" + : "Active - session running"; + } + + return state.Mode switch + { + AwakeStateMode.Indefinite => "Active - indefinite", + AwakeStateMode.Timed => state.Duration is { } span + ? $"Active - timer {FormatDuration(span)}" + : "Active - timer", + AwakeStateMode.Expirable => state.Expiration is { } expiry + ? $"Active - until {expiry.ToLocalTime():t}" + : "Active - scheduled", + _ => "Awake is running", + }; + } + + private static string FormatDuration(TimeSpan span) + { + if (span.TotalHours >= 1) + { + var hours = (int)Math.Floor(span.TotalHours); + var minutes = span.Minutes; + return minutes > 0 ? $"{hours}h {minutes}m" : $"{hours}h"; + } + + if (span.TotalMinutes >= 1) + { + return $"{(int)Math.Round(span.TotalMinutes)}m"; + } + + return span.TotalSeconds >= 1 ? $"{(int)Math.Round(span.TotalSeconds)}s" : "\u2014"; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ColorSwatchIconFactory.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ColorSwatchIconFactory.cs new file mode 100644 index 0000000000..3fde50882f --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ColorSwatchIconFactory.cs @@ -0,0 +1,45 @@ +// 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.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.InteropServices.WindowsRuntime; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Storage.Streams; + +namespace PowerToysExtension.Helpers; + +internal static class ColorSwatchIconFactory +{ + public static IconInfo Create(byte r, byte g, byte b, byte a) + { + try + { + using var bmp = new Bitmap(32, 32, PixelFormat.Format32bppArgb); + using var gfx = Graphics.FromImage(bmp); + gfx.SmoothingMode = SmoothingMode.AntiAlias; + gfx.Clear(Color.Transparent); + + using var brush = new SolidBrush(Color.FromArgb(a, r, g, b)); + const int padding = 4; + gfx.FillEllipse(brush, padding, padding, bmp.Width - (padding * 2), bmp.Height - (padding * 2)); + + using var ms = new MemoryStream(); + bmp.Save(ms, ImageFormat.Png); + var ras = new InMemoryRandomAccessStream(); + var writer = new DataWriter(ras); + writer.WriteBytes(ms.ToArray()); + writer.StoreAsync().GetResults(); + ras.Seek(0); + return IconInfo.FromStream(ras); + } + catch + { + // Fallback to a simple colored glyph when drawing fails. + return new IconInfo("\u25CF"); // Black circle glyph + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesDataService.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesDataService.cs new file mode 100644 index 0000000000..0a55859409 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesDataService.cs @@ -0,0 +1,517 @@ +// 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 System.Text.Json; + +using FancyZonesEditorCommon.Data; +using FancyZonesEditorCommon.Utils; +using ManagedCommon; + +using FZPaths = FancyZonesEditorCommon.Data.FancyZonesPaths; + +namespace PowerToysExtension.Helpers; + +internal static class FancyZonesDataService +{ + private const string ZeroUuid = "{00000000-0000-0000-0000-000000000000}"; + + public static bool TryGetMonitors(out IReadOnlyList monitors, out string error) + { + monitors = Array.Empty(); + error = string.Empty; + + Logger.LogInfo($"TryGetMonitors: Starting. EditorParametersPath={FZPaths.EditorParameters}"); + + try + { + if (!File.Exists(FZPaths.EditorParameters)) + { + error = "FancyZones monitor data not found. Open FancyZones Editor once to initialize."; + Logger.LogWarning($"TryGetMonitors: File not found. Path={FZPaths.EditorParameters}"); + return false; + } + + Logger.LogInfo($"TryGetMonitors: File exists, reading..."); + var editorParams = FancyZonesDataIO.ReadEditorParameters(); + Logger.LogInfo($"TryGetMonitors: ReadEditorParameters returned. Monitors={editorParams.Monitors?.Count ?? -1}"); + + var editorMonitors = editorParams.Monitors; + if (editorMonitors is null || editorMonitors.Count == 0) + { + error = "No FancyZones monitors found."; + Logger.LogWarning($"TryGetMonitors: No monitors in file."); + return false; + } + + monitors = editorMonitors + .Select((monitor, i) => new FancyZonesMonitorDescriptor(i + 1, monitor)) + .ToArray(); + Logger.LogInfo($"TryGetMonitors: Succeeded. MonitorCount={monitors.Count}"); + return true; + } + catch (Exception ex) + { + error = $"Failed to read FancyZones monitor data: {ex.Message}"; + Logger.LogError($"TryGetMonitors: Exception. Message={ex.Message} Stack={ex.StackTrace}"); + return false; + } + } + + public static IReadOnlyList GetLayouts() + { + Logger.LogInfo($"GetLayouts: Starting. LayoutTemplatesPath={FZPaths.LayoutTemplates} CustomLayoutsPath={FZPaths.CustomLayouts}"); + var layouts = new List(); + try + { + var templates = GetTemplateLayouts().ToArray(); + Logger.LogInfo($"GetLayouts: GetTemplateLayouts returned {templates.Length} layouts"); + layouts.AddRange(templates); + } + catch (Exception ex) + { + Logger.LogError($"GetLayouts: GetTemplateLayouts failed. Message={ex.Message} Stack={ex.StackTrace}"); + } + + try + { + var customLayouts = GetCustomLayouts().ToArray(); + Logger.LogInfo($"GetLayouts: GetCustomLayouts returned {customLayouts.Length} layouts"); + layouts.AddRange(customLayouts); + } + catch (Exception ex) + { + Logger.LogError($"GetLayouts: GetCustomLayouts failed. Message={ex.Message} Stack={ex.StackTrace}"); + } + + Logger.LogInfo($"GetLayouts: Total layouts={layouts.Count}"); + return layouts; + } + + public static bool TryGetAppliedLayoutForMonitor(EditorParameters.NativeMonitorDataWrapper monitor, out AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper? appliedLayout) + => TryGetAppliedLayoutForMonitor(monitor, FancyZonesVirtualDesktop.GetCurrentVirtualDesktopIdString(), out appliedLayout); + + public static bool TryGetAppliedLayoutForMonitor(EditorParameters.NativeMonitorDataWrapper monitor, string virtualDesktopId, out AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper? appliedLayout) + { + appliedLayout = null; + + if (!TryReadAppliedLayouts(out var file)) + { + return false; + } + + var match = FindAppliedLayoutEntry(file, monitor, virtualDesktopId); + if (match is not null) + { + appliedLayout = match.Value.AppliedLayout; + return true; + } + + return false; + } + + public static (bool Success, string Message) ApplyLayoutToAllMonitors(FancyZonesLayoutDescriptor layout) + { + if (!TryGetMonitors(out var monitors, out var error)) + { + return (false, error); + } + + return ApplyLayoutToMonitors(layout, monitors.Select(m => m.Data)); + } + + public static (bool Success, string Message) ApplyLayoutToMonitor(FancyZonesLayoutDescriptor layout, FancyZonesMonitorDescriptor monitor) + { + if (!TryGetMonitors(out var monitors, out var error)) + { + return (false, error); + } + + EditorParameters.NativeMonitorDataWrapper? monitorData = null; + foreach (var candidate in monitors) + { + if (candidate.Data.MonitorInstanceId == monitor.Data.MonitorInstanceId) + { + monitorData = candidate.Data; + break; + } + } + + if (monitorData is null) + { + return (false, "Monitor not found."); + } + + return ApplyLayoutToMonitors(layout, [monitorData.Value]); + } + + private static (bool Success, string Message) ApplyLayoutToMonitors(FancyZonesLayoutDescriptor layout, IEnumerable monitors) + { + AppliedLayouts.AppliedLayoutsListWrapper appliedFile; + if (!TryReadAppliedLayouts(out var existingFile)) + { + appliedFile = new AppliedLayouts.AppliedLayoutsListWrapper { AppliedLayouts = new List() }; + } + else + { + appliedFile = existingFile; + } + + appliedFile.AppliedLayouts ??= new List(); + + var currentVirtualDesktop = FancyZonesVirtualDesktop.GetCurrentVirtualDesktopIdString(); + + foreach (var monitor in monitors) + { + var existingEntry = FindAppliedLayoutEntry(appliedFile, monitor, currentVirtualDesktop); + if (existingEntry is not null) + { + // Remove the existing entry so we can add a new one + appliedFile.AppliedLayouts.Remove(existingEntry.Value); + } + + var newEntry = new AppliedLayouts.AppliedLayoutWrapper + { + Device = new AppliedLayouts.AppliedLayoutWrapper.DeviceIdWrapper + { + Monitor = monitor.Monitor, + MonitorInstance = monitor.MonitorInstanceId ?? string.Empty, + SerialNumber = monitor.MonitorSerialNumber ?? string.Empty, + MonitorNumber = monitor.MonitorNumber, + VirtualDesktop = currentVirtualDesktop, + }, + AppliedLayout = new AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper + { + Uuid = layout.ApplyLayout.Uuid, + Type = layout.ApplyLayout.Type, + ZoneCount = layout.ApplyLayout.ZoneCount, + ShowSpacing = layout.ApplyLayout.ShowSpacing, + Spacing = layout.ApplyLayout.Spacing, + SensitivityRadius = layout.ApplyLayout.SensitivityRadius, + }, + }; + + appliedFile.AppliedLayouts.Add(newEntry); + } + + try + { + FancyZonesDataIO.WriteAppliedLayouts(appliedFile); + } + catch (Exception ex) + { + return (false, $"Failed to write applied layouts: {ex.Message}"); + } + + try + { + FancyZonesNotifier.NotifyAppliedLayoutsChanged(); + } + catch (Exception ex) + { + return (true, $"Layout applied, but FancyZones could not be notified: {ex.Message}"); + } + + return (true, "Layout applied."); + } + + private static AppliedLayouts.AppliedLayoutWrapper? FindAppliedLayoutEntry(AppliedLayouts.AppliedLayoutsListWrapper file, EditorParameters.NativeMonitorDataWrapper monitor, string virtualDesktopId) + { + if (file.AppliedLayouts is null) + { + return null; + } + + return file.AppliedLayouts.FirstOrDefault(e => + string.Equals(e.Device.Monitor, monitor.Monitor, StringComparison.OrdinalIgnoreCase) && + string.Equals(e.Device.MonitorInstance ?? string.Empty, monitor.MonitorInstanceId ?? string.Empty, StringComparison.OrdinalIgnoreCase) && + string.Equals(e.Device.SerialNumber ?? string.Empty, monitor.MonitorSerialNumber ?? string.Empty, StringComparison.OrdinalIgnoreCase) && + e.Device.MonitorNumber == monitor.MonitorNumber && + string.Equals(e.Device.VirtualDesktop, virtualDesktopId, StringComparison.OrdinalIgnoreCase)); + } + + private static bool TryReadAppliedLayouts(out AppliedLayouts.AppliedLayoutsListWrapper file) + { + file = default; + try + { + if (!File.Exists(FZPaths.AppliedLayouts)) + { + return false; + } + + file = FancyZonesDataIO.ReadAppliedLayouts(); + return true; + } + catch + { + return false; + } + } + + private static IEnumerable GetTemplateLayouts() + { + Logger.LogInfo($"GetTemplateLayouts: Starting. Path={FZPaths.LayoutTemplates} Exists={File.Exists(FZPaths.LayoutTemplates)}"); + + LayoutTemplates.TemplateLayoutsListWrapper templates; + try + { + if (!File.Exists(FZPaths.LayoutTemplates)) + { + Logger.LogWarning($"GetTemplateLayouts: File not found."); + yield break; + } + + templates = FancyZonesDataIO.ReadLayoutTemplates(); + Logger.LogInfo($"GetTemplateLayouts: ReadLayoutTemplates succeeded. Count={templates.LayoutTemplates?.Count ?? -1}"); + } + catch (Exception ex) + { + Logger.LogError($"GetTemplateLayouts: ReadLayoutTemplates failed. Message={ex.Message} Stack={ex.StackTrace}"); + yield break; + } + + var templateLayouts = templates.LayoutTemplates; + if (templateLayouts is null) + { + Logger.LogWarning($"GetTemplateLayouts: LayoutTemplates is null."); + yield break; + } + + foreach (var template in templateLayouts) + { + if (string.IsNullOrWhiteSpace(template.Type)) + { + continue; + } + + var type = template.Type.Trim(); + var zoneCount = type.Equals("blank", StringComparison.OrdinalIgnoreCase) + ? 0 + : template.ZoneCount > 0 ? template.ZoneCount : 3; + var title = $"Template: {type}"; + var subtitle = $"{zoneCount} zones"; + + yield return new FancyZonesLayoutDescriptor + { + Id = $"template:{type.ToLowerInvariant()}", + Source = FancyZonesLayoutSource.Template, + Title = title, + Subtitle = subtitle, + Template = template, + ApplyLayout = new AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper + { + Type = type.ToLowerInvariant(), + Uuid = ZeroUuid, + ZoneCount = zoneCount, + ShowSpacing = template.ShowSpacing, + Spacing = template.Spacing, + SensitivityRadius = template.SensitivityRadius, + }, + }; + } + } + + private static IEnumerable GetCustomLayouts() + { + CustomLayouts.CustomLayoutListWrapper customLayouts; + try + { + if (!File.Exists(FZPaths.CustomLayouts)) + { + yield break; + } + + customLayouts = FancyZonesDataIO.ReadCustomLayouts(); + } + catch + { + yield break; + } + + var layouts = customLayouts.CustomLayouts; + if (layouts is null) + { + yield break; + } + + foreach (var custom in layouts) + { + if (string.IsNullOrWhiteSpace(custom.Uuid) || string.IsNullOrWhiteSpace(custom.Name)) + { + continue; + } + + var uuid = custom.Uuid.Trim(); + var customType = custom.Type?.Trim().ToLowerInvariant() ?? string.Empty; + + if (!TryBuildAppliedLayoutForCustom(custom, out var applied)) + { + continue; + } + + var title = custom.Name.Trim(); + var subtitle = customType switch + { + "grid" => $"Custom grid \u2022 {applied.ZoneCount} zones", + "canvas" => $"Custom canvas \u2022 {applied.ZoneCount} zones", + _ => $"Custom \u2022 {applied.ZoneCount} zones", + }; + + yield return new FancyZonesLayoutDescriptor + { + Id = $"custom:{uuid}", + Source = FancyZonesLayoutSource.Custom, + Title = title, + Subtitle = subtitle, + Custom = custom, + ApplyLayout = applied, + }; + } + } + + private static bool TryBuildAppliedLayoutForCustom(CustomLayouts.CustomLayoutWrapper custom, out AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper applied) + { + applied = new AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper + { + Type = "custom", + Uuid = custom.Uuid.Trim(), + ShowSpacing = false, + Spacing = 0, + ZoneCount = 0, + SensitivityRadius = 20, + }; + + if (custom.Info.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null) + { + return false; + } + + var customType = custom.Type?.Trim().ToLowerInvariant() ?? string.Empty; + if (customType == "grid") + { + if (!TryParseCustomGridInfo(custom.Info, out var zoneCount, out var showSpacing, out var spacing, out var sensitivity)) + { + return false; + } + + applied.ZoneCount = zoneCount; + applied.ShowSpacing = showSpacing; + applied.Spacing = spacing; + applied.SensitivityRadius = sensitivity; + return true; + } + + if (customType == "canvas") + { + if (!TryParseCustomCanvasInfo(custom.Info, out var zoneCount, out var sensitivity)) + { + return false; + } + + applied.ZoneCount = zoneCount; + applied.SensitivityRadius = sensitivity; + applied.ShowSpacing = false; + applied.Spacing = 0; + return true; + } + + return false; + } + + internal static bool TryParseCustomGridInfo(JsonElement info, out int zoneCount, out bool showSpacing, out int spacing, out int sensitivityRadius) + { + zoneCount = 0; + showSpacing = false; + spacing = 0; + sensitivityRadius = 20; + + if (!info.TryGetProperty("rows", out var rowsProp) || + !info.TryGetProperty("columns", out var columnsProp) || + rowsProp.ValueKind != JsonValueKind.Number || + columnsProp.ValueKind != JsonValueKind.Number) + { + return false; + } + + var rows = rowsProp.GetInt32(); + var columns = columnsProp.GetInt32(); + if (rows <= 0 || columns <= 0) + { + return false; + } + + if (info.TryGetProperty("cell-child-map", out var cellMap) && cellMap.ValueKind == JsonValueKind.Array) + { + var max = -1; + foreach (var row in cellMap.EnumerateArray()) + { + if (row.ValueKind != JsonValueKind.Array) + { + continue; + } + + foreach (var cell in row.EnumerateArray()) + { + if (cell.ValueKind == JsonValueKind.Number && cell.TryGetInt32(out var value)) + { + max = Math.Max(max, value); + } + } + } + + zoneCount = max + 1; + } + else + { + zoneCount = rows * columns; + } + + if (zoneCount <= 0) + { + return false; + } + + if (info.TryGetProperty("show-spacing", out var showSpacingProp) && + (showSpacingProp.ValueKind == JsonValueKind.True || showSpacingProp.ValueKind == JsonValueKind.False)) + { + showSpacing = showSpacingProp.GetBoolean(); + } + + if (info.TryGetProperty("spacing", out var spacingProp) && spacingProp.ValueKind == JsonValueKind.Number) + { + spacing = spacingProp.GetInt32(); + } + + if (info.TryGetProperty("sensitivity-radius", out var sensitivityProp) && sensitivityProp.ValueKind == JsonValueKind.Number) + { + sensitivityRadius = sensitivityProp.GetInt32(); + } + + return true; + } + + internal static bool TryParseCustomCanvasInfo(JsonElement info, out int zoneCount, out int sensitivityRadius) + { + zoneCount = 0; + sensitivityRadius = 20; + + if (!info.TryGetProperty("zones", out var zones) || zones.ValueKind != JsonValueKind.Array) + { + return false; + } + + zoneCount = zones.GetArrayLength(); + + if (info.TryGetProperty("sensitivity-radius", out var sensitivityProp) && sensitivityProp.ValueKind == JsonValueKind.Number) + { + sensitivityRadius = sensitivityProp.GetInt32(); + } + + return zoneCount >= 0; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutDescriptor.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutDescriptor.cs new file mode 100644 index 0000000000..0c99dcc8f4 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutDescriptor.cs @@ -0,0 +1,24 @@ +// 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 FancyZonesEditorCommon.Data; + +namespace PowerToysExtension.Helpers; + +internal sealed class FancyZonesLayoutDescriptor +{ + public required string Id { get; init; } // "template:" or "custom:" + + public required FancyZonesLayoutSource Source { get; init; } + + public required string Title { get; init; } + + public required string Subtitle { get; init; } + + public required AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper ApplyLayout { get; init; } + + public LayoutTemplates.TemplateLayoutWrapper? Template { get; init; } + + public CustomLayouts.CustomLayoutWrapper? Custom { get; init; } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutDescriptor1.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutDescriptor1.cs new file mode 100644 index 0000000000..194a9a206c --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutDescriptor1.cs @@ -0,0 +1,5 @@ +// 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. + +// Intentionally empty: this file was an accidental duplicate of FancyZonesLayoutDescriptor.cs. diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutSource.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutSource.cs new file mode 100644 index 0000000000..d75e3faccd --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesLayoutSource.cs @@ -0,0 +1,11 @@ +// 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 PowerToysExtension.Helpers; + +internal enum FancyZonesLayoutSource +{ + Template, + Custom, +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorDescriptor.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorDescriptor.cs new file mode 100644 index 0000000000..4e3f7092a4 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorDescriptor.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; + +using FancyZonesEditorCommon.Data; + +namespace PowerToysExtension.Helpers; + +internal readonly record struct FancyZonesMonitorDescriptor( + int Index, + EditorParameters.NativeMonitorDataWrapper Data) +{ + public string Title => Data.Monitor; + + public string Subtitle + { + get + { + var size = $"{Data.MonitorWidth}×{Data.MonitorHeight}"; + var scaling = Data.Dpi > 0 ? string.Format(CultureInfo.InvariantCulture, "{0}%", (int)Math.Round(Data.Dpi * 100 / 96.0)) : "n/a"; + return $"{size} \u2022 {scaling}"; + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorIdentifier.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorIdentifier.cs new file mode 100644 index 0000000000..afa908e08b --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorIdentifier.cs @@ -0,0 +1,461 @@ +// 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.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace PowerToysExtension.Helpers; + +internal static class FancyZonesMonitorIdentifier +{ + private const string WindowClassName = "PowerToys_FancyZones_MonitorIdentify"; + + private const uint WsExToolWindow = 0x00000080; + private const uint WsExTopmost = 0x00000008; + private const uint WsExTransparent = 0x00000020; + private const uint WsPopup = 0x80000000; + + private const uint WmDestroy = 0x0002; + private const uint WmPaint = 0x000F; + private const uint WmTimer = 0x0113; + + private const uint CsVRedraw = 0x0001; + private const uint CsHRedraw = 0x0002; + + private const int SwShowNoActivate = 4; + + private const int Transparent = 1; + + private const int BaseFontHeightPx = 52; + private const int BaseDpi = 96; + + private const uint DtCenter = 0x00000001; + private const uint DtVCenter = 0x00000004; + private const uint DtSingleLine = 0x00000020; + + private const uint MonitorDefaultToNearest = 2; + + private static readonly nint DpiAwarenessContextUnaware = new(-1); + + private static readonly object Sync = new(); + private static bool classRegistered; + + private static GCHandle? currentPinnedTextHandle; + + public static void Show(int left, int top, int width, int height, string text, int durationMs = 1200) + { + if (string.IsNullOrWhiteSpace(text)) + { + text = "Monitor"; + } + + _ = Task.Run(() => RunWindow(left, top, width, height, text, durationMs)) + .ContinueWith(static t => _ = t.Exception, TaskContinuationOptions.OnlyOnFaulted); + } + + private static unsafe void RunWindow(int left, int top, int width, int height, string text, int durationMs) + { + EnsureClassRegistered(); + + var workArea = TryGetWorkAreaFromFancyZonesCoordinates(left, top, width, height, out var resolvedWorkArea) + ? resolvedWorkArea + : new RECT + { + Left = left, + Top = top, + Right = left + width, + Bottom = top + height, + }; + + var workAreaWidth = Math.Max(0, workArea.Right - workArea.Left); + var workAreaHeight = Math.Max(0, workArea.Bottom - workArea.Top); + + var overlayWidth = Math.Clamp(workAreaWidth / 4, 220, 420); + var overlayHeight = Math.Clamp(workAreaHeight / 6, 120, 240); + + var x = workArea.Left + ((workAreaWidth - overlayWidth) / 2); + var y = workArea.Top + ((workAreaHeight - overlayHeight) / 2); + + lock (Sync) + { + currentPinnedTextHandle?.Free(); + currentPinnedTextHandle = GCHandle.Alloc(text, GCHandleType.Pinned); + } + + var hwnd = CreateWindowExW( + WsExToolWindow | WsExTopmost | WsExTransparent, + WindowClassName, + "MonitorIdentify", + WsPopup, + x, + y, + overlayWidth, + overlayHeight, + nint.Zero, + nint.Zero, + GetModuleHandleW(null), + nint.Zero); + + if (hwnd == nint.Zero) + { + return; + } + + _ = ShowWindow(hwnd, SwShowNoActivate); + _ = UpdateWindow(hwnd); + + _ = SetTimer(hwnd, 1, (uint)durationMs, nint.Zero); + + MSG msg; + while (GetMessageW(out msg, nint.Zero, 0, 0) != 0) + { + _ = TranslateMessage(in msg); + _ = DispatchMessageW(in msg); + } + + lock (Sync) + { + currentPinnedTextHandle?.Free(); + currentPinnedTextHandle = null; + } + } + + private static unsafe void EnsureClassRegistered() + { + lock (Sync) + { + if (classRegistered) + { + return; + } + + fixed (char* className = WindowClassName ?? string.Empty) + { + var wc = new WNDCLASSEXW + { + CbSize = (uint)sizeof(WNDCLASSEXW), + Style = CsHRedraw | CsVRedraw, + LpfnWndProc = &WndProc, + HInstance = GetModuleHandleW(null), + HCursor = LoadCursorW(nint.Zero, new IntPtr(32512)), // IDC_ARROW + LpszClassName = className, + }; + + _ = RegisterClassExW(in wc); + classRegistered = true; + } + } + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])] + private static unsafe nint WndProc(nint hwnd, uint msg, nuint wParam, nint lParam) + { + switch (msg) + { + case WmTimer: + _ = KillTimer(hwnd, 1); + _ = DestroyWindow(hwnd); + return nint.Zero; + + case WmDestroy: + PostQuitMessage(0); + return nint.Zero; + + case WmPaint: + { + var hdc = BeginPaint(hwnd, out var ps); + + _ = GetClientRect(hwnd, out var rect); + + var bgBrush = CreateSolidBrush(0x202020); + _ = FillRect(hdc, in rect, bgBrush); + + _ = SetBkMode(hdc, Transparent); + _ = SetTextColor(hdc, 0xFFFFFF); + + var dpi = GetDpiForWindow(hwnd); + var fontHeight = -MulDiv(BaseFontHeightPx, (int)dpi, BaseDpi); + var font = CreateFontW( + fontHeight, + 0, + 0, + 0, + 700, + 0, + 0, + 0, + 1, // DEFAULT_CHARSET + 0, // OUT_DEFAULT_PRECIS + 0, // CLIP_DEFAULT_PRECIS + 5, // CLEARTYPE_QUALITY + 0x20, // FF_SWISS + "Segoe UI"); + + var oldFont = SelectObject(hdc, font); + + var textPtr = GetPinnedTextPointer(); + if (textPtr is not null) + { + var textNint = (nint)textPtr; + _ = DrawTextW(hdc, textNint, -1, ref rect, DtCenter | DtVCenter | DtSingleLine); + } + + _ = SelectObject(hdc, oldFont); + _ = DeleteObject(font); + _ = DeleteObject(bgBrush); + + _ = EndPaint(hwnd, ref ps); + return nint.Zero; + } + } + + return DefWindowProcW(hwnd, msg, wParam, lParam); + } + + private static unsafe char* GetPinnedTextPointer() + { + lock (Sync) + { + if (!currentPinnedTextHandle.HasValue || !currentPinnedTextHandle.Value.IsAllocated) + { + return null; + } + + return (char*)currentPinnedTextHandle.Value.AddrOfPinnedObject(); + } + } + + private static bool TryGetWorkAreaFromFancyZonesCoordinates(int left, int top, int width, int height, out RECT workArea) + { + workArea = default; + + if (width <= 0 || height <= 0) + { + return false; + } + + var logicalRect = new RECT + { + Left = left, + Top = top, + Right = left + width, + Bottom = top + height, + }; + + var previousContext = SetThreadDpiAwarenessContext(DpiAwarenessContextUnaware); + nint monitor; + try + { + monitor = MonitorFromRect(ref logicalRect, MonitorDefaultToNearest); + } + finally + { + _ = SetThreadDpiAwarenessContext(previousContext); + } + + if (monitor == nint.Zero) + { + return false; + } + + var mi = new MONITORINFOEXW + { + CbSize = (uint)Marshal.SizeOf(), + }; + + if (!GetMonitorInfoW(monitor, ref mi)) + { + return false; + } + + workArea = mi.RcWork; + return true; + } + + [StructLayout(LayoutKind.Sequential)] + private unsafe struct WNDCLASSEXW + { + public uint CbSize; + public uint Style; + public delegate* unmanaged[Stdcall] LpfnWndProc; + public int CbClsExtra; + public int CbWndExtra; + public nint HInstance; + public nint HIcon; + public nint HCursor; + public nint HbrBackground; + public char* LpszMenuName; + public char* LpszClassName; + public nint HIconSm; + } + + [StructLayout(LayoutKind.Sequential)] + private struct POINT + { + public int X; + public int Y; + } + + [StructLayout(LayoutKind.Sequential)] + private struct MSG + { + public nint Hwnd; + public uint Message; + public nuint WParam; + public nint LParam; + public uint Time; + public POINT Pt; + public uint LPrivate; + } + + [StructLayout(LayoutKind.Sequential)] + private struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct MONITORINFOEXW + { + public uint CbSize; + public RECT RcMonitor; + public RECT RcWork; + public uint DwFlags; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + public string SzDevice; + } + + [StructLayout(LayoutKind.Sequential)] + private unsafe struct PAINTSTRUCT + { + public nint Hdc; + public int FErase; + public RECT RcPaint; + public int FRestore; + public int FIncUpdate; + public fixed byte RgbReserved[32]; + } + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern nint GetModuleHandleW(string? lpModuleName); + + [DllImport("kernel32.dll")] + private static extern int MulDiv(int nNumber, int nNumerator, int nDenominator); + + [DllImport("user32.dll")] + private static extern uint GetDpiForWindow(nint hwnd); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool UpdateWindow(nint hWnd); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool ShowWindow(nint hWnd, int nCmdShow); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool DestroyWindow(nint hWnd); + + [DllImport("user32.dll")] + private static extern void PostQuitMessage(int nExitCode); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool KillTimer(nint hWnd, nuint uIDEvent); + + [DllImport("user32.dll", SetLastError = true)] + private static extern nuint SetTimer(nint hWnd, nuint nIDEvent, uint uElapse, nint lpTimerFunc); + + [DllImport("user32.dll")] + private static extern int GetMessageW(out MSG lpMsg, nint hWnd, uint wMsgFilterMin, uint wMsgFilterMax); + + [DllImport("user32.dll")] + private static extern bool TranslateMessage(in MSG lpMsg); + + [DllImport("user32.dll")] + private static extern nint DispatchMessageW(in MSG lpMsg); + + [DllImport("user32.dll")] + private static extern nint SetThreadDpiAwarenessContext(nint dpiContext); + + [DllImport("user32.dll")] + private static extern nint MonitorFromRect(ref RECT lprc, uint dwFlags); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern bool GetMonitorInfoW(nint hMonitor, ref MONITORINFOEXW lpmi); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern ushort RegisterClassExW(in WNDCLASSEXW lpwcx); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern nint CreateWindowExW( + uint dwExStyle, + string lpClassName, + string lpWindowName, + uint dwStyle, + int x, + int y, + int nWidth, + int nHeight, + nint hWndParent, + nint hMenu, + nint hInstance, + nint lpParam); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern nint LoadCursorW(nint hInstance, nint lpCursorName); + + [DllImport("user32.dll")] + private static extern nint DefWindowProcW(nint hWnd, uint msg, nuint wParam, nint lParam); + + [DllImport("user32.dll")] + private static extern nint BeginPaint(nint hWnd, out PAINTSTRUCT lpPaint); + + [DllImport("user32.dll")] + private static extern bool EndPaint(nint hWnd, ref PAINTSTRUCT lpPaint); + + [DllImport("user32.dll")] + private static extern bool GetClientRect(nint hWnd, out RECT lpRect); + + [DllImport("user32.dll")] + private static extern int FillRect(nint hDC, in RECT lprc, nint hbr); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern int DrawTextW(nint hdc, nint lpchText, int cchText, ref RECT lprc, uint format); + + [DllImport("gdi32.dll")] + private static extern nint CreateSolidBrush(uint colorRef); + + [DllImport("gdi32.dll")] + private static extern int SetBkMode(nint hdc, int mode); + + [DllImport("gdi32.dll")] + private static extern uint SetTextColor(nint hdc, uint colorRef); + + [DllImport("gdi32.dll", CharSet = CharSet.Unicode)] + private static extern nint CreateFontW( + int nHeight, + int nWidth, + int nEscapement, + int nOrientation, + int fnWeight, + uint fdwItalic, + uint fdwUnderline, + uint fdwStrikeOut, + uint fdwCharSet, + uint fdwOutputPrecision, + uint fdwClipPrecision, + uint fdwQuality, + uint fdwPitchAndFamily, + string lpszFace); + + [DllImport("gdi32.dll")] + private static extern nint SelectObject(nint hdc, nint hgdiobj); + + [DllImport("gdi32.dll")] + private static extern bool DeleteObject(nint hObject); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorPreviewRenderer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorPreviewRenderer.cs new file mode 100644 index 0000000000..fc22b1a0cd --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesMonitorPreviewRenderer.cs @@ -0,0 +1,399 @@ +// 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 System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +using FancyZonesEditorCommon.Data; + +using ManagedCommon; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Graphics.Imaging; +using Windows.Storage.Streams; + +using InteropConstants = PowerToys.Interop.Constants; + +namespace PowerToysExtension.Helpers; + +internal static class FancyZonesMonitorPreviewRenderer +{ + public static IconInfo? TryRenderMonitorHeroImage(FancyZonesMonitorDescriptor monitor) + { + try + { + var cached = TryGetCachedIcon(monitor); + if (cached is not null) + { + return cached; + } + + var icon = RenderMonitorHeroImageAsync(monitor).GetAwaiter().GetResult(); + return icon; + } + catch (Exception ex) + { + Logger.LogWarning($"FancyZones monitor hero render failed. Monitor={monitor.Data.Monitor} Index={monitor.Index} Exception={ex}"); + return null; + } + } + + private static IconInfo? TryGetCachedIcon(FancyZonesMonitorDescriptor monitor) + { + var cachePath = GetCachePath(monitor); + if (string.IsNullOrEmpty(cachePath)) + { + return null; + } + + try + { + if (File.Exists(cachePath)) + { + return new IconInfo(cachePath); + } + } + catch (Exception ex) + { + Logger.LogWarning($"FancyZones monitor hero cache check failed. Path=\"{cachePath}\" Monitor={monitor.Data.Monitor} Index={monitor.Index} Exception={ex}"); + } + + return null; + } + + private static async Task RenderMonitorHeroImageAsync(FancyZonesMonitorDescriptor monitor) + { + var cachePath = GetCachePath(monitor); + if (string.IsNullOrEmpty(cachePath)) + { + return null; + } + + var (widthPx, heightPx) = ComputeCanvasSize(monitor.Data); + Logger.LogDebug($"FancyZones monitor hero render starting. Monitor={monitor.Data.Monitor} Index={monitor.Index} Size={widthPx}x{heightPx}"); + + var (layoutRectangles, spacing) = GetLayoutRectangles(monitor.Data); + var pixelBytes = RenderMonitorPreviewBgra(widthPx, heightPx, layoutRectangles, spacing); + + var stream = new InMemoryRandomAccessStream(); + var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream); + encoder.SetPixelData( + BitmapPixelFormat.Bgra8, + BitmapAlphaMode.Premultiplied, + (uint)widthPx, + (uint)heightPx, + 96, + 96, + pixelBytes); + await encoder.FlushAsync(); + stream.Seek(0); + + try + { + var tempPath = FormattableString.Invariant($"{cachePath}.{Guid.NewGuid():N}.tmp"); + Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!); + await WriteStreamToFileAsync(stream, tempPath); + File.Move(tempPath, cachePath, overwrite: true); + } + catch (Exception ex) + { + Logger.LogWarning($"FancyZones monitor hero cache write failed. Path=\"{cachePath}\" Monitor={monitor.Data.Monitor} Index={monitor.Index} Exception={ex}"); + return null; + } + + Logger.LogDebug($"FancyZones monitor hero render succeeded. Monitor={monitor.Data.Monitor} Index={monitor.Index} Path=\"{cachePath}\""); + return new IconInfo(cachePath); + } + + private static (int WidthPx, int HeightPx) ComputeCanvasSize(EditorParameters.NativeMonitorDataWrapper monitor) + { + const int maxDim = 320; + var w = monitor.WorkAreaWidth > 0 ? monitor.WorkAreaWidth : monitor.MonitorWidth; + var h = monitor.WorkAreaHeight > 0 ? monitor.WorkAreaHeight : monitor.MonitorHeight; + + if (w <= 0 || h <= 0) + { + return (maxDim, 180); + } + + var aspect = (float)w / h; + if (aspect >= 1) + { + var height = (int)Math.Clamp(Math.Round(maxDim / aspect), 90, maxDim); + return (maxDim, height); + } + else + { + var width = (int)Math.Clamp(Math.Round(maxDim * aspect), 90, maxDim); + return (width, maxDim); + } + } + + private static (List Rects, int Spacing) GetLayoutRectangles(EditorParameters.NativeMonitorDataWrapper monitor) + { + if (!FancyZonesDataService.TryGetAppliedLayoutForMonitor(monitor, out var applied) || applied is null) + { + return ([], 0); + } + + var layout = FindLayoutDescriptor(applied.Value); + if (layout is null) + { + return ([], 0); + } + + var rects = FancyZonesThumbnailRenderer.GetNormalizedRectsForLayout(layout); + var spacing = layout.ApplyLayout.ShowSpacing && layout.ApplyLayout.Spacing > 0 ? layout.ApplyLayout.Spacing : 0; + return (rects, spacing); + } + + private static FancyZonesLayoutDescriptor? FindLayoutDescriptor(AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper applied) + { + try + { + var layouts = FancyZonesDataService.GetLayouts(); + + if (!string.IsNullOrWhiteSpace(applied.Uuid) && + !applied.Uuid.Equals("{00000000-0000-0000-0000-000000000000}", StringComparison.OrdinalIgnoreCase)) + { + return layouts.FirstOrDefault(l => l.Source == FancyZonesLayoutSource.Custom && + l.Custom is not null && + string.Equals(l.Custom.Value.Uuid?.Trim(), applied.Uuid.Trim(), StringComparison.OrdinalIgnoreCase)); + } + + var type = applied.Type?.Trim().ToLowerInvariant() ?? string.Empty; + var zoneCount = applied.ZoneCount; + return layouts.FirstOrDefault(l => + l.Source == FancyZonesLayoutSource.Template && + string.Equals(l.ApplyLayout.Type, type, StringComparison.OrdinalIgnoreCase) && + l.ApplyLayout.ZoneCount == zoneCount && + l.ApplyLayout.ShowSpacing == applied.ShowSpacing && + l.ApplyLayout.Spacing == applied.Spacing); + } + catch + { + return null; + } + } + + private static string? GetCachePath(FancyZonesMonitorDescriptor monitor) + { + try + { + var basePath = InteropConstants.AppDataPath(); + if (string.IsNullOrWhiteSpace(basePath)) + { + return null; + } + + var cacheFolder = Path.Combine(basePath, "CmdPal", "PowerToysExtension", "Cache", "FancyZones", "MonitorPreviews"); + var fileName = ComputeMonitorHash(monitor) + ".png"; + return Path.Combine(cacheFolder, fileName); + } + catch + { + return null; + } + } + + private static string ComputeMonitorHash(FancyZonesMonitorDescriptor monitor) + { + var currentVirtualDesktop = FancyZonesVirtualDesktop.GetCurrentVirtualDesktopIdString(); + var appliedFingerprint = string.Empty; + if (FancyZonesDataService.TryGetAppliedLayoutForMonitor(monitor.Data, out var applied) && applied is not null) + { + appliedFingerprint = FormattableString.Invariant($"{applied.Value.Type}|{applied.Value.Uuid}|{applied.Value.ZoneCount}|{applied.Value.ShowSpacing}|{applied.Value.Spacing}"); + } + + var identity = FormattableString.Invariant( + $"{monitor.Data.Monitor}|{monitor.Data.MonitorInstanceId}|{monitor.Data.MonitorSerialNumber}|{monitor.Data.MonitorNumber}|{currentVirtualDesktop}|{monitor.Data.WorkAreaWidth}x{monitor.Data.WorkAreaHeight}|{monitor.Data.MonitorWidth}x{monitor.Data.MonitorHeight}|{appliedFingerprint}"); + + var bytes = Encoding.UTF8.GetBytes(identity); + var hash = SHA256.HashData(bytes); + return Convert.ToHexString(hash).ToLowerInvariant(); + } + + private static byte[] RenderMonitorPreviewBgra( + int widthPx, + int heightPx, + IReadOnlyList rects, + int spacing) + { + var pixels = new byte[widthPx * heightPx * 4]; + + var frame = Premultiply(new BgraColor(0x80, 0x80, 0x80, 0xFF)); + var bezelFill = Premultiply(new BgraColor(0x20, 0x20, 0x20, 0x18)); + var screenFill = Premultiply(new BgraColor(0x00, 0x00, 0x00, 0x00)); + var border = Premultiply(new BgraColor(0xFF, 0xD8, 0x8C, 0xFF)); + var fill = Premultiply(new BgraColor(0xFF, 0xD8, 0x8C, 0xC0)); + var background = Premultiply(new BgraColor(0x00, 0x00, 0x00, 0x00)); + + for (var i = 0; i < pixels.Length; i += 4) + { + pixels[i + 0] = background.B; + pixels[i + 1] = background.G; + pixels[i + 2] = background.R; + pixels[i + 3] = background.A; + } + + DrawRectBorder(pixels, widthPx, heightPx, 0, 0, widthPx, heightPx, frame); + + const int bezel = 3; + FillRect(pixels, widthPx, heightPx, 1, 1, widthPx - 1, heightPx - 1, bezelFill); + FillRect(pixels, widthPx, heightPx, 1 + bezel, 1 + bezel, widthPx - 1 - bezel, heightPx - 1 - bezel, screenFill); + + var innerLeft = 1 + bezel; + var innerTop = 1 + bezel; + var innerRight = widthPx - 1 - bezel; + var innerBottom = heightPx - 1 - bezel; + var innerWidth = Math.Max(1, innerRight - innerLeft); + var innerHeight = Math.Max(1, innerBottom - innerTop); + + var gapPx = spacing > 0 ? Math.Clamp(spacing / 8, 1, 3) : 0; + foreach (var rect in rects) + { + var (x1, y1, x2, y2) = ToPixelBounds(rect, innerLeft, innerTop, innerWidth, innerHeight, gapPx); + if (x2 <= x1 || y2 <= y1) + { + continue; + } + + FillRect(pixels, widthPx, heightPx, x1, y1, x2, y2, fill); + DrawRectBorder(pixels, widthPx, heightPx, x1, y1, x2, y2, border); + } + + return pixels; + } + + private static (int X1, int Y1, int X2, int Y2) ToPixelBounds( + FancyZonesThumbnailRenderer.NormalizedRect rect, + int originX, + int originY, + int widthPx, + int heightPx, + int gapPx) + { + var x1 = originX + (int)MathF.Round(rect.X * widthPx); + var y1 = originY + (int)MathF.Round(rect.Y * heightPx); + var x2 = originX + (int)MathF.Round((rect.X + rect.Width) * widthPx); + var y2 = originY + (int)MathF.Round((rect.Y + rect.Height) * heightPx); + + x1 = Math.Clamp(x1 + gapPx, originX, originX + widthPx - 1); + y1 = Math.Clamp(y1 + gapPx, originY, originY + heightPx - 1); + x2 = Math.Clamp(x2 - gapPx, originX + 1, originX + widthPx); + y2 = Math.Clamp(y2 - gapPx, originY + 1, originY + heightPx); + + if (x2 <= x1 + 1) + { + x2 = Math.Min(originX + widthPx, x1 + 2); + } + + if (y2 <= y1 + 1) + { + y2 = Math.Min(originY + heightPx, y1 + 2); + } + + return (x1, y1, x2, y2); + } + + private static void FillRect(byte[] pixels, int widthPx, int heightPx, int x1, int y1, int x2, int y2, BgraColor color) + { + for (var y = y1; y < y2; y++) + { + if ((uint)y >= (uint)heightPx) + { + continue; + } + + var rowStart = y * widthPx * 4; + for (var x = x1; x < x2; x++) + { + if ((uint)x >= (uint)widthPx) + { + continue; + } + + var i = rowStart + (x * 4); + pixels[i + 0] = color.B; + pixels[i + 1] = color.G; + pixels[i + 2] = color.R; + pixels[i + 3] = color.A; + } + } + } + + private static void DrawRectBorder(byte[] pixels, int widthPx, int heightPx, int x1, int y1, int x2, int y2, BgraColor color) + { + var left = x1; + var right = x2 - 1; + var top = y1; + var bottom = y2 - 1; + + for (var x = left; x <= right; x++) + { + SetPixel(pixels, widthPx, heightPx, x, top, color); + SetPixel(pixels, widthPx, heightPx, x, bottom, color); + } + + for (var y = top; y <= bottom; y++) + { + SetPixel(pixels, widthPx, heightPx, left, y, color); + SetPixel(pixels, widthPx, heightPx, right, y, color); + } + } + + private static void SetPixel(byte[] pixels, int widthPx, int heightPx, int x, int y, BgraColor color) + { + if ((uint)x >= (uint)widthPx || (uint)y >= (uint)heightPx) + { + return; + } + + var i = ((y * widthPx) + x) * 4; + pixels[i + 0] = color.B; + pixels[i + 1] = color.G; + pixels[i + 2] = color.R; + pixels[i + 3] = color.A; + } + + private static BgraColor Premultiply(BgraColor color) + { + if (color.A == 0 || color.A == 255) + { + return color; + } + + byte Premul(byte c) => (byte)(((c * color.A) + 127) / 255); + return new BgraColor(Premul(color.B), Premul(color.G), Premul(color.R), color.A); + } + + private readonly record struct BgraColor(byte B, byte G, byte R, byte A); + + private static async Task WriteStreamToFileAsync(IRandomAccessStream stream, string filePath) + { + stream.Seek(0); + var size = stream.Size; + if (size == 0) + { + File.WriteAllBytes(filePath, Array.Empty()); + return; + } + + if (size > int.MaxValue) + { + throw new InvalidOperationException("Icon stream too large."); + } + + using var input = stream.GetInputStreamAt(0); + using var reader = new DataReader(input); + await reader.LoadAsync((uint)size); + var bytes = new byte[(int)size]; + reader.ReadBytes(bytes); + File.WriteAllBytes(filePath, bytes); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesNotifier.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesNotifier.cs new file mode 100644 index 0000000000..ada8ee7b17 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesNotifier.cs @@ -0,0 +1,25 @@ +// 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 PowerToysExtension.Helpers; + +internal static class FancyZonesNotifier +{ + private const string AppliedLayoutsFileUpdateMessage = "{2ef2c8a7-e0d5-4f31-9ede-52aade2d284d}"; + private static readonly uint WmPrivAppliedLayoutsFileUpdate = RegisterWindowMessageW(AppliedLayoutsFileUpdateMessage); + + public static void NotifyAppliedLayoutsChanged() + { + _ = PostMessageW(new IntPtr(0xFFFF), WmPrivAppliedLayoutsFileUpdate, UIntPtr.Zero, IntPtr.Zero); + } + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern uint RegisterWindowMessageW(string lpString); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool PostMessageW(IntPtr hWnd, uint msg, UIntPtr wParam, IntPtr lParam); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesThumbnailRenderer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesThumbnailRenderer.cs new file mode 100644 index 0000000000..579b4e2d0e --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesThumbnailRenderer.cs @@ -0,0 +1,716 @@ +// 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 System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using FancyZonesEditorCommon.Data; +using ManagedCommon; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Graphics.Imaging; +using Windows.Storage.Streams; + +using InteropConstants = PowerToys.Interop.Constants; + +namespace PowerToysExtension.Helpers; + +internal static class FancyZonesThumbnailRenderer +{ + internal readonly record struct NormalizedRect(float X, float Y, float Width, float Height); + + private readonly record struct BgraColor(byte B, byte G, byte R, byte A); + + public static async Task RenderLayoutIconAsync(FancyZonesLayoutDescriptor layout, int sizePx = 72) + { + try + { + Logger.LogDebug($"FancyZones thumbnail render starting. LayoutId={layout.Id} Type={layout.ApplyLayout.Type} ZoneCount={layout.ApplyLayout.ZoneCount} Source={layout.Source}"); + if (sizePx < 16) + { + sizePx = 16; + } + + var cachedIcon = TryGetCachedIcon(layout); + if (cachedIcon is not null) + { + Logger.LogDebug($"FancyZones thumbnail cache hit. LayoutId={layout.Id}"); + return cachedIcon; + } + + var rects = GetNormalizedRectsForLayout(layout); + Logger.LogDebug($"FancyZones thumbnail rects computed. LayoutId={layout.Id} RectCount={rects.Count}"); + var pixelBytes = RenderBgra(rects, sizePx, layout.ApplyLayout.ShowSpacing && layout.ApplyLayout.Spacing > 0 ? layout.ApplyLayout.Spacing : 0); + var stream = new InMemoryRandomAccessStream(); + + var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream); + encoder.SetPixelData( + BitmapPixelFormat.Bgra8, + BitmapAlphaMode.Premultiplied, + (uint)sizePx, + (uint)sizePx, + 96, + 96, + pixelBytes); + + await encoder.FlushAsync(); + stream.Seek(0); + + var cachePath = GetCachePath(layout); + if (!string.IsNullOrEmpty(cachePath)) + { + try + { + var tempPath = FormattableString.Invariant($"{cachePath}.{Guid.NewGuid():N}.tmp"); + Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!); + await WriteStreamToFileAsync(stream, tempPath); + File.Move(tempPath, cachePath, overwrite: true); + + var fileIcon = new IconInfo(cachePath); + Logger.LogDebug($"FancyZones thumbnail render succeeded (file cache). LayoutId={layout.Id} Path=\"{cachePath}\""); + return fileIcon; + } + catch (Exception ex) + { + Logger.LogWarning($"FancyZones thumbnail write cache failed. LayoutId={layout.Id} Path=\"{cachePath}\" Exception={ex}"); + } + } + + // Fallback: return an in-memory stream icon. This may not marshal reliably cross-proc, + // so prefer the file-cached path above. + stream.Seek(0); + var inMemoryIcon = IconInfo.FromStream(stream); + Logger.LogDebug($"FancyZones thumbnail render succeeded (in-memory). LayoutId={layout.Id}"); + return inMemoryIcon; + } + catch (Exception ex) + { + Logger.LogWarning($"FancyZones thumbnail render failed. LayoutId={layout.Id} Type={layout.ApplyLayout.Type} ZoneCount={layout.ApplyLayout.ZoneCount} Source={layout.Source} Exception={ex}"); + return null; + } + } + + private static IconInfo? TryGetCachedIcon(FancyZonesLayoutDescriptor layout) + { + var cachePath = GetCachePath(layout); + if (string.IsNullOrEmpty(cachePath)) + { + return null; + } + + try + { + if (File.Exists(cachePath)) + { + return new IconInfo(cachePath); + } + } + catch (Exception ex) + { + Logger.LogWarning($"FancyZones thumbnail cache check failed. LayoutId={layout.Id} Path=\"{cachePath}\" Exception={ex}"); + } + + return null; + } + + /// + /// Removes cached thumbnail files that no longer correspond to any current layout. + /// Call this on startup or periodically to prevent unbounded cache growth. + /// + public static void PurgeOrphanedCache() + { + try + { + var cacheFolder = GetCacheFolder(); + if (string.IsNullOrEmpty(cacheFolder) || !Directory.Exists(cacheFolder)) + { + return; + } + + // Get all current layouts and compute their expected cache file names + var layouts = FancyZonesDataService.GetLayouts(); + var validHashes = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var layout in layouts) + { + validHashes.Add(ComputeLayoutHash(layout) + ".png"); + } + + // Delete any .png files not in the valid set + var deletedCount = 0; + foreach (var filePath in Directory.EnumerateFiles(cacheFolder, "*.png")) + { + var fileName = Path.GetFileName(filePath); + if (!validHashes.Contains(fileName)) + { + try + { + File.Delete(filePath); + deletedCount++; + } + catch (Exception ex) + { + Logger.LogWarning($"FancyZones thumbnail cache purge: failed to delete \"{filePath}\". Exception={ex.Message}"); + } + } + } + + if (deletedCount > 0) + { + Logger.LogInfo($"FancyZones thumbnail cache purge: deleted {deletedCount} orphaned file(s)."); + } + } + catch (Exception ex) + { + Logger.LogWarning($"FancyZones thumbnail cache purge failed. Exception={ex}"); + } + } + + private static string? GetCacheFolder() + { + var basePath = InteropConstants.AppDataPath(); + if (string.IsNullOrWhiteSpace(basePath)) + { + return null; + } + + return Path.Combine(basePath, "CmdPal", "PowerToysExtension", "Cache", "FancyZones", "LayoutThumbnails"); + } + + private static string? GetCachePath(FancyZonesLayoutDescriptor layout) + { + try + { + var cacheFolder = GetCacheFolder(); + if (string.IsNullOrEmpty(cacheFolder)) + { + return null; + } + + var fileName = ComputeLayoutHash(layout) + ".png"; + return Path.Combine(cacheFolder, fileName); + } + catch (Exception ex) + { + Logger.LogWarning($"FancyZones thumbnail cache path failed. LayoutId={layout.Id} Exception={ex}"); + return null; + } + } + + private static string ComputeLayoutHash(FancyZonesLayoutDescriptor layout) + { + var customType = layout.Custom?.Type?.Trim() ?? string.Empty; + var customInfo = layout.Custom is not null && layout.Custom.Value.Info.ValueKind is not JsonValueKind.Undefined and not JsonValueKind.Null + ? layout.Custom.Value.Info.GetRawText() + : string.Empty; + + var fingerprint = FormattableString.Invariant( + $"{layout.Id}|{layout.Source}|{layout.ApplyLayout.Type}|{layout.ApplyLayout.ZoneCount}|{layout.ApplyLayout.ShowSpacing}|{layout.ApplyLayout.Spacing}|{customType}|{customInfo}"); + + var bytes = Encoding.UTF8.GetBytes(fingerprint); + var hash = SHA256.HashData(bytes); + return Convert.ToHexString(hash).ToLowerInvariant(); + } + + private static async Task WriteStreamToFileAsync(IRandomAccessStream stream, string filePath) + { + stream.Seek(0); + var size = stream.Size; + if (size == 0) + { + File.WriteAllBytes(filePath, Array.Empty()); + return; + } + + if (size > int.MaxValue) + { + throw new InvalidOperationException("Icon stream too large."); + } + + using var input = stream.GetInputStreamAt(0); + using var reader = new DataReader(input); + await reader.LoadAsync((uint)size); + var bytes = new byte[(int)size]; + reader.ReadBytes(bytes); + File.WriteAllBytes(filePath, bytes); + } + + internal static List GetNormalizedRectsForLayout(FancyZonesLayoutDescriptor layout) + { + var type = layout.ApplyLayout.Type.ToLowerInvariant(); + if (layout.Source == FancyZonesLayoutSource.Custom && layout.Custom is not null) + { + return GetCustomRects(layout.Custom.Value); + } + + return type switch + { + "columns" => GetColumnsRects(layout.ApplyLayout.ZoneCount), + "rows" => GetRowsRects(layout.ApplyLayout.ZoneCount), + "grid" => GetGridRects(layout.ApplyLayout.ZoneCount), + "priority-grid" => GetPriorityGridRects(layout.ApplyLayout.ZoneCount), + "focus" => GetFocusRects(layout.ApplyLayout.ZoneCount), + "blank" => new List(), + _ => GetGridRects(layout.ApplyLayout.ZoneCount), + }; + } + + private static List GetCustomRects(CustomLayouts.CustomLayoutWrapper custom) + { + var type = custom.Type?.Trim().ToLowerInvariant() ?? string.Empty; + if (custom.Info.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null) + { + return new List(); + } + + return type switch + { + "grid" => GetCustomGridRects(custom.Info), + "canvas" => GetCustomCanvasRects(custom.Info), + _ => new List(), + }; + } + + private static List GetCustomCanvasRects(JsonElement info) + { + if (!info.TryGetProperty("ref-width", out var refWidthProp) || + !info.TryGetProperty("ref-height", out var refHeightProp) || + !info.TryGetProperty("zones", out var zonesProp)) + { + return new List(); + } + + if (refWidthProp.ValueKind != JsonValueKind.Number || refHeightProp.ValueKind != JsonValueKind.Number || zonesProp.ValueKind != JsonValueKind.Array) + { + return new List(); + } + + var refWidth = Math.Max(1, refWidthProp.GetInt32()); + var refHeight = Math.Max(1, refHeightProp.GetInt32()); + var rects = new List(zonesProp.GetArrayLength()); + + foreach (var zone in zonesProp.EnumerateArray()) + { + if (zone.ValueKind != JsonValueKind.Object) + { + continue; + } + + if (!zone.TryGetProperty("X", out var xProp) || + !zone.TryGetProperty("Y", out var yProp) || + !zone.TryGetProperty("width", out var wProp) || + !zone.TryGetProperty("height", out var hProp)) + { + continue; + } + + if (xProp.ValueKind != JsonValueKind.Number || + yProp.ValueKind != JsonValueKind.Number || + wProp.ValueKind != JsonValueKind.Number || + hProp.ValueKind != JsonValueKind.Number) + { + continue; + } + + var x = xProp.GetSingle() / refWidth; + var y = yProp.GetSingle() / refHeight; + var w = wProp.GetSingle() / refWidth; + var h = hProp.GetSingle() / refHeight; + rects.Add(NormalizeRect(x, y, w, h)); + } + + return rects; + } + + private static List GetCustomGridRects(JsonElement info) + { + if (!TryGetGridDefinition(info, out var rows, out var cols, out var rowsPercents, out var colsPercents, out var cellMap)) + { + return new List(); + } + + return BuildRectsFromGridDefinition(rows, cols, rowsPercents, colsPercents, cellMap); + } + + private static bool TryGetGridDefinition( + JsonElement info, + out int rows, + out int cols, + out int[] rowPercents, + out int[] colPercents, + out int[][] cellChildMap) + { + rows = 0; + cols = 0; + rowPercents = Array.Empty(); + colPercents = Array.Empty(); + cellChildMap = Array.Empty(); + + if (!info.TryGetProperty("rows", out var rowsProp) || + !info.TryGetProperty("columns", out var colsProp) || + !info.TryGetProperty("rows-percentage", out var rowsPercentsProp) || + !info.TryGetProperty("columns-percentage", out var colsPercentsProp) || + !info.TryGetProperty("cell-child-map", out var cellMapProp)) + { + return false; + } + + if (rowsProp.ValueKind != JsonValueKind.Number || + colsProp.ValueKind != JsonValueKind.Number || + rowsPercentsProp.ValueKind != JsonValueKind.Array || + colsPercentsProp.ValueKind != JsonValueKind.Array || + cellMapProp.ValueKind != JsonValueKind.Array) + { + return false; + } + + rows = rowsProp.GetInt32(); + cols = colsProp.GetInt32(); + if (rows <= 0 || cols <= 0) + { + return false; + } + + rowPercents = rowsPercentsProp.EnumerateArray().Where(v => v.ValueKind == JsonValueKind.Number).Select(v => v.GetInt32()).ToArray(); + colPercents = colsPercentsProp.EnumerateArray().Where(v => v.ValueKind == JsonValueKind.Number).Select(v => v.GetInt32()).ToArray(); + + if (rowPercents.Length != rows || colPercents.Length != cols) + { + return false; + } + + var mapRows = new List(rows); + foreach (var row in cellMapProp.EnumerateArray()) + { + if (row.ValueKind != JsonValueKind.Array) + { + return false; + } + + var cells = row.EnumerateArray().Where(v => v.ValueKind == JsonValueKind.Number).Select(v => v.GetInt32()).ToArray(); + if (cells.Length != cols) + { + return false; + } + + mapRows.Add(cells); + } + + if (mapRows.Count != rows) + { + return false; + } + + cellChildMap = mapRows.ToArray(); + return true; + } + + private static List GetColumnsRects(int zoneCount) + { + zoneCount = Math.Clamp(zoneCount, 1, 16); + var rects = new List(zoneCount); + for (var i = 0; i < zoneCount; i++) + { + rects.Add(new NormalizedRect(i / (float)zoneCount, 0, 1f / zoneCount, 1f)); + } + + return rects; + } + + private static List GetRowsRects(int zoneCount) + { + zoneCount = Math.Clamp(zoneCount, 1, 16); + var rects = new List(zoneCount); + for (var i = 0; i < zoneCount; i++) + { + rects.Add(new NormalizedRect(0, i / (float)zoneCount, 1f, 1f / zoneCount)); + } + + return rects; + } + + private static List GetGridRects(int zoneCount) + { + zoneCount = Math.Clamp(zoneCount, 1, 25); + var rows = 1; + while (zoneCount / rows >= rows) + { + rows++; + } + + rows--; + var cols = zoneCount / rows; + if (zoneCount % rows != 0) + { + cols++; + } + + var rowPercents = Enumerable.Repeat(10000 / rows, rows).ToArray(); + var colPercents = Enumerable.Repeat(10000 / cols, cols).ToArray(); + var cellMap = new int[rows][]; + + var index = 0; + for (var r = 0; r < rows; r++) + { + cellMap[r] = new int[cols]; + for (var c = 0; c < cols; c++) + { + cellMap[r][c] = index; + index++; + if (index == zoneCount) + { + index--; + } + } + } + + return BuildRectsFromGridDefinition(rows, cols, rowPercents, colPercents, cellMap); + } + + private static List GetPriorityGridRects(int zoneCount) + { + zoneCount = Math.Clamp(zoneCount, 1, 25); + + if (zoneCount is >= 1 and <= 11 && PriorityGrid.TryGetValue(zoneCount, out var def)) + { + return BuildRectsFromGridDefinition(def.Rows, def.Cols, def.RowPercents, def.ColPercents, def.CellMap); + } + + return GetGridRects(zoneCount); + } + + private static List GetFocusRects(int zoneCount) + { + zoneCount = Math.Clamp(zoneCount, 1, 8); + var rects = new List(zoneCount); + for (var i = 0; i < zoneCount; i++) + { + var offset = i * 0.06f; + rects.Add(new NormalizedRect(0.1f + offset, 0.1f + offset, 0.8f, 0.8f)); + } + + return rects; + } + + private static List BuildRectsFromGridDefinition(int rows, int cols, int[] rowPercents, int[] colPercents, int[][] cellChildMap) + { + const float multiplier = 10000f; + + var rowPrefix = new float[rows + 1]; + var colPrefix = new float[cols + 1]; + + for (var r = 0; r < rows; r++) + { + rowPrefix[r + 1] = rowPrefix[r] + (rowPercents[r] / multiplier); + } + + for (var c = 0; c < cols; c++) + { + colPrefix[c + 1] = colPrefix[c] + (colPercents[c] / multiplier); + } + + var maxZone = -1; + for (var r = 0; r < rows; r++) + { + for (var c = 0; c < cols; c++) + { + maxZone = Math.Max(maxZone, cellChildMap[r][c]); + } + } + + var rects = new List(maxZone + 1); + for (var i = 0; i <= maxZone; i++) + { + rects.Add(new NormalizedRect(1, 1, 0, 0)); + } + + for (var r = 0; r < rows; r++) + { + for (var c = 0; c < cols; c++) + { + var zoneId = cellChildMap[r][c]; + if (zoneId < 0 || zoneId >= rects.Count) + { + continue; + } + + var x1 = colPrefix[c]; + var y1 = rowPrefix[r]; + var x2 = colPrefix[c + 1]; + var y2 = rowPrefix[r + 1]; + + var existing = rects[zoneId]; + if (existing.Width <= 0 || existing.Height <= 0) + { + rects[zoneId] = new NormalizedRect(x1, y1, x2 - x1, y2 - y1); + } + else + { + var ex2 = existing.X + existing.Width; + var ey2 = existing.Y + existing.Height; + var nx1 = Math.Min(existing.X, x1); + var ny1 = Math.Min(existing.Y, y1); + var nx2 = Math.Max(ex2, x2); + var ny2 = Math.Max(ey2, y2); + rects[zoneId] = new NormalizedRect(nx1, ny1, nx2 - nx1, ny2 - ny1); + } + } + } + + return rects + .Where(r => r.Width > 0 && r.Height > 0) + .Select(r => NormalizeRect(r.X, r.Y, r.Width, r.Height)) + .ToList(); + } + + private static NormalizedRect NormalizeRect(float x, float y, float w, float h) + { + x = Math.Clamp(x, 0, 1); + y = Math.Clamp(y, 0, 1); + w = Math.Clamp(w, 0, 1 - x); + h = Math.Clamp(h, 0, 1 - y); + return new NormalizedRect(x, y, w, h); + } + + private static byte[] RenderBgra(IReadOnlyList rects, int sizePx, int spacing) + { + var pixels = new byte[sizePx * sizePx * 4]; + + var border = Premultiply(new BgraColor(0x30, 0x30, 0x30, 0xFF)); + var frame = Premultiply(new BgraColor(0x40, 0x40, 0x40, 0xA0)); + var fill = Premultiply(new BgraColor(0xFF, 0xD8, 0x8C, 0xC0)); // light-ish blue with alpha + var background = Premultiply(new BgraColor(0x00, 0x00, 0x00, 0x00)); + + for (var i = 0; i < pixels.Length; i += 4) + { + pixels[i + 0] = background.B; + pixels[i + 1] = background.G; + pixels[i + 2] = background.R; + pixels[i + 3] = background.A; + } + + DrawRectBorder(pixels, sizePx, 1, 1, sizePx - 1, sizePx - 1, frame); + + var gapPx = spacing > 0 ? Math.Clamp(spacing / 8, 1, 3) : 0; + foreach (var rect in rects) + { + var (x1, y1, x2, y2) = ToPixelBounds(rect, sizePx, gapPx); + if (x2 <= x1 || y2 <= y1) + { + continue; + } + + FillRect(pixels, sizePx, x1, y1, x2, y2, fill); + DrawRectBorder(pixels, sizePx, x1, y1, x2, y2, border); + } + + return pixels; + } + + private static (int X1, int Y1, int X2, int Y2) ToPixelBounds(NormalizedRect rect, int sizePx, int gapPx) + { + var x1 = (int)MathF.Round(rect.X * sizePx); + var y1 = (int)MathF.Round(rect.Y * sizePx); + var x2 = (int)MathF.Round((rect.X + rect.Width) * sizePx); + var y2 = (int)MathF.Round((rect.Y + rect.Height) * sizePx); + + x1 = Math.Clamp(x1 + gapPx, 0, sizePx - 1); + y1 = Math.Clamp(y1 + gapPx, 0, sizePx - 1); + x2 = Math.Clamp(x2 - gapPx, 1, sizePx); + y2 = Math.Clamp(y2 - gapPx, 1, sizePx); + + if (x2 <= x1 + 1) + { + x2 = Math.Min(sizePx, x1 + 2); + } + + if (y2 <= y1 + 1) + { + y2 = Math.Min(sizePx, y1 + 2); + } + + return (x1, y1, x2, y2); + } + + private static void FillRect(byte[] pixels, int sizePx, int x1, int y1, int x2, int y2, BgraColor color) + { + for (var y = y1; y < y2; y++) + { + var rowStart = y * sizePx * 4; + for (var x = x1; x < x2; x++) + { + var i = rowStart + (x * 4); + pixels[i + 0] = color.B; + pixels[i + 1] = color.G; + pixels[i + 2] = color.R; + pixels[i + 3] = color.A; + } + } + } + + private static void DrawRectBorder(byte[] pixels, int sizePx, int x1, int y1, int x2, int y2, BgraColor color) + { + var left = x1; + var right = x2 - 1; + var top = y1; + var bottom = y2 - 1; + + for (var x = left; x <= right; x++) + { + SetPixel(pixels, sizePx, x, top, color); + SetPixel(pixels, sizePx, x, bottom, color); + } + + for (var y = top; y <= bottom; y++) + { + SetPixel(pixels, sizePx, left, y, color); + SetPixel(pixels, sizePx, right, y, color); + } + } + + private static void SetPixel(byte[] pixels, int sizePx, int x, int y, BgraColor color) + { + if ((uint)x >= (uint)sizePx || (uint)y >= (uint)sizePx) + { + return; + } + + var i = ((y * sizePx) + x) * 4; + pixels[i + 0] = color.B; + pixels[i + 1] = color.G; + pixels[i + 2] = color.R; + pixels[i + 3] = color.A; + } + + private static BgraColor Premultiply(BgraColor color) + { + if (color.A == 0 || color.A == 255) + { + return color; + } + + byte Premul(byte c) => (byte)(((c * color.A) + 127) / 255); + return new BgraColor(Premul(color.B), Premul(color.G), Premul(color.R), color.A); + } + + private sealed record PriorityGridDefinition(int Rows, int Cols, int[] RowPercents, int[] ColPercents, int[][] CellMap); + + private static readonly IReadOnlyDictionary PriorityGrid = new Dictionary + { + [1] = new PriorityGridDefinition(1, 1, [10000], [10000], [[0]]), + [2] = new PriorityGridDefinition(1, 2, [10000], [6667, 3333], [[0, 1]]), + [3] = new PriorityGridDefinition(1, 3, [10000], [2500, 5000, 2500], [[0, 1, 2]]), + [4] = new PriorityGridDefinition(2, 3, [5000, 5000], [2500, 5000, 2500], [[0, 1, 2], [0, 1, 3]]), + [5] = new PriorityGridDefinition(2, 3, [5000, 5000], [2500, 5000, 2500], [[0, 1, 2], [3, 1, 4]]), + [6] = new PriorityGridDefinition(3, 3, [3333, 3334, 3333], [2500, 5000, 2500], [[0, 1, 2], [0, 1, 3], [4, 1, 5]]), + [7] = new PriorityGridDefinition(3, 3, [3333, 3334, 3333], [2500, 5000, 2500], [[0, 1, 2], [3, 1, 4], [5, 1, 6]]), + [8] = new PriorityGridDefinition(3, 4, [3333, 3334, 3333], [2500, 2500, 2500, 2500], [[0, 1, 2, 3], [4, 1, 2, 5], [6, 1, 2, 7]]), + [9] = new PriorityGridDefinition(3, 4, [3333, 3334, 3333], [2500, 2500, 2500, 2500], [[0, 1, 2, 3], [4, 1, 2, 5], [6, 1, 7, 8]]), + [10] = new PriorityGridDefinition(3, 4, [3333, 3334, 3333], [2500, 2500, 2500, 2500], [[0, 1, 2, 3], [4, 1, 5, 6], [7, 1, 8, 9]]), + [11] = new PriorityGridDefinition(3, 4, [3333, 3334, 3333], [2500, 2500, 2500, 2500], [[0, 1, 2, 3], [4, 1, 5, 6], [7, 8, 9, 10]]), + }; +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesVirtualDesktop.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesVirtualDesktop.cs new file mode 100644 index 0000000000..274b6ef5c7 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/FancyZonesVirtualDesktop.cs @@ -0,0 +1,103 @@ +// 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 Microsoft.Win32; + +namespace PowerToysExtension.Helpers; + +internal static class FancyZonesVirtualDesktop +{ + private const string VirtualDesktopsKey = @"Software\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops"; + private const string SessionVirtualDesktopsKeyPrefix = @"Software\Microsoft\Windows\CurrentVersion\Explorer\SessionInfo\"; + private const string SessionVirtualDesktopsKeySuffix = @"\VirtualDesktops"; + private const string CurrentVirtualDesktopValue = "CurrentVirtualDesktop"; + private const string VirtualDesktopIdsValue = "VirtualDesktopIDs"; + + public static string GetCurrentVirtualDesktopIdString() + { + var id = TryGetCurrentVirtualDesktopId() + ?? TryGetCurrentVirtualDesktopIdFromSession() + ?? TryGetFirstVirtualDesktopId() + ?? Guid.Empty; + + return "{" + id.ToString().ToUpperInvariant() + "}"; + } + + private static Guid? TryGetCurrentVirtualDesktopId() + { + try + { + using var key = Registry.CurrentUser.OpenSubKey(VirtualDesktopsKey, writable: false); + var bytes = key?.GetValue(CurrentVirtualDesktopValue) as byte[]; + return TryGetGuid(bytes); + } + catch + { + return null; + } + } + + private static Guid? TryGetCurrentVirtualDesktopIdFromSession() + { + try + { + if (!ProcessIdToSessionId((uint)Environment.ProcessId, out var sessionId)) + { + return null; + } + + var path = SessionVirtualDesktopsKeyPrefix + sessionId + SessionVirtualDesktopsKeySuffix; + using var key = Registry.CurrentUser.OpenSubKey(path, writable: false); + var bytes = key?.GetValue(CurrentVirtualDesktopValue) as byte[]; + return TryGetGuid(bytes); + } + catch + { + return null; + } + } + + private static Guid? TryGetFirstVirtualDesktopId() + { + try + { + using var key = Registry.CurrentUser.OpenSubKey(VirtualDesktopsKey, writable: false); + var bytes = key?.GetValue(VirtualDesktopIdsValue) as byte[]; + if (bytes is null || bytes.Length < 16) + { + return null; + } + + var first = new byte[16]; + Array.Copy(bytes, 0, first, 0, 16); + return TryGetGuid(first); + } + catch + { + return null; + } + } + + private static Guid? TryGetGuid(byte[]? bytes) + { + try + { + if (bytes is null || bytes.Length < 16) + { + return null; + } + + return new Guid(bytes.AsSpan(0, 16)); + } + catch + { + return null; + } + } + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool ProcessIdToSessionId(uint dwProcessId, out uint pSessionId); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/GpoEnablementService.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/GpoEnablementService.cs new file mode 100644 index 0000000000..244870ebe5 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/GpoEnablementService.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Win32; + +namespace PowerToysExtension.Helpers; + +internal enum GpoRuleConfiguredValue +{ + WrongValue = -3, + Unavailable = -2, + NotConfigured = -1, + Disabled = 0, + Enabled = 1, +} + +/// +/// Lightweight GPO reader for module/feature enablement policies. +/// Mirrors the logic in src/common/utils/gpo.h but avoids taking a dependency on the full GPOWrapper. +/// +internal static class GpoEnablementService +{ + private const string PoliciesPath = @"SOFTWARE\Policies\PowerToys"; + private const string PolicyConfigureEnabledGlobalAllUtilities = "ConfigureGlobalUtilityEnabledState"; + + internal static GpoRuleConfiguredValue GetUtilityEnabledValue(string individualPolicyValueName) + { + if (!string.IsNullOrEmpty(individualPolicyValueName)) + { + var individual = GetConfiguredValue(individualPolicyValueName); + if (individual is GpoRuleConfiguredValue.Disabled or GpoRuleConfiguredValue.Enabled) + { + return individual; + } + } + + return GetConfiguredValue(PolicyConfigureEnabledGlobalAllUtilities); + } + + private static GpoRuleConfiguredValue GetConfiguredValue(string registryValueName) + { + try + { + // Machine scope has priority over user scope. + var value = ReadRegistryValue(Registry.LocalMachine, registryValueName); + value ??= ReadRegistryValue(Registry.CurrentUser, registryValueName); + + if (!value.HasValue) + { + return GpoRuleConfiguredValue.NotConfigured; + } + + return value.Value switch + { + 0 => GpoRuleConfiguredValue.Disabled, + 1 => GpoRuleConfiguredValue.Enabled, + _ => GpoRuleConfiguredValue.WrongValue, + }; + } + catch + { + return GpoRuleConfiguredValue.Unavailable; + } + } + + private static int? ReadRegistryValue(RegistryKey rootKey, string valueName) + { + try + { + using var key = rootKey.OpenSubKey(PoliciesPath, writable: false); + if (key is null) + { + return null; + } + + var value = key.GetValue(valueName); + return value as int?; + } + catch + { + return null; + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ModuleCommandCatalog.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ModuleCommandCatalog.cs new file mode 100644 index 0000000000..679c94bde0 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ModuleCommandCatalog.cs @@ -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.Collections.Generic; +using System.Linq; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Modules; + +namespace PowerToysExtension.Helpers; + +/// +/// Aggregates commands exposed by individual module providers and applies fuzzy filtering. +/// +internal static class ModuleCommandCatalog +{ + private static readonly ModuleCommandProvider[] Providers = + [ + new AwakeModuleCommandProvider(), + new AdvancedPasteModuleCommandProvider(), + new WorkspacesModuleCommandProvider(), + new LightSwitchModuleCommandProvider(), + new PowerToysRunModuleCommandProvider(), + new ScreenRulerModuleCommandProvider(), + new ShortcutGuideModuleCommandProvider(), + new TextExtractorModuleCommandProvider(), + new ZoomItModuleCommandProvider(), + new ColorPickerModuleCommandProvider(), + new AlwaysOnTopModuleCommandProvider(), + new CropAndLockModuleCommandProvider(), + new FancyZonesModuleCommandProvider(), + new KeyboardManagerModuleCommandProvider(), + new MouseUtilsModuleCommandProvider(), + new MouseWithoutBordersModuleCommandProvider(), + new QuickAccentModuleCommandProvider(), + new FileExplorerAddonsModuleCommandProvider(), + new FileLocksmithModuleCommandProvider(), + new ImageResizerModuleCommandProvider(), + new NewPlusModuleCommandProvider(), + new PeekModuleCommandProvider(), + new PowerRenameModuleCommandProvider(), + new CommandNotFoundModuleCommandProvider(), + new EnvironmentVariablesModuleCommandProvider(), + new HostsModuleCommandProvider(), + new RegistryPreviewModuleCommandProvider(), + ]; + + public static IListItem[] GetAllItems() + { + return [.. Providers.SelectMany(provider => provider.BuildCommands())]; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ModuleEnablementService.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ModuleEnablementService.cs new file mode 100644 index 0000000000..fccf5b8687 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ModuleEnablementService.cs @@ -0,0 +1,162 @@ +// 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.Text.Json; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Helpers; + +/// +/// Reads PowerToys module enablement flags from the global settings.json. +/// +internal static class ModuleEnablementService +{ + internal static string SettingsFilePath { get; } = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Microsoft", + "PowerToys", + "settings.json"); + + internal static bool IsModuleEnabled(SettingsWindow module) + { + var key = GetEnabledKey(module); + if (string.IsNullOrEmpty(key)) + { + var globalRule = GpoEnablementService.GetUtilityEnabledValue(string.Empty); + return globalRule != GpoRuleConfiguredValue.Disabled; + } + + return IsKeyEnabled(key); + } + + internal static bool IsKeyEnabled(string enabledKey) + { + if (string.IsNullOrWhiteSpace(enabledKey)) + { + return true; + } + + var gpoPolicy = GetGpoPolicyForEnabledKey(enabledKey); + var gpoRule = GpoEnablementService.GetUtilityEnabledValue(gpoPolicy); + if (gpoRule == GpoRuleConfiguredValue.Disabled) + { + return false; + } + + if (gpoRule == GpoRuleConfiguredValue.Enabled) + { + return true; + } + + try + { + var enabled = ReadEnabledFlags(); + return enabled is null || !enabled.TryGetValue(enabledKey, out var value) || value; + } + catch + { + return true; + } + } + + private static Dictionary? ReadEnabledFlags() + { + if (!File.Exists(SettingsFilePath)) + { + return null; + } + + var json = File.ReadAllText(SettingsFilePath).Trim('\0'); + using var doc = JsonDocument.Parse(json); + + if (!doc.RootElement.TryGetProperty("enabled", out var enabledRoot) || + enabledRoot.ValueKind != JsonValueKind.Object) + { + return null; + } + + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var prop in enabledRoot.EnumerateObject()) + { + if (prop.Value.ValueKind is JsonValueKind.True or JsonValueKind.False) + { + result[prop.Name] = prop.Value.GetBoolean(); + } + } + + return result; + } + + private static string GetEnabledKey(SettingsWindow module) => module switch + { + SettingsWindow.Awake => "Awake", + SettingsWindow.AdvancedPaste => "AdvancedPaste", + SettingsWindow.AlwaysOnTop => "AlwaysOnTop", + SettingsWindow.ColorPicker => "ColorPicker", + SettingsWindow.CropAndLock => "CropAndLock", + SettingsWindow.EnvironmentVariables => "EnvironmentVariables", + SettingsWindow.FancyZones => "FancyZones", + SettingsWindow.FileExplorer => "File Explorer Preview", + SettingsWindow.FileLocksmith => "FileLocksmith", + SettingsWindow.Hosts => "Hosts", + SettingsWindow.ImageResizer => "Image Resizer", + SettingsWindow.KBM => "Keyboard Manager", + SettingsWindow.LightSwitch => "LightSwitch", + SettingsWindow.MeasureTool => "Measure Tool", + SettingsWindow.MouseWithoutBorders => "MouseWithoutBorders", + SettingsWindow.NewPlus => "NewPlus", + SettingsWindow.Peek => "Peek", + SettingsWindow.PowerAccent => "QuickAccent", + SettingsWindow.PowerLauncher => "PowerToys Run", + SettingsWindow.Run => "PowerToys Run", + SettingsWindow.PowerRename => "PowerRename", + SettingsWindow.PowerOCR => "TextExtractor", + SettingsWindow.RegistryPreview => "RegistryPreview", + SettingsWindow.ShortcutGuide => "Shortcut Guide", + SettingsWindow.Workspaces => "Workspaces", + SettingsWindow.ZoomIt => "ZoomIt", + SettingsWindow.CmdNotFound => "CmdNotFound", + SettingsWindow.CmdPal => "CmdPal", + _ => string.Empty, + }; + + private static string GetGpoPolicyForEnabledKey(string enabledKey) => enabledKey switch + { + "AdvancedPaste" => "ConfigureEnabledUtilityAdvancedPaste", + "AlwaysOnTop" => "ConfigureEnabledUtilityAlwaysOnTop", + "Awake" => "ConfigureEnabledUtilityAwake", + "CmdNotFound" => "ConfigureEnabledUtilityCmdNotFound", + "CmdPal" => "ConfigureEnabledUtilityCmdPal", + "ColorPicker" => "ConfigureEnabledUtilityColorPicker", + "CropAndLock" => "ConfigureEnabledUtilityCropAndLock", + "CursorWrap" => "ConfigureEnabledUtilityCursorWrap", + "EnvironmentVariables" => "ConfigureEnabledUtilityEnvironmentVariables", + "FancyZones" => "ConfigureEnabledUtilityFancyZones", + "FileLocksmith" => "ConfigureEnabledUtilityFileLocksmith", + "FindMyMouse" => "ConfigureEnabledUtilityFindMyMouse", + "Hosts" => "ConfigureEnabledUtilityHostsFileEditor", + "Image Resizer" => "ConfigureEnabledUtilityImageResizer", + "Keyboard Manager" => "ConfigureEnabledUtilityKeyboardManager", + "LightSwitch" => "ConfigureEnabledUtilityLightSwitch", + "Measure Tool" => "ConfigureEnabledUtilityScreenRuler", + "MouseHighlighter" => "ConfigureEnabledUtilityMouseHighlighter", + "MouseJump" => "ConfigureEnabledUtilityMouseJump", + "MousePointerCrosshairs" => "ConfigureEnabledUtilityMousePointerCrosshairs", + "MouseWithoutBorders" => "ConfigureEnabledUtilityMouseWithoutBorders", + "NewPlus" => "ConfigureEnabledUtilityNewPlus", + "Peek" => "ConfigureEnabledUtilityPeek", + "PowerRename" => "ConfigureEnabledUtilityPowerRename", + "PowerToys Run" => "ConfigureEnabledUtilityPowerLauncher", + "QuickAccent" => "ConfigureEnabledUtilityQuickAccent", + "RegistryPreview" => "ConfigureEnabledUtilityRegistryPreview", + "Shortcut Guide" => "ConfigureEnabledUtilityShortcutGuide", + "TextExtractor" => "ConfigureEnabledUtilityTextExtractor", + "Workspaces" => "ConfigureEnabledUtilityWorkspaces", + "ZoomIt" => "ConfigureEnabledUtilityZoomIt", + _ => string.Empty, + }; +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysFallbackCommandItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysFallbackCommandItem.cs new file mode 100644 index 0000000000..7ce4d2b27b --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysFallbackCommandItem.cs @@ -0,0 +1,88 @@ +// 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 Common.Search.FuzzSearch; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace PowerToysExtension.Helpers; + +/// +/// A fallback item that filters itself based on the landing-page query. +/// It hides itself (empty Title) when the query doesn't fuzzy-match the title or subtitle. +/// +internal sealed partial class PowerToysFallbackCommandItem : FallbackCommandItem, IFallbackHandler +{ + private readonly string _baseTitle; + private readonly string _baseSubtitle; + private readonly string _baseName; + private readonly Command? _mutableCommand; + + public PowerToysFallbackCommandItem(ICommand command, string title, string subtitle, IIconInfo? icon, IContextItem[]? moreCommands) + : base(command, title) + { + _baseTitle = title ?? string.Empty; + _baseSubtitle = subtitle ?? string.Empty; + _baseName = command?.Name ?? string.Empty; + _mutableCommand = command as Command; + + // Start hidden; we only surface when the query matches + Title = string.Empty; + Subtitle = string.Empty; + if (_mutableCommand is not null) + { + _mutableCommand.Name = string.Empty; + } + + if (icon != null) + { + Icon = icon; + } + + MoreCommands = moreCommands ?? Array.Empty(); + + // Ensure fallback updates route to this instance + FallbackHandler = this; + } + + public override void UpdateQuery(string query) + { + if (string.IsNullOrWhiteSpace(query)) + { + Title = string.Empty; + Subtitle = string.Empty; + if (_mutableCommand is not null) + { + _mutableCommand.Name = string.Empty; + } + + return; + } + + // Simple fuzzy match against title/subtitle; hide if neither match + var titleMatch = Common.Search.FuzzSearch.StringMatcher.FuzzyMatch(query, _baseTitle); + var subtitleMatch = Common.Search.FuzzSearch.StringMatcher.FuzzyMatch(query, _baseSubtitle); + var matches = (titleMatch.Success && titleMatch.Score > 0) || (subtitleMatch.Success && subtitleMatch.Score > 0); + + if (matches) + { + Title = _baseTitle; + Subtitle = _baseSubtitle; + if (_mutableCommand is not null) + { + _mutableCommand.Name = _baseName; + } + } + else + { + Title = string.Empty; + Subtitle = string.Empty; + if (_mutableCommand is not null) + { + _mutableCommand.Name = string.Empty; + } + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysPathResolver.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysPathResolver.cs new file mode 100644 index 0000000000..d9ac9443fe --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysPathResolver.cs @@ -0,0 +1,161 @@ +// 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 Microsoft.Win32; + +namespace PowerToysExtension.Helpers; + +/// +/// Helper methods for locating the installed PowerToys binaries. +/// +internal static class PowerToysPathResolver +{ + private const string PowerToysProtocolKey = @"Software\Classes\powertoys"; + private const string PowerToysUserKey = @"Software\Microsoft\PowerToys"; + + internal static string GetPowerToysInstallPath() + { + var perUser = GetInstallPathFromRegistry(RegistryHive.CurrentUser); + if (!string.IsNullOrEmpty(perUser)) + { + return perUser; + } + + return GetInstallPathFromRegistry(RegistryHive.LocalMachine); + } + + internal static string TryResolveExecutable(string executableName) + { + if (string.IsNullOrEmpty(executableName)) + { + return string.Empty; + } + + var baseDirectory = GetPowerToysInstallPath(); + if (string.IsNullOrEmpty(baseDirectory)) + { + return string.Empty; + } + + var candidate = Path.Combine(baseDirectory, executableName); + return File.Exists(candidate) ? candidate : string.Empty; + } + + private static string GetInstallPathFromRegistry(RegistryHive hive) + { + try + { + using var baseKey = RegistryKey.OpenBaseKey(hive, RegistryView.Registry64); + + var protocolPath = GetPathFromProtocolRegistration(baseKey); + if (!string.IsNullOrEmpty(protocolPath)) + { + return protocolPath; + } + + if (hive == RegistryHive.CurrentUser) + { + var userPath = GetPathFromUserRegistration(baseKey); + if (!string.IsNullOrEmpty(userPath)) + { + return userPath; + } + } + } + catch + { + // Ignore registry access failures and fall back to other checks. + } + + return string.Empty; + } + + private static string GetPathFromProtocolRegistration(RegistryKey baseKey) + { + try + { + using var commandKey = baseKey.OpenSubKey($@"{PowerToysProtocolKey}\shell\open\command"); + if (commandKey == null) + { + return string.Empty; + } + + var command = commandKey.GetValue(string.Empty)?.ToString() ?? string.Empty; + if (string.IsNullOrEmpty(command)) + { + return string.Empty; + } + + return ExtractInstallDirectory(command); + } + catch + { + return string.Empty; + } + } + + private static string GetPathFromUserRegistration(RegistryKey baseKey) + { + try + { + using var userKey = baseKey.OpenSubKey(PowerToysUserKey); + if (userKey == null) + { + return string.Empty; + } + + var installedValue = userKey.GetValue("installed"); + if (installedValue != null && installedValue.ToString() == "1") + { + return GetPathFromProtocolRegistration(baseKey); + } + } + catch + { + // Ignore registry access failures. + } + + return string.Empty; + } + + private static string ExtractInstallDirectory(string command) + { + if (string.IsNullOrEmpty(command)) + { + return string.Empty; + } + + try + { + if (command.StartsWith('"')) + { + var closingQuote = command.IndexOf('"', 1); + if (closingQuote > 1) + { + var quotedPath = command.Substring(1, closingQuote - 1); + if (File.Exists(quotedPath)) + { + return Path.GetDirectoryName(quotedPath) ?? string.Empty; + } + } + } + else + { + var parts = command.Split(' '); + if (parts.Length > 0 && File.Exists(parts[0])) + { + return Path.GetDirectoryName(parts[0]) ?? string.Empty; + } + } + } + catch + { + // Fall through and report no path. + } + + return string.Empty; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysResourcesHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysResourcesHelper.cs new file mode 100644 index 0000000000..91dd3f05b1 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysResourcesHelper.cs @@ -0,0 +1,99 @@ +// 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.CommandPalette.Extensions.Toolkit; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Helpers; + +internal static class PowerToysResourcesHelper +{ + private const string SettingsIconRoot = "WinUI3Apps\\Assets\\Settings\\Icons\\"; + + internal static IconInfo IconFromSettingsIcon(string fileName) => IconHelpers.FromRelativePath($"{SettingsIconRoot}{fileName}"); + + public static IconInfo ProviderIcon() => IconFromSettingsIcon("PowerToys.png"); + + public static IconInfo ModuleIcon(this SettingsWindow module) + { + var iconFile = module switch + { + SettingsWindow.ColorPicker => "ColorPicker.png", + SettingsWindow.FancyZones => "FancyZones.png", + SettingsWindow.Hosts => "Hosts.png", + SettingsWindow.PowerOCR => "TextExtractor.png", + SettingsWindow.RegistryPreview => "RegistryPreview.png", + SettingsWindow.MeasureTool => "ScreenRuler.png", + SettingsWindow.ShortcutGuide => "ShortcutGuide.png", + SettingsWindow.CropAndLock => "CropAndLock.png", + SettingsWindow.EnvironmentVariables => "EnvironmentVariables.png", + SettingsWindow.Awake => "Awake.png", + SettingsWindow.PowerRename => "PowerRename.png", + SettingsWindow.Run => "PowerToysRun.png", + SettingsWindow.ImageResizer => "ImageResizer.png", + SettingsWindow.KBM => "KeyboardManager.png", + SettingsWindow.MouseUtils => "MouseUtils.png", + SettingsWindow.Workspaces => "Workspaces.png", + SettingsWindow.AdvancedPaste => "AdvancedPaste.png", + SettingsWindow.CmdPal => "CmdPal.png", + SettingsWindow.ZoomIt => "ZoomIt.png", + SettingsWindow.FileExplorer => "FileExplorerPreview.png", + SettingsWindow.FileLocksmith => "FileLocksmith.png", + SettingsWindow.NewPlus => "NewPlus.png", + SettingsWindow.Peek => "Peek.png", + SettingsWindow.LightSwitch => "LightSwitch.png", + SettingsWindow.AlwaysOnTop => "AlwaysOnTop.png", + SettingsWindow.CmdNotFound => "CommandNotFound.png", + SettingsWindow.MouseWithoutBorders => "MouseWithoutBorders.png", + SettingsWindow.PowerAccent => "QuickAccent.png", + SettingsWindow.PowerLauncher => "PowerToysRun.png", + SettingsWindow.PowerPreview => "FileExplorerPreview.png", + SettingsWindow.Overview => "PowerToys.png", + SettingsWindow.Dashboard => "PowerToys.png", + _ => "PowerToys.png", + }; + + return IconFromSettingsIcon(iconFile); + } + + public static string ModuleDisplayName(this SettingsWindow module) + { + return module switch + { + SettingsWindow.ColorPicker => "Color Picker", + SettingsWindow.FancyZones => "FancyZones", + SettingsWindow.Hosts => "Hosts File Editor", + SettingsWindow.PowerOCR => "Text Extractor", + SettingsWindow.RegistryPreview => "Registry Preview", + SettingsWindow.MeasureTool => "Screen Ruler", + SettingsWindow.ShortcutGuide => "Shortcut Guide", + SettingsWindow.CropAndLock => "Crop And Lock", + SettingsWindow.EnvironmentVariables => "Environment Variables", + SettingsWindow.Awake => "Awake", + SettingsWindow.PowerRename => "PowerRename", + SettingsWindow.Run => "PowerToys Run", + SettingsWindow.ImageResizer => "Image Resizer", + SettingsWindow.KBM => "Keyboard Manager", + SettingsWindow.MouseUtils => "Mouse Utilities", + SettingsWindow.Workspaces => "Workspaces", + SettingsWindow.AdvancedPaste => "Advanced Paste", + SettingsWindow.CmdPal => "Command Palette", + SettingsWindow.ZoomIt => "ZoomIt", + SettingsWindow.FileExplorer => "File Explorer Add-ons", + SettingsWindow.FileLocksmith => "File Locksmith", + SettingsWindow.NewPlus => "New+", + SettingsWindow.Peek => "Peek", + SettingsWindow.LightSwitch => "Light Switch", + SettingsWindow.AlwaysOnTop => "Always On Top", + SettingsWindow.CmdNotFound => "Command Not Found", + SettingsWindow.MouseWithoutBorders => "Mouse Without Borders", + SettingsWindow.PowerAccent => "Quick Accent", + SettingsWindow.Overview => "General", + SettingsWindow.Dashboard => "Dashboard", + SettingsWindow.PowerLauncher => "PowerToys Run", + SettingsWindow.PowerPreview => "File Explorer Add-ons", + _ => module.ToString(), + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/SettingsChangeNotifier.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/SettingsChangeNotifier.cs new file mode 100644 index 0000000000..c271bc853b --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/SettingsChangeNotifier.cs @@ -0,0 +1,72 @@ +// 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.Threading; +using ManagedCommon; + +namespace PowerToysExtension.Helpers; + +/// +/// Watches the global PowerToys settings.json and notifies listeners when it changes. +/// +internal static class SettingsChangeNotifier +{ + private static readonly object Sync = new(); + private static FileSystemWatcher? _watcher; + private static Timer? _debounceTimer; + + internal static event Action? SettingsChanged; + + static SettingsChangeNotifier() + { + TryStartWatcher(); + } + + private static void TryStartWatcher() + { + try + { + var filePath = ModuleEnablementService.SettingsFilePath; + var directory = Path.GetDirectoryName(filePath); + var fileName = Path.GetFileName(filePath); + + if (string.IsNullOrEmpty(directory) || string.IsNullOrEmpty(fileName)) + { + return; + } + + _watcher = new FileSystemWatcher(directory) + { + Filter = fileName, + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size, + IncludeSubdirectories = false, + EnableRaisingEvents = true, + }; + + _watcher.Changed += (_, _) => ScheduleRaise(); + _watcher.Created += (_, _) => ScheduleRaise(); + _watcher.Deleted += (_, _) => ScheduleRaise(); + _watcher.Renamed += (_, _) => ScheduleRaise(); + } + catch (Exception ex) + { + Logger.LogError($"SettingsChangeNotifier failed to start: {ex.Message}"); + } + } + + private static void ScheduleRaise() + { + lock (Sync) + { + _debounceTimer?.Dispose(); + _debounceTimer = new Timer( + _ => SettingsChanged?.Invoke(), + null, + 200, + Timeout.Infinite); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj new file mode 100644 index 0000000000..36cfeb93f4 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj @@ -0,0 +1,72 @@ + + + + + + + WinExe + PowerToysExtension + app.manifest + win-$(Platform).pubxml + false + None + false + $(SolutionDir)$(Platform)\$(Configuration)\ + false + false + enable + true + true + $(CmdPalVersion) + + + + win-arm64 + win-x64 + + + + + PreserveNewest + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + true + + + + + + + + + + + + + + + + true + true + false + true + false + + diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AdvancedPasteModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AdvancedPasteModuleCommandProvider.cs new file mode 100644 index 0000000000..58083919c0 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AdvancedPasteModuleCommandProvider.cs @@ -0,0 +1,38 @@ +// 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 Common.UI; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; + +namespace PowerToysExtension.Modules; + +internal sealed class AdvancedPasteModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var module = SettingsDeepLink.SettingsWindow.AdvancedPaste; + var title = module.ModuleDisplayName(); + var icon = module.ModuleIcon(); + + if (ModuleEnablementService.IsModuleEnabled(module)) + { + yield return new ListItem(new OpenAdvancedPasteCommand()) + { + Title = "Open Advanced Paste", + Subtitle = "Launch the Advanced Paste UI", + Icon = icon, + }; + } + + yield return new ListItem(new OpenInSettingsCommand(module, title)) + { + Title = title, + Subtitle = "Open Advanced Paste settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AlwaysOnTopModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AlwaysOnTopModuleCommandProvider.cs new file mode 100644 index 0000000000..cad8a282da --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AlwaysOnTopModuleCommandProvider.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class AlwaysOnTopModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var title = SettingsWindow.AlwaysOnTop.ModuleDisplayName(); + var icon = SettingsWindow.AlwaysOnTop.ModuleIcon(); + + yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.AlwaysOnTop, title)) + { + Title = title, + Subtitle = "Open Always On Top settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AwakeModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AwakeModuleCommandProvider.cs new file mode 100644 index 0000000000..935371fba4 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/AwakeModuleCommandProvider.cs @@ -0,0 +1,91 @@ +// 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 Awake.ModuleServices; +using Common.UI; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using PowerToysExtension.Pages; + +namespace PowerToysExtension.Modules; + +internal sealed class AwakeModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var items = new List(); + var module = SettingsDeepLink.SettingsWindow.Awake; + var title = module.ModuleDisplayName(); + var icon = PowerToysResourcesHelper.IconFromSettingsIcon("Awake.png"); + var moduleIcon = module.ModuleIcon(); + + items.Add(new ListItem(new OpenInSettingsCommand(module, title)) + { + Title = title, + Subtitle = "Open Awake settings", + Icon = moduleIcon, + }); + + if (!ModuleEnablementService.IsModuleEnabled(module)) + { + return items; + } + + // Direct commands surfaced in the PowerToys list page. + ListItem? statusItem = null; + Action refreshStatus = () => + { + if (statusItem is not null) + { + statusItem.Subtitle = AwakeStatusService.GetStatusSubtitle(); + } + }; + + var refreshCommand = new RefreshAwakeStatusCommand(refreshStatus); + + statusItem = new ListItem(new CommandItem(refreshCommand)) + { + Title = "Awake: Current status", + Subtitle = AwakeStatusService.GetStatusSubtitle(), + Icon = icon, + }; + items.Add(statusItem); + + items.Add(new ListItem(new StartAwakeCommand("Awake: Keep awake indefinitely", () => AwakeService.Instance.SetIndefiniteAsync(), "Awake set to indefinite", refreshStatus)) + { + Title = "Awake: Keep awake indefinitely", + Subtitle = "Run Awake in indefinite mode", + Icon = icon, + }); + items.Add(new ListItem(new StartAwakeCommand("Awake: Keep awake for 30 minutes", () => AwakeService.Instance.SetTimedAsync(30), "Awake set for 30 minutes", refreshStatus)) + { + Title = "Awake: Keep awake for 30 minutes", + Subtitle = "Run Awake timed for 30 minutes", + Icon = icon, + }); + items.Add(new ListItem(new StartAwakeCommand("Awake: Keep awake for 1 hour", () => AwakeService.Instance.SetTimedAsync(60), "Awake set for 1 hour", refreshStatus)) + { + Title = "Awake: Keep awake for 1 hour", + Subtitle = "Run Awake timed for 1 hour", + Icon = icon, + }); + items.Add(new ListItem(new StartAwakeCommand("Awake: Keep awake for 2 hours", () => AwakeService.Instance.SetTimedAsync(120), "Awake set for 2 hours", refreshStatus)) + { + Title = "Awake: Keep awake for 2 hours", + Subtitle = "Run Awake timed for 2 hours", + Icon = icon, + }); + items.Add(new ListItem(new StopAwakeCommand(refreshStatus)) + { + Title = "Awake: Turn off", + Subtitle = "Switch Awake back to Off", + Icon = icon, + }); + + return items; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ColorPickerModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ColorPickerModuleCommandProvider.cs new file mode 100644 index 0000000000..27b3be6f05 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ColorPickerModuleCommandProvider.cs @@ -0,0 +1,53 @@ +// 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 Common.UI; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using PowerToysExtension.Pages; + +namespace PowerToysExtension.Modules; + +internal sealed class ColorPickerModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var module = SettingsDeepLink.SettingsWindow.ColorPicker; + var title = module.ModuleDisplayName(); + var icon = module.ModuleIcon(); + + var commands = new List(); + + commands.Add(new ListItem(new OpenInSettingsCommand(module, title)) + { + Title = title, + Subtitle = "Open Color Picker settings", + Icon = icon, + }); + + if (!ModuleEnablementService.IsModuleEnabled(module)) + { + return commands; + } + + // Direct entries in the module list. + commands.Add(new ListItem(new OpenColorPickerCommand()) + { + Title = "Open Color Picker", + Subtitle = "Start a color pick session", + Icon = icon, + }); + + commands.Add(new ListItem(new CommandItem(new ColorPickerSavedColorsPage())) + { + Title = "Saved colors", + Subtitle = "Browse and copy saved colors", + Icon = icon, + }); + + return commands; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CommandNotFoundModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CommandNotFoundModuleCommandProvider.cs new file mode 100644 index 0000000000..2ec95172f9 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CommandNotFoundModuleCommandProvider.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class CommandNotFoundModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var title = SettingsWindow.CmdNotFound.ModuleDisplayName(); + var icon = SettingsWindow.CmdNotFound.ModuleIcon(); + + yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.CmdNotFound, title)) + { + Title = title, + Subtitle = "Open Command Not Found settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CropAndLockModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CropAndLockModuleCommandProvider.cs new file mode 100644 index 0000000000..c3f6d1ccd4 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CropAndLockModuleCommandProvider.cs @@ -0,0 +1,45 @@ +// 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 Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class CropAndLockModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var module = SettingsWindow.CropAndLock; + var title = module.ModuleDisplayName(); + var icon = module.ModuleIcon(); + + if (ModuleEnablementService.IsModuleEnabled(module)) + { + yield return new ListItem(new CropAndLockReparentCommand()) + { + Title = "Crop and Lock (Reparent)", + Subtitle = "Create a cropped reparented window", + Icon = icon, + }; + + yield return new ListItem(new CropAndLockThumbnailCommand()) + { + Title = "Crop and Lock (Thumbnail)", + Subtitle = "Create a cropped thumbnail window", + Icon = icon, + }; + } + + yield return new ListItem(new OpenInSettingsCommand(module, title)) + { + Title = title, + Subtitle = "Open Crop and Lock settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/EnvironmentVariablesModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/EnvironmentVariablesModuleCommandProvider.cs new file mode 100644 index 0000000000..d72644c1bf --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/EnvironmentVariablesModuleCommandProvider.cs @@ -0,0 +1,45 @@ +// 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 Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class EnvironmentVariablesModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var module = SettingsWindow.EnvironmentVariables; + var title = module.ModuleDisplayName(); + var icon = module.ModuleIcon(); + + if (ModuleEnablementService.IsModuleEnabled(module)) + { + yield return new ListItem(new OpenEnvironmentVariablesCommand()) + { + Title = "Open Environment Variables", + Subtitle = "Launch Environment Variables editor", + Icon = icon, + }; + + yield return new ListItem(new OpenEnvironmentVariablesAdminCommand()) + { + Title = "Open Environment Variables (Admin)", + Subtitle = "Launch Environment Variables editor as admin", + Icon = icon, + }; + } + + yield return new ListItem(new OpenInSettingsCommand(module, title)) + { + Title = title, + Subtitle = "Open Environment Variables settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FancyZonesModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FancyZonesModuleCommandProvider.cs new file mode 100644 index 0000000000..6a4287d60f --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FancyZonesModuleCommandProvider.cs @@ -0,0 +1,53 @@ +// 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 Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using PowerToysExtension.Pages; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class FancyZonesModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var module = SettingsWindow.FancyZones; + var title = module.ModuleDisplayName(); + var icon = module.ModuleIcon(); + + if (ModuleEnablementService.IsModuleEnabled(module)) + { + yield return new ListItem(new CommandItem(new FancyZonesLayoutsPage())) + { + Title = "FancyZones: Layouts", + Subtitle = "Apply a layout to all monitors or a specific monitor", + Icon = icon, + }; + + yield return new ListItem(new CommandItem(new FancyZonesMonitorsPage())) + { + Title = "FancyZones: Monitors", + Subtitle = "Identify monitors and apply layouts", + Icon = icon, + }; + + yield return new ListItem(new OpenFancyZonesEditorCommand()) + { + Title = "Open FancyZones Editor", + Subtitle = "Launch layout editor", + Icon = icon, + }; + } + + yield return new ListItem(new OpenInSettingsCommand(module, title)) + { + Title = title, + Subtitle = "Open FancyZones settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FileExplorerAddonsModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FileExplorerAddonsModuleCommandProvider.cs new file mode 100644 index 0000000000..5fa5162cf8 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FileExplorerAddonsModuleCommandProvider.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class FileExplorerAddonsModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var title = SettingsWindow.FileExplorer.ModuleDisplayName(); + var icon = SettingsWindow.FileExplorer.ModuleIcon(); + + yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.FileExplorer, title)) + { + Title = title, + Subtitle = "Open File Explorer add-ons settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FileLocksmithModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FileLocksmithModuleCommandProvider.cs new file mode 100644 index 0000000000..19e5a135e5 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/FileLocksmithModuleCommandProvider.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class FileLocksmithModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var title = SettingsWindow.FileLocksmith.ModuleDisplayName(); + var icon = SettingsWindow.FileLocksmith.ModuleIcon(); + + yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.FileLocksmith, title)) + { + Title = title, + Subtitle = "Open File Locksmith settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/HostsModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/HostsModuleCommandProvider.cs new file mode 100644 index 0000000000..839598b428 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/HostsModuleCommandProvider.cs @@ -0,0 +1,45 @@ +// 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 Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class HostsModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var module = SettingsWindow.Hosts; + var title = module.ModuleDisplayName(); + var icon = module.ModuleIcon(); + + if (ModuleEnablementService.IsModuleEnabled(module)) + { + yield return new ListItem(new OpenHostsEditorCommand()) + { + Title = "Open Hosts File Editor", + Subtitle = "Launch Hosts File Editor", + Icon = icon, + }; + + yield return new ListItem(new OpenHostsEditorAdminCommand()) + { + Title = "Open Hosts File Editor (Admin)", + Subtitle = "Launch Hosts File Editor as admin", + Icon = icon, + }; + } + + yield return new ListItem(new OpenInSettingsCommand(module, title)) + { + Title = title, + Subtitle = "Open Hosts File Editor settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ImageResizerModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ImageResizerModuleCommandProvider.cs new file mode 100644 index 0000000000..6ec2f1b3a6 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ImageResizerModuleCommandProvider.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class ImageResizerModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var title = SettingsWindow.ImageResizer.ModuleDisplayName(); + var icon = SettingsWindow.ImageResizer.ModuleIcon(); + + yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.ImageResizer, title)) + { + Title = title, + Subtitle = "Open Image Resizer settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/KeyboardManagerModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/KeyboardManagerModuleCommandProvider.cs new file mode 100644 index 0000000000..bb42f484a7 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/KeyboardManagerModuleCommandProvider.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var title = SettingsWindow.KBM.ModuleDisplayName(); + var icon = SettingsWindow.KBM.ModuleIcon(); + + yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.KBM, title)) + { + Title = title, + Subtitle = "Open Keyboard Manager settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/LightSwitchModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/LightSwitchModuleCommandProvider.cs new file mode 100644 index 0000000000..a07c06fedb --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/LightSwitchModuleCommandProvider.cs @@ -0,0 +1,42 @@ +// 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 Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class LightSwitchModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var module = SettingsWindow.LightSwitch; + var title = module.ModuleDisplayName(); + var icon = module.ModuleIcon(); + + var items = new List(); + + if (ModuleEnablementService.IsModuleEnabled(module)) + { + items.Add(new ListItem(new ToggleLightSwitchCommand()) + { + Title = "Toggle Light Switch", + Subtitle = "Toggle system/apps theme immediately", + Icon = icon, + }); + } + + items.Add(new ListItem(new OpenInSettingsCommand(module, title)) + { + Title = title, + Subtitle = "Open Light Switch settings", + Icon = icon, + }); + + return items; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ModuleCommandProvider.cs new file mode 100644 index 0000000000..4e06731a2d --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ModuleCommandProvider.cs @@ -0,0 +1,16 @@ +// 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 Microsoft.CommandPalette.Extensions.Toolkit; + +namespace PowerToysExtension.Modules; + +/// +/// Base contract for a PowerToys module to expose its command palette entries. +/// +internal abstract class ModuleCommandProvider +{ + public abstract IEnumerable BuildCommands(); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseUtilsModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseUtilsModuleCommandProvider.cs new file mode 100644 index 0000000000..34c6be193e --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseUtilsModuleCommandProvider.cs @@ -0,0 +1,78 @@ +// 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 Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class MouseUtilsModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var module = SettingsWindow.MouseUtils; + var title = module.ModuleDisplayName(); + var icon = module.ModuleIcon(); + + if (ModuleEnablementService.IsKeyEnabled("FindMyMouse")) + { + yield return new ListItem(new ToggleFindMyMouseCommand()) + { + Title = "Trigger Find My Mouse", + Subtitle = "Focus the mouse pointer", + Icon = icon, + }; + } + + if (ModuleEnablementService.IsKeyEnabled("MouseHighlighter")) + { + yield return new ListItem(new ToggleMouseHighlighterCommand()) + { + Title = "Toggle Mouse Highlighter", + Subtitle = "Highlight mouse clicks", + Icon = icon, + }; + } + + if (ModuleEnablementService.IsKeyEnabled("MousePointerCrosshairs")) + { + yield return new ListItem(new ToggleMouseCrosshairsCommand()) + { + Title = "Toggle Mouse Crosshairs", + Subtitle = "Enable or disable pointer crosshairs", + Icon = icon, + }; + } + + if (ModuleEnablementService.IsKeyEnabled("CursorWrap")) + { + yield return new ListItem(new ToggleCursorWrapCommand()) + { + Title = "Toggle Cursor Wrap", + Subtitle = "Wrap the cursor across monitor edges", + Icon = icon, + }; + } + + if (ModuleEnablementService.IsKeyEnabled("MouseJump")) + { + yield return new ListItem(new ShowMouseJumpPreviewCommand()) + { + Title = "Show Mouse Jump Preview", + Subtitle = "Jump the pointer to a target", + Icon = icon, + }; + } + + yield return new ListItem(new OpenInSettingsCommand(module, title)) + { + Title = title, + Subtitle = "Open Mouse Utilities settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseWithoutBordersModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseWithoutBordersModuleCommandProvider.cs new file mode 100644 index 0000000000..49a3f3635a --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseWithoutBordersModuleCommandProvider.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class MouseWithoutBordersModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var title = SettingsWindow.MouseWithoutBorders.ModuleDisplayName(); + var icon = SettingsWindow.MouseWithoutBorders.ModuleIcon(); + + yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.MouseWithoutBorders, title)) + { + Title = title, + Subtitle = "Open Mouse Without Borders settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/NewPlusModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/NewPlusModuleCommandProvider.cs new file mode 100644 index 0000000000..f88d104b73 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/NewPlusModuleCommandProvider.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class NewPlusModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var title = SettingsWindow.NewPlus.ModuleDisplayName(); + var icon = SettingsWindow.NewPlus.ModuleIcon(); + + yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.NewPlus, title)) + { + Title = title, + Subtitle = "Open New+ settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PeekModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PeekModuleCommandProvider.cs new file mode 100644 index 0000000000..a55a187206 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PeekModuleCommandProvider.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class PeekModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var title = SettingsWindow.Peek.ModuleDisplayName(); + var icon = SettingsWindow.Peek.ModuleIcon(); + + yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.Peek, title)) + { + Title = title, + Subtitle = "Open Peek settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PowerRenameModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PowerRenameModuleCommandProvider.cs new file mode 100644 index 0000000000..434a1d53cf --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PowerRenameModuleCommandProvider.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class PowerRenameModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var title = SettingsWindow.PowerRename.ModuleDisplayName(); + var icon = SettingsWindow.PowerRename.ModuleIcon(); + + yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.PowerRename, title)) + { + Title = title, + Subtitle = "Open PowerRename settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PowerToysRunModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PowerToysRunModuleCommandProvider.cs new file mode 100644 index 0000000000..593bebb3a9 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/PowerToysRunModuleCommandProvider.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class PowerToysRunModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var title = SettingsWindow.PowerLauncher.ModuleDisplayName(); + var icon = SettingsWindow.PowerLauncher.ModuleIcon(); + + yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.PowerLauncher, title)) + { + Title = title, + Subtitle = "Open PowerToys Run settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/QuickAccentModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/QuickAccentModuleCommandProvider.cs new file mode 100644 index 0000000000..9122b3534c --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/QuickAccentModuleCommandProvider.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class QuickAccentModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var title = SettingsWindow.PowerAccent.ModuleDisplayName(); + var icon = SettingsWindow.PowerAccent.ModuleIcon(); + + yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.PowerAccent, title)) + { + Title = title, + Subtitle = "Open Quick Accent settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/RegistryPreviewModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/RegistryPreviewModuleCommandProvider.cs new file mode 100644 index 0000000000..7dbe3f841b --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/RegistryPreviewModuleCommandProvider.cs @@ -0,0 +1,38 @@ +// 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 Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class RegistryPreviewModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var module = SettingsWindow.RegistryPreview; + var title = module.ModuleDisplayName(); + var icon = module.ModuleIcon(); + + if (ModuleEnablementService.IsModuleEnabled(module)) + { + yield return new ListItem(new OpenRegistryPreviewCommand()) + { + Title = "Open Registry Preview", + Subtitle = "Launch Registry Preview", + Icon = icon, + }; + } + + yield return new ListItem(new OpenInSettingsCommand(module, title)) + { + Title = title, + Subtitle = "Open Registry Preview settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ScreenRulerModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ScreenRulerModuleCommandProvider.cs new file mode 100644 index 0000000000..23674c3dfe --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ScreenRulerModuleCommandProvider.cs @@ -0,0 +1,38 @@ +// 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 Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class ScreenRulerModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var module = SettingsWindow.MeasureTool; + var title = module.ModuleDisplayName(); + var icon = module.ModuleIcon(); + + if (ModuleEnablementService.IsModuleEnabled(module)) + { + yield return new ListItem(new ToggleScreenRulerCommand()) + { + Title = "Toggle Screen Ruler", + Subtitle = "Start or close Screen Ruler", + Icon = icon, + }; + } + + yield return new ListItem(new OpenInSettingsCommand(module, title)) + { + Title = title, + Subtitle = "Open Screen Ruler settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ShortcutGuideModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ShortcutGuideModuleCommandProvider.cs new file mode 100644 index 0000000000..20f487c1f3 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ShortcutGuideModuleCommandProvider.cs @@ -0,0 +1,38 @@ +// 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 Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class ShortcutGuideModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var module = SettingsWindow.ShortcutGuide; + var title = module.ModuleDisplayName(); + var icon = module.ModuleIcon(); + + if (ModuleEnablementService.IsModuleEnabled(module)) + { + yield return new ListItem(new ToggleShortcutGuideCommand()) + { + Title = "Toggle Shortcut Guide", + Subtitle = "Show or hide Shortcut Guide", + Icon = icon, + }; + } + + yield return new ListItem(new OpenInSettingsCommand(module, title)) + { + Title = title, + Subtitle = "Open Shortcut Guide settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/TextExtractorModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/TextExtractorModuleCommandProvider.cs new file mode 100644 index 0000000000..a8e816ccc6 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/TextExtractorModuleCommandProvider.cs @@ -0,0 +1,38 @@ +// 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 Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class TextExtractorModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var module = SettingsWindow.PowerOCR; + var title = module.ModuleDisplayName(); + var icon = module.ModuleIcon(); + + if (ModuleEnablementService.IsModuleEnabled(module)) + { + yield return new ListItem(new ToggleTextExtractorCommand()) + { + Title = "Toggle Text Extractor", + Subtitle = "Start or close Text Extractor", + Icon = icon, + }; + } + + yield return new ListItem(new OpenInSettingsCommand(module, title)) + { + Title = title, + Subtitle = "Open Text Extractor settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/WorkspacesModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/WorkspacesModuleCommandProvider.cs new file mode 100644 index 0000000000..49d585ba6d --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/WorkspacesModuleCommandProvider.cs @@ -0,0 +1,64 @@ +// 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 Common.UI; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using Workspaces.ModuleServices; +using WorkspacesCsharpLibrary.Data; + +namespace PowerToysExtension.Modules; + +internal sealed class WorkspacesModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var items = new List(); + var module = SettingsDeepLink.SettingsWindow.Workspaces; + var title = module.ModuleDisplayName(); + var icon = PowerToysResourcesHelper.IconFromSettingsIcon("Workspaces.png"); + var moduleIcon = module.ModuleIcon(); + + items.Add(new ListItem(new OpenInSettingsCommand(module, title)) + { + Title = title, + Subtitle = "Open Workspaces settings", + Icon = moduleIcon, + }); + + if (!ModuleEnablementService.IsModuleEnabled(module)) + { + return items; + } + + // Settings entry plus common actions. + items.Add(new ListItem(new OpenWorkspaceEditorCommand()) + { + Title = "Workspaces: Open editor", + Subtitle = "Create or edit workspaces", + Icon = icon, + }); + + // Per-workspace entries via the shared service. + foreach (var workspace in LoadWorkspaces()) + { + if (string.IsNullOrWhiteSpace(workspace.Id) || string.IsNullOrWhiteSpace(workspace.Name)) + { + continue; + } + + items.Add(new WorkspaceListItem(workspace, icon)); + } + + return items; + } + + private static IReadOnlyList LoadWorkspaces() + { + var result = WorkspaceService.Instance.GetWorkspacesAsync().GetAwaiter().GetResult(); + return result.Success && result.Value is not null ? result.Value : System.Array.Empty(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ZoomItModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ZoomItModuleCommandProvider.cs new file mode 100644 index 0000000000..a73ccdfbe3 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/ZoomItModuleCommandProvider.cs @@ -0,0 +1,69 @@ +// 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 Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; +using static Common.UI.SettingsDeepLink; + +namespace PowerToysExtension.Modules; + +internal sealed class ZoomItModuleCommandProvider : ModuleCommandProvider +{ + public override IEnumerable BuildCommands() + { + var module = SettingsWindow.ZoomIt; + var title = module.ModuleDisplayName(); + var icon = module.ModuleIcon(); + + if (ModuleEnablementService.IsModuleEnabled(module)) + { + // Action commands via ZoomIt IPC + yield return new ListItem(new ZoomItActionCommand("zoom", "ZoomIt: Zoom")) + { + Title = "ZoomIt: Zoom", + Subtitle = "Enter zoom mode", + Icon = icon, + }; + yield return new ListItem(new ZoomItActionCommand("draw", "ZoomIt: Draw")) + { + Title = "ZoomIt: Draw", + Subtitle = "Enter drawing mode", + Icon = icon, + }; + yield return new ListItem(new ZoomItActionCommand("break", "ZoomIt: Break")) + { + Title = "ZoomIt: Break", + Subtitle = "Enter break timer", + Icon = icon, + }; + yield return new ListItem(new ZoomItActionCommand("liveZoom", "ZoomIt: Live Zoom")) + { + Title = "ZoomIt: Live Zoom", + Subtitle = "Toggle live zoom", + Icon = icon, + }; + yield return new ListItem(new ZoomItActionCommand("snip", "ZoomIt: Snip")) + { + Title = "ZoomIt: Snip", + Subtitle = "Enter snip mode", + Icon = icon, + }; + yield return new ListItem(new ZoomItActionCommand("record", "ZoomIt: Record")) + { + Title = "ZoomIt: Record", + Subtitle = "Start recording", + Icon = icon, + }; + } + + yield return new ListItem(new OpenInSettingsCommand(module, title)) + { + Title = title, + Subtitle = "Open ZoomIt settings", + Icon = icon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/ColorPickerSavedColorsPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/ColorPickerSavedColorsPage.cs new file mode 100644 index 0000000000..5e06951794 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/ColorPickerSavedColorsPage.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using System.Text; +using ColorPicker.ModuleServices; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; + +namespace PowerToysExtension.Pages; + +internal sealed partial class ColorPickerSavedColorsPage : DynamicListPage +{ + private readonly CommandItem _emptyContent; + + public ColorPickerSavedColorsPage() + { + Icon = PowerToysResourcesHelper.IconFromSettingsIcon("ColorPicker.png"); + Title = "Saved colors"; + Name = "ColorPickerSavedColors"; + Id = "com.microsoft.powertoys.colorpicker.savedColors"; + + _emptyContent = new CommandItem() + { + Title = "No saved colors", + Subtitle = "Pick a color first, then try again.", + Icon = PowerToysResourcesHelper.IconFromSettingsIcon("ColorPicker.png"), + }; + + EmptyContent = _emptyContent; + } + + public override IListItem[] GetItems() + { + var result = ColorPickerService.Instance.GetSavedColorsAsync().GetAwaiter().GetResult(); + if (!result.Success || result.Value is null || result.Value.Count == 0) + { + return Array.Empty(); + } + + var search = SearchText; + var filtered = string.IsNullOrWhiteSpace(search) + ? result.Value + : result.Value.Where(saved => + saved.Hex.Contains(search, StringComparison.OrdinalIgnoreCase) || + saved.Formats.Any(f => f.Value.Contains(search, StringComparison.OrdinalIgnoreCase) || + f.Format.Contains(search, StringComparison.OrdinalIgnoreCase))); + + var items = filtered.Select(saved => + { + var copyValue = SelectPreferredFormat(saved); + var subtitle = BuildSubtitle(saved); + + var command = new CopySavedColorCommand(saved, copyValue); + return (IListItem)new ListItem(new CommandItem(command)) + { + Title = saved.Hex, + Subtitle = subtitle, + Icon = ColorSwatchIconFactory.Create(saved.R, saved.G, saved.B, saved.A), + }; + }).ToArray(); + + return items; + } + + public override void UpdateSearchText(string oldSearch, string newSearch) + { + _emptyContent.Subtitle = string.IsNullOrWhiteSpace(newSearch) + ? "Pick a color first, then try again." + : $"No saved colors matching '{newSearch}'"; + + RaiseItemsChanged(0); + } + + private static string SelectPreferredFormat(SavedColor saved) => saved.Hex; + + private static string BuildSubtitle(SavedColor saved) + { + var sb = new StringBuilder(); + foreach (var format in saved.Formats.Take(3)) + { + if (sb.Length > 0) + { + sb.Append(" · "); + } + + sb.Append(format.Value); + } + + return sb.Length > 0 ? sb.ToString() : saved.Hex; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesLayoutsPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesLayoutsPage.cs new file mode 100644 index 0000000000..1b568da7d6 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesLayoutsPage.cs @@ -0,0 +1,103 @@ +// 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.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; + +namespace PowerToysExtension.Pages; + +internal sealed partial class FancyZonesLayoutsPage : DynamicListPage +{ + private readonly CommandItem _emptyMessage; + + public FancyZonesLayoutsPage() + { + Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png"); + Name = Title = "FancyZones Layouts"; + Id = "com.microsoft.cmdpal.powertoys.fancyzones.layouts"; + + _emptyMessage = new CommandItem() + { + Title = "No layouts found", + Subtitle = "Open FancyZones Editor once to initialize layouts.", + Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png"), + }; + EmptyContent = _emptyMessage; + + // Purge orphaned cache files in background (non-blocking) + Task.Run(FancyZonesThumbnailRenderer.PurgeOrphanedCache); + } + + public override void UpdateSearchText(string oldSearch, string newSearch) + { + RaiseItemsChanged(0); + } + + public override IListItem[] GetItems() + { + try + { + var layouts = FancyZonesDataService.GetLayouts(); + if (!string.IsNullOrWhiteSpace(SearchText)) + { + layouts = layouts + .Where(l => l.Title.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase) || + l.Subtitle.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase)) + .ToArray(); + } + + if (layouts.Count == 0) + { + return Array.Empty(); + } + + _ = FancyZonesDataService.TryGetMonitors(out var monitors, out _); + var fallbackIcon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png"); + + var items = new List(layouts.Count); + foreach (var layout in layouts) + { + var defaultCommand = new ApplyFancyZonesLayoutCommand(layout, monitor: null); + + var item = new FancyZonesLayoutListItem(defaultCommand, layout, fallbackIcon) + { + MoreCommands = BuildLayoutContext(layout, monitors), + }; + + items.Add(item); + } + + return items.ToArray(); + } + catch (Exception ex) + { + _emptyMessage.Subtitle = ex.Message; + return Array.Empty(); + } + } + + private static IContextItem[] BuildLayoutContext(FancyZonesLayoutDescriptor layout, IReadOnlyList monitors) + { + var commands = new List(monitors.Count); + + for (var i = 0; i < monitors.Count; i++) + { + var monitor = monitors[i]; + commands.Add(new CommandContextItem(new ApplyFancyZonesLayoutCommand(layout, monitor)) + { + Title = string.Format(CultureInfo.CurrentCulture, "Apply to {0}", monitor.Title), + Subtitle = monitor.Subtitle, + }); + } + + return commands.ToArray(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesMonitorLayoutPickerPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesMonitorLayoutPickerPage.cs new file mode 100644 index 0000000000..0269f84ed0 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesMonitorLayoutPickerPage.cs @@ -0,0 +1,68 @@ +// 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.Linq; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; +using PowerToysExtension.Helpers; + +namespace PowerToysExtension.Pages; + +internal sealed partial class FancyZonesMonitorLayoutPickerPage : DynamicListPage +{ + private readonly FancyZonesMonitorDescriptor _monitor; + private readonly CommandItem _emptyMessage; + + public FancyZonesMonitorLayoutPickerPage(FancyZonesMonitorDescriptor monitor) + { + _monitor = monitor; + Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png"); + Name = Title = $"Set active layout for {_monitor.Title}"; + Id = $"com.microsoft.cmdpal.powertoys.fancyzones.monitor.{_monitor.Index}.layouts"; + + _emptyMessage = new CommandItem() + { + Title = "No layouts found", + Subtitle = "Open FancyZones Editor once to initialize layouts.", + Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png"), + }; + EmptyContent = _emptyMessage; + } + + public override void UpdateSearchText(string oldSearch, string newSearch) + { + RaiseItemsChanged(0); + } + + public override IListItem[] GetItems() + { + var layouts = FancyZonesDataService.GetLayouts(); + if (!string.IsNullOrWhiteSpace(SearchText)) + { + layouts = layouts + .Where(l => l.Title.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase) || + l.Subtitle.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase)) + .ToArray(); + } + + if (layouts.Count == 0) + { + return Array.Empty(); + } + + var fallbackIcon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png"); + var items = new List(layouts.Count); + foreach (var layout in layouts) + { + var command = new ApplyFancyZonesLayoutCommand(layout, _monitor); + var item = new FancyZonesLayoutListItem(command, layout, fallbackIcon); + items.Add(item); + } + + return [.. items]; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesMonitorsPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesMonitorsPage.cs new file mode 100644 index 0000000000..8422038d3d --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/FancyZonesMonitorsPage.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Helpers; + +namespace PowerToysExtension.Pages; + +internal sealed partial class FancyZonesMonitorsPage : DynamicListPage +{ + private readonly CommandItem _emptyMessage; + + public FancyZonesMonitorsPage() + { + Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png"); + Name = Title = "FancyZones Monitors"; + Id = "com.microsoft.cmdpal.powertoys.fancyzones.monitors"; + + _emptyMessage = new CommandItem() + { + Title = "No monitors found", + Subtitle = "Open FancyZones Editor once to initialize monitor data.", + Icon = PowerToysResourcesHelper.IconFromSettingsIcon("FancyZones.png"), + }; + EmptyContent = _emptyMessage; + } + + public override void UpdateSearchText(string oldSearch, string newSearch) + { + RaiseItemsChanged(0); + } + + public override IListItem[] GetItems() + { + if (!FancyZonesDataService.TryGetMonitors(out var monitors, out var error)) + { + _emptyMessage.Subtitle = error; + return Array.Empty(); + } + + var monitorIcon = new IconInfo("\uE7F4"); + var items = new List(monitors.Count); + + foreach (var monitor in monitors) + { + if (!string.IsNullOrWhiteSpace(SearchText) && + !monitor.Title.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase) && + !monitor.Subtitle.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase)) + { + continue; + } + + var layoutDescription = FancyZonesDataService.TryGetAppliedLayoutForMonitor(monitor.Data, out var applied) && applied is not null + ? $"Current layout: {applied.Value.Type}" + : "Current layout: unknown"; + + var item = new FancyZonesMonitorListItem(monitor, layoutDescription, monitorIcon); + items.Add(item); + } + + return [.. items]; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysExtensionPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysExtensionPage.cs new file mode 100644 index 0000000000..0d81573280 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysExtensionPage.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Awake.ModuleServices; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Commands; + +namespace PowerToysExtension; + +internal sealed partial class PowerToysExtensionPage : ListPage +{ + public PowerToysExtensionPage() + { + Icon = Helpers.PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png"); + Title = "PowerToys"; + Name = "PowerToys commands"; + } + + public override IListItem[] GetItems() + { + return [ + new ListItem(new LaunchModuleCommand("PowerToys", executableName: "PowerToys.exe", displayName: "Open PowerToys")) + { + Title = "Open PowerToys", + Subtitle = "Launch the PowerToys shell", + }, + new ListItem(new OpenPowerToysSettingsCommand("PowerToys", "General")) + { + Title = "Open PowerToys settings", + Subtitle = "Open the main PowerToys settings window", + }, + new ListItem(new OpenPowerToysSettingsCommand("Workspaces", "Workspaces")) + { + Title = "Open Workspaces settings", + Subtitle = "Jump directly to Workspaces settings", + }, + new ListItem(new OpenWorkspaceEditorCommand()) + { + Title = "Open Workspaces editor", + Subtitle = "Launch the Workspaces editor", + }, + ]; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysListPage.cs new file mode 100644 index 0000000000..c7eed2594f --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysListPage.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Helpers; + +namespace PowerToysExtension.Pages; + +internal sealed partial class PowerToysListPage : ListPage +{ + private readonly CommandItem _empty; + + public PowerToysListPage() + { + Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png"); + Name = Title = "PowerToys"; + Id = "com.microsoft.cmdpal.powertoys"; + SettingsChangeNotifier.SettingsChanged += OnSettingsChanged; + _empty = new CommandItem() + { + Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png"), + Title = "No matching module found", + Subtitle = SearchText, + }; + EmptyContent = _empty; + } + + private void OnSettingsChanged() + { + RaiseItemsChanged(0); + } + + public override IListItem[] GetItems() => ModuleCommandCatalog.GetAllItems(); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysCommandsProvider.cs new file mode 100644 index 0000000000..d4dde03b46 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysCommandsProvider.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using ManagedCommon; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Helpers; + +namespace PowerToysExtension; + +public sealed partial class PowerToysCommandsProvider : CommandProvider +{ + public PowerToysCommandsProvider() + { + DisplayName = "PowerToys"; + Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png"); + } + + public override ICommandItem[] TopLevelCommands() => + [ + new CommandItem(new Pages.PowerToysListPage()) + { + Title = "PowerToys", + Subtitle = "PowerToys commands and settings", + } + ]; + + public override IFallbackCommandItem[] FallbackCommands() + { + var items = ModuleCommandCatalog.GetAllItems(); + var fallbacks = new List(items.Length); + foreach (var item in items) + { + if (item?.Command is not ICommand cmd) + { + continue; + } + + fallbacks.Add(new PowerToysFallbackCommandItem(cmd, item.Title, item.Subtitle, item.Icon, item.MoreCommands)); + } + + return fallbacks.ToArray(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtension.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtension.cs new file mode 100644 index 0000000000..f4100db51a --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtension.cs @@ -0,0 +1,41 @@ +// 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 ManagedCommon; +using Microsoft.CommandPalette.Extensions; + +namespace PowerToysExtension; + +[Guid("7EC02C7D-8F98-4A2E-9F23-B58C2C2F2B17")] +public sealed partial class PowerToysExtension : IExtension, IDisposable +{ + private readonly ManualResetEvent _extensionDisposedEvent; + + private readonly PowerToysExtensionCommandsProvider _provider = new(); + + public PowerToysExtension(ManualResetEvent extensionDisposedEvent) + { + this._extensionDisposedEvent = extensionDisposedEvent; + Logger.LogInfo($"PowerToysExtension constructed. ProcArch={RuntimeInformation.ProcessArchitecture} OSArch={RuntimeInformation.OSArchitecture} BaseDir={AppContext.BaseDirectory}"); + } + + public object? GetProvider(ProviderType providerType) + { + Logger.LogInfo($"GetProvider requested: {providerType}"); + return providerType switch + { + ProviderType.Commands => _provider, + _ => null, + }; + } + + public void Dispose() + { + Logger.LogInfo("PowerToysExtension disposing; signalling exit."); + this._extensionDisposedEvent.Set(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtensionCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtensionCommandsProvider.cs new file mode 100644 index 0000000000..beba6b484a --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtensionCommandsProvider.cs @@ -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.Collections.Generic; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Helpers; + +namespace PowerToysExtension; + +public partial class PowerToysExtensionCommandsProvider : CommandProvider +{ + private readonly ICommandItem[] _commands; + + public PowerToysExtensionCommandsProvider() + { + DisplayName = "PowerToys"; + Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png"); + _commands = [ + new CommandItem(new Pages.PowerToysListPage()) + { + Title = "PowerToys", + Subtitle = "PowerToys commands and settings", + }, + ]; + } + + public override ICommandItem[] TopLevelCommands() + { + return _commands; + } + + public override IFallbackCommandItem[] FallbackCommands() + { + var items = ModuleCommandCatalog.GetAllItems(); + var fallbacks = new List(items.Length); + foreach (var item in items) + { + if (item?.Command is not ICommand cmd) + { + continue; + } + + fallbacks.Add(new PowerToysFallbackCommandItem(cmd, item.Title, item.Subtitle, item.Icon, item.MoreCommands)); + } + + return fallbacks.ToArray(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Program.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Program.cs new file mode 100644 index 0000000000..2706f50f90 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Program.cs @@ -0,0 +1,70 @@ +// 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 ManagedCommon; +using Microsoft.CommandPalette.Extensions; +using Shmuelie.WinRTServer; +using Shmuelie.WinRTServer.CsWinRT; + +namespace PowerToysExtension; + +public class Program +{ + [MTAThread] + public static void Main(string[] args) + { + try + { + // Initialize per-extension log under CmdPal/PowerToysExtension. + Logger.InitializeLogger("\\CmdPal\\PowerToysExtension\\Logs"); + Logger.LogInfo($"PowerToysExtension starting. Args=\"{string.Join(' ', args)}\" ProcArch={RuntimeInformation.ProcessArchitecture} OSArch={RuntimeInformation.OSArchitecture} BaseDir={AppContext.BaseDirectory}"); + } + catch + { + // Continue even if logging fails. + } + + try + { + if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer") + { + Logger.LogInfo("RegisterProcessAsComServer mode detected."); + ComServer server = new(); + ManualResetEvent extensionDisposedEvent = new(false); + try + { + PowerToysExtension extensionInstance = new(extensionDisposedEvent); + Logger.LogInfo("Registering extension via Shmuelie.WinRTServer."); + server.RegisterClass(() => extensionInstance); + server.Start(); + Logger.LogInfo("Extension instance registered; waiting for disposal signal."); + + extensionDisposedEvent.WaitOne(); + Logger.LogInfo("Extension disposed signal received; exiting server loop."); + } + finally + { + server.Stop(); + server.UnsafeDispose(); + } + } + else + { + Console.WriteLine("Not being launched as a Extension... exiting."); + Logger.LogInfo("Exited: not launched with -RegisterProcessAsComServer."); + } + } + catch (Exception ex) + { + Logger.LogError("Unhandled exception in PowerToysExtension.Main", ex); + throw; + } + finally + { + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Public/README.md b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Public/README.md new file mode 100644 index 0000000000..99bded1694 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Public/README.md @@ -0,0 +1,6 @@ +# PowerToys Command Palette Extension + +This folder is exposed to the Windows Command Palette host via the +`PublicFolder` attribute in the AppExtension registration. It intentionally +contains only documentation today, but can be used for additional metadata in +the future without requiring code changes. diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/app.manifest b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/app.manifest new file mode 100644 index 0000000000..013aaee199 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/app.manifest @@ -0,0 +1,21 @@ + + + + + + + + PerMonitorV2 + + + + + + + + + + diff --git a/src/modules/colorPicker/ColorPicker.ModuleServices/ColorFormatValue.cs b/src/modules/colorPicker/ColorPicker.ModuleServices/ColorFormatValue.cs new file mode 100644 index 0000000000..90e71e6f18 --- /dev/null +++ b/src/modules/colorPicker/ColorPicker.ModuleServices/ColorFormatValue.cs @@ -0,0 +1,7 @@ +// 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 ColorPicker.ModuleServices; + +public sealed record ColorFormatValue(string Format, string Value); diff --git a/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPicker.ModuleServices.csproj b/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPicker.ModuleServices.csproj new file mode 100644 index 0000000000..1efe5cdc8d --- /dev/null +++ b/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPicker.ModuleServices.csproj @@ -0,0 +1,29 @@ + + + + + + + enable + enable + false + false + false + + + + + + + + + + + + + + + + + + diff --git a/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPickerService.cs b/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPickerService.cs new file mode 100644 index 0000000000..6407ae6ed9 --- /dev/null +++ b/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPickerService.cs @@ -0,0 +1,157 @@ +// 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.Drawing; +using System.Text.Json; +using Common.UI; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; +using PowerToys.Interop; +using PowerToys.ModuleContracts; + +namespace ColorPicker.ModuleServices; + +/// +/// Provides programmatic control for Color Picker actions. +/// +public sealed class ColorPickerService : ModuleServiceBase, IColorPickerService +{ + public static ColorPickerService Instance { get; } = new(); + + public override string Key => SettingsDeepLink.SettingsWindow.ColorPicker.ToString(); + + protected override SettingsDeepLink.SettingsWindow SettingsWindow => SettingsDeepLink.SettingsWindow.ColorPicker; + + public override Task LaunchAsync(CancellationToken cancellationToken = default) + { + // Default launch -> open picker. + return OpenPickerAsync(cancellationToken); + } + + public Task OpenPickerAsync(CancellationToken cancellationToken = default) + { + return SignalEventAsync(Constants.ShowColorPickerSharedEvent(), "Color Picker"); + } + + public Task>> GetSavedColorsAsync(CancellationToken cancellationToken = default) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var historyPath = Path.Combine(localAppData, "Microsoft", "PowerToys", "ColorPicker", "colorHistory.json"); + if (!File.Exists(historyPath)) + { + return Task.FromResult(OperationResults.Ok>(Array.Empty())); + } + + using var stream = File.OpenRead(historyPath); + var colors = JsonSerializer.Deserialize(stream, ColorPickerServiceJsonContext.Default.ListString) ?? new List(); + + var settingsUtils = SettingsUtils.Default; + var settings = settingsUtils.GetSettingsOrDefault(ColorPickerSettings.ModuleName); + + var results = new List(colors.Count); + foreach (var entry in colors) + { + if (!TryParseArgb(entry, out var color)) + { + continue; + } + + var formats = BuildFormats(color, settings); + var hex = $"#{color.R:X2}{color.G:X2}{color.B:X2}"; + + results.Add(new SavedColor( + hex, + color.A, + color.R, + color.G, + color.B, + formats)); + } + + return Task.FromResult(OperationResults.Ok>(results)); + } + catch (OperationCanceledException) + { + return Task.FromResult(OperationResults.Fail>("Reading saved colors was cancelled.")); + } + catch (Exception ex) + { + return Task.FromResult(OperationResults.Fail>($"Failed to read saved colors: {ex.Message}")); + } + } + + private static Task SignalEventAsync(string eventName, string actionDescription) + { + try + { + using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + if (!eventHandle.Set()) + { + return Task.FromResult(OperationResult.Fail($"Failed to signal {actionDescription}.")); + } + + return Task.FromResult(OperationResult.Ok()); + } + catch (Exception ex) + { + return Task.FromResult(OperationResult.Fail($"Failed to signal {actionDescription}: {ex.Message}")); + } + } + + private static bool TryParseArgb(string value, out Color color) + { + color = Color.Empty; + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + var parts = value.Split('|'); + if (parts.Length != 4) + { + return false; + } + + if (byte.TryParse(parts[0], out var a) && + byte.TryParse(parts[1], out var r) && + byte.TryParse(parts[2], out var g) && + byte.TryParse(parts[3], out var b)) + { + color = Color.FromArgb(a, r, g, b); + return true; + } + + return false; + } + + private static IReadOnlyList BuildFormats(Color color, ColorPickerSettings settings) + { + var formats = new List(); + foreach (var kvp in settings.Properties.VisibleColorFormats) + { + var formatName = kvp.Key; + var (isVisible, formatString) = kvp.Value; + if (!isVisible) + { + continue; + } + + var formatted = ColorFormatHelper.GetStringRepresentation(color, formatString); + if (formatName.Equals("HEX", StringComparison.OrdinalIgnoreCase) && !formatted.StartsWith('#')) + { + formatted = "#" + formatted; + } + + formats.Add(new ColorFormatValue(formatName, formatted)); + } + + return formats; + } +} diff --git a/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPickerServiceJsonContext.cs b/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPickerServiceJsonContext.cs new file mode 100644 index 0000000000..f26e9009d3 --- /dev/null +++ b/src/modules/colorPicker/ColorPicker.ModuleServices/ColorPickerServiceJsonContext.cs @@ -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.Collections.Generic; +using System.Text.Json.Serialization; +using Microsoft.PowerToys.Settings.UI.Library; + +namespace ColorPicker.ModuleServices; + +[JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(SavedColor))] +[JsonSerializable(typeof(ColorFormatValue))] +[JsonSerializable(typeof(ColorPickerSettings))] +internal sealed partial class ColorPickerServiceJsonContext : JsonSerializerContext +{ +} diff --git a/src/modules/colorPicker/ColorPicker.ModuleServices/IColorPickerService.cs b/src/modules/colorPicker/ColorPicker.ModuleServices/IColorPickerService.cs new file mode 100644 index 0000000000..4ad2ca3da3 --- /dev/null +++ b/src/modules/colorPicker/ColorPicker.ModuleServices/IColorPickerService.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using PowerToys.ModuleContracts; + +namespace ColorPicker.ModuleServices; + +public interface IColorPickerService : IModuleService +{ + Task OpenPickerAsync(CancellationToken cancellationToken = default); + + Task>> GetSavedColorsAsync(CancellationToken cancellationToken = default); +} diff --git a/src/modules/colorPicker/ColorPicker.ModuleServices/SavedColor.cs b/src/modules/colorPicker/ColorPicker.ModuleServices/SavedColor.cs new file mode 100644 index 0000000000..3697129aa0 --- /dev/null +++ b/src/modules/colorPicker/ColorPicker.ModuleServices/SavedColor.cs @@ -0,0 +1,7 @@ +// 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 ColorPicker.ModuleServices; + +public sealed record SavedColor(string Hex, byte A, byte R, byte G, byte B, IReadOnlyList Formats); diff --git a/src/modules/colorPicker/ColorPickerUI/Helpers/AppStateHandler.cs b/src/modules/colorPicker/ColorPickerUI/Helpers/AppStateHandler.cs index 705324cb93..72c874d839 100644 --- a/src/modules/colorPicker/ColorPickerUI/Helpers/AppStateHandler.cs +++ b/src/modules/colorPicker/ColorPickerUI/Helpers/AppStateHandler.cs @@ -205,7 +205,7 @@ namespace ColorPicker.Helpers private void ColorEditorViewModel_OpenSettingsRequested(object sender, EventArgs e) { - SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ColorPicker, false); + SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ColorPicker); } internal void RegisterWindowHandle(System.Windows.Interop.HwndSource hwndSource) diff --git a/src/modules/fancyzones/FancyZones.FuzzTests/FancyZones.FuzzTests.csproj b/src/modules/fancyzones/FancyZones.FuzzTests/FancyZones.FuzzTests.csproj index 9428e10608..dd1665bc0d 100644 --- a/src/modules/fancyzones/FancyZones.FuzzTests/FancyZones.FuzzTests.csproj +++ b/src/modules/fancyzones/FancyZones.FuzzTests/FancyZones.FuzzTests.csproj @@ -10,10 +10,16 @@ + - + + + + + + diff --git a/src/modules/fancyzones/FancyZones/FancyZonesApp.cpp b/src/modules/fancyzones/FancyZones/FancyZonesApp.cpp index 97faec65d5..a20a6fc7f6 100644 --- a/src/modules/fancyzones/FancyZones/FancyZonesApp.cpp +++ b/src/modules/fancyzones/FancyZones/FancyZonesApp.cpp @@ -22,7 +22,7 @@ FancyZonesApp::FancyZonesApp(const std::wstring& appName, const std::wstring& ap m_app = MakeFancyZones(reinterpret_cast(&__ImageBase), std::bind(&FancyZonesApp::DisableModule, this)); m_mainThreadId = GetCurrentThreadId(); - m_exitEventWaiter = EventWaiter(CommonSharedConstants::FZE_EXIT_EVENT, [&](int err) { + m_exitEventWaiter.start(CommonSharedConstants::FZE_EXIT_EVENT, [&](DWORD err) { if (err == ERROR_SUCCESS) { DisableModule(); diff --git a/src/modules/fancyzones/FancyZonesEditor.UITests/Utils/AppZoneHistory.cs b/src/modules/fancyzones/FancyZonesEditor.UITests/Utils/AppZoneHistory.cs index e8daa83348..f246665aea 100644 --- a/src/modules/fancyzones/FancyZonesEditor.UITests/Utils/AppZoneHistory.cs +++ b/src/modules/fancyzones/FancyZonesEditor.UITests/Utils/AppZoneHistory.cs @@ -55,24 +55,24 @@ namespace FancyZonesEditorCommon.Data public JsonElement ToJsonElement(ZoneHistoryWrapper info) { - string json = JsonSerializer.Serialize(info, this.JsonOptions); + string json = JsonSerializer.Serialize(info, JsonOptions); return JsonSerializer.Deserialize(json); } public JsonElement ToJsonElement(DeviceIdWrapper info) { - string json = JsonSerializer.Serialize(info, this.JsonOptions); + string json = JsonSerializer.Serialize(info, JsonOptions); return JsonSerializer.Deserialize(json); } public ZoneHistoryWrapper ZoneHistoryFromJsonElement(string json) { - return JsonSerializer.Deserialize(json, this.JsonOptions); + return JsonSerializer.Deserialize(json, JsonOptions); } public DeviceIdWrapper GridFromJsonElement(string json) { - return JsonSerializer.Deserialize(json, this.JsonOptions); + return JsonSerializer.Deserialize(json, JsonOptions); } } } diff --git a/src/modules/fancyzones/FancyZonesEditorCommon/Data/CustomLayouts.cs b/src/modules/fancyzones/FancyZonesEditorCommon/Data/CustomLayouts.cs index 7be8746964..75671e8ed8 100644 --- a/src/modules/fancyzones/FancyZonesEditorCommon/Data/CustomLayouts.cs +++ b/src/modules/fancyzones/FancyZonesEditorCommon/Data/CustomLayouts.cs @@ -78,24 +78,24 @@ namespace FancyZonesEditorCommon.Data public JsonElement ToJsonElement(CanvasInfoWrapper info) { - string json = JsonSerializer.Serialize(info, this.JsonOptions); + string json = JsonSerializer.Serialize(info, FancyZonesJsonContext.Default.CanvasInfoWrapper); return JsonSerializer.Deserialize(json); } public JsonElement ToJsonElement(GridInfoWrapper info) { - string json = JsonSerializer.Serialize(info, this.JsonOptions); + string json = JsonSerializer.Serialize(info, FancyZonesJsonContext.Default.GridInfoWrapper); return JsonSerializer.Deserialize(json); } public CanvasInfoWrapper CanvasFromJsonElement(string json) { - return JsonSerializer.Deserialize(json, this.JsonOptions); + return JsonSerializer.Deserialize(json, FancyZonesJsonContext.Default.CanvasInfoWrapper); } public GridInfoWrapper GridFromJsonElement(string json) { - return JsonSerializer.Deserialize(json, this.JsonOptions); + return JsonSerializer.Deserialize(json, FancyZonesJsonContext.Default.GridInfoWrapper); } } } diff --git a/src/modules/fancyzones/FancyZonesEditorCommon/Data/EditorData`1.cs b/src/modules/fancyzones/FancyZonesEditorCommon/Data/EditorData`1.cs index fdfa32cc28..83b35f4ac8 100644 --- a/src/modules/fancyzones/FancyZonesEditorCommon/Data/EditorData`1.cs +++ b/src/modules/fancyzones/FancyZonesEditorCommon/Data/EditorData`1.cs @@ -4,6 +4,7 @@ using System; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using FancyZonesEditorCommon.Utils; @@ -16,28 +17,20 @@ namespace FancyZonesEditorCommon.Data return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); } - protected JsonSerializerOptions JsonOptions - { - get - { - return new JsonSerializerOptions - { - PropertyNamingPolicy = new DashCaseNamingPolicy(), - WriteIndented = true, - }; - } - } + protected static JsonSerializerOptions JsonOptions => FancyZonesJsonContext.Default.Options; + + protected static JsonTypeInfo TypeInfo => (JsonTypeInfo)FancyZonesJsonContext.Default.GetTypeInfo(typeof(T)); public T Read(string file) { IOUtils ioUtils = new IOUtils(); string data = ioUtils.ReadFile(file); - return JsonSerializer.Deserialize(data, JsonOptions); + return JsonSerializer.Deserialize(data, TypeInfo); } public string Serialize(T data) { - return JsonSerializer.Serialize(data, JsonOptions); + return JsonSerializer.Serialize(data, TypeInfo); } } } diff --git a/src/modules/fancyzones/FancyZonesEditorCommon/Data/FancyZonesJsonContext.cs b/src/modules/fancyzones/FancyZonesEditorCommon/Data/FancyZonesJsonContext.cs new file mode 100644 index 0000000000..69fa8a97b7 --- /dev/null +++ b/src/modules/fancyzones/FancyZonesEditorCommon/Data/FancyZonesJsonContext.cs @@ -0,0 +1,38 @@ +// 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.Serialization; + +using FancyZonesEditorCommon.Utils; + +namespace FancyZonesEditorCommon.Data +{ + /// + /// JSON serialization context for AOT-compatible serialization of FancyZones data types. + /// + [JsonSourceGenerationOptions( + PropertyNamingPolicy = JsonKnownNamingPolicy.KebabCaseLower, + WriteIndented = true)] + [JsonSerializable(typeof(AppliedLayouts.AppliedLayoutsListWrapper))] + [JsonSerializable(typeof(AppliedLayouts.AppliedLayoutWrapper))] + [JsonSerializable(typeof(AppliedLayouts.AppliedLayoutWrapper.DeviceIdWrapper))] + [JsonSerializable(typeof(AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper), TypeInfoPropertyName = "AppliedLayoutLayoutWrapper")] + [JsonSerializable(typeof(CustomLayouts.CustomLayoutListWrapper))] + [JsonSerializable(typeof(CustomLayouts.CustomLayoutWrapper))] + [JsonSerializable(typeof(CustomLayouts.CanvasInfoWrapper))] + [JsonSerializable(typeof(CustomLayouts.CanvasInfoWrapper.CanvasZoneWrapper))] + [JsonSerializable(typeof(CustomLayouts.GridInfoWrapper))] + [JsonSerializable(typeof(LayoutTemplates.TemplateLayoutsListWrapper))] + [JsonSerializable(typeof(LayoutTemplates.TemplateLayoutWrapper))] + [JsonSerializable(typeof(LayoutHotkeys.LayoutHotkeysWrapper))] + [JsonSerializable(typeof(LayoutHotkeys.LayoutHotkeyWrapper))] + [JsonSerializable(typeof(EditorParameters.ParamsWrapper))] + [JsonSerializable(typeof(EditorParameters.NativeMonitorDataWrapper))] + [JsonSerializable(typeof(DefaultLayouts.DefaultLayoutsListWrapper))] + [JsonSerializable(typeof(DefaultLayouts.DefaultLayoutWrapper))] + [JsonSerializable(typeof(DefaultLayouts.DefaultLayoutWrapper.LayoutWrapper), TypeInfoPropertyName = "DefaultLayoutLayoutWrapper")] + public partial class FancyZonesJsonContext : JsonSerializerContext + { + } +} diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp b/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp index 68bd08c6af..4e83a450f5 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp +++ b/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp @@ -261,7 +261,7 @@ FancyZones::Run() noexcept } }) .wait(); - m_toggleEditorEventWaiter = EventWaiter(CommonSharedConstants::FANCY_ZONES_EDITOR_TOGGLE_EVENT, [&](int err) { + m_toggleEditorEventWaiter.start(CommonSharedConstants::FANCY_ZONES_EDITOR_TOGGLE_EVENT, [&](DWORD err) { if (err == ERROR_SUCCESS) { Logger::trace(L"{} event was signaled", CommonSharedConstants::FANCY_ZONES_EDITOR_TOGGLE_EVENT); diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/EditorWindow.cs b/src/modules/fancyzones/editor/FancyZonesEditor/EditorWindow.cs index 2543568436..c6e5e5aec5 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/EditorWindow.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/EditorWindow.cs @@ -4,7 +4,6 @@ using System; using System.Windows; - using FancyZonesEditor.Models; using ManagedCommon; diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/MainWindow.xaml.cs b/src/modules/fancyzones/editor/FancyZonesEditor/MainWindow.xaml.cs index 3562175bca..eebd0bb54f 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/MainWindow.xaml.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/MainWindow.xaml.cs @@ -563,7 +563,7 @@ namespace FancyZonesEditor private void SettingsBtn_Click(object sender, RoutedEventArgs e) { - SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.FancyZones, false); + SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.FancyZones); } private void EditLayoutDialogTitle_Loaded(object sender, RoutedEventArgs e) diff --git a/src/modules/imageresizer/ui/ViewModels/InputViewModel.cs b/src/modules/imageresizer/ui/ViewModels/InputViewModel.cs index c05768d9e0..c241728276 100644 --- a/src/modules/imageresizer/ui/ViewModels/InputViewModel.cs +++ b/src/modules/imageresizer/ui/ViewModels/InputViewModel.cs @@ -195,7 +195,7 @@ namespace ImageResizer.ViewModels public static void OpenSettings() { - SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ImageResizer, false); + SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ImageResizer); } private void HandleEnterKeyPress(KeyPressParams parameters) diff --git a/src/modules/keyboardmanager/KeyboardManagerEngine/main.cpp b/src/modules/keyboardmanager/KeyboardManagerEngine/main.cpp index 5969ed6cfd..df48555df5 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEngine/main.cpp +++ b/src/modules/keyboardmanager/KeyboardManagerEngine/main.cpp @@ -51,7 +51,8 @@ int WINAPI wWinMain(_In_ HINSTANCE /*hInstance*/, auto mainThreadId = GetCurrentThreadId(); - EventWaiter ev = EventWaiter(CommonSharedConstants::TERMINATE_KBM_SHARED_EVENT, [&](int) { + EventWaiter ev; + ev.start(CommonSharedConstants::TERMINATE_KBM_SHARED_EVENT, [&](DWORD) { PostThreadMessage(mainThreadId, WM_QUIT, 0, 0); }); diff --git a/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardManager.cpp b/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardManager.cpp index 8d9ec63698..3eb3261524 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardManager.cpp +++ b/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardManager.cpp @@ -70,7 +70,7 @@ KeyboardManager::KeyboardManager() }; editorIsRunningEvent = CreateEvent(nullptr, true, false, KeyboardManagerConstants::EditorWindowEventName.c_str()); - settingsEventWaiter = EventWaiter(KeyboardManagerConstants::SettingsEventName, changeSettingsCallback); + settingsEventWaiter.start(KeyboardManagerConstants::SettingsEventName, changeSettingsCallback); } void KeyboardManager::LoadSettings() diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/Utility.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/Utility.cs index 03f0090c80..0b154f8e9f 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/Utility.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/Utility.cs @@ -98,7 +98,7 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys.Components AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift, Action = _ => { - SettingsDeepLink.OpenSettings(settingsWindow.Value, false); + SettingsDeepLink.OpenSettings(settingsWindow.Value); return true; }, }); diff --git a/src/modules/registrypreview/RegistryPreviewExt/dllmain.cpp b/src/modules/registrypreview/RegistryPreviewExt/dllmain.cpp index fb39833553..70e63d9bb0 100644 --- a/src/modules/registrypreview/RegistryPreviewExt/dllmain.cpp +++ b/src/modules/registrypreview/RegistryPreviewExt/dllmain.cpp @@ -161,7 +161,7 @@ public: init_settings(); triggerEvent = CreateEvent(nullptr, false, false, CommonSharedConstants::REGISTRY_PREVIEW_TRIGGER_EVENT); - triggerEventWaiter = EventWaiter(CommonSharedConstants::REGISTRY_PREVIEW_TRIGGER_EVENT, [this](int) { + triggerEventWaiter.start(CommonSharedConstants::REGISTRY_PREVIEW_TRIGGER_EVENT, [this](DWORD) { on_hotkey(0); }); } diff --git a/src/settings-ui/Settings.UI.XamlIndexBuilder/Settings.UI.XamlIndexBuilder.csproj b/src/settings-ui/Settings.UI.XamlIndexBuilder/Settings.UI.XamlIndexBuilder.csproj index f8138b6bfa..dba7fdcd3b 100644 --- a/src/settings-ui/Settings.UI.XamlIndexBuilder/Settings.UI.XamlIndexBuilder.csproj +++ b/src/settings-ui/Settings.UI.XamlIndexBuilder/Settings.UI.XamlIndexBuilder.csproj @@ -40,4 +40,4 @@ - + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj index a6751adb98..dd396112d3 100644 --- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj +++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj @@ -214,4 +214,4 @@ - + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs index 774245eb34..6edc318089 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs @@ -336,7 +336,7 @@ namespace Microsoft.PowerToys.Settings.UI }); #else /* If we try to run Settings as a standalone app, it will start PowerToys.exe if not running and open Settings again through it in the Dashboard page. */ - Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Dashboard, true); + Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Dashboard); Exit(); #endif } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs index cf4488b759..9d7f0e78f3 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs @@ -348,7 +348,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views private void DataDiagnostics_OpenSettings_Click(Microsoft.UI.Xaml.Documents.Hyperlink sender, Microsoft.UI.Xaml.Documents.HyperlinkClickEventArgs args) { - Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Overview, true); + Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Overview); } private async void LoadReleaseNotes_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml.cs index 19973a69bc..7f82ab4b97 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml.cs @@ -96,7 +96,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views private void NavigateCmdPalSettings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) { - SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.CmdPal, true); + SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.CmdPal); } /* diff --git a/tools/build/build-installer.ps1 b/tools/build/build-installer.ps1 index 257f70e6b6..a0e1f3c809 100644 --- a/tools/build/build-installer.ps1 +++ b/tools/build/build-installer.ps1 @@ -28,11 +28,8 @@ Runs the pipeline for x64 Release with machine-wide installer. .NOTES - Generated MSIX files will be signed using cert-sign-package.ps1. -- This script uses git to manage workspace state: - * Uncommitted changes are stashed before build and popped afterwards. - * Version files and manifests modified during build are reverted. - * Untracked generated files are cleaned up. -- Use the -Clean parameter to clean build outputs (bin/obj) and ignored files. +- If the working tree is not clean, the script will prompt before continuing (use -Force to skip the prompt). +- Use the -Clean parameter to clean build outputs (bin/obj) and MSBuild outputs. - The built installer will be placed under: installer/PowerToysSetupVNext/[Platform]/[Configuration]/User[Machine]Setup relative to the solution root directory. - To run the full installation in other machines, call "./cert-management.ps1" to export the cert used to sign the packages. @@ -44,6 +41,7 @@ param ( [string]$Configuration = 'Release', [string]$PerUser = 'true', [string]$Version, + [switch]$Force, [switch]$EnableCmdPalAOT, [switch]$Clean, [switch]$SkipBuild, @@ -51,11 +49,12 @@ param ( ) if ($Help) { - Write-Host "Usage: .\build-installer.ps1 [-Platform ] [-Configuration ] [-PerUser ] [-Version <0.0.1>] [-EnableCmdPalAOT] [-Clean] [-SkipBuild]" + Write-Host "Usage: .\build-installer.ps1 [-Platform ] [-Configuration ] [-PerUser ] [-Version <0.0.1>] [-Force] [-EnableCmdPalAOT] [-Clean] [-SkipBuild]" Write-Host " -Platform Target platform (default: auto-detect or x64)" Write-Host " -Configuration Build configuration (default: Release)" Write-Host " -PerUser Build per-user installer (default: true)" Write-Host " -Version Sets the PowerToys version (default: from src\Version.props)" + Write-Host " -Force Continue even if the git working tree is not clean (skips the interactive prompt)." Write-Host " -EnableCmdPalAOT Enable AOT compilation for CmdPal (slower build)" Write-Host " -Clean Clean output directories before building" Write-Host " -SkipBuild Skip building the main solution and tools (assumes they are already built)" @@ -103,6 +102,73 @@ if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot "PowerToys.slnx"))) Write-Host "PowerToys repository root detected: $repoRoot" +# Safety check: avoid mixing build outputs with existing local changes unless the user confirms. +if (-not $Force) { + Push-Location $repoRoot + try { + $gitStatus = $null + $gitRelevantStatus = @() + try { + $gitStatus = git status --porcelain=v1 --untracked-files=all --ignore-submodules=all + } catch { + Write-Warning ("[GIT] Failed to query git status: {0}" -f $_.Exception.Message) + } + + if ($gitStatus -and $gitStatus.Length -gt 0) { + foreach ($line in $gitStatus) { + if (-not $line) { continue } + + # Porcelain v1 format: XY + # We only care about changes that affect the working tree (Y != ' ') or untracked files (??). + # Index-only changes (staged, Y == ' ') are ignored per user request. + if ($line.StartsWith('??')) { + $gitRelevantStatus += $line + continue + } + + if ($line.StartsWith('!!')) { + continue + } + + if ($line.Length -ge 2) { + $workTreeStatus = $line[1] + if ($workTreeStatus -ne ' ') { + $gitRelevantStatus += $line + } + } + } + } + + if ($gitRelevantStatus.Count -gt 0) { + Write-Warning "[GIT] Working tree is NOT clean." + Write-Warning "[GIT] This build will generate untracked files and may modify tracked files, which can mix with your current changes." + Write-Host "[GIT] Unstaged/untracked status (first 50 lines):" + $gitRelevantStatus | Select-Object -First 50 | ForEach-Object { Write-Host (" {0}" -f $_) } + + $shouldContinue = $false + try { + $choices = [System.Management.Automation.Host.ChoiceDescription[]]@( + (New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Continue the build."), + (New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Cancel the build.") + ) + $decision = $Host.UI.PromptForChoice("Working tree not clean", "Continue anyway?", $choices, 1) + $shouldContinue = ($decision -eq 0) + } catch { + Write-Warning "[GIT] Interactive prompt not available." + Write-Error "Refusing to proceed with a dirty working tree. Re-run with -Force to continue anyway." + exit 1 + } + + if (-not $shouldContinue) { + Write-Host "[GIT] Cancelled by user." + exit 1 + } + } + } finally { + Pop-Location + } +} + $cmdpalOutputPath = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps\CmdPal" $buildOutputPath = Join-Path $repoRoot "$Platform\$Configuration" @@ -117,53 +183,10 @@ if ($Clean) { Remove-Item $buildOutputPath -Recurse -Force -ErrorAction Ignore } - Write-Host "[CLEAN] Cleaning all build artifacts (git clean -Xfd)..." - Push-Location $repoRoot - try { - git clean -Xfd | Out-Null - } catch { - Write-Warning "[CLEAN] git clean failed: $_" - } finally { - Pop-Location - } - Write-Host "[CLEAN] Cleaning solution (msbuild /t:Clean)..." RunMSBuild 'PowerToys.slnx' '/t:Clean' $Platform $Configuration } -# Git Stash Logic to handle workspace cleanup -$stashedChanges = $false -$scriptPathRelative = "tools/build/build-installer.ps1" - -# Calculate relative path of this script to exclude it from stash/reset -$currentScriptPath = $MyInvocation.MyCommand.Definition -if ($currentScriptPath.StartsWith($repoRoot)) { - $scriptPathRelative = $currentScriptPath.Substring($repoRoot.Length).TrimStart('\', '/') - $scriptPathRelative = $scriptPathRelative -replace '\\', '/' -} - -Push-Location $repoRoot -try { - $gitStatus = git status --porcelain - if ($gitStatus.Length -gt 0) { - Write-Host "[GIT] Uncommitted changes detected. Stashing (excluding this script)..." - $stashCountBefore = (git stash list).Count - - # Exclude the current script from stash so we don't revert it while running - git stash push --include-untracked -m "PowerToys Build Auto-Stash" -- . ":(exclude)$scriptPathRelative" - - $stashCountAfter = (git stash list).Count - if ($stashCountAfter -gt $stashCountBefore) { - $stashedChanges = $true - Write-Host "[GIT] Changes stashed." - } else { - Write-Host "[GIT] No changes to stash (likely only this script is modified)." - } - } -} finally { - Pop-Location -} - try { if ($Version) { Write-Host "[VERSION] Setting PowerToys version to $Version using versionSetting.ps1..." @@ -384,28 +407,7 @@ try { RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /m /t:PowerToysBootstrapperVNext /p:PerUser=$PerUser" $Platform $Configuration } finally { - # Restore workspace state using Git - Write-Host "[GIT] Cleaning up build artifacts..." - Push-Location $repoRoot - try { - # Revert all changes EXCEPT the script itself - # This cleans up Version.props, AppxManifests, etc. - git checkout HEAD -- . ":(exclude)$scriptPathRelative" - - # Remove untracked files (generated manifests, etc.) - # -f: force, -d: remove directories, -q: quiet - git clean -fd -q - - if ($stashedChanges) { - Write-Host "[GIT] Restoring stashed changes..." - git stash pop --index - if ($LASTEXITCODE -ne 0) { - Write-Warning "[GIT] 'git stash pop' reported conflicts or errors. Your changes are in the stash list." - } - } - } finally { - Pop-Location - } + # No git cleanup; leave workspace state as-is. } Write-Host '[PIPELINE] Completed'