diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index a60ecf2414..a7f69137d9 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1029,6 +1029,7 @@ NOSIZE NOTIFICATIONSDLL NOTIFYICONDATAW NOTIMPL +notlike NOTOPMOST NOTRACK NOTSRCCOPY diff --git a/.gitignore b/.gitignore index f8390b9d56..89541c3a2e 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,8 @@ bld/ # Visual Studio 2017 auto generated files Generated\ Files/ +Generated/ + # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* diff --git a/.pipelines/ESRPSigning_DSC.json b/.pipelines/ESRPSigning_DSC.json new file mode 100644 index 0000000000..438cc14c40 --- /dev/null +++ b/.pipelines/ESRPSigning_DSC.json @@ -0,0 +1,51 @@ +{ + "Version": "1.0.0", + "UseMinimatch": false, + "SignBatches": [ + { + "MatchedPath": [ + "Microsoft.PowerToys.Configure.psm1", + "Microsoft.PowerToys.Configure.psd1" + ], + "SigningInfo": { + "Operations": [ + { + "KeyCode": "CP-230012", + "OperationSetCode": "SigntoolSign", + "Parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "Microsoft" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "http://www.microsoft.com" + }, + { + "parameterName": "FileDigest", + "parameterValue": "/fd \"SHA256\"" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + ], + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-230012", + "OperationSetCode": "SigntoolVerify", + "Parameters": [], + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + } + } + ] +} diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index 5c451f4b72..1388566d1f 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -7,7 +7,7 @@ "*.resources.dll", "WinUI3Apps\\Assets\\Settings\\Scripts\\*.ps1", - + "PowerToys.ActionRunner.exe", "PowerToys.Update.exe", "PowerToys.BackgroundActivatorDLL.dll", diff --git a/.pipelines/release.yml b/.pipelines/release.yml index 7f05b3a418..da99729590 100644 --- a/.pipelines/release.yml +++ b/.pipelines/release.yml @@ -349,6 +349,15 @@ extends: batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_core.json' ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' + - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@3 + displayName: Sign DSC Powershell files + inputs: + ConnectedServiceName: 'Terminal/Console/WinAppDriver Team Code Signing Connection' + FolderPath: 'src/dsc/Microsoft.PowerToys.Configure' + signType: batchSigning + batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_DSC.json' + ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' + - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@3 displayName: Sign x86 directshow VCM inputs: diff --git a/PowerToys.sln b/PowerToys.sln index 9f8aac046c..ace30f844d 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -574,6 +574,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UITests-FancyZonesEditor", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FancyZonesEditorCommon", "src\modules\fancyzones\FancyZonesEditorCommon\FancyZonesEditorCommon.csproj", "{C0974915-8A1D-4BF0-977B-9587D3807AB7}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DSC", "DSC", "{557C4636-D7E1-4838-A504-7D19B725EE95}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerToys.Settings.DSC.Schema.Generator", "src\dsc\PowerToys.Settings.DSC.Schema.Generator\PowerToys.Settings.DSC.Schema.Generator.csproj", "{1D6893CB-BC0C-46A8-A76C-9728706CA51A}" + ProjectSection(ProjectDependencies) = postProject + {020A7474-3601-4160-A159-D7B70B77B15F} = {020A7474-3601-4160-A159-D7B70B77B15F} + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -2536,6 +2543,18 @@ Global {C0974915-8A1D-4BF0-977B-9587D3807AB7}.Release|x64.Build.0 = Release|x64 {C0974915-8A1D-4BF0-977B-9587D3807AB7}.Release|x86.ActiveCfg = Release|x64 {C0974915-8A1D-4BF0-977B-9587D3807AB7}.Release|x86.Build.0 = Release|x64 + {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Debug|ARM64.Build.0 = Debug|ARM64 + {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Debug|x64.ActiveCfg = Debug|x64 + {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Debug|x64.Build.0 = Debug|x64 + {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Debug|x86.ActiveCfg = Debug|x64 + {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Debug|x86.Build.0 = Debug|x64 + {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Release|ARM64.ActiveCfg = Release|ARM64 + {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Release|ARM64.Build.0 = Release|ARM64 + {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Release|x64.ActiveCfg = Release|x64 + {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Release|x64.Build.0 = Release|x64 + {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Release|x86.ActiveCfg = Release|x64 + {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Release|x86.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2748,6 +2767,7 @@ Global {FE38FC07-1C05-4B57-ADA3-2FE2F53C6A52} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} {3A9A791E-94A9-49F8-8401-C11CE288D5FB} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} {C0974915-8A1D-4BF0-977B-9587D3807AB7} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} + {1D6893CB-BC0C-46A8-A76C-9728706CA51A} = {557C4636-D7E1-4838-A504-7D19B725EE95} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/doc/devdocs/settingsv2/dsc-configure.md b/doc/devdocs/settingsv2/dsc-configure.md new file mode 100644 index 0000000000..b70315f2f5 --- /dev/null +++ b/doc/devdocs/settingsv2/dsc-configure.md @@ -0,0 +1,98 @@ +# What is it + +We would like to enable our users to use [`winget configure`](https://learn.microsoft.com/en-us/windows/package-manager/winget/configure) command to install PowerToys and configure its settings with a [Winget configuration file](https://learn.microsoft.com/en-us/windows/package-manager/configuration/create). For example: + +```yaml +properties: + resources: + - resource: Microsoft.WinGet.DSC/WinGetPackage + directives: + description: Install PowerToys + allowPrerelease: true + settings: + id: PowerToys (Preview) + source: winget + + - resource: PowerToysConfigure + directives: + description: Configure PowerToys + settings: + ShortcutGuide: + Enabled: false + OverlayOpacity: 1 + FancyZones: + Enabled: true + FancyzonesEditorHotkey: "Shift+Ctrl+Alt+F" + configurationVersion: 0.2.0 +``` + +This should install PowerToys and make `PowerToysConfigure` resource available. We can use it in the same file. + +# How it works + +`PowerToysConfigure` is a [class-based DSC resource](https://learn.microsoft.com/en-us/powershell/dsc/concepts/class-based-resources?view=dsc-2.0). It looks up whether each setting was specified or not by checking whether it's `$null` or `0` for `enum`s and invokes `PowerToys.Settings.exe` with the updated value like so: +``` +PowerToys.Settings.exe set . +``` + +So for the example the config above should perform 3 following invocations: +``` +PowerToys.Settings.exe set ShortcutGuide.Enabled false +PowerToys.Settings.exe set FancyZones.Enabled true +PowerToys.Settings.exe set FancyZones.FancyzonesEditorHotkey "Shift+Ctrl+Alt+F" +``` + +`PowerToys.Settings` uses dotnet reflection capabilities to determine `SettingName` type and tries to convert the supplied `SettingValue` string accordingly. We use `ICmdReprParsable` for custom setting types. + + +# How DSC is implemented + +We use `PowerToys.Settings.DSC.Schema.Generator` to generate the bulk of `PowerToysConfigure.psm1` file. It also uses dotnet reflection capabilities to inspect `PowerToys.Settings.UI.Lib.dll` assembly and generate properties for the modules we have. The actual generation is done as a `PowerToys.Settings.DSC.Schema.Generator.csproj` post-build action. + +# Debugging DSC resources + +First, make sure that PowerShell 7.4+ is installed. Then make sure that you have DSC installed: + +```ps +Install-Module -Name PSDesiredStateConfiguration -RequiredVersion 2.0.7 +``` + +After that, start a new `pwsh` session and `cd` to `src\dsc\Microsoft.PowerToys.Configure\Generated` directory. From there, you should execute: +```ps +$env:PSModulePath += ";$pwd" +``` + +Now build `PowerToys.sln` and **move** `src\dsc\Microsoft.PowerToys.Configure\Microsoft.PowerToys.Configure.psd1` temporarily to `src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\0.0.1\` folder, so it's located alongside with the generated `Microsoft.PowerToys.Configure.psm1`. + +This will allow DSC to discover our DSC Resource module. See [PSModulePath](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_psmodulepath?view=powershell-7.4#long-description) for more info. + +If everything works, you should see that your module is discovered by executing the following command: + +```ps +Get-Module -ListAvailable | grep PowerToys +``` + +The resource itself should also be available: +```ps +Get-DSCResource | grep PowerToys +``` + +Otherwise, you can force-import the module to diagnose issues: + +``` +Import-Module .\Microsoft.PowerToys.Configure.psd1 +``` + +If it's imported successfully, you could also try to invoke it directly: + +```ps +Invoke-DscResource -Name PowerToysConfigure -Method Set -ModuleName Microsoft.PowerToys.Configure -Property @{ Debug = $true; Awake = @{ Enabled = $false; Mode = "TIMED"; IntervalMinutes = "10" } } +``` + +Note that we've supplied `Debug` option, so a `%TEMP\PowerToys.DSC.TestConfigure.txt` is created with the supplied properties, a current timestamp, and other debug output. + +Finally, you can test it with winget by invoking it as such: + +```ps +winget configure .\configuration.dsc.yaml --accept-configuration-agreements --disable-interactivity +``` \ No newline at end of file diff --git a/installer/PowerToysSetup/Core.wxs b/installer/PowerToysSetup/Core.wxs index 1a72d82c10..d49b24357a 100644 --- a/installer/PowerToysSetup/Core.wxs +++ b/installer/PowerToysSetup/Core.wxs @@ -46,6 +46,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index ee15ec090a..8da49101bc 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -395,6 +395,7 @@ + diff --git a/src/common/interop/interop.cpp b/src/common/interop/interop.cpp index 9a8801fa14..a870985e5c 100644 --- a/src/common/interop/interop.cpp +++ b/src/common/interop/interop.cpp @@ -20,7 +20,6 @@ #include - using namespace System; using namespace System::Runtime::InteropServices; using System::Collections::Generic::List; @@ -40,11 +39,17 @@ public delete _map; } - String ^ GetKeyName(DWORD key) { + String ^ GetKeyName(DWORD key) + { return gcnew String(_map->GetKeyName(key).c_str()); } - void Updatelayout() + DWORD GetKeyValue(String ^ name) + { + return _map->GetKeyFromName(msclr::interop::marshal_as(name)); + } + + void Updatelayout() { _map->UpdateLayout(); } @@ -129,13 +134,13 @@ public } static List ^ GetAllActiveMicrophoneDeviceNames() { - auto names = gcnew List(); - for (const auto& device : MicrophoneDevice::getAllActive()) - { - names->Add(gcnew String(device->name().data())); + auto names = gcnew List(); + for (const auto& device : MicrophoneDevice::getAllActive()) + { + names->Add(gcnew String(device->name().data())); + } + return names; } - return names; - } static List ^ GetAllVideoCaptureDeviceNames() { diff --git a/src/dsc/Microsoft.PowerToys.Configure/Microsoft.PowerToys.Configure.psd1 b/src/dsc/Microsoft.PowerToys.Configure/Microsoft.PowerToys.Configure.psd1 new file mode 100644 index 0000000000..40682245b8 --- /dev/null +++ b/src/dsc/Microsoft.PowerToys.Configure/Microsoft.PowerToys.Configure.psd1 @@ -0,0 +1,83 @@ +# +# Module manifest for module 'Microsoft.PowerToys.Configure' +# +# Generated by: Microsoft Corporation +# +# Generated on: 20.11.2023 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'Microsoft.PowerToys.Configure.psm1' + +# Version number of this module. +ModuleVersion = '0.0.1' + +# ID used to uniquely identify this module +GUID = '778ed7a1-489d-4dc9-b0f2-2da3b1fe14cb' + +# Author of this module +Author = 'Microsoft Corporation' + +# Company or vendor of this module +CompanyName = 'Microsoft' + +# Copyright statement for this module +Copyright = '(c) Microsoft Corporation. All rights reserved.' + +# Description of the functionality provided by this module +Description = 'The module enables settings configuration for an installed PowerToys application.' + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = '*' + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = @() + +# Variables to export from this module +VariablesToExport = @() + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = @() + +# DSC resources to export from this module +DscResourcesToExport = @( + 'PowerToysConfigure' +) + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + # ProjectUri = '' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +} + diff --git a/src/dsc/Microsoft.PowerToys.Configure/examples/configuration.dsc.yaml b/src/dsc/Microsoft.PowerToys.Configure/examples/configuration.dsc.yaml new file mode 100644 index 0000000000..900f81034c --- /dev/null +++ b/src/dsc/Microsoft.PowerToys.Configure/examples/configuration.dsc.yaml @@ -0,0 +1,23 @@ +properties: + resources: + # - resource: Microsoft.WinGet.DSC/WinGetPackage + # directives: + # description: Install PowerToys + # allowPrerelease: true + # settings: + # id: PowerToys (Preview) + # source: winget + + - resource: PowerToysConfigure + directives: + description: Configure PowerToys + settings: + ShortcutGuide: + Enabled: false + OverlayOpacity: 50 + FancyZones: + Enabled: true + FancyzonesEditorHotkey: "Shift+Ctrl+Alt+F" + FileLocksmith: + Enabled: false + configurationVersion: 0.2.0 diff --git a/src/dsc/Microsoft.PowerToys.Configure/examples/configureLauncherPlugins.dsc.yaml b/src/dsc/Microsoft.PowerToys.Configure/examples/configureLauncherPlugins.dsc.yaml new file mode 100644 index 0000000000..6aec96f489 --- /dev/null +++ b/src/dsc/Microsoft.PowerToys.Configure/examples/configureLauncherPlugins.dsc.yaml @@ -0,0 +1,53 @@ +properties: + resources: + - resource: PowerToysConfigure + directives: + description: Configure PowerToys + settings: + PowerLauncher: + Enabled: true + Plugins: + - Name: "Calculator" + Disabled: false + - Name: "Folder" + Disabled: false + - Name: "History" + Disabled: false + - Name: "Windows Search" + Disabled: false + - Name: "OneNote" + Disabled: false + - Name: "PowerToys" + Disabled: false + - Name: "Program" + Disabled: false + ActionKeyword: "P:" + IsGlobal: false + - Name: "Registry Plugin" + Disabled: false + - Name: "Service" + Disabled: false + - Name: "Shell" + Disabled: false + - Name: "Windows System Commands" + Disabled: false + - Name: "Time and Date" + Disabled: false + - Name: "Unit Converter" + Disabled: false + - Name: "URI Handler" + Disabled: false + - Name: "Value Generator" + Disabled: false + - Name: "Visual Studio Code Workspaces" + Disabled: false + - Name: "Web Search" + Disabled: false + - Name: "Windows settings" + Disabled: false + - Name: "Windows Terminal" + Disabled: false + - Name: "Window Walker" + Disabled: false + + configurationVersion: 0.2.0 diff --git a/src/dsc/Microsoft.PowerToys.Configure/examples/disableAllModules.dsc.yaml b/src/dsc/Microsoft.PowerToys.Configure/examples/disableAllModules.dsc.yaml new file mode 100644 index 0000000000..5cbcad3e33 --- /dev/null +++ b/src/dsc/Microsoft.PowerToys.Configure/examples/disableAllModules.dsc.yaml @@ -0,0 +1,70 @@ +properties: + resources: + - resource: PowerToysConfigure + directives: + description: Configure PowerToys + settings: + AlwaysOnTop: + Enabled: false + Awake: + Enabled: false + ColorPicker: + Enabled: false + CropAndLock: + Enabled: false + EnvironmentVariables: + Enabled: false + FancyZones: + Enabled: false + FileLocksmith: + Enabled: false + ImageResizer: + Enabled: false + KeyboardManager: + Enabled: false + FindMyMouse: + Enabled: false + MouseHighlighter: + Enabled: false + MouseJump: + Enabled: false + MousePointerCrosshairs: + Enabled: false + MouseWithoutBorders: + Enabled: false + Peek: + Enabled: false + PowerRename: + Enabled: false + PowerLauncher: + Enabled: false + PowerAccent: + Enabled: false + PowerPreview: + EnableSvgPreview: false + EnableSvgThumbnail: false + EnableMdPreview: false + EnableMonacoPreview: false + EnablePdfPreview: false + EnablePdfThumbnail: false + EnableGcodePreview: false + EnableGcodeThumbnail: false + EnableStlThumbnail: false + EnableQoiPreview: false + EnableQoiThumbnail: false + PowerOcr: + Enabled: false + ShortcutGuide: + Enabled: false + VideoConference: + Enabled: false + MeasureTool: + Enabled: false + Hosts: + Enabled: false + PastePlain: + Enabled: false + RegistryPreview: + Enabled: false + + configurationVersion: 0.2.0 diff --git a/src/dsc/Microsoft.PowerToys.Configure/examples/enableAllModules.dsc.yaml b/src/dsc/Microsoft.PowerToys.Configure/examples/enableAllModules.dsc.yaml new file mode 100644 index 0000000000..cfa23df014 --- /dev/null +++ b/src/dsc/Microsoft.PowerToys.Configure/examples/enableAllModules.dsc.yaml @@ -0,0 +1,70 @@ +properties: + resources: + - resource: PowerToysConfigure + directives: + description: Configure PowerToys + settings: + AlwaysOnTop: + Enabled: true + Awake: + Enabled: true + ColorPicker: + Enabled: true + CropAndLock: + Enabled: true + EnvironmentVariables: + Enabled: true + FancyZones: + Enabled: true + FileLocksmith: + Enabled: true + ImageResizer: + Enabled: true + KeyboardManager: + Enabled: true + FindMyMouse: + Enabled: true + MouseHighlighter: + Enabled: true + MouseJump: + Enabled: true + MousePointerCrosshairs: + Enabled: true + MouseWithoutBorders: + Enabled: true + Peek: + Enabled: true + PowerRename: + Enabled: true + PowerLauncher: + Enabled: true + PowerAccent: + Enabled: true + PowerPreview: + EnableSvgPreview: true + EnableSvgThumbnail: true + EnableMdPreview: true + EnableMonacoPreview: true + EnablePdfPreview: true + EnablePdfThumbnail: true + EnableGcodePreview: true + EnableGcodeThumbnail: true + EnableStlThumbnail: true + EnableQoiPreview: true + EnableQoiThumbnail: true + PowerOcr: + Enabled: true + ShortcutGuide: + Enabled: true + VideoConference: + Enabled: true + MeasureTool: + Enabled: true + Hosts: + Enabled: true + PastePlain: + Enabled: true + RegistryPreview: + Enabled: true + + configurationVersion: 0.2.0 diff --git a/src/dsc/PowerToys.Settings.DSC.Schema.Generator/Common.cs b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/Common.cs new file mode 100644 index 0000000000..5bfaee5a5b --- /dev/null +++ b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/Common.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace PowerToys.Settings.DSC.Schema; + +internal sealed class Common +{ + private static string[] TypeParts(string name) + { + return Regex.Split(name.ToLower(CultureInfo.CurrentCulture), @"(? word.Equals("Bool", StringComparison.OrdinalIgnoreCase) || word.Equals("Boolean", StringComparison.OrdinalIgnoreCase)); + } + + internal static bool InferIsInt(Type propertyInfo) + { + return TypeParts(propertyInfo.Name).Any(word => word.Contains("Int", StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/src/dsc/PowerToys.Settings.DSC.Schema.Generator/DSCGeneration.cs b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/DSCGeneration.cs new file mode 100644 index 0000000000..bc6ebe33a7 --- /dev/null +++ b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/DSCGeneration.cs @@ -0,0 +1,357 @@ +// 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.Text.RegularExpressions; +using static PowerToys.Settings.DSC.Schema.Introspection; + +namespace PowerToys.Settings.DSC.Schema; + +internal sealed class DSCGeneration +{ + private static readonly string DoubleNewLine = Environment.NewLine + Environment.NewLine; + + private struct AdditionalPropertiesInfo + { + public string Name; + + public string Type; + } + + private static readonly Dictionary AdditionalPropertiesInfoPerModule = new Dictionary { { "PowerLauncher", new AdditionalPropertiesInfo { Name = "Plugins", Type = "Hashtable[]" } } }; + + private static string EmitEnumDefinition(Type type) + { + var values = string.Empty; + + int i = 0; + foreach (var name in Enum.GetNames(type)) + { + values += " " + name; + + // Nullable enums seem to be not supported by winget, so the workaround is to always start with '1', because by default the values are initialized to zero. That allows us to use zero as a "lack of value" indicator. + if (i == 0) + { + values += " = 1"; + } + + values += Environment.NewLine; + i++; + } + + return $$""" + enum {{type.Name}} { + {{values}}} + """; + } + + private struct PropertyEmitInfo + { + public string Name; + public string Type; + public string Initializer; + public string EqualityOperator; + public string DefaultValue; + + public PropertyEmitInfo(string name, Type property) + { + Name = name; + + bool intLike = Common.InferIsInt(property); + bool boolLike = Common.InferIsBool(property); + + var rawType = "string"; + var isNullable = true; + DefaultValue = "$null"; + EqualityOperator = "-ne"; + Initializer = "= $null"; + + if (intLike) + { + rawType = "int"; + isNullable = false; + } + else if (boolLike) + { + rawType = "bool"; + isNullable = false; + } + else if (property.IsEnum) + { + rawType = property.Name; + isNullable = true; + Initializer = string.Empty; + DefaultValue = "0"; + } + + // For strings + else + { + EqualityOperator = "-notlike"; + DefaultValue = "''"; + } + + // We must make all our properties nullable to be able to detect which of them weren't supplied + Type = isNullable ? rawType : $"Nullable[{rawType}]"; + } + } + + private static string EmitPropertyDefinition(PropertyEmitInfo info) + { + return $$""" + [DscProperty()] [{{info.Type}}] + ${{info.Name}} {{info.Initializer}} +"""; + } + + private static string EmitPropertyApplyChangeStatements(string moduleName, PropertyEmitInfo info, string localPropertyName = null) + { + if (localPropertyName == null) + { + localPropertyName = info.Name; + } + + return $$""" + if ($this.{{localPropertyName}} {{info.EqualityOperator}} {{info.DefaultValue}}) { + $Changes.Value += "set {{moduleName}}.{{info.Name}} `"$($this.{{localPropertyName}})`"" + } + """; + } + + private static string EmitModuleDefinition(SettingsStructure module) + { + bool generalSettings = module.Name == "GeneralSettings"; + + var properties = module.Properties + .Where(property => !property.Value.IsIgnored) + .Select(property => new PropertyEmitInfo(property.Key, property.Value.Type)); + + var propertyDefinitionsBlock = string.Empty; + var applyChangesBlock = string.Empty; + + foreach (var property in properties) + { + var definition = EmitPropertyDefinition(property); + var applyChanges = EmitPropertyApplyChangeStatements(module.Name, property); + + propertyDefinitionsBlock += definition + DoubleNewLine; + applyChangesBlock += applyChanges + DoubleNewLine; + } + + bool hasAdditionalProperties = AdditionalPropertiesInfoPerModule.TryGetValue(module.Name, out var additionalPropertiesInfo); + + // Enabled property of each module is contained in General settings + if (!generalSettings) + { + propertyDefinitionsBlock += $$""" + [DscProperty(Key)] [Nullable[bool]] + $Enabled = $null + + """; + + if (hasAdditionalProperties) + { + propertyDefinitionsBlock += $$""" + + [DscProperty()] [{{additionalPropertiesInfo.Type}}] + ${{additionalPropertiesInfo.Name}} = @() + + + """; + } + + applyChangesBlock += EmitPropertyApplyChangeStatements("General.Enabled", new PropertyEmitInfo($"{module.Name}", typeof(bool)), "Enabled"); + } + + var additionalPropertiesCheckBlock = string.Empty; + if (hasAdditionalProperties) + { + additionalPropertiesCheckBlock = $$""" + if ($this.{{additionalPropertiesInfo.Name}}.Count -gt 0) { + $AdditionalPropertiesTmpPath = [System.IO.Path]::GetTempFileName() + $this.{{additionalPropertiesInfo.Name}} | ConvertTo-Json | Set-Content -Path $AdditionalPropertiesTmpPath + $Changes.Value += "setAdditional {{module.Name}} `"$AdditionalPropertiesTmpPath`"" + } + """; + } + + return $$""" +class {{module.Name}} { +{{propertyDefinitionsBlock}} ApplyChanges([ref]$Changes) { +{{applyChangesBlock}} + +{{additionalPropertiesCheckBlock}} + } +} + + +"""; + } + + public static string EmitModuleFileContents(SettingsStructure[] moduleSettings, SettingsStructure generalSettings, string debugSettingsPath) + { + var enumsToEmit = new HashSet(); + + var modulesBlock = string.Empty; + var modulesResourcePropertiesBlock = string.Empty; + var applyModulesChangesBlock = string.Empty; + + foreach (var module in moduleSettings.Append(generalSettings)) + { + enumsToEmit.UnionWith(module.Properties + .Where(property => property.Value.Type.IsEnum) + .Select(property => property.Value.Type)); + + modulesBlock += EmitModuleDefinition(module); + + applyModulesChangesBlock += $$""" + $this.{{module.Name}}.ApplyChanges([ref]$ChangesToApply) + + """; + + modulesResourcePropertiesBlock += $$""" + [DscProperty()] + [{{module.Name}}]${{module.Name}} = [{{module.Name}}]::new() + + + """; + } + + var enumsBlock = string.Join(DoubleNewLine, enumsToEmit.Select(EmitEnumDefinition)); + var version = interop.CommonManaged.GetProductVersion().Replace("v", string.Empty); + + return $$""" + #region enums + enum PowerToysConfigureEnsure { + Absent + Present + } + + {{enumsBlock}} + #endregion enums + + #region DscResources + {{modulesBlock}} + [DscResource()] + class PowerToysConfigure { + [DscProperty(Key)] [PowerToysConfigureEnsure] + $Ensure = [PowerToysConfigureEnsure]::Present + + [bool] $Debug = $false + + {{modulesResourcePropertiesBlock}} + [string] GetPowerToysSettingsPath() { + # Obtain PowerToys install location + if ($this.Debug -eq $true) { + $SettingsExePath = "{{debugSettingsPath}}" + } else { + $installation = Get-WmiObject Win32_Product | Where-Object {$_.Name -eq "PowerToys (Preview)" -and $_.Version -eq "{{version}}"} + + if ($installation) { + $SettingsExePath = Join-Path (Join-Path $installation.InstallLocation WinUI3Apps) PowerToys.Settings.exe + # Handle spaces in the path + $SettingsExePath = "`"$SettingsExePath`"" + } else { + throw "PowerToys installation wasn't found." + } + } + + return $SettingsExePath + } + + [PowerToysConfigure] Get() { + $CurrentState = [PowerToysConfigure]::new() + $SettingsExePath = $this.GetPowerToysSettingsPath() + $SettingsTmpFilePath = [System.IO.Path]::GetTempFileName() + + $SettingsToRequest = @{} + foreach ($module in $CurrentState.PSObject.Properties) { + $moduleName = $module.Name + # Skip utility properties + if ($moduleName -eq "Ensure" -or $moduleName -eq "Debug") { + continue + } + + $moduleProperties = $module.Value + $propertiesArray = @() + foreach ($property in $moduleProperties.PSObject.Properties) { + $propertyName = $property.Name + # Skip Enabled properties - they should be requested from GeneralSettings + if ($propertyName -eq "Enabled") { + continue + } + + $propertiesArray += $propertyName + } + + $SettingsToRequest[$moduleName] = $propertiesArray + } + + $settingsJson = $SettingsToRequest | ConvertTo-Json + $settingsJson | Set-Content -Path $SettingsTmpFilePath + + Start-Process -FilePath $SettingsExePath -Wait -Args "get `"$SettingsTmpFilePath`"" + $SettingsValues = Get-Content -Path $SettingsTmpFilePath -Raw + + if ($this.Debug -eq $true) { + $TempFilePath = Join-Path -Path $env:TEMP -ChildPath "PowerToys.DSC.TestConfigure.txt" + Set-Content -Path "$TempFilePath" -Value ("Requested:`r`n" + $settingsJson + "`r`n" + "Got:`r`n" + $SettingsValues + "`r`n" + (Get-Date -Format "o")) -Force + } + + $SettingsValues = $SettingsValues | ConvertFrom-Json + foreach ($module in $SettingsValues.PSObject.Properties) { + $moduleName = $module.Name + $obtainedModuleSettings = $module.Value + $moduleRef = $CurrentState.$moduleName + foreach ($property in $obtainedModuleSettings.PSObject.Properties) { + $propertyName = $property.Name + $moduleRef.$propertyName = $property.Value + } + } + + Remove-Item -Path $SettingsTmpFilePath + + return $CurrentState + } + + [bool] Test() { + # NB: we must always assume that the configuration isn't applied, because changing some settings produce external side-effects + return $false + } + + [void] Set() { + $SettingsExePath = $this.GetPowerToysSettingsPath() + $ChangesToApply = @() + + {{applyModulesChangesBlock}} + if ($this.Debug -eq $true) { + $tmp_info = $ChangesToApply + # $tmp_info = $this | ConvertTo-Json -Depth 10 + + $TempFilePath = Join-Path -Path $env:TEMP -ChildPath "PowerToys.DSC.TestConfigure.txt" + Set-Content -Path "$TempFilePath" -Value ($tmp_info + "`r`n" + (Get-Date -Format "o")) -Force + } + + # Stop any running PowerToys instances + Stop-Process -Name "PowerToys.Settings" -PassThru | Wait-Process + $PowerToysProcessStopped = Stop-Process -Name "PowerToys" -PassThru + $PowerToysProcessStopped | Wait-Process + + foreach ($change in $ChangesToApply) { + Start-Process -FilePath $SettingsExePath -Wait -Args "$change" + } + + # If the PowerToys was stopped, restart it. + if ($PowerToysProcessStopped -ne $null) { + Start-Process -FilePath $SettingsExePath + } + } + } + #endregion DscResources + """; + } +} diff --git a/src/dsc/PowerToys.Settings.DSC.Schema.Generator/DocumentationGeneration.cs b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/DocumentationGeneration.cs new file mode 100644 index 0000000000..ffa42b3d7d --- /dev/null +++ b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/DocumentationGeneration.cs @@ -0,0 +1,79 @@ +// 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 static PowerToys.Settings.DSC.Schema.Introspection; + +namespace PowerToys.Settings.DSC.Schema; + +internal sealed class DocumentationGeneration +{ + private static readonly string IsAvailableSymbol = "✅"; + private static readonly string IsUnavailableSymbol = "❌"; + private static readonly string MissingValueIndicator = "—"; + + private static readonly string PropertySuffix = "Property"; + + private static string SimplifyPropertyType(string typeName) + { + if (typeName.EndsWith(PropertySuffix, StringComparison.InvariantCulture)) + { + typeName = typeName.Remove(typeName.LastIndexOf(PropertySuffix, StringComparison.InvariantCulture), PropertySuffix.Length); + } + + return typeName; + } + + private static string EmitPropertyTableLine(string name, ModulePropertyStructure info) + { + bool isAvailable = !info.IsIgnored; + var availabilitySymbol = isAvailable ? IsAvailableSymbol : IsUnavailableSymbol; + var documentation = MissingValueIndicator; + if (info.Type.IsEnum) + { + documentation = "Possible values: "; + foreach (var enumValue in Enum.GetValues(info.Type)) + { + documentation += enumValue.ToString() + ' '; + } + } + + var propertyType = isAvailable ? SimplifyPropertyType(info.Type.Name) : MissingValueIndicator; + return $"| {name} | {propertyType} | {documentation} | {availabilitySymbol} |"; + } + + private static string EmitModulePropertiesTable(SettingsStructure module) + { + bool generalSettings = module.Name == "GeneralSettings"; + + var properties = module.Properties + .Where(p => !p.Value.IsIgnoredByJsonSerializer) + .Select(property => EmitPropertyTableLine(property.Key, property.Value)).Aggregate((acc, line) => string.Join(Environment.NewLine, [acc, line])); + + var propertyDefinitionsBlock = string.Empty; + var applyChangesBlock = string.Empty; + return $$""" +### {{module.Name}} + +| Name | Type | Description | Available | +| :--- | :--- | :--- | :--- | +{{properties}} + + +"""; + } + + public static string EmitDocumentationFileContents(SettingsStructure[] moduleSettings, SettingsStructure generalSettings) + { + var moduleTables = string.Empty; + + foreach (var module in moduleSettings.Append(generalSettings)) + { + moduleTables += EmitModulePropertiesTable(module); + } + + return moduleTables; + } +} diff --git a/src/dsc/PowerToys.Settings.DSC.Schema.Generator/Introspection.cs b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/Introspection.cs new file mode 100644 index 0000000000..c004cc5003 --- /dev/null +++ b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/Introspection.cs @@ -0,0 +1,93 @@ +// 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 System.Reflection; +using System.Text.Json.Serialization; +using Settings.UI.Library.Attributes; + +namespace PowerToys.Settings.DSC.Schema; + +public class Introspection +{ + public struct ModulePropertyStructure + { + public bool IsIgnoredByJsonSerializer; + public bool IsIgnoredByCmdConfigureAttribute; + + public bool IsIgnored + { + get { return IsIgnoredByJsonSerializer || IsIgnoredByCmdConfigureAttribute; } + } + + public Type Type; + } + + public struct SettingsStructure + { + public string Name; + public Dictionary Properties; + } + + private static bool IsModuleNameField(FieldInfo info) + { + return info != null && info.IsLiteral && !info.IsInitOnly + && info.FieldType == typeof(string); + } + + private static bool IsSettingsClassType(Type type) + { + return type.IsClass && type.FullName.EndsWith("Settings", StringComparison.InvariantCulture); + } + + private static Dictionary ParseProperties(Type propertiesType) + { + return propertiesType.GetProperties().Select(property => + { + var jsonIgnoreAttr = property.GetCustomAttribute(); + var cmdIgnoreAttr = property.GetCustomAttribute(); + + return (property.Name, new ModulePropertyStructure + { + Type = property.PropertyType, + IsIgnoredByJsonSerializer = jsonIgnoreAttr != null, + IsIgnoredByCmdConfigureAttribute = cmdIgnoreAttr != null, + }); + }).ToDictionary(); + } + + public static SettingsStructure ParseGeneralSettings(Assembly assembly) + { + return assembly + .GetTypes() + .Where(IsSettingsClassType) + .Where(type => type.Name == "GeneralSettings") + .Select(type => new SettingsStructure + { + Name = type.Name, + Properties = ParseProperties(type), + }).FirstOrDefault(); + } + + public static SettingsStructure[] ParseModuleSettings(Assembly assembly) + { + return assembly + .GetTypes() + .Where(IsSettingsClassType) + .Select(type => new + { + Properties = type.GetProperty("Properties"), + ModuleNameInfo = type.GetField("ModuleName", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy), + TypeName = type.Name, + }) + .Where(x => x.Properties?.PropertyType.IsClass == true && IsModuleNameField(x.ModuleNameInfo)) + .Select(x => new SettingsStructure + { + Name = x.TypeName.Replace("Settings", string.Empty), + Properties = ParseProperties(x.Properties.PropertyType), + }) + .ToArray(); + } +} diff --git a/src/dsc/PowerToys.Settings.DSC.Schema.Generator/PowerToys.Settings.DSC.Schema.Generator.csproj b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/PowerToys.Settings.DSC.Schema.Generator.csproj new file mode 100644 index 0000000000..e13717e8f8 --- /dev/null +++ b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/PowerToys.Settings.DSC.Schema.Generator.csproj @@ -0,0 +1,65 @@ + + + + Exe + net8.0-windows + PowerToys.Settings.DSC.Schema + app.manifest + win-x64;win-arm64 + None + false + false + true + + + + + win-x64 + + + win-arm64 + + + + + True + CA1720 + False + full + true + DEBUG;TRACE + + + + + True + CA1720 + true + + + + + $(OutDir) + false + + + + + + + + + "$(ProjectDir)..\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(Version)\Microsoft.PowerToys.Configure.psm1" + + + + + + + + + + + + + diff --git a/src/dsc/PowerToys.Settings.DSC.Schema.Generator/Program.cs b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/Program.cs new file mode 100644 index 0000000000..43042f8e69 --- /dev/null +++ b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/Program.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 System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace PowerToys.Settings.DSC.Schema; + +internal sealed class Program +{ + public static int Main(string[] args) + { + if (args.Length != 2) + { + Console.WriteLine("Usage: Generator.exe "); + return 1; + } + + var dllPath = args[0]; + var outputPath = args[1]; + + bool documentationMode = Path.GetExtension(outputPath) == ".md"; + bool sampleMode = Path.GetExtension(outputPath) == ".yaml"; + + try + { + Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + + var assembly = Assembly.LoadFrom(dllPath); + var moduleSettings = Introspection.ParseModuleSettings(assembly); + var generalSettings = Introspection.ParseGeneralSettings(assembly); +#if DEBUG + PrintUniquePropertyTypes(moduleSettings); +#endif + var outputFileContents = string.Empty; + if (documentationMode) + { + outputFileContents = DocumentationGeneration.EmitDocumentationFileContents(moduleSettings, generalSettings); + } + else if (sampleMode) + { + outputFileContents = SampleGeneration.EmitSampleFileContents(moduleSettings, generalSettings); + } + else + { + var debugSettingsPath = Path.Combine(Directory.GetParent(dllPath).FullName, "PowerToys.Settings.exe"); + outputFileContents = DSCGeneration.EmitModuleFileContents(moduleSettings, generalSettings, debugSettingsPath); + } + + File.WriteAllText(outputPath, outputFileContents); + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + return 1; + } + + return 0; + } + + private static void PrintUniquePropertyTypes(Introspection.SettingsStructure[] moduleSettings) + { + Console.WriteLine("Detected the following module properties types:"); + var propertyTypes = new HashSet(); + foreach (var settings in moduleSettings) + { + Console.WriteLine($"{settings.Name}"); + foreach (var (_, property) in settings.Properties) + { + if (!property.IsIgnored) + { + propertyTypes.Add(property.Type); + } + } + } + + Console.WriteLine("\nDetected the following unique property types:"); + foreach (var type in propertyTypes) + { + Console.WriteLine($"{type}"); + } + } +} diff --git a/src/dsc/PowerToys.Settings.DSC.Schema.Generator/SampleGeneration.cs b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/SampleGeneration.cs new file mode 100644 index 0000000000..916dc45d86 --- /dev/null +++ b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/SampleGeneration.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using System.Linq; +using static PowerToys.Settings.DSC.Schema.Introspection; + +namespace PowerToys.Settings.DSC.Schema; + +internal sealed class SampleGeneration +{ + private const int FixedSeed = 12345; + private static readonly Random _random = new Random(FixedSeed); + + private static string EmitPropertySetter(string name, ModulePropertyStructure info) + { + var randomPropertyValue = "\"\""; + if (Common.InferIsBool(info.Type)) + { + randomPropertyValue = _random.Next(2) == 1 ? "true" : "false"; + } + else if (Common.InferIsInt(info.Type)) + { + randomPropertyValue = _random.Next(256).ToString(CultureInfo.InvariantCulture); + } + else if (info.Type.IsEnum) + { + var enumValues = Enum.GetValues(info.Type); + randomPropertyValue = enumValues.GetValue(_random.Next(enumValues.Length)).ToString(); + } + + return $" {name}: {randomPropertyValue}"; + } + + private static string EmitModulePropertiesSection(SettingsStructure module) + { + bool generalSettings = module.Name == "GeneralSettings"; + + var propertiesCollection = module.Properties + .Where(p => !p.Value.IsIgnored) + .Select(property => EmitPropertySetter(property.Key, property.Value)) + .ToList(); + + string properties = propertiesCollection.Count != 0 + ? propertiesCollection.Aggregate((acc, line) => string.Join(Environment.NewLine, acc, line)) + : string.Empty; + + var propertyDefinitionsBlock = string.Empty; + var applyChangesBlock = string.Empty; + return $$""" + {{module.Name}}: +{{properties}} + + +"""; + } + + public static string EmitSampleFileContents(SettingsStructure[] moduleSettings, SettingsStructure generalSettings) + { + var moduleTables = $$""" +properties: +resources: + - resource: PowerToysConfigure + directives: + description: Configure PowerToys + settings: + +"""; + + foreach (var module in moduleSettings.Append(generalSettings)) + { + moduleTables += EmitModulePropertiesSection(module); + } + + moduleTables += " configurationVersion: 0.2.0"; + return moduleTables; + } +} diff --git a/src/dsc/PowerToys.Settings.DSC.Schema.Generator/app.manifest b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/app.manifest new file mode 100644 index 0000000000..9742e0b540 --- /dev/null +++ b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/app.manifest @@ -0,0 +1,21 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + + + + + + + diff --git a/src/modules/FileLocksmith/FileLocksmithExt/PowerToysModule.cpp b/src/modules/FileLocksmith/FileLocksmithExt/PowerToysModule.cpp index ee692603d2..78808a9428 100644 --- a/src/modules/FileLocksmith/FileLocksmithExt/PowerToysModule.cpp +++ b/src/modules/FileLocksmith/FileLocksmithExt/PowerToysModule.cpp @@ -90,14 +90,12 @@ public: } m_enabled = true; - save_settings(); } virtual void disable() override { Logger::info(L"File Locksmith disabled"); m_enabled = false; - save_settings(); } virtual bool is_enabled() override @@ -123,7 +121,7 @@ public: } private: - bool m_enabled; + bool m_enabled = false; bool m_extended_only; void init_settings() @@ -136,7 +134,7 @@ private: void save_settings() { auto& settings = FileLocksmithSettingsInstance(); - settings.SetEnabled(m_enabled); + m_enabled = FileLocksmithSettingsInstance().GetEnabled(); settings.SetExtendedContextMenuOnly(m_extended_only); settings.Save(); diff --git a/src/modules/FileLocksmith/FileLocksmithLib/Settings.cpp b/src/modules/FileLocksmith/FileLocksmithLib/Settings.cpp index 64d2f05598..de997144ca 100644 --- a/src/modules/FileLocksmith/FileLocksmithLib/Settings.cpp +++ b/src/modules/FileLocksmith/FileLocksmithLib/Settings.cpp @@ -18,10 +18,12 @@ static bool LastModifiedTime(const std::wstring& filePath, FILETIME* lpFileTime) FileLocksmithSettings::FileLocksmithSettings() { + generalJsonFilePath = PTSettingsHelper::get_powertoys_general_save_file_location(); std::wstring savePath = PTSettingsHelper::get_module_save_folder_location(constants::nonlocalizable::PowerToyKey); std::error_code ec; jsonFilePath = savePath + constants::nonlocalizable::DataFilePath; + RefreshEnabledState(); Load(); } @@ -29,7 +31,6 @@ void FileLocksmithSettings::Save() { json::JsonObject jsonData; - jsonData.SetNamedValue(constants::nonlocalizable::JsonKeyEnabled, json::value(settings.enabled)); jsonData.SetNamedValue(constants::nonlocalizable::JsonKeyShowInExtendedContextMenu, json::value(settings.showInExtendedContextMenu)); json::to_file(jsonFilePath, jsonData); @@ -48,6 +49,32 @@ void FileLocksmithSettings::Load() } } +void FileLocksmithSettings::RefreshEnabledState() +{ + // Load json settings from data file if it is modified in the meantime. + FILETIME lastModifiedTime{}; + if (!(LastModifiedTime(generalJsonFilePath, &lastModifiedTime) && + CompareFileTime(&lastModifiedTime, &lastLoadedGeneralSettingsTime) == 1)) + return; + + lastLoadedGeneralSettingsTime = lastModifiedTime; + + auto json = json::from_file(generalJsonFilePath); + if (!json) + return; + + const json::JsonObject& jsonSettings = json.value(); + try + { + json::JsonObject modulesEnabledState; + json::get(jsonSettings, L"enabled", modulesEnabledState, json::JsonObject{}); + json::get(modulesEnabledState, L"File Locksmith", settings.enabled, true); + } + catch (const winrt::hresult_error&) + { + } +} + void FileLocksmithSettings::Reload() { // Load json settings from data file if it is modified in the meantime. @@ -67,11 +94,6 @@ void FileLocksmithSettings::ParseJson() const json::JsonObject& jsonSettings = json.value(); try { - if (json::has(jsonSettings, constants::nonlocalizable::JsonKeyEnabled, json::JsonValueType::Boolean)) - { - settings.enabled = jsonSettings.GetNamedBoolean(constants::nonlocalizable::JsonKeyEnabled); - } - if (json::has(jsonSettings, constants::nonlocalizable::JsonKeyShowInExtendedContextMenu, json::JsonValueType::Boolean)) { settings.showInExtendedContextMenu = jsonSettings.GetNamedBoolean(constants::nonlocalizable::JsonKeyShowInExtendedContextMenu); diff --git a/src/modules/FileLocksmith/FileLocksmithLib/Settings.h b/src/modules/FileLocksmith/FileLocksmithLib/Settings.h index b99a884bed..2cd3e8ee24 100644 --- a/src/modules/FileLocksmith/FileLocksmithLib/Settings.h +++ b/src/modules/FileLocksmith/FileLocksmithLib/Settings.h @@ -15,16 +15,10 @@ public: return true; if (gpoSetting == powertoys_gpo::gpo_rule_configured_disabled) return false; - Reload(); + RefreshEnabledState(); return settings.enabled; } - inline void SetEnabled(bool enabled) - { - settings.enabled = enabled; - Save(); - } - inline bool GetShowInExtendedContextMenu() const { return settings.showInExtendedContextMenu; @@ -45,12 +39,15 @@ private: bool showInExtendedContextMenu{ false }; }; + void RefreshEnabledState(); void Reload(); void ParseJson(); Settings settings; + std::wstring generalJsonFilePath; std::wstring jsonFilePath; - FILETIME lastLoadedTime; + FILETIME lastLoadedTime{}; + FILETIME lastLoadedGeneralSettingsTime{}; }; FileLocksmithSettings& FileLocksmithSettingsInstance(); diff --git a/src/modules/MouseWithoutBorders/App/Class/Setting.cs b/src/modules/MouseWithoutBorders/App/Class/Setting.cs index edb74665cb..337f82af1f 100644 --- a/src/modules/MouseWithoutBorders/App/Class/Setting.cs +++ b/src/modules/MouseWithoutBorders/App/Class/Setting.cs @@ -26,6 +26,7 @@ using Microsoft.PowerToys.Settings.UI.Library.Utilities; // 2023- Included in PowerToys. // using Microsoft.Win32; +using Settings.UI.Library.Attributes; [module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "MouseWithoutBorders.Properties.Setting.Values.#LoadIntSetting(System.String,System.Int32)", Justification = "Dotnet port with style preservation")] [module: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "MouseWithoutBorders.Properties.Setting.Values.#SaveSetting(System.String,System.Object)", Justification = "Dotnet port with style preservation")] @@ -766,6 +767,7 @@ namespace MouseWithoutBorders.Class } } + [CmdConfigureIgnore] internal bool DrawMouseEx { get diff --git a/src/modules/imageresizer/ImageResizerLib/Settings.cpp b/src/modules/imageresizer/ImageResizerLib/Settings.cpp index 3631ebd68a..1e1e42f2b4 100644 --- a/src/modules/imageresizer/ImageResizerLib/Settings.cpp +++ b/src/modules/imageresizer/ImageResizerLib/Settings.cpp @@ -11,7 +11,8 @@ namespace { const wchar_t c_imageResizerDataFilePath[] = L"\\image-resizer-settings.json"; const wchar_t c_rootRegPath[] = L"Software\\Microsoft\\ImageResizer"; - const wchar_t c_enabled[] = L"Enabled"; + const wchar_t c_enabled[] = L"enabled"; + const wchar_t c_ImageResizer[] = L"Image Resizer"; unsigned int RegReadInteger(const std::wstring& valueName, unsigned int defaultValue) { @@ -45,6 +46,7 @@ namespace CSettings::CSettings() { + generalJsonFilePath = PTSettingsHelper::get_powertoys_general_save_file_location(); std::wstring oldSavePath = PTSettingsHelper::get_module_save_folder_location(ImageResizerConstants::ModuleOldSaveFolderKey); std::wstring savePath = PTSettingsHelper::get_module_save_folder_location(ImageResizerConstants::ModuleSaveFolderKey); std::error_code ec; @@ -62,8 +64,6 @@ void CSettings::Save() { json::JsonObject jsonData; - jsonData.SetNamedValue(c_enabled, json::value(settings.enabled)); - json::to_file(jsonFilePath, jsonData); GetSystemTimeAsFileTime(&lastLoadedTime); } @@ -82,6 +82,32 @@ void CSettings::Load() } } +void CSettings::RefreshEnabledState() +{ + // Load json settings from data file if it is modified in the meantime. + FILETIME lastModifiedTime{}; + if (!(LastModifiedTime(generalJsonFilePath, &lastModifiedTime) && + CompareFileTime(&lastModifiedTime, &lastLoadedGeneralSettingsTime) == 1)) + return; + + lastLoadedGeneralSettingsTime = lastModifiedTime; + + auto json = json::from_file(generalJsonFilePath); + if (!json) + return; + + const json::JsonObject& jsonSettings = json.value(); + try + { + json::JsonObject modulesEnabledState; + json::get(jsonSettings, c_enabled, modulesEnabledState, json::JsonObject{}); + json::get(modulesEnabledState, c_ImageResizer, settings.enabled, true); + } + catch (const winrt::hresult_error&) + { + } +} + void CSettings::Reload() { // Load json settings from data file if it is modified in the meantime. @@ -106,10 +132,7 @@ void CSettings::ParseJson() const json::JsonObject& jsonSettings = json.value(); try { - if (json::has(jsonSettings, c_enabled, json::JsonValueType::Boolean)) - { - settings.enabled = jsonSettings.GetNamedBoolean(c_enabled); - } + // NB: add any new settings here } catch (const winrt::hresult_error&) { diff --git a/src/modules/imageresizer/ImageResizerLib/Settings.h b/src/modules/imageresizer/ImageResizerLib/Settings.h index fa1e15a50f..acd4ea2ac4 100644 --- a/src/modules/imageresizer/ImageResizerLib/Settings.h +++ b/src/modules/imageresizer/ImageResizerLib/Settings.h @@ -1,5 +1,6 @@ #pragma once +#include "pch.h" #include class CSettings @@ -14,16 +15,10 @@ public: return true; if (gpoSetting == powertoys_gpo::gpo_rule_configured_disabled) return false; - Reload(); + RefreshEnabledState(); return settings.enabled; } - inline void SetEnabled(bool enabled) - { - settings.enabled = enabled; - Save(); - } - void Save(); void Load(); @@ -33,13 +28,16 @@ private: bool enabled{ true }; }; + void RefreshEnabledState(); void Reload(); void MigrateFromRegistry(); void ParseJson(); Settings settings; std::wstring jsonFilePath; + std::wstring generalJsonFilePath; FILETIME lastLoadedTime; + FILETIME lastLoadedGeneralSettingsTime{}; }; CSettings& CSettingsInstance(); \ No newline at end of file diff --git a/src/modules/imageresizer/dll/dllmain.cpp b/src/modules/imageresizer/dll/dllmain.cpp index a7ea651e39..4be5bc61f8 100644 --- a/src/modules/imageresizer/dll/dllmain.cpp +++ b/src/modules/imageresizer/dll/dllmain.cpp @@ -37,7 +37,7 @@ class ImageResizerModule : public PowertoyModuleIface { private: // Enabled by default - bool m_enabled = true; + bool m_enabled = false; std::wstring app_name; //contains the non localized key of the powertoy std::wstring app_key; @@ -101,7 +101,6 @@ public: virtual void enable() { m_enabled = true; - CSettingsInstance().SetEnabled(m_enabled); if (package::IsWin11OrGreater()) { @@ -121,7 +120,6 @@ public: virtual void disable() { m_enabled = false; - CSettingsInstance().SetEnabled(m_enabled); Trace::EnableImageResizer(m_enabled); } diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityProvider.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityProvider.cs index 6497da89db..aaf1564404 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityProvider.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityProvider.cs @@ -92,7 +92,7 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys _utilities.Add(new Utility( UtilityKey.PowerOCR, Resources.Text_Extractor, - generalSettings.Enabled.PowerOCR, + generalSettings.Enabled.PowerOcr, (_) => { using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowPowerOCRSharedEvent()); @@ -223,7 +223,7 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys case UtilityKey.ColorPicker: u.Enable(generalSettings.Enabled.ColorPicker); break; case UtilityKey.FancyZones: u.Enable(generalSettings.Enabled.FancyZones); break; case UtilityKey.Hosts: u.Enable(generalSettings.Enabled.Hosts); break; - case UtilityKey.PowerOCR: u.Enable(generalSettings.Enabled.PowerOCR); break; + case UtilityKey.PowerOCR: u.Enable(generalSettings.Enabled.PowerOcr); break; case UtilityKey.MeasureTool: u.Enable(generalSettings.Enabled.MeasureTool); break; case UtilityKey.ShortcutGuide: u.Enable(generalSettings.Enabled.ShortcutGuide); break; case UtilityKey.RegistryPreview: u.Enable(generalSettings.Enabled.RegistryPreview); break; diff --git a/src/modules/launcher/PowerLauncher/SettingsReader.cs b/src/modules/launcher/PowerLauncher/SettingsReader.cs index 29f5bcdf2a..541cd6e5a5 100644 --- a/src/modules/launcher/PowerLauncher/SettingsReader.cs +++ b/src/modules/launcher/PowerLauncher/SettingsReader.cs @@ -266,20 +266,25 @@ namespace PowerLauncher private static void UpdateSettings(PowerLauncherSettings settings) { var defaultPlugins = GetDefaultPluginsSettings().ToDictionary(x => x.Id); + var defaultPluginsByName = GetDefaultPluginsSettings().ToDictionary(x => x.Name); + foreach (PowerLauncherPluginSettings plugin in settings.Plugins) { - if (defaultPlugins.TryGetValue(plugin.Id, out PowerLauncherPluginSettings value)) + PowerLauncherPluginSettings value = null; + if ((plugin.Id != null && defaultPlugins.TryGetValue(plugin.Id, out value)) || (!string.IsNullOrEmpty(plugin.Name) && defaultPluginsByName.TryGetValue(plugin.Name, out value))) { - var additionalOptions = CombineAdditionalOptions(value.AdditionalOptions, plugin.AdditionalOptions); - var enabledPolicyState = GPOWrapper.GetRunPluginEnabledValue(plugin.Id); - plugin.Name = value.Name; + var id = value.Id; + var name = value.Name; + var additionalOptions = plugin.AdditionalOptions != null ? CombineAdditionalOptions(value.AdditionalOptions, plugin.AdditionalOptions) : value.AdditionalOptions; + var enabledPolicyState = GPOWrapper.GetRunPluginEnabledValue(id); + plugin.Name = name; plugin.Description = value.Description; plugin.Author = value.Author; plugin.IconPathDark = value.IconPathDark; plugin.IconPathLight = value.IconPathLight; plugin.EnabledPolicyUiState = (int)enabledPolicyState; - defaultPlugins[plugin.Id] = plugin; - defaultPlugins[plugin.Id].AdditionalOptions = additionalOptions; + defaultPlugins[id] = plugin; + defaultPlugins[id].AdditionalOptions = additionalOptions; } } diff --git a/src/modules/launcher/Wox.Infrastructure/UserSettings/PowerToysRunSettings.cs b/src/modules/launcher/Wox.Infrastructure/UserSettings/PowerToysRunSettings.cs index 0478e63f09..4dac4f4f62 100644 --- a/src/modules/launcher/Wox.Infrastructure/UserSettings/PowerToysRunSettings.cs +++ b/src/modules/launcher/Wox.Infrastructure/UserSettings/PowerToysRunSettings.cs @@ -5,7 +5,6 @@ using System.Text.Json.Serialization; using ManagedCommon; - using Wox.Plugin; namespace Wox.Infrastructure.UserSettings diff --git a/src/modules/powerrename/dll/PowerRenameExt.h b/src/modules/powerrename/dll/PowerRenameExt.h index 5383d88777..e2ffcda85c 100644 --- a/src/modules/powerrename/dll/PowerRenameExt.h +++ b/src/modules/powerrename/dll/PowerRenameExt.h @@ -63,9 +63,6 @@ public: static HRESULT s_CreateInstance(_In_opt_ IUnknown* punkOuter, _In_ REFIID riid, _Outptr_ void** ppv); - static bool SetEnabled(_In_ bool enabled); - static bool IsEnabled(); - private: ~CPowerRenameMenu(); diff --git a/src/modules/powerrename/dll/dllmain.cpp b/src/modules/powerrename/dll/dllmain.cpp index 633632e70d..0506b158c6 100644 --- a/src/modules/powerrename/dll/dllmain.cpp +++ b/src/modules/powerrename/dll/dllmain.cpp @@ -162,7 +162,7 @@ class PowerRenameModule : public PowertoyModuleIface { private: // Enabled by default - bool m_enabled = true; + bool m_enabled = false; std::wstring app_name; //contains the non localized key of the powertoy std::wstring app_key; @@ -202,16 +202,13 @@ public: package::RegisterSparsePackage(path, packageUri); } } - - save_settings(); } // Disable the powertoy virtual void disable() { - Logger::info(L"PowerRename disabled"); m_enabled = false; - save_settings(); + Logger::info(L"PowerRename disabled"); } // Returns if the powertoy is enabled @@ -316,8 +313,6 @@ public: void save_settings() { - CSettingsInstance().SetEnabled(m_enabled); - CSettingsInstance().Save(); Trace::EnablePowerRename(m_enabled); } diff --git a/src/modules/powerrename/lib/Settings.cpp b/src/modules/powerrename/lib/Settings.cpp index 6f90de9d4c..d2deb5cc49 100644 --- a/src/modules/powerrename/lib/Settings.cpp +++ b/src/modules/powerrename/lib/Settings.cpp @@ -33,9 +33,11 @@ namespace CSettings::CSettings() { + generalJsonFilePath = PTSettingsHelper::get_powertoys_general_save_file_location(); std::wstring result = PTSettingsHelper::get_module_save_folder_location(PowerRenameConstants::ModuleKey); - jsonFilePath = result + std::wstring(c_powerRenameDataFilePath); + moduleJsonFilePath = result + std::wstring(c_powerRenameDataFilePath); UIFlagsFilePath = result + std::wstring(c_powerRenameUIFlagsFilePath); + RefreshEnabledState(); Load(); } @@ -43,7 +45,6 @@ void CSettings::Save() { json::JsonObject jsonData; - jsonData.SetNamedValue(c_enabled, json::value(settings.enabled)); jsonData.SetNamedValue(c_showIconOnMenu, json::value(settings.showIconOnMenu)); jsonData.SetNamedValue(c_extendedContextMenuOnly, json::value(settings.extendedContextMenuOnly)); jsonData.SetNamedValue(c_persistState, json::value(settings.persistState)); @@ -51,13 +52,13 @@ void CSettings::Save() jsonData.SetNamedValue(c_maxMRUSize, json::value(settings.maxMRUSize)); jsonData.SetNamedValue(c_useBoostLib, json::value(settings.useBoostLib)); - json::to_file(jsonFilePath, jsonData); + json::to_file(moduleJsonFilePath, jsonData); GetSystemTimeAsFileTime(&lastLoadedTime); } void CSettings::Load() { - if (!std::filesystem::exists(jsonFilePath)) + if (!std::filesystem::exists(moduleJsonFilePath)) { MigrateFromRegistry(); @@ -71,20 +72,35 @@ void CSettings::Load() } } -void CSettings::Reload() +void CSettings::RefreshEnabledState() { // Load json settings from data file if it is modified in the meantime. FILETIME lastModifiedTime{}; - if (LastModifiedTime(jsonFilePath, &lastModifiedTime) && - CompareFileTime(&lastModifiedTime, &lastLoadedTime) == 1) + if (!(LastModifiedTime(generalJsonFilePath, &lastModifiedTime) && + CompareFileTime(&lastModifiedTime, &lastLoadedGeneralSettingsTime) == 1)) + return; + + lastLoadedGeneralSettingsTime = lastModifiedTime; + + auto json = json::from_file(generalJsonFilePath); + if (!json) + return; + + const json::JsonObject& jsonSettings = json.value(); + try + { + json::JsonObject modulesEnabledState; + json::get(jsonSettings, L"enabled", modulesEnabledState, json::JsonObject{}); + json::get(modulesEnabledState, L"PowerRename", settings.enabled, true); + } + catch (const winrt::hresult_error&) { - Load(); } } void CSettings::MigrateFromRegistry() { - settings.enabled = GetRegBoolean(c_enabled, true); + //settings.enabled = GetRegBoolean(c_enabled, true); settings.showIconOnMenu = GetRegBoolean(c_showIconOnMenu, true); settings.extendedContextMenuOnly = GetRegBoolean(c_extendedContextMenuOnly, false); // Disabled by default. settings.persistState = GetRegBoolean(c_persistState, true); @@ -100,16 +116,12 @@ void CSettings::MigrateFromRegistry() void CSettings::ParseJson() { - auto json = json::from_file(jsonFilePath); + auto json = json::from_file(moduleJsonFilePath); if (json) { const json::JsonObject& jsonSettings = json.value(); try { - if (json::has(jsonSettings, c_enabled, json::JsonValueType::Boolean)) - { - settings.enabled = jsonSettings.GetNamedBoolean(c_enabled); - } if (json::has(jsonSettings, c_showIconOnMenu, json::JsonValueType::Boolean)) { settings.showIconOnMenu = jsonSettings.GetNamedBoolean(c_showIconOnMenu); diff --git a/src/modules/powerrename/lib/Settings.h b/src/modules/powerrename/lib/Settings.h index 633b1fca24..ce52f87e0f 100644 --- a/src/modules/powerrename/lib/Settings.h +++ b/src/modules/powerrename/lib/Settings.h @@ -15,16 +15,10 @@ public: return true; if (gpoSetting == powertoys_gpo::gpo_rule_configured_disabled) return false; - Reload(); + RefreshEnabledState(); return settings.enabled; } - inline void SetEnabled(bool enabled) - { - settings.enabled = enabled; - Save(); - } - inline bool GetShowIconOnMenu() const { return settings.showIconOnMenu; @@ -112,7 +106,7 @@ private: unsigned int flags{ 0 }; }; - void Reload(); + void RefreshEnabledState(); void MigrateFromRegistry(); void ParseJson(); @@ -120,9 +114,11 @@ private: void WriteFlags(); Settings settings; - std::wstring jsonFilePath; + std::wstring generalJsonFilePath; + std::wstring moduleJsonFilePath; std::wstring UIFlagsFilePath; - FILETIME lastLoadedTime; + FILETIME lastLoadedTime{}; + FILETIME lastLoadedGeneralSettingsTime{}; }; CSettings& CSettingsInstance(); diff --git a/src/settings-ui/Settings.UI.Library/Attributes/CmdConfigureIgnoreAttribute.cs b/src/settings-ui/Settings.UI.Library/Attributes/CmdConfigureIgnoreAttribute.cs new file mode 100644 index 0000000000..50b0aaa5df --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/Attributes/CmdConfigureIgnoreAttribute.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; + +namespace Settings.UI.Library.Attributes; + +/// +/// Adding this attribute to a property makes it not configurable from the command line. +/// Typical use cases: +/// - Property represents internal module state. +/// - Property has a type that is unwieldy to type as a command line string. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +public class CmdConfigureIgnoreAttribute : Attribute; diff --git a/src/settings-ui/Settings.UI.Library/AwakeProperties.cs b/src/settings-ui/Settings.UI.Library/AwakeProperties.cs index 6c049933c6..f9b0b5e4b4 100644 --- a/src/settings-ui/Settings.UI.Library/AwakeProperties.cs +++ b/src/settings-ui/Settings.UI.Library/AwakeProperties.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { @@ -36,6 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public DateTimeOffset ExpirationDateTime { get; set; } [JsonPropertyName("customTrayTimes")] + [CmdConfigureIgnoreAttribute] public Dictionary CustomTrayTimes { get; set; } } diff --git a/src/settings-ui/Settings.UI.Library/BoolProperty.cs b/src/settings-ui/Settings.UI.Library/BoolProperty.cs index 3744a6ba4e..6842770a79 100644 --- a/src/settings-ui/Settings.UI.Library/BoolProperty.cs +++ b/src/settings-ui/Settings.UI.Library/BoolProperty.cs @@ -7,7 +7,7 @@ using System.Text.Json.Serialization; namespace Microsoft.PowerToys.Settings.UI.Library { - public class BoolProperty + public record BoolProperty : ICmdLineRepresentable { public BoolProperty() { @@ -22,9 +22,28 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("value")] public bool Value { get; set; } + public static bool TryParseFromCmd(string cmd, out object result) + { + result = null; + + if (!bool.TryParse(cmd, out bool value)) + { + return false; + } + + result = new BoolProperty { Value = value }; + return true; + } + public override string ToString() { return JsonSerializer.Serialize(this); } + + public bool TryToCmdRepresentable(out string result) + { + result = Value.ToString(); + return true; + } } } diff --git a/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs b/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs index 6f974faba7..31dcd92440 100644 --- a/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs +++ b/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs @@ -7,11 +7,13 @@ using System.Text.Json; using System.Text.Json.Serialization; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library.Enumerations; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { public class ColorPickerProperties { + [CmdConfigureIgnore] public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x43); public ColorPickerProperties() @@ -43,6 +45,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("changecursor")] [JsonConverter(typeof(BoolPropertyJsonConverter))] + [CmdConfigureIgnoreAttribute] public bool ChangeCursor { get; set; } [JsonPropertyName("copiedcolorrepresentation")] @@ -53,12 +56,15 @@ namespace Microsoft.PowerToys.Settings.UI.Library // Property ColorHistory is not used, the color history is saved separately in the colorHistory.json file [JsonPropertyName("colorhistory")] + [CmdConfigureIgnoreAttribute] public List ColorHistory { get; set; } [JsonPropertyName("colorhistorylimit")] + [CmdConfigureIgnoreAttribute] public int ColorHistoryLimit { get; set; } [JsonPropertyName("visiblecolorformats")] + [CmdConfigureIgnoreAttribute] public Dictionary> VisibleColorFormats { get; set; } [JsonPropertyName("showcolorname")] diff --git a/src/settings-ui/Settings.UI.Library/EnabledModules.cs b/src/settings-ui/Settings.UI.Library/EnabledModules.cs index a9ef81c1a8..ec93d1cd29 100644 --- a/src/settings-ui/Settings.UI.Library/EnabledModules.cs +++ b/src/settings-ui/Settings.UI.Library/EnabledModules.cs @@ -15,6 +15,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library { private Action notifyEnabledChangedAction; + // Default values for enabled modules should match their expected "enabled by default" values. + // Otherwise, a run of DSC on clean settings will not match the expected default result. public EnabledModules() { } @@ -55,7 +57,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library private bool fileExplorerPreview = true; [JsonPropertyName("File Explorer Preview")] - public bool FileExplorerPreview + public bool PowerPreview { get => fileExplorerPreview; set @@ -116,7 +118,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } - private bool keyboardManager = true; + private bool keyboardManager; // defaulting to off [JsonPropertyName("Keyboard Manager")] public bool KeyboardManager @@ -183,7 +185,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } - private bool awake; + private bool awake = true; [JsonPropertyName("Awake")] public bool Awake @@ -199,7 +201,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } - private bool mouseWithoutBorders = true; + private bool mouseWithoutBorders; // defaulting to off [JsonPropertyName("MouseWithoutBorders")] public bool MouseWithoutBorders @@ -247,7 +249,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } - private bool mouseJump = true; + private bool mouseJump; // defaulting to off [JsonPropertyName("MouseJump")] public bool MouseJump @@ -279,7 +281,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } - private bool mousePointerCrosshairs = true; + private bool mousePointerCrosshairs; // defaulting to off [JsonPropertyName("MousePointerCrosshairs")] public bool MousePointerCrosshairs @@ -295,7 +297,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } - private bool powerAccent; + private bool powerAccent; // defaulting to off [JsonPropertyName("QuickAccent")] public bool PowerAccent @@ -311,10 +313,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } - private bool powerOCR = true; + private bool powerOCR; // defaulting to off [JsonPropertyName("TextExtractor")] - public bool PowerOCR + public bool PowerOcr { get => powerOCR; set diff --git a/src/settings-ui/Settings.UI.Library/FZConfigProperties.cs b/src/settings-ui/Settings.UI.Library/FZConfigProperties.cs index 4b3fe2f33d..51a0269523 100644 --- a/src/settings-ui/Settings.UI.Library/FZConfigProperties.cs +++ b/src/settings-ui/Settings.UI.Library/FZConfigProperties.cs @@ -4,6 +4,7 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { @@ -110,6 +111,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public BoolProperty FancyzonesMakeDraggedWindowTransparent { get; set; } [JsonPropertyName("fancyzones_allowPopupWindowSnap")] + [CmdConfigureIgnore] public BoolProperty FancyzonesAllowPopupWindowSnap { get; set; } [JsonPropertyName("fancyzones_allowChildWindowSnap")] diff --git a/src/settings-ui/Settings.UI.Library/FindMyMouseProperties.cs b/src/settings-ui/Settings.UI.Library/FindMyMouseProperties.cs index 2256898b45..b5a8e5555b 100644 --- a/src/settings-ui/Settings.UI.Library/FindMyMouseProperties.cs +++ b/src/settings-ui/Settings.UI.Library/FindMyMouseProperties.cs @@ -3,11 +3,13 @@ // See the LICENSE file in the project root for more information. using System.Text.Json.Serialization; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { public class FindMyMouseProperties { + [CmdConfigureIgnore] public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x46); [JsonPropertyName("activation_method")] diff --git a/src/settings-ui/Settings.UI.Library/GeneralSettings.cs b/src/settings-ui/Settings.UI.Library/GeneralSettings.cs index f8d4030d3d..79e18a07b5 100644 --- a/src/settings-ui/Settings.UI.Library/GeneralSettings.cs +++ b/src/settings-ui/Settings.UI.Library/GeneralSettings.cs @@ -8,6 +8,7 @@ using System.Text.Json.Serialization; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Utilities; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { @@ -18,15 +19,18 @@ namespace Microsoft.PowerToys.Settings.UI.Library public bool Startup { get; set; } // Gets or sets a value indicating whether the powertoy elevated. + [CmdConfigureIgnoreAttribute] [JsonPropertyName("is_elevated")] public bool IsElevated { get; set; } // Gets or sets a value indicating whether powertoys should run elevated. [JsonPropertyName("run_elevated")] + [CmdConfigureIgnoreAttribute] public bool RunElevated { get; set; } // Gets or sets a value indicating whether is admin. [JsonPropertyName("is_admin")] + [CmdConfigureIgnoreAttribute] public bool IsAdmin { get; set; } // Gets or sets a value indicating whether is warnings of elevated apps enabled. @@ -39,16 +43,20 @@ namespace Microsoft.PowerToys.Settings.UI.Library // Gets or sets system theme name. [JsonPropertyName("system_theme")] + [CmdConfigureIgnore] public string SystemTheme { get; set; } // Gets or sets powertoys version number. [JsonPropertyName("powertoys_version")] + [CmdConfigureIgnore] public string PowertoysVersion { get; set; } [JsonPropertyName("action_name")] + [CmdConfigureIgnore] public string CustomActionName { get; set; } [JsonPropertyName("enabled")] + [CmdConfigureIgnore] public EnabledModules Enabled { get; set; } [JsonPropertyName("show_new_updates_toast_notification")] diff --git a/src/settings-ui/Settings.UI.Library/GenericProperty`1.cs b/src/settings-ui/Settings.UI.Library/GenericProperty`1.cs index a8ce9d123f..8a52222806 100644 --- a/src/settings-ui/Settings.UI.Library/GenericProperty`1.cs +++ b/src/settings-ui/Settings.UI.Library/GenericProperty`1.cs @@ -3,10 +3,11 @@ // See the LICENSE file in the project root for more information. using System.Text.Json.Serialization; +using System.Text.RegularExpressions; namespace Microsoft.PowerToys.Settings.UI.Library { - public class GenericProperty + public class GenericProperty : ICmdLineRepresentable { [JsonPropertyName("value")] public T Value { get; set; } @@ -20,5 +21,25 @@ namespace Microsoft.PowerToys.Settings.UI.Library public GenericProperty() { } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "Adding ICmdLineRepresentable support")] + public static bool TryParseFromCmd(string cmd, out object result) + { + result = null; + + if (ICmdLineRepresentable.TryParseFromCmdFor(typeof(T), cmd, out var value)) + { + result = new GenericProperty { Value = (T)value }; + return true; + } + + return false; + } + + public bool TryToCmdRepresentable(out string result) + { + result = Value.ToString(); + return true; + } } } diff --git a/src/settings-ui/Settings.UI.Library/HostsProperties.cs b/src/settings-ui/Settings.UI.Library/HostsProperties.cs index 03bd89f2bd..ea05a34400 100644 --- a/src/settings-ui/Settings.UI.Library/HostsProperties.cs +++ b/src/settings-ui/Settings.UI.Library/HostsProperties.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Text.Json.Serialization; +using Settings.UI.Library.Attributes; using Settings.UI.Library.Enumerations; namespace Microsoft.PowerToys.Settings.UI.Library diff --git a/src/settings-ui/Settings.UI.Library/HotkeySettings.cs b/src/settings-ui/Settings.UI.Library/HotkeySettings.cs index 3172a3be31..38018f59d1 100644 --- a/src/settings-ui/Settings.UI.Library/HotkeySettings.cs +++ b/src/settings-ui/Settings.UI.Library/HotkeySettings.cs @@ -2,14 +2,16 @@ // 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.Text; using System.Text.Json.Serialization; using Microsoft.PowerToys.Settings.UI.Library.Utilities; namespace Microsoft.PowerToys.Settings.UI.Library { - public class HotkeySettings + public record HotkeySettings : ICmdLineRepresentable { private const int VKTAB = 0x09; @@ -39,11 +41,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library Code = code; } - public HotkeySettings Clone() - { - return new HotkeySettings(Win, Ctrl, Alt, Shift, Code); - } - [JsonPropertyName("win")] public bool Win { get; set; } @@ -176,5 +173,72 @@ namespace Microsoft.PowerToys.Settings.UI.Library return false; } + + public static bool TryParseFromCmd(string cmd, out object result) + { + bool win = false, ctrl = false, alt = false, shift = false; + int code = 0; + + var parts = cmd.Split('+'); + foreach (var part in parts) + { + switch (part.Trim().ToLower(CultureInfo.InvariantCulture)) + { + case "win": + win = true; + break; + case "ctrl": + ctrl = true; + break; + case "alt": + alt = true; + break; + case "shift": + shift = true; + break; + default: + if (!TryParseKeyCode(part, out code)) + { + result = null; + return false; + } + + break; + } + } + + result = new HotkeySettings(win, ctrl, alt, shift, code); + return true; + } + + private static bool TryParseKeyCode(string key, out int keyCode) + { + // ASCII symbol + if (key.Length == 1 && char.IsLetterOrDigit(key[0])) + { + keyCode = char.ToUpper(key[0], CultureInfo.InvariantCulture); + return true; + } + + // VK code + else if (key.Length == 4 && key.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + return int.TryParse(key.AsSpan(2), NumberStyles.HexNumber, null, out keyCode); + } + + // Alias + else + { + keyCode = (int)Utilities.Helper.GetKeyValue(key); + return keyCode != 0; + } + } + + public bool TryToCmdRepresentable(out string result) + { + result = ToString(); + result = result.Replace(" ", null); + return true; + } } } diff --git a/src/settings-ui/Settings.UI.Library/ImageResizerProperties.cs b/src/settings-ui/Settings.UI.Library/ImageResizerProperties.cs index ce6d752e2d..8d57055881 100644 --- a/src/settings-ui/Settings.UI.Library/ImageResizerProperties.cs +++ b/src/settings-ui/Settings.UI.Library/ImageResizerProperties.cs @@ -6,6 +6,7 @@ using System; using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { @@ -69,6 +70,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public StringProperty ImageresizerFileName { get; set; } [JsonPropertyName("imageresizer_sizes")] + [CmdConfigureIgnoreAttribute] public ImageResizerSizes ImageresizerSizes { get; set; } [JsonPropertyName("imageresizer_keepDateModified")] @@ -78,6 +80,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public StringProperty ImageresizerFallbackEncoder { get; set; } [JsonPropertyName("imageresizer_customSize")] + [CmdConfigureIgnoreAttribute] public ImageResizerCustomSizeProperty ImageresizerCustomSize { get; set; } public string ToJsonString() diff --git a/src/settings-ui/Settings.UI.Library/IntProperty.cs b/src/settings-ui/Settings.UI.Library/IntProperty.cs index c36950568b..ac6a87ad4a 100644 --- a/src/settings-ui/Settings.UI.Library/IntProperty.cs +++ b/src/settings-ui/Settings.UI.Library/IntProperty.cs @@ -3,13 +3,14 @@ // See the LICENSE file in the project root for more information. using System; +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; namespace Microsoft.PowerToys.Settings.UI.Library { // Represents the configuration property of the settings that store Integer type. - public class IntProperty + public record IntProperty : ICmdLineRepresentable { public IntProperty() { @@ -25,6 +26,19 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("value")] public int Value { get; set; } + public static bool TryParseFromCmd(string cmd, out object result) + { + result = null; + + if (!int.TryParse(cmd, out var value)) + { + return false; + } + + result = new IntProperty { Value = value }; + return true; + } + // Returns a JSON version of the class settings configuration class. public override string ToString() { @@ -40,5 +54,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library { throw new NotImplementedException(); } + + public bool TryToCmdRepresentable(out string result) + { + result = Value.ToString(CultureInfo.InvariantCulture); + return true; + } } } diff --git a/src/settings-ui/Settings.UI.Library/Interfaces/ICmdLineRepresentable.cs b/src/settings-ui/Settings.UI.Library/Interfaces/ICmdLineRepresentable.cs new file mode 100644 index 0000000000..67b90674cd --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/Interfaces/ICmdLineRepresentable.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using System.Reflection; + +namespace Microsoft.PowerToys.Settings.UI.Library; + +/// +/// A helper interface to allow parsing property values from their command line representation. +/// +public interface ICmdLineRepresentable +{ + public static abstract bool TryParseFromCmd(string cmd, out object result); + + public abstract bool TryToCmdRepresentable(out string result); + + public static sealed bool TryToCmdRepresentableFor(Type type, object value, out string result) + { + result = null; + if (!typeof(ICmdLineRepresentable).IsAssignableFrom(type)) + { + throw new ArgumentException($"{type} doesn't implement {nameof(ICmdLineRepresentable)}"); + } + + var method = type.GetMethod(nameof(TryToCmdRepresentable)); + var parameters = new object[] { result }; + if ((bool)method.Invoke(value, parameters)) + { + result = (string)parameters[0]; + return true; + } + + return false; + } + + public static sealed bool TryParseFromCmdFor(Type type, string cmd, out object result) + { + result = null; + if (!typeof(ICmdLineRepresentable).IsAssignableFrom(type)) + { + throw new ArgumentException($"{type} doesn't implement {nameof(ICmdLineRepresentable)}"); + } + + var method = type.GetMethod(nameof(TryParseFromCmd), BindingFlags.Static | BindingFlags.Public); + var parameters = new object[] { cmd, null }; + if ((bool)method.Invoke(null, parameters)) + { + result = parameters[1]; + return true; + } + + return false; + } + + public static sealed object ParseFor(Type type, string cmdRepr) + { + if (type.IsEnum) + { + return Enum.Parse(type, cmdRepr); + } + else if (type.IsPrimitive) + { + if (type == typeof(bool)) + { + return bool.Parse(cmdRepr.ToLowerInvariant()); + } + else + { + // Converts numeric types like Uint32 + return Convert.ChangeType(cmdRepr, type, CultureInfo.InvariantCulture); + } + } + else if (type.IsValueType && type == typeof(DateTimeOffset)) + { + if (DateTimeOffset.TryParse(cmdRepr, out var structResult)) + { + return structResult; + } + + throw new ArgumentException($"Invalid DateTimeOffset format '{cmdRepr}'"); + } + else if (type.IsClass) + { + if (type == typeof(string)) + { + return cmdRepr; + } + else + { + TryParseFromCmdFor(type, cmdRepr, out var classResult); + return classResult; + } + } + + throw new NotImplementedException($"Parsing type {type} is not supported yet"); + } + + public static string ToCmdRepr(Type type, object value) + { + if (type.IsEnum || type.IsPrimitive) + { + return value.ToString(); + } + else if (type.IsValueType && type == typeof(DateTimeOffset)) + { + return ((DateTimeOffset)value).ToString("o"); + } + else if (type.IsClass) + { + if (type == typeof(string)) + { + return (string)value; + } + else + { + TryToCmdRepresentableFor(type, value, out var result); + return result; + } + } + + throw new NotImplementedException($"CmdRepr of {type} is not supported yet"); + } +} diff --git a/src/settings-ui/Settings.UI.Library/KeyBoardKeysProperty.cs b/src/settings-ui/Settings.UI.Library/KeyBoardKeysProperty.cs index 00e8a797e5..28f9e9ee3c 100644 --- a/src/settings-ui/Settings.UI.Library/KeyBoardKeysProperty.cs +++ b/src/settings-ui/Settings.UI.Library/KeyBoardKeysProperty.cs @@ -2,11 +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. +using System.Globalization; using System.Text.Json.Serialization; namespace Microsoft.PowerToys.Settings.UI.Library { - public class KeyboardKeysProperty + public record KeyboardKeysProperty : ICmdLineRepresentable { public KeyboardKeysProperty() { @@ -20,5 +21,24 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("value")] public HotkeySettings Value { get; set; } + + public static bool TryParseFromCmd(string cmd, out object result) + { + if (!HotkeySettings.TryParseFromCmd(cmd, out var hotkey)) + { + result = null; + return false; + } + else + { + result = new KeyboardKeysProperty { Value = (HotkeySettings)hotkey }; + return true; + } + } + + public bool TryToCmdRepresentable(out string result) + { + return Value.TryToCmdRepresentable(out result); + } } } diff --git a/src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs b/src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs index 1c8f17ed7a..86ab049f7c 100644 --- a/src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs +++ b/src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs @@ -5,16 +5,19 @@ using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { public class KeyboardManagerProperties { [JsonPropertyName("activeConfiguration")] + [CmdConfigureIgnoreAttribute] public GenericProperty ActiveConfiguration { get; set; } // List of all Keyboard Configurations. [JsonPropertyName("keyboardConfigurations")] + [CmdConfigureIgnoreAttribute] public GenericProperty> KeyboardConfigurations { get; set; } public KeyboardManagerProperties() diff --git a/src/settings-ui/Settings.UI.Library/MeasureToolProperties.cs b/src/settings-ui/Settings.UI.Library/MeasureToolProperties.cs index 59841978e4..609f027dbe 100644 --- a/src/settings-ui/Settings.UI.Library/MeasureToolProperties.cs +++ b/src/settings-ui/Settings.UI.Library/MeasureToolProperties.cs @@ -4,12 +4,14 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Settings.UI.Library.Attributes; using Settings.UI.Library.Enumerations; namespace Microsoft.PowerToys.Settings.UI.Library { public class MeasureToolProperties { + [CmdConfigureIgnore] public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x4D); public MeasureToolProperties() @@ -35,6 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonConverter(typeof(BoolPropertyJsonConverter))] public bool PerColorChannelEdgeDetection { get; set; } + [CmdConfigureIgnore] public IntProperty UnitsOfMeasure { get; set; } public IntProperty PixelTolerance { get; set; } diff --git a/src/settings-ui/Settings.UI.Library/MouseHighlighterProperties.cs b/src/settings-ui/Settings.UI.Library/MouseHighlighterProperties.cs index e40bed943c..bb847c2e30 100644 --- a/src/settings-ui/Settings.UI.Library/MouseHighlighterProperties.cs +++ b/src/settings-ui/Settings.UI.Library/MouseHighlighterProperties.cs @@ -3,11 +3,13 @@ // See the LICENSE file in the project root for more information. using System.Text.Json.Serialization; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { public class MouseHighlighterProperties { + [CmdConfigureIgnore] public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x48); [JsonPropertyName("activation_shortcut")] @@ -20,6 +22,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public StringProperty RightButtonClickColor { get; set; } [JsonPropertyName("highlight_opacity")] + [CmdConfigureIgnore] public IntProperty HighlightOpacity { get; set; } [JsonPropertyName("always_color")] diff --git a/src/settings-ui/Settings.UI.Library/MouseJumpProperties.cs b/src/settings-ui/Settings.UI.Library/MouseJumpProperties.cs index 6f8b3db52a..c24d949dc1 100644 --- a/src/settings-ui/Settings.UI.Library/MouseJumpProperties.cs +++ b/src/settings-ui/Settings.UI.Library/MouseJumpProperties.cs @@ -3,11 +3,13 @@ // See the LICENSE file in the project root for more information. using System.Text.Json.Serialization; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { public class MouseJumpProperties { + [CmdConfigureIgnore] public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x44); [JsonPropertyName("activation_shortcut")] diff --git a/src/settings-ui/Settings.UI.Library/MouseJumpThumbnailSize.cs b/src/settings-ui/Settings.UI.Library/MouseJumpThumbnailSize.cs index 58af7d3683..2b7d375163 100644 --- a/src/settings-ui/Settings.UI.Library/MouseJumpThumbnailSize.cs +++ b/src/settings-ui/Settings.UI.Library/MouseJumpThumbnailSize.cs @@ -9,7 +9,7 @@ using System.Text.Json.Serialization; namespace Microsoft.PowerToys.Settings.UI.Library { - public class MouseJumpThumbnailSize : INotifyPropertyChanged + public record MouseJumpThumbnailSize : INotifyPropertyChanged, ICmdLineRepresentable { private int _width; private int _height; @@ -64,5 +64,30 @@ namespace Microsoft.PowerToys.Settings.UI.Library { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } + + public static bool TryParseFromCmd(string cmd, out object result) + { + result = null; + + var parts = cmd.Split('x'); + if (parts.Length != 2) + { + return false; + } + + if (int.TryParse(parts[0], out int width) && int.TryParse(parts[1], out int height)) + { + result = new MouseJumpThumbnailSize { Width = width, Height = height }; + return true; + } + + return false; + } + + public bool TryToCmdRepresentable(out string result) + { + result = $"{Width}x{Height}"; + return true; + } } } diff --git a/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsProperties.cs b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsProperties.cs index 6003aa0ade..46c9e4fe03 100644 --- a/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsProperties.cs +++ b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsProperties.cs @@ -3,11 +3,13 @@ // See the LICENSE file in the project root for more information. using System.Text.Json.Serialization; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { public class MousePointerCrosshairsProperties { + [CmdConfigureIgnore] public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, true, false, 0x50); // Win + Alt + P [JsonPropertyName("activation_shortcut")] diff --git a/src/settings-ui/Settings.UI.Library/MouseWithoutBordersProperties.cs b/src/settings-ui/Settings.UI.Library/MouseWithoutBordersProperties.cs index 7ec3604562..5bf4ae53de 100644 --- a/src/settings-ui/Settings.UI.Library/MouseWithoutBordersProperties.cs +++ b/src/settings-ui/Settings.UI.Library/MouseWithoutBordersProperties.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { @@ -23,16 +24,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library public class MouseWithoutBordersProperties : ICloneable { + [CmdConfigureIgnore] public static HotkeySettings DefaultHotKeySwitch2AllPC => new HotkeySettings(); + [CmdConfigureIgnore] public static HotkeySettings DefaultHotKeyLockMachine => new HotkeySettings(true, true, true, false, 0x4C); + [CmdConfigureIgnore] public static HotkeySettings DefaultHotKeyReconnect => new HotkeySettings(true, true, true, false, 0x52); + [CmdConfigureIgnore] public static HotkeySettings DefaultHotKeyToggleEasyMouse => new HotkeySettings(true, true, true, false, 0x45); + [CmdConfigureIgnore] public StringProperty SecurityKey { get; set; } + [CmdConfigureIgnore] [JsonConverter(typeof(BoolPropertyJsonConverter))] public bool UseService { get; set; } @@ -72,42 +79,54 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonConverter(typeof(BoolPropertyJsonConverter))] public bool ShowClipboardAndNetworkStatusMessages { get; set; } + [CmdConfigureIgnoreAttribute] public List MachineMatrixString { get; set; } + [CmdConfigureIgnoreAttribute] public StringProperty MachinePool { get; set; } [JsonConverter(typeof(BoolPropertyJsonConverter))] + [CmdConfigureIgnoreAttribute] public bool MatrixOneRow { get; set; } public IntProperty EasyMouse { get; set; } + [CmdConfigureIgnore] public IntProperty MachineID { get; set; } + [CmdConfigureIgnoreAttribute] public IntProperty LastX { get; set; } + [CmdConfigureIgnoreAttribute] public IntProperty LastY { get; set; } + [CmdConfigureIgnoreAttribute] public IntProperty PackageID { get; set; } [JsonConverter(typeof(BoolPropertyJsonConverter))] + [CmdConfigureIgnoreAttribute] public bool FirstRun { get; set; } public IntProperty HotKeySwitchMachine { get; set; } [ObsoleteAttribute("Use ToggleEasyMouseShortcut instead", false)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [CmdConfigureIgnoreAttribute] public IntProperty HotKeyToggleEasyMouse { get; set; } [ObsoleteAttribute("Use LockMachineShortcut instead", false)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [CmdConfigureIgnoreAttribute] public IntProperty HotKeyLockMachine { get; set; } [ObsoleteAttribute("Use ReconnectShortcut instead", false)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [CmdConfigureIgnoreAttribute] public IntProperty HotKeyReconnect { get; set; } [ObsoleteAttribute("Use Switch2AllPCShortcut instead", false)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [CmdConfigureIgnoreAttribute] public IntProperty HotKeySwitch2AllPC { get; set; } public HotkeySettings ToggleEasyMouseShortcut { get; set; } @@ -118,6 +137,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public HotkeySettings Switch2AllPCShortcut { get; set; } + [CmdConfigureIgnoreAttribute] public IntProperty TCPPort { get; set; } [JsonConverter(typeof(BoolPropertyJsonConverter))] @@ -126,8 +146,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library public StringProperty Name2IP { get; set; } [JsonConverter(typeof(BoolPropertyJsonConverter))] + [CmdConfigureIgnoreAttribute] public bool FirstCtrlShiftS { get; set; } + [CmdConfigureIgnoreAttribute] public StringProperty DeviceID { get; set; } public MouseWithoutBordersProperties() diff --git a/src/settings-ui/Settings.UI.Library/PastePlainProperties.cs b/src/settings-ui/Settings.UI.Library/PastePlainProperties.cs index a2537dd9fd..0238c0e25e 100644 --- a/src/settings-ui/Settings.UI.Library/PastePlainProperties.cs +++ b/src/settings-ui/Settings.UI.Library/PastePlainProperties.cs @@ -3,11 +3,13 @@ // See the LICENSE file in the project root for more information. using System.Text.Json; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { public class PastePlainProperties { + [CmdConfigureIgnore] public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, true, true, false, 0x56); // Ctrl+Win+Alt+V public PastePlainProperties() diff --git a/src/settings-ui/Settings.UI.Library/PeekProperties.cs b/src/settings-ui/Settings.UI.Library/PeekProperties.cs index 8635d184b0..6932177df1 100644 --- a/src/settings-ui/Settings.UI.Library/PeekProperties.cs +++ b/src/settings-ui/Settings.UI.Library/PeekProperties.cs @@ -3,11 +3,13 @@ // See the LICENSE file in the project root for more information. using System.Text.Json; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { public class PeekProperties { + [CmdConfigureIgnore] public HotkeySettings DefaultActivationShortcut => new HotkeySettings(false, true, false, false, 0x20); public PeekProperties() diff --git a/src/settings-ui/Settings.UI.Library/PowerLauncherProperties.cs b/src/settings-ui/Settings.UI.Library/PowerLauncherProperties.cs index 1c8122c45a..efe7e1e446 100644 --- a/src/settings-ui/Settings.UI.Library/PowerLauncherProperties.cs +++ b/src/settings-ui/Settings.UI.Library/PowerLauncherProperties.cs @@ -4,15 +4,18 @@ using System.Text.Json.Serialization; using ManagedCommon; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { public class PowerLauncherProperties { [JsonPropertyName("search_result_preference")] + [CmdConfigureIgnoreAttribute] public string SearchResultPreference { get; set; } [JsonPropertyName("search_type_preference")] + [CmdConfigureIgnoreAttribute] public string SearchTypePreference { get; set; } [JsonPropertyName("maximum_number_of_results")] @@ -22,18 +25,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library public HotkeySettings OpenPowerLauncher { get; set; } [JsonPropertyName("open_file_location")] + [CmdConfigureIgnoreAttribute] public HotkeySettings OpenFileLocation { get; set; } [JsonPropertyName("copy_path_location")] + [CmdConfigureIgnoreAttribute] public HotkeySettings CopyPathLocation { get; set; } [JsonPropertyName("open_console")] + [CmdConfigureIgnoreAttribute] public HotkeySettings OpenConsole { get; set; } [JsonPropertyName("override_win_r_key")] + [CmdConfigureIgnoreAttribute] public bool OverrideWinkeyR { get; set; } [JsonPropertyName("override_win_s_key")] + [CmdConfigureIgnoreAttribute] public bool OverrideWinkeyS { get; set; } [JsonPropertyName("ignore_hotkeys_in_fullscreen")] @@ -49,6 +57,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public Theme Theme { get; set; } [JsonPropertyName("show_plugins_overview")] + [CmdConfigureIgnore] public int ShowPluginsOverview { get; set; } [JsonPropertyName("title_fontsize")] @@ -84,10 +93,13 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("generate_thumbnails_from_files")] public bool GenerateThumbnailsFromFiles { get; set; } + [CmdConfigureIgnoreAttribute] public HotkeySettings DefaultOpenPowerLauncher => new HotkeySettings(false, false, true, false, 32); + [CmdConfigureIgnoreAttribute] public HotkeySettings DefaultOpenFileLocation => new HotkeySettings(); + [CmdConfigureIgnoreAttribute] public HotkeySettings DefaultCopyPathLocation => new HotkeySettings(); public PowerLauncherProperties() diff --git a/src/settings-ui/Settings.UI.Library/PowerOcrProperties.cs b/src/settings-ui/Settings.UI.Library/PowerOcrProperties.cs index 48e1018b71..0ea4fcca54 100644 --- a/src/settings-ui/Settings.UI.Library/PowerOcrProperties.cs +++ b/src/settings-ui/Settings.UI.Library/PowerOcrProperties.cs @@ -3,11 +3,13 @@ // See the LICENSE file in the project root for more information. using System.Text.Json; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { public class PowerOcrProperties { + [CmdConfigureIgnore] public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x54); // Win+Shift+T public PowerOcrProperties() diff --git a/src/settings-ui/Settings.UI.Library/PowerRenameProperties.cs b/src/settings-ui/Settings.UI.Library/PowerRenameProperties.cs index 6cc5bc4b3d..dfe27855ec 100644 --- a/src/settings-ui/Settings.UI.Library/PowerRenameProperties.cs +++ b/src/settings-ui/Settings.UI.Library/PowerRenameProperties.cs @@ -2,7 +2,9 @@ // 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.Text.Json.Serialization; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { @@ -16,12 +18,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library ShowIcon = new BoolProperty(); ExtendedContextMenuOnly = new BoolProperty(); UseBoostLib = new BoolProperty(); - Enabled = new BoolProperty(); } + [ObsoleteAttribute("Now controlled from the general settings", false)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public BoolProperty Enabled { get; set; } [JsonPropertyName("bool_persist_input")] + [CmdConfigureIgnoreAttribute] public BoolProperty PersistState { get; set; } [JsonPropertyName("bool_mru_enabled")] @@ -31,6 +35,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public IntProperty MaxMRUSize { get; set; } [JsonPropertyName("bool_show_icon_on_menu")] + [CmdConfigureIgnoreAttribute] public BoolProperty ShowIcon { get; set; } [JsonPropertyName("bool_show_extended_menu")] diff --git a/src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj b/src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj index 437874b15d..c967118010 100644 --- a/src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj +++ b/src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj @@ -1,8 +1,10 @@  - + + net8.0-windows + win-x64;win-arm64 net8.0-windows $(Version).0 Microsoft Corporation @@ -20,42 +22,42 @@ win-arm64 - - DEBUG;TRACE - full - true - + + DEBUG;TRACE + full + true + - - PreserveNewest - - + + PreserveNewest + + - - - + + + - - - - - + + + + + - - - Resources.resx - True - True - - + + + Resources.resx + True + True + + - - - Designer - Resources.Designer.cs - PublicResXFileCodeGenerator - - + + + Designer + Resources.Designer.cs + PublicResXFileCodeGenerator + + diff --git a/src/settings-ui/Settings.UI.Library/ShortcutGuideProperties.cs b/src/settings-ui/Settings.UI.Library/ShortcutGuideProperties.cs index e942b61ca2..b84fe8b0ae 100644 --- a/src/settings-ui/Settings.UI.Library/ShortcutGuideProperties.cs +++ b/src/settings-ui/Settings.UI.Library/ShortcutGuideProperties.cs @@ -3,11 +3,13 @@ // See the LICENSE file in the project root for more information. using System.Text.Json.Serialization; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { public class ShortcutGuideProperties { + [CmdConfigureIgnore] public HotkeySettings DefaultOpenShortcutGuide => new HotkeySettings(true, false, false, true, 0xBF); public ShortcutGuideProperties() diff --git a/src/settings-ui/Settings.UI.Library/StringProperty.cs b/src/settings-ui/Settings.UI.Library/StringProperty.cs index e3a4261132..8b0d86b177 100644 --- a/src/settings-ui/Settings.UI.Library/StringProperty.cs +++ b/src/settings-ui/Settings.UI.Library/StringProperty.cs @@ -8,7 +8,7 @@ using System.Text.Json.Serialization; namespace Microsoft.PowerToys.Settings.UI.Library { // Represents the configuration property of the settings that store string type. - public class StringProperty + public record StringProperty : ICmdLineRepresentable { public StringProperty() { @@ -35,6 +35,18 @@ namespace Microsoft.PowerToys.Settings.UI.Library return new StringProperty(v); } + public static bool TryParseFromCmd(string cmd, out object result) + { + result = new StringProperty(cmd); + return true; + } + + public bool TryToCmdRepresentable(out string result) + { + result = Value; + return true; + } + public static implicit operator StringProperty(string v) { return new StringProperty(v); diff --git a/src/settings-ui/Settings.UI.Library/Utilities/CommandLineUtils.cs b/src/settings-ui/Settings.UI.Library/Utilities/CommandLineUtils.cs new file mode 100644 index 0000000000..91703a69c6 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/Utilities/CommandLineUtils.cs @@ -0,0 +1,81 @@ +// 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.Reflection; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; + +namespace Microsoft.PowerToys.Settings.UI.Library; + +public class CommandLineUtils +{ + private static Type GetSettingsConfigType(string moduleName, Assembly settingsLibraryAssembly) + { + var settingsClassName = moduleName == "GeneralSettings" ? moduleName : moduleName + "Settings"; + return settingsLibraryAssembly.GetType(typeof(CommandLineUtils).Namespace + "." + settingsClassName); + } + + public static ISettingsConfig GetSettingsConfigFor(string moduleName, ISettingsUtils settingsUtils, Assembly settingsLibraryAssembly) + { + return GetSettingsConfigFor(GetSettingsConfigType(moduleName, settingsLibraryAssembly), settingsUtils); + } + + /// Executes SettingsRepository.GetInstance(settingsUtils).SettingsConfig + public static ISettingsConfig GetSettingsConfigFor(Type moduleSettingsType, ISettingsUtils settingsUtils) + { + var genericSettingsRepositoryType = typeof(SettingsRepository<>); + var moduleSettingsRepositoryType = genericSettingsRepositoryType.MakeGenericType(moduleSettingsType); + + // Note: GeneralSettings is only used here only to satisfy nameof constrains, i.e. the choice of this particular type doesn't have any special significance. + var getInstanceInfo = moduleSettingsRepositoryType.GetMethod(nameof(SettingsRepository.GetInstance)); + var settingsRepository = getInstanceInfo.Invoke(null, new object[] { settingsUtils }); + var settingsConfigProperty = getInstanceInfo.ReturnType.GetProperty(nameof(SettingsRepository.SettingsConfig)); + return settingsConfigProperty.GetValue(settingsRepository) as ISettingsConfig; + } + + public static Assembly GetSettingsAssembly() + { + return AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == "PowerToys.Settings.UI.Lib"); + } + + public static object GetPropertyValue(string propertyName, ISettingsConfig settingsConfig) + { + var (settingInfo, properties) = LocateSetting(propertyName, settingsConfig); + return settingInfo.GetValue(properties); + } + + public static object GetProperties(ISettingsConfig settingsConfig) + { + var settingsType = settingsConfig.GetType(); + if (settingsType == typeof(GeneralSettings)) + { + return settingsConfig; + } + + var settingsConfigInfo = settingsType.GetProperty("Properties"); + return settingsConfigInfo.GetValue(settingsConfig); + } + + public static (PropertyInfo SettingInfo, object Properties) LocateSetting(string propertyName, ISettingsConfig settingsConfig) + { + var properties = GetProperties(settingsConfig); + var propertiesType = properties.GetType(); + if (propertiesType == typeof(GeneralSettings) && propertyName.StartsWith("Enabled.", StringComparison.InvariantCulture)) + { + var moduleNameToToggle = propertyName.Replace("Enabled.", string.Empty); + properties = propertiesType.GetProperty("Enabled").GetValue(properties); + propertiesType = properties.GetType(); + propertyName = moduleNameToToggle; + } + + return (propertiesType.GetProperty(propertyName), properties); + } + + public static PropertyInfo GetSettingPropertyInfo(string propertyName, ISettingsConfig settingsConfig) + { + return LocateSetting(propertyName, settingsConfig).SettingInfo; + } +} diff --git a/src/settings-ui/Settings.UI.Library/Utilities/GetSettingCommandLineCommand.cs b/src/settings-ui/Settings.UI.Library/Utilities/GetSettingCommandLineCommand.cs new file mode 100644 index 0000000000..e74469795e --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/Utilities/GetSettingCommandLineCommand.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; +using System.Collections.Generic; +using System.Globalization; +using System.Text.Json; +using System.Xml; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using Settings.UI.Library.Attributes; + +namespace Microsoft.PowerToys.Settings.UI.Library; + +/// +/// This user flow allows DSC resources to use PowerToys.Settings executable to get settings values by querying them from command line using the following syntax: +/// PowerToys.Settings.exe get +/// +/// Example: PowerToys.Settings.exe get %TEMP%\properties.json +/// `properties.json` file contents: +/// { +/// "AlwaysOnTop": ["FrameEnabled", "FrameAccentColor"], +/// "FancyZones": ["FancyzonesShiftDrag", "FancyzonesShowOnAllMonitors"] +/// } +/// +/// Upon PowerToys.Settings.exe completion, it'll update `properties.json` file to contain something like this: +/// { +/// "AlwaysOnTop": { +/// "FrameEnabled": true, +/// "FrameAccentColor": "#0099cc" +/// }, +/// "FancyZones": { +/// "FancyzonesShiftDrag": true, +/// "FancyzonesShowOnAllMonitors": false +/// } +/// } +/// +public sealed class GetSettingCommandLineCommand +{ + private static JsonSerializerOptions _serializerOptions = new JsonSerializerOptions + { + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; + + public static string Execute(Dictionary> settingNamesForModules) + { + var modulesSettings = new Dictionary>(); + + var settingsAssembly = CommandLineUtils.GetSettingsAssembly(); + var settingsUtils = new SettingsUtils(); + + var enabledModules = SettingsRepository.GetInstance(settingsUtils).SettingsConfig.Enabled; + + foreach (var (moduleName, settings) in settingNamesForModules) + { + var moduleSettings = new Dictionary(); + if (moduleName != nameof(GeneralSettings)) + { + moduleSettings.Add("Enabled", typeof(EnabledModules).GetProperty(moduleName).GetValue(enabledModules)); + } + + var settingsConfig = CommandLineUtils.GetSettingsConfigFor(moduleName, settingsUtils, settingsAssembly); + foreach (var settingName in settings) + { + var value = CommandLineUtils.GetPropertyValue(settingName, settingsConfig); + if (value != null) + { + var cmdReprValue = ICmdLineRepresentable.ToCmdRepr(value.GetType(), value); + moduleSettings.Add(settingName, cmdReprValue); + } + } + + modulesSettings.Add(moduleName, moduleSettings); + } + + return JsonSerializer.Serialize(modulesSettings, _serializerOptions); + } +} diff --git a/src/settings-ui/Settings.UI.Library/Utilities/Helper.cs b/src/settings-ui/Settings.UI.Library/Utilities/Helper.cs index 49f31e4442..af8aa34a40 100644 --- a/src/settings-ui/Settings.UI.Library/Utilities/Helper.cs +++ b/src/settings-ui/Settings.UI.Library/Utilities/Helper.cs @@ -103,6 +103,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Utilities return LayoutMap.GetKeyName(key); } + public static uint GetKeyValue(string key) + { + return LayoutMap.GetKeyValue(key); + } + public static string GetProductVersion() { return interop.CommonManaged.GetProductVersion(); diff --git a/src/settings-ui/Settings.UI.Library/Utilities/SetAdditionalSettingsCommandLineCommand.cs b/src/settings-ui/Settings.UI.Library/Utilities/SetAdditionalSettingsCommandLineCommand.cs new file mode 100644 index 0000000000..2e8d99ec7b --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/Utilities/SetAdditionalSettingsCommandLineCommand.cs @@ -0,0 +1,109 @@ +// 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.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using Settings.UI.Library.Attributes; + +namespace Microsoft.PowerToys.Settings.UI.Library; + +/// +/// This user flow allows DSC resources to use PowerToys.Settings executable to set custom settings values by suppling them from command line using the following syntax: +/// PowerToys.Settings.exe setAdditional +/// +public sealed class SetAdditionalSettingsCommandLineCommand +{ + private static readonly string KeyPropertyName = "Name"; + + private static JsonSerializerOptions _serializerOptions = new JsonSerializerOptions + { + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; + + private struct AdditionalPropertyInfo + { + public string RootPropertyName; + public JsonValueKind RootObjectType; + } + + private static readonly Dictionary SupportedAdditionalPropertiesInfoForModules = new Dictionary { { "PowerLauncher", new AdditionalPropertyInfo { RootPropertyName = "Plugins", RootObjectType = JsonValueKind.Array } } }; + + private static void ExecuteRootArray(JsonElement.ArrayEnumerator properties, IEnumerable currentPropertyValuesArray) + { + // In case it's an array of object -> combine the existing values with the provided + var currentPropertyValueType = currentPropertyValuesArray.FirstOrDefault()?.GetType(); + + object matchedElement = null; + foreach (var arrayElement in properties) + { + var newElementPropertyValues = new Dictionary(); + foreach (var elementProperty in arrayElement.EnumerateObject()) + { + var elementPropertyName = elementProperty.Name; + var elementPropertyType = currentPropertyValueType.GetProperty(elementPropertyName).PropertyType; + var elemePropertyValue = ICmdLineRepresentable.ParseFor(elementPropertyType, elementProperty.Value.ToString()); + if (elementPropertyName == KeyPropertyName) + { + foreach (var currentElementValue in currentPropertyValuesArray) + { + var currentElementType = currentElementValue.GetType(); + var keyPropertyNameInfo = currentElementType.GetProperty(KeyPropertyName); + var keyPropertyValue = keyPropertyNameInfo.GetValue(currentElementValue); + if (string.Equals(keyPropertyValue, elemePropertyValue)) + { + matchedElement = currentElementValue; + break; + } + } + } + else + { + newElementPropertyValues.Add(elementPropertyName, elemePropertyValue); + } + } + + if (matchedElement != null) + { + foreach (var overriddenProperty in newElementPropertyValues) + { + var propertyInfo = currentPropertyValueType.GetProperty(overriddenProperty.Key); + propertyInfo.SetValue(matchedElement, overriddenProperty.Value); + } + } + } + } + + public static void Execute(string moduleName, JsonDocument settings, ISettingsUtils settingsUtils) + { + Assembly settingsLibraryAssembly = CommandLineUtils.GetSettingsAssembly(); + + var settingsConfig = CommandLineUtils.GetSettingsConfigFor(moduleName, settingsUtils, settingsLibraryAssembly); + var settingsConfigType = settingsConfig.GetType(); + + if (!SupportedAdditionalPropertiesInfoForModules.TryGetValue(moduleName, out var additionalPropertiesInfo)) + { + return; + } + + var propertyValueInfo = settingsConfigType.GetProperty(additionalPropertiesInfo.RootPropertyName); + var currentPropertyValue = propertyValueInfo.GetValue(settingsConfig); + + // For now, only a certain data shapes are supported + switch (additionalPropertiesInfo.RootObjectType) + { + case JsonValueKind.Array: + ExecuteRootArray(settings.RootElement.EnumerateArray(), currentPropertyValue as IEnumerable); + break; + default: + throw new NotImplementedException(); + } + + settingsUtils.SaveSettings(settingsConfig.ToJsonString(), settingsConfig.GetModuleName()); + } +} diff --git a/src/settings-ui/Settings.UI.Library/Utilities/SetSettingCommandLineCommand.cs b/src/settings-ui/Settings.UI.Library/Utilities/SetSettingCommandLineCommand.cs new file mode 100644 index 0000000000..1e839a17c8 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/Utilities/SetSettingCommandLineCommand.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; +using System.Reflection; +using Settings.UI.Library.Attributes; + +namespace Microsoft.PowerToys.Settings.UI.Library; + +/// +/// This user flow allows DSC resources to use PowerToys.Settings executable to set settings values by suppling them from command line using the following syntax: +/// PowerToys.Settings.exe set . +/// +/// Example: PowerToys.Settings.exe set MeasureTool.MeasureCrossColor "#00FF00" +/// +public sealed class SetSettingCommandLineCommand +{ + private static readonly char[] SettingNameSeparator = { '.' }; + + private static (string ModuleName, string PropertyName) ParseSettingName(string settingName) + { + var parts = settingName.Split(SettingNameSeparator, 2, StringSplitOptions.RemoveEmptyEntries); + return (parts[0], parts[1]); + } + + public static void Execute(string settingName, string settingValue, ISettingsUtils settingsUtils) + { + Assembly settingsLibraryAssembly = CommandLineUtils.GetSettingsAssembly(); + + var (moduleName, propertyName) = ParseSettingName(settingName); + + var settingsConfig = CommandLineUtils.GetSettingsConfigFor(moduleName, settingsUtils, settingsLibraryAssembly); + + var propertyInfo = CommandLineUtils.GetSettingPropertyInfo(propertyName, settingsConfig); + if (propertyInfo == null) + { + throw new ArgumentException($"Property '{propertyName}' wasn't found"); + } + + if (propertyInfo.PropertyType.GetCustomAttribute() != null) + { + throw new ArgumentException($"Property '{propertyName}' is explicitly ignored"); + } + + // Execute settingsConfig.Properties. = settingValue + var propertyValue = ICmdLineRepresentable.ParseFor(propertyInfo.PropertyType, settingValue); + var (settingInfo, properties) = CommandLineUtils.LocateSetting(propertyName, settingsConfig); + settingInfo.SetValue(properties, propertyValue); + + settingsUtils.SaveSettings(settingsConfig.ToJsonString(), settingsConfig.GetModuleName()); + } +} diff --git a/src/settings-ui/Settings.UI.Library/VideoConferenceConfigProperties.cs b/src/settings-ui/Settings.UI.Library/VideoConferenceConfigProperties.cs index 16a0ab5dd9..c1df38908e 100644 --- a/src/settings-ui/Settings.UI.Library/VideoConferenceConfigProperties.cs +++ b/src/settings-ui/Settings.UI.Library/VideoConferenceConfigProperties.cs @@ -4,11 +4,13 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { public class VideoConferenceConfigProperties { + [CmdConfigureIgnoreAttribute] public HotkeySettings DefaultMuteCameraAndMicrophoneHotkey => new HotkeySettings() { Win = true, @@ -19,6 +21,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library Code = 81, }; + [CmdConfigureIgnoreAttribute] public HotkeySettings DefaultMuteMicrophoneHotkey => new HotkeySettings() { Win = true, @@ -29,6 +32,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library Code = 65, }; + [CmdConfigureIgnoreAttribute] public HotkeySettings DefaultPushToTalkMicrophoneHotkey => new HotkeySettings() { Win = true, @@ -39,6 +43,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library Code = 73, }; + [CmdConfigureIgnoreAttribute] public HotkeySettings DefaultMuteCameraHotkey => new HotkeySettings() { Win = true, diff --git a/src/settings-ui/Settings.UI.Library/VideoConferenceSettings.cs b/src/settings-ui/Settings.UI.Library/VideoConferenceSettings.cs index 53a24cc070..80ead8732e 100644 --- a/src/settings-ui/Settings.UI.Library/VideoConferenceSettings.cs +++ b/src/settings-ui/Settings.UI.Library/VideoConferenceSettings.cs @@ -9,10 +9,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library { public class VideoConferenceSettings : BasePTModuleSettings, ISettingsConfig { + public const string ModuleName = "Video Conference"; + public VideoConferenceSettings() { Version = "1"; - Name = "Video Conference"; + Name = ModuleName; Properties = new VideoConferenceConfigProperties(); } diff --git a/src/settings-ui/Settings.UI.UnitTests/Cmd/ICmdReprParsableTests.cs b/src/settings-ui/Settings.UI.UnitTests/Cmd/ICmdReprParsableTests.cs new file mode 100644 index 0000000000..bf2c7cecdb --- /dev/null +++ b/src/settings-ui/Settings.UI.UnitTests/Cmd/ICmdReprParsableTests.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 Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Settings.UI.UnitTests.Settings; + +[TestClass] +public class ICmdReprParsableTests +{ + [TestMethod] + public void KeyboardKeysPropertyParsing() + { + { + Assert.IsTrue(KeyboardKeysProperty.TryParseFromCmd("win+ctrl+Alt+sHifT+Q", out var hotkey)); + + Assert.AreEqual(new KeyboardKeysProperty { Value = new HotkeySettings(true, true, true, true, 0x51) }, hotkey); + } + + { + Assert.IsTrue(KeyboardKeysProperty.TryParseFromCmd("CTRL+z", out var hotkey)); + Assert.AreEqual(new KeyboardKeysProperty { Value = new HotkeySettings(false, true, false, false, 0x5A) }, hotkey); + } + + { + Assert.IsTrue(KeyboardKeysProperty.TryParseFromCmd("shift+ALT+0x59", out var hotkey)); + Assert.AreEqual(new KeyboardKeysProperty { Value = new HotkeySettings(false, false, true, true, 0x59) }, hotkey); + } + + { + Assert.IsTrue(KeyboardKeysProperty.TryParseFromCmd("alt+Space", out var hotkey)); + Assert.AreEqual(new KeyboardKeysProperty { Value = new HotkeySettings(false, false, true, false, 0x20) }, hotkey); + } + } + + [TestMethod] + public void BoolPropertyParsing() + { + { + Assert.IsTrue(BoolProperty.TryParseFromCmd("True", out var result)); + Assert.AreEqual(new BoolProperty(true), result); + } + + { + Assert.IsTrue(BoolProperty.TryParseFromCmd("false", out var result)); + Assert.AreEqual(new BoolProperty(false), result); + } + } + + [TestMethod] + public void IntPropertyParsing() + { + { + Assert.IsTrue(IntProperty.TryParseFromCmd("123", out var result)); + Assert.AreEqual(new IntProperty(123), result); + } + + { + Assert.IsTrue(IntProperty.TryParseFromCmd("1500", out var result)); + Assert.AreEqual(new IntProperty(1500), result); + Assert.AreNotEqual(new IntProperty(15), result); + } + } + + [TestMethod] + public void MouseJumpThumbnailSizeParsing() + { + { + Assert.IsTrue(MouseJumpThumbnailSize.TryParseFromCmd("1920x1080", out var result)); + Assert.AreEqual(new MouseJumpThumbnailSize { Width = 1920, Height = 1080 }, result); + } + } +} diff --git a/src/settings-ui/Settings.UI.UnitTests/Cmd/SetSettingCommandTests.cs b/src/settings-ui/Settings.UI.UnitTests/Cmd/SetSettingCommandTests.cs new file mode 100644 index 0000000000..572682854e --- /dev/null +++ b/src/settings-ui/Settings.UI.UnitTests/Cmd/SetSettingCommandTests.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO.Abstractions.TestingHelpers; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using static Microsoft.PowerToys.Settings.UI.Library.SetSettingCommandLineCommand; + +namespace Settings.UI.UnitTests.Cmd; + +[TestClass] +public class SetSettingCommandTests +{ + private SettingsUtils settingsUtils; + + [TestInitialize] + public void Setup() + { + settingsUtils = new SettingsUtils(new MockFileSystem()); + } + + private void SetSetting(Type moduleSettingsType, string settingName, string newValueStr) + { + var settings = CommandLineUtils.GetSettingsConfigFor(moduleSettingsType, settingsUtils); + var defaultValue = CommandLineUtils.GetPropertyValue(settingName, settings); + var qualifiedName = moduleSettingsType.Name.Replace("Settings", string.Empty) + "." + settingName; + var type = CommandLineUtils.GetSettingPropertyInfo(settingName, settings).PropertyType; + var newValue = ICmdLineRepresentable.ParseFor(type, newValueStr); + + Execute(qualifiedName, newValueStr, settingsUtils); + + Assert.AreNotEqual(defaultValue, newValue); + Assert.AreEqual(newValue, CommandLineUtils.GetPropertyValue(settingName, settings)); + } + + // Each setting has a different type. + [TestMethod] + [DataRow(typeof(PowerRenameSettings), nameof(PowerRenameProperties.MaxMRUSize), "123")] + [DataRow(typeof(FancyZonesSettings), nameof(FZConfigProperties.FancyzonesBorderColor), "#00FF00")] + [DataRow(typeof(MeasureToolSettings), nameof(MeasureToolProperties.ActivationShortcut), "Ctrl+Alt+Delete")] + [DataRow(typeof(AlwaysOnTopSettings), nameof(AlwaysOnTopProperties.SoundEnabled), "False")] + [DataRow(typeof(PowerAccentSettings), nameof(PowerAccentProperties.ShowUnicodeDescription), "true")] + [DataRow(typeof(AwakeSettings), nameof(AwakeProperties.Mode), "EXPIRABLE")] + [DataRow(typeof(AwakeSettings), nameof(AwakeProperties.ExpirationDateTime), "March 31, 2020 15:00 +00:00")] + [DataRow(typeof(PowerLauncherSettings), nameof(PowerLauncherProperties.MaximumNumberOfResults), "322")] + + [DataRow(typeof(ColorPickerSettings), nameof(ColorPickerProperties.CopiedColorRepresentation), "RGB")] + public void SetModuleSetting(Type moduleSettingsType, string settingName, string newValueStr) + { + SetSetting(moduleSettingsType, settingName, newValueStr); + } + + [DataRow(typeof(GeneralSettings), "Enabled.MouseWithoutBorders", "true")] + [DataRow(typeof(GeneralSettings), nameof(GeneralSettings.AutoDownloadUpdates), "true")] + [TestMethod] + public void SetGeneralSetting(Type moduleSettingsType, string settingName, string newValueStr) + { + SetSetting(moduleSettingsType, settingName, newValueStr); + } +} diff --git a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/General.cs b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/General.cs index bec4597b6a..11eaefb195 100644 --- a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/General.cs +++ b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/General.cs @@ -237,10 +237,9 @@ namespace ViewModelTests // Assert Assert.IsTrue(modules.FancyZones); Assert.IsTrue(modules.ImageResizer); - Assert.IsTrue(modules.FileExplorerPreview); + Assert.IsTrue(modules.PowerPreview); Assert.IsTrue(modules.ShortcutGuide); Assert.IsTrue(modules.PowerRename); - Assert.IsTrue(modules.KeyboardManager); Assert.IsTrue(modules.PowerLauncher); Assert.IsTrue(modules.ColorPicker); } diff --git a/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs b/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs index a1936dfccf..78334d54a8 100644 --- a/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs +++ b/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs @@ -67,7 +67,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers case ModuleType.RegistryPreview: return generalSettingsConfig.Enabled.RegistryPreview; case ModuleType.MeasureTool: return generalSettingsConfig.Enabled.MeasureTool; case ModuleType.ShortcutGuide: return generalSettingsConfig.Enabled.ShortcutGuide; - case ModuleType.PowerOCR: return generalSettingsConfig.Enabled.PowerOCR; + case ModuleType.PowerOCR: return generalSettingsConfig.Enabled.PowerOcr; default: return false; } } @@ -99,7 +99,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers case ModuleType.RegistryPreview: generalSettingsConfig.Enabled.RegistryPreview = isEnabled; break; case ModuleType.MeasureTool: generalSettingsConfig.Enabled.MeasureTool = isEnabled; break; case ModuleType.ShortcutGuide: generalSettingsConfig.Enabled.ShortcutGuide = isEnabled; break; - case ModuleType.PowerOCR: generalSettingsConfig.Enabled.PowerOCR = isEnabled; break; + case ModuleType.PowerOCR: generalSettingsConfig.Enabled.PowerOcr = isEnabled; break; } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs index 00559e4a2e..b00a018e8a 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs @@ -3,9 +3,15 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using System.IO; using System.Linq; +using System.Net.Http.Json; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Common.UI; using interop; @@ -42,8 +48,11 @@ namespace Microsoft.PowerToys.Settings.UI ContainsFlyoutPosition, } - // Quantity of arguments - private const int RequiredArgumentsQty = 12; + private const int RequiredArgumentsSetSettingQty = 4; + private const int RequiredArgumentsSetAdditionalSettingsQty = 4; + private const int RequiredArgumentsGetSettingQty = 3; + + private const int RequiredArgumentsLaunchedFromRunnerQty = 12; // Create an instance of the IPC wrapper. private static TwoWayPipeMessageIPCManaged ipcmanager; @@ -100,6 +109,171 @@ namespace Microsoft.PowerToys.Settings.UI } } + private void OnLaunchedToSetSetting(string[] cmdArgs) + { + var settingName = cmdArgs[2]; + var settingValue = cmdArgs[3]; + try + { + SetSettingCommandLineCommand.Execute(settingName, settingValue, new SettingsUtils()); + } + catch (Exception ex) + { + Logger.LogError($"SetSettingCommandLineCommand exception: '{settingName}' setting couldn't be set to {settingValue}", ex); + } + + Exit(); + } + + private void OnLaunchedToSetAdditionalSetting(string[] cmdArgs) + { + var moduleName = cmdArgs[2]; + var ipcFileName = cmdArgs[3]; + try + { + using (var settings = JsonDocument.Parse(File.ReadAllText(ipcFileName))) + { + SetAdditionalSettingsCommandLineCommand.Execute(moduleName, settings, new SettingsUtils()); + } + } + catch (Exception ex) + { + Logger.LogError($"SetAdditionalSettingsCommandLineCommand exception: couldn't set additional settings for '{moduleName}'", ex); + } + + Exit(); + } + + private void OnLaunchedToGetSetting(string[] cmdArgs) + { + var ipcFileName = cmdArgs[2]; + + try + { + var requestedSettings = JsonSerializer.Deserialize>>(File.ReadAllText(ipcFileName)); + File.WriteAllText(ipcFileName, GetSettingCommandLineCommand.Execute(requestedSettings)); + } + catch (Exception ex) + { + Logger.LogError($"GetSettingCommandLineCommand exception", ex); + } + + Exit(); + } + + private void OnLaunchedFromRunner(string[] cmdArgs) + { + // Skip the first argument which is prepended when launched by explorer + if (cmdArgs[0].EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase) && cmdArgs[1].EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase) && (cmdArgs.Length >= RequiredArgumentsLaunchedFromRunnerQty + 1)) + { + cmdArgs = cmdArgs.Skip(1).ToArray(); + } + + _ = int.TryParse(cmdArgs[(int)Arguments.PTPid], out int powerToysPID); + PowerToysPID = powerToysPID; + + IsElevated = cmdArgs[(int)Arguments.ElevatedStatus] == "true"; + IsUserAnAdmin = cmdArgs[(int)Arguments.IsUserAdmin] == "true"; + ShowOobe = cmdArgs[(int)Arguments.ShowOobeWindow] == "true"; + ShowScoobe = cmdArgs[(int)Arguments.ShowScoobeWindow] == "true"; + ShowFlyout = cmdArgs[(int)Arguments.ShowFlyout] == "true"; + bool containsSettingsWindow = cmdArgs[(int)Arguments.ContainsSettingsWindow] == "true"; + bool containsFlyoutPosition = cmdArgs[(int)Arguments.ContainsFlyoutPosition] == "true"; + + // To keep track of variable arguments + int currentArgumentIndex = RequiredArgumentsLaunchedFromRunnerQty; + + if (containsSettingsWindow) + { + // Open specific window + StartupPage = GetPage(cmdArgs[currentArgumentIndex]); + + currentArgumentIndex++; + } + + int flyout_x = 0; + int flyout_y = 0; + if (containsFlyoutPosition) + { + // get the flyout position arguments + _ = int.TryParse(cmdArgs[currentArgumentIndex++], out flyout_x); + _ = int.TryParse(cmdArgs[currentArgumentIndex++], out flyout_y); + } + + RunnerHelper.WaitForPowerToysRunner(PowerToysPID, () => + { + Environment.Exit(0); + }); + + ipcmanager = new TwoWayPipeMessageIPCManaged(cmdArgs[(int)Arguments.SettingsPipeName], cmdArgs[(int)Arguments.PTPipeName], (string message) => + { + if (IPCMessageReceivedCallback != null && message.Length > 0) + { + IPCMessageReceivedCallback(message); + } + }); + ipcmanager.Start(); + + if (!ShowOobe && !ShowScoobe && !ShowFlyout) + { + settingsWindow = new MainWindow(); + settingsWindow.Activate(); + settingsWindow.ExtendsContentIntoTitleBar = true; + settingsWindow.NavigateToSection(StartupPage); + + // https://github.com/microsoft/microsoft-ui-xaml/issues/7595 - Activate doesn't bring window to the foreground + // Need to call SetForegroundWindow to actually gain focus. + WindowHelpers.BringToForeground(settingsWindow.GetWindowHandle()); + } + else + { + // Create the Settings window hidden so that it's fully initialized and + // it will be ready to receive the notification if the user opens + // the Settings from the tray icon. + settingsWindow = new MainWindow(true); + + if (ShowOobe) + { + PowerToysTelemetry.Log.WriteEvent(new OobeStartedEvent()); + OobeWindow oobeWindow = new OobeWindow(OOBE.Enums.PowerToysModules.Overview); + oobeWindow.Activate(); + oobeWindow.ExtendsContentIntoTitleBar = true; + SetOobeWindow(oobeWindow); + } + else if (ShowScoobe) + { + PowerToysTelemetry.Log.WriteEvent(new ScoobeStartedEvent()); + OobeWindow scoobeWindow = new OobeWindow(OOBE.Enums.PowerToysModules.WhatsNew); + scoobeWindow.Activate(); + scoobeWindow.ExtendsContentIntoTitleBar = true; + SetOobeWindow(scoobeWindow); + } + else if (ShowFlyout) + { + POINT? p = null; + if (containsFlyoutPosition) + { + p = new POINT(flyout_x, flyout_y); + } + + ShellPage.OpenFlyoutCallback(p); + } + } + + if (SelectedTheme() == ElementTheme.Default) + { + try + { + themeListener = new ThemeListener(); + themeListener.ThemeChanged += (_) => HandleThemeChange(); + } + catch (Exception ex) + { + Logger.LogError($"HandleThemeChange exception. Please install .NET 4.", ex); + } + } + } + /// /// Invoked when the application is launched normally by the end user. Other entry points /// will be used such as when the application is launched to open a specific file. @@ -109,117 +283,21 @@ namespace Microsoft.PowerToys.Settings.UI { var cmdArgs = Environment.GetCommandLineArgs(); - if (cmdArgs != null && cmdArgs.Length >= RequiredArgumentsQty) + if (cmdArgs?.Length >= RequiredArgumentsLaunchedFromRunnerQty) { - // Skip the first argument which is prepended when launched by explorer - if (cmdArgs[0].EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase) && cmdArgs[1].EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase) && (cmdArgs.Length >= RequiredArgumentsQty + 1)) - { - cmdArgs = cmdArgs.Skip(1).ToArray(); - } - - _ = int.TryParse(cmdArgs[(int)Arguments.PTPid], out int powerToysPID); - PowerToysPID = powerToysPID; - - IsElevated = cmdArgs[(int)Arguments.ElevatedStatus] == "true"; - IsUserAnAdmin = cmdArgs[(int)Arguments.IsUserAdmin] == "true"; - ShowOobe = cmdArgs[(int)Arguments.ShowOobeWindow] == "true"; - ShowScoobe = cmdArgs[(int)Arguments.ShowScoobeWindow] == "true"; - ShowFlyout = cmdArgs[(int)Arguments.ShowFlyout] == "true"; - bool containsSettingsWindow = cmdArgs[(int)Arguments.ContainsSettingsWindow] == "true"; - bool containsFlyoutPosition = cmdArgs[(int)Arguments.ContainsFlyoutPosition] == "true"; - - // To keep track of variable arguments - int currentArgumentIndex = RequiredArgumentsQty; - - if (containsSettingsWindow) - { - // Open specific window - StartupPage = GetPage(cmdArgs[currentArgumentIndex]); - - currentArgumentIndex++; - } - - int flyout_x = 0; - int flyout_y = 0; - if (containsFlyoutPosition) - { - // get the flyout position arguments - _ = int.TryParse(cmdArgs[currentArgumentIndex++], out flyout_x); - _ = int.TryParse(cmdArgs[currentArgumentIndex++], out flyout_y); - } - - RunnerHelper.WaitForPowerToysRunner(PowerToysPID, () => - { - Environment.Exit(0); - }); - - ipcmanager = new TwoWayPipeMessageIPCManaged(cmdArgs[(int)Arguments.SettingsPipeName], cmdArgs[(int)Arguments.PTPipeName], (string message) => - { - if (IPCMessageReceivedCallback != null && message.Length > 0) - { - IPCMessageReceivedCallback(message); - } - }); - ipcmanager.Start(); - - if (!ShowOobe && !ShowScoobe && !ShowFlyout) - { - settingsWindow = new MainWindow(); - settingsWindow.Activate(); - settingsWindow.ExtendsContentIntoTitleBar = true; - settingsWindow.NavigateToSection(StartupPage); - - // https://github.com/microsoft/microsoft-ui-xaml/issues/7595 - Activate doesn't bring window to the foreground - // Need to call SetForegroundWindow to actually gain focus. - WindowHelpers.BringToForeground(settingsWindow.GetWindowHandle()); - } - else - { - // Create the Settings window hidden so that it's fully initialized and - // it will be ready to receive the notification if the user opens - // the Settings from the tray icon. - settingsWindow = new MainWindow(true); - - if (ShowOobe) - { - PowerToysTelemetry.Log.WriteEvent(new OobeStartedEvent()); - OobeWindow oobeWindow = new OobeWindow(OOBE.Enums.PowerToysModules.Overview); - oobeWindow.Activate(); - oobeWindow.ExtendsContentIntoTitleBar = true; - SetOobeWindow(oobeWindow); - } - else if (ShowScoobe) - { - PowerToysTelemetry.Log.WriteEvent(new ScoobeStartedEvent()); - OobeWindow scoobeWindow = new OobeWindow(OOBE.Enums.PowerToysModules.WhatsNew); - scoobeWindow.Activate(); - scoobeWindow.ExtendsContentIntoTitleBar = true; - SetOobeWindow(scoobeWindow); - } - else if (ShowFlyout) - { - POINT? p = null; - if (containsFlyoutPosition) - { - p = new POINT(flyout_x, flyout_y); - } - - ShellPage.OpenFlyoutCallback(p); - } - } - - if (SelectedTheme() == ElementTheme.Default) - { - try - { - themeListener = new ThemeListener(); - themeListener.ThemeChanged += (_) => HandleThemeChange(); - } - catch (Exception ex) - { - Logger.LogError($"HandleThemeChange exception. Please install .NET 4.", ex); - } - } + OnLaunchedFromRunner(cmdArgs); + } + else if (cmdArgs?.Length == RequiredArgumentsSetSettingQty && cmdArgs[1] == "set") + { + OnLaunchedToSetSetting(cmdArgs); + } + else if (cmdArgs?.Length == RequiredArgumentsSetAdditionalSettingsQty && cmdArgs[1] == "setAdditional") + { + OnLaunchedToSetAdditionalSetting(cmdArgs); + } + else if (cmdArgs?.Length == RequiredArgumentsGetSettingQty && cmdArgs[1] == "get") + { + OnLaunchedToGetSetting(cmdArgs); } else { @@ -417,7 +495,7 @@ namespace Microsoft.PowerToys.Settings.UI case "QuickAccent": return typeof(PowerAccentPage); case "FileExplorer": return typeof(PowerPreviewPage); case "ShortcutGuide": return typeof(ShortcutGuidePage); - case "PowerOCR": return typeof(PowerOcrPage); + case "PowerOcr": return typeof(PowerOcrPage); case "VideoConference": return typeof(VideoConferencePage); case "MeasureTool": return typeof(MeasureToolPage); case "Hosts": return typeof(HostsPage); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs index 7a31408e5f..461a5a3ac3 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs @@ -321,7 +321,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls // Tab and Shift+Tab are accessible keys and should not be displayed in the hotkey control. if (internalSettings.Code > 0 && !internalSettings.IsAccessibleShortcut()) { - lastValidSettings = internalSettings.Clone(); + lastValidSettings = internalSettings with { }; if (!ComboIsValid(lastValidSettings)) { @@ -436,7 +436,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls { if (ComboIsValid(lastValidSettings)) { - HotkeySettings = lastValidSettings.Clone(); + HotkeySettings = lastValidSettings with { }; } PreviewKeysControl.ItemsSource = hotkeySettings.GetKeysList(); diff --git a/src/settings-ui/Settings.UI/ViewModels/PowerOcrViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/PowerOcrViewModel.cs index 644fd8458f..a52924860b 100644 --- a/src/settings-ui/Settings.UI/ViewModels/PowerOcrViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/PowerOcrViewModel.cs @@ -109,7 +109,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } else { - _isEnabled = GeneralSettingsConfig.Enabled.PowerOCR; + _isEnabled = GeneralSettingsConfig.Enabled.PowerOcr; } } @@ -129,8 +129,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels _isEnabled = value; OnPropertyChanged(nameof(IsEnabled)); - // Set the status of PowerOCR in the general settings - GeneralSettingsConfig.Enabled.PowerOCR = value; + // Set the status of PowerOcr in the general settings + GeneralSettingsConfig.Enabled.PowerOcr = value; var outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig); SendConfigMSG(outgoing.ToString());