diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 4b8e0ce0a0..49012460e6 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -163,6 +163,7 @@ bricelam BRIGHTGREEN Browsable bsd +Bson bstr bthprops bti @@ -221,6 +222,7 @@ CLASSNOTAVAILABLE clickable clickonce CLIENTEDGE +clientid clientside CLIPCHILDREN Clipperton @@ -347,6 +349,7 @@ DARKYELLOW datareader datatemplate Datavalue +dataversion DATAW davidegiacometti Dayof @@ -1859,6 +1862,7 @@ TRAYMOUSEMESSAGE triaging TRK trl +TServer Tshuapa TStr Tuva @@ -1911,6 +1915,7 @@ unmute UNORM unregistering unremapped +Unstub unsubscribe unvirtualized Updatelayout @@ -1936,6 +1941,7 @@ UYVY vabdq validmodulename Vanara +variantassignment vcamp vccorlib vcdl diff --git a/.github/actions/spell-check/patterns.txt b/.github/actions/spell-check/patterns.txt index 774349778a..42ceb68d7f 100644 --- a/.github/actions/spell-check/patterns.txt +++ b/.github/actions/spell-check/patterns.txt @@ -62,6 +62,9 @@ https?://(?:(?:www\.|)youtube\.com|youtu.be)/[-a-zA-Z0-9?&=]* link\.medium\.com/[a-zA-Z0-9]+ \bmedium\.com/\@[^/]+/[-\w]+ +# experimentation urls +https?://default\.exp-tas\.com/[-_a-zA-Z0-9/]* + publicKeyToken=(['"]|)[0-9a-f]+\g{-1} \@sha256:[0-9a-f]{64}\b diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index af84380a24..ce467a6317 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -23,6 +23,7 @@ "PowerToys.Settings.UI.Lib.dll", "PowerToys.GPOWrapper.dll", "PowerToys.GPOWrapperProjection.dll", + "PowerToys.AllExperiments.dll", "modules\\AlwaysOnTop\\PowerToys.AlwaysOnTop.exe", "modules\\AlwaysOnTop\\PowerToys.AlwaysOnTopModuleInterface.dll", @@ -202,6 +203,8 @@ "Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll", "Mono.Cecil.Rocks.dll", + "Newtonsoft.Json.dll", + "Newtonsoft.Json.Bson.dll", "NLog.dll", "HtmlAgilityPack.dll", "Markdig.Signed.dll", diff --git a/.pipelines/ci/templates/build-powertoys-steps.yml b/.pipelines/ci/templates/build-powertoys-steps.yml index 26754babe8..703d520f10 100644 --- a/.pipelines/ci/templates/build-powertoys-steps.yml +++ b/.pipelines/ci/templates/build-powertoys-steps.yml @@ -69,7 +69,7 @@ steps: configPath: NuGet.config restoreSolution: PowerToys.sln restoreDirectory: '$(Build.SourcesDirectory)\packages' - + - task: VSBuild@1 displayName: 'Build PowerToys.sln' inputs: @@ -227,10 +227,17 @@ steps: **\powerpreviewTest.dll **\UnitTests-FancyZones.dll !**\obj\** - + +- task: PowerShell@2 + displayName: Trigger dotnet welcome message so that it does not cause errors on other scripts + inputs: + targetType: 'inline' + script: | + dotnet list $(build.sourcesdirectory)\src\common\Common.UI\Common.UI.csproj package + - task: PowerShell@2 displayName: Verifying Notice.md and Nuget packages match inputs: filePath: '$(build.sourcesdirectory)\.pipelines\verifyNoticeMdAgainstNugetPackages.ps1' arguments: -path '$(build.sourcesdirectory)\' - pwsh: true \ No newline at end of file + pwsh: true diff --git a/.pipelines/release.yml b/.pipelines/release.yml index 01665c74c1..81da06f416 100644 --- a/.pipelines/release.yml +++ b/.pipelines/release.yml @@ -23,6 +23,7 @@ parameters: variables: IsPipeline: 1 # The installer uses this to detect whether it should pick up localizations SkipCppCodeAnalysis: 1 # Skip the code analysis to speed up release CI. It runs on PR CI, anyway + IsExperimentationLive: 1 # The build and installer use this to turn on experimentation name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) resources: diff --git a/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1 b/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1 index affff4c782..2ee58da745 100644 --- a/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1 +++ b/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1 @@ -38,6 +38,8 @@ $totalList = $projFiles | ForEach-Object -Parallel { if($nugetTemp -is [array] -and $nugetTemp.count -gt 3) { + # Need to debug this script? Uncomment this line. + # Write-Host $csproj "`r`n" $nugetTemp "`r`n" $temp = New-Object System.Collections.ArrayList $temp.AddRange($nugetTemp) $temp.RemoveRange(0, 3) diff --git a/Directory.Packages.props b/Directory.Packages.props index 493af35f66..7b11df8764 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -57,4 +57,9 @@ + + + + + \ No newline at end of file diff --git a/PowerToys.sln b/PowerToys.sln index 63b54af475..df3b5bf6e7 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -487,6 +487,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StlThumbnailProviderCpp", " EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SvgThumbnailProviderCpp", "src\modules\previewpane\SvgThumbnailProviderCpp\SvgThumbnailProviderCpp.vcxproj", "{2BBC9E33-21EC-401C-84DA-BB6590A9B2AA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AllExperiments", "src\common\AllExperiments\AllExperiments.csproj", "{9CE59ED5-7087-4353-88EB-788038A73CEC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -2022,6 +2024,18 @@ Global {2BBC9E33-21EC-401C-84DA-BB6590A9B2AA}.Release|x64.Build.0 = Release|x64 {2BBC9E33-21EC-401C-84DA-BB6590A9B2AA}.Release|x86.ActiveCfg = Release|x64 {2BBC9E33-21EC-401C-84DA-BB6590A9B2AA}.Release|x86.Build.0 = Release|x64 + {9CE59ED5-7087-4353-88EB-788038A73CEC}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {9CE59ED5-7087-4353-88EB-788038A73CEC}.Debug|ARM64.Build.0 = Debug|ARM64 + {9CE59ED5-7087-4353-88EB-788038A73CEC}.Debug|x64.ActiveCfg = Debug|x64 + {9CE59ED5-7087-4353-88EB-788038A73CEC}.Debug|x64.Build.0 = Debug|x64 + {9CE59ED5-7087-4353-88EB-788038A73CEC}.Debug|x86.ActiveCfg = Debug|x64 + {9CE59ED5-7087-4353-88EB-788038A73CEC}.Debug|x86.Build.0 = Debug|x64 + {9CE59ED5-7087-4353-88EB-788038A73CEC}.Release|ARM64.ActiveCfg = Release|ARM64 + {9CE59ED5-7087-4353-88EB-788038A73CEC}.Release|ARM64.Build.0 = Release|ARM64 + {9CE59ED5-7087-4353-88EB-788038A73CEC}.Release|x64.ActiveCfg = Release|x64 + {9CE59ED5-7087-4353-88EB-788038A73CEC}.Release|x64.Build.0 = Release|x64 + {9CE59ED5-7087-4353-88EB-788038A73CEC}.Release|x86.ActiveCfg = Release|x64 + {9CE59ED5-7087-4353-88EB-788038A73CEC}.Release|x86.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2192,6 +2206,7 @@ Global {CA5518ED-0458-4B09-8F53-4122B9888655} = {2F305555-C296-497E-AC20-5FA1B237996A} {D6DCC3AE-18C0-488A-B978-BAA9E3CFF09D} = {2F305555-C296-497E-AC20-5FA1B237996A} {2BBC9E33-21EC-401C-84DA-BB6590A9B2AA} = {2F305555-C296-497E-AC20-5FA1B237996A} + {9CE59ED5-7087-4353-88EB-788038A73CEC} = {1AFB6476-670D-4E80-A464-657E01DFF482} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/installer/PowerToysSetup/Settings.wxs b/installer/PowerToysSetup/Settings.wxs index a1072b59f6..a162135bd8 100644 --- a/installer/PowerToysSetup/Settings.wxs +++ b/installer/PowerToysSetup/Settings.wxs @@ -4,12 +4,15 @@ - + + + + @@ -18,6 +21,13 @@ + + + + + + + @@ -78,6 +88,11 @@ + + + + + diff --git a/src/common/AllExperiments/AllExperiments.csproj b/src/common/AllExperiments/AllExperiments.csproj new file mode 100644 index 0000000000..44b823cbbc --- /dev/null +++ b/src/common/AllExperiments/AllExperiments.csproj @@ -0,0 +1,27 @@ + + + + net7.0-windows10.0.19041.0 + enable + enable + PowerToys.AllExperiments + .\Microsoft.VariantAssignment\ + + + + + + + + + + + + + + + + + + + diff --git a/src/common/AllExperiments/Experiments.cs b/src/common/AllExperiments/Experiments.cs new file mode 100644 index 0000000000..9663c28b96 --- /dev/null +++ b/src/common/AllExperiments/Experiments.cs @@ -0,0 +1,211 @@ +// 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.Numerics; +using System.Runtime.InteropServices; +using System.Text.Json; +using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events; +using Microsoft.PowerToys.Telemetry; +using Microsoft.VariantAssignment.Client; +using Microsoft.VariantAssignment.Contract; +using Windows.System.Profile; + +namespace AllExperiments +{ +// The dependencies required to build this project are only available in the official build pipeline and are internal to Microsoft. +// However, this project is not required to build a test version of the application. + public class Experiments + { + public enum ExperimentState + { + Enabled, + Disabled, + NotLoaded, + } + +#pragma warning disable SA1401 // Need to use LandingPageExperiment as a static property in OobeShellPage.xaml.cs + public static ExperimentState LandingPageExperiment = ExperimentState.NotLoaded; +#pragma warning restore SA1401 + + public async Task EnableLandingPageExperimentAsync() + { + if (Experiments.LandingPageExperiment != ExperimentState.NotLoaded) + { + return Experiments.LandingPageExperiment == ExperimentState.Enabled; + } + + Experiments varServ = new Experiments(); + await varServ.VariantAssignmentProvider_Initialize(); + var landingPageExperiment = varServ.IsExperiment; + + Experiments.LandingPageExperiment = landingPageExperiment ? ExperimentState.Enabled : ExperimentState.Disabled; + + return landingPageExperiment; + } + + private async Task VariantAssignmentProvider_Initialize() + { + IsExperiment = false; + string jsonFilePath = CreateFilePath(); + + var vaSettings = new VariantAssignmentClientSettings + { + Endpoint = new Uri("https://default.exp-tas.com/exptas77/a7a397e7-6fbe-4f21-a4e9-3f542e4b000e-exppowertoys/api/v1/tas"), + EnableCaching = true, + ResponseCacheTime = TimeSpan.FromMinutes(5), + }; + + try + { + var vaClient = vaSettings.GetTreatmentAssignmentServiceClient(); + var vaRequest = GetVariantAssignmentRequest(); + using var variantAssignments = await vaClient.GetVariantAssignmentsAsync(vaRequest).ConfigureAwait(false); + + if (variantAssignments.AssignedVariants.Count != 0) + { + var dataVersion = variantAssignments.DataVersion; + var featureVariables = variantAssignments.GetFeatureVariables(); + var assignmentContext = variantAssignments.GetAssignmentContext(); + var featureFlagValue = featureVariables[0].GetStringValue(); + + var experimentGroup = string.Empty; + string json = File.ReadAllText(jsonFilePath); + var jsonDictionary = JsonSerializer.Deserialize>(json); + + if (jsonDictionary != null) + { + if (!jsonDictionary.ContainsKey("dataversion")) + { + jsonDictionary.Add("dataversion", dataVersion); + } + + if (!jsonDictionary.ContainsKey("variantassignment")) + { + jsonDictionary.Add("variantassignment", featureFlagValue); + } + else + { + var jsonDataVersion = jsonDictionary["dataversion"].ToString(); + if (jsonDataVersion != null && int.Parse(jsonDataVersion) < dataVersion) + { + jsonDictionary["dataversion"] = dataVersion; + jsonDictionary["variantassignment"] = featureFlagValue; + } + } + + experimentGroup = jsonDictionary["variantassignment"].ToString(); + + string output = JsonSerializer.Serialize(jsonDictionary); + File.WriteAllText(jsonFilePath, output); + } + + if (experimentGroup == "alternate" && AssignmentUnit != string.Empty) + { + IsExperiment = true; + } + + PowerToysTelemetry.Log.WriteEvent(new OobeVariantAssignmentEvent() { AssignmentContext = assignmentContext, ClientID = AssignmentUnit }); + } + } + catch (HttpRequestException ex) + { + string json = File.ReadAllText(jsonFilePath); + var jsonDictionary = JsonSerializer.Deserialize>(json); + + if (jsonDictionary != null) + { + if (jsonDictionary.ContainsKey("variantassignment")) + { + if (jsonDictionary["variantassignment"].ToString() == "alternate" && AssignmentUnit != string.Empty) + { + IsExperiment = true; + } + } + else + { + jsonDictionary["variantassignment"] = "current"; + } + } + + string output = JsonSerializer.Serialize(jsonDictionary); + File.WriteAllText(jsonFilePath, output); + + Logger.LogError("Error getting to TAS endpoint", ex); + } + catch (Exception ex) + { + Logger.LogError("Error getting variant assignments for experiment", ex); + } + } + + public bool IsExperiment { get; set; } + + private string? AssignmentUnit { get; set; } + + private IVariantAssignmentRequest GetVariantAssignmentRequest() + { + var jsonFilePath = CreateFilePath(); + try + { + if (!File.Exists(jsonFilePath)) + { + AssignmentUnit = Guid.NewGuid().ToString(); + var data = new Dictionary() + { + ["clientid"] = AssignmentUnit, + }; + string jsonData = JsonSerializer.Serialize(data); + File.WriteAllText(jsonFilePath, jsonData); + } + else + { + string json = File.ReadAllText(jsonFilePath); + var jsonDictionary = System.Text.Json.JsonSerializer.Deserialize>(json); + if (jsonDictionary != null) + { + AssignmentUnit = jsonDictionary["clientid"]?.ToString(); + } + } + } + catch (Exception ex) + { + Logger.LogError("Error creating/getting AssignmentUnit", ex); + } + + var attrNames = new List { "FlightRing", "c:InstallLanguage" }; + var attrData = AnalyticsInfo.GetSystemPropertiesAsync(attrNames).AsTask().GetAwaiter().GetResult(); + + var flightRing = string.Empty; + var installLanguage = string.Empty; + + if (attrData.ContainsKey("FlightRing")) + { + flightRing = attrData["FlightRing"]; + } + + if (attrData.ContainsKey("InstallLanguage")) + { + installLanguage = attrData["InstallLanguage"]; + } + + return new VariantAssignmentRequest + { + Parameters = + { + { "installLanguage", installLanguage }, + { "flightRing", flightRing }, + { "clientid", AssignmentUnit }, + }, + }; + } + + private string CreateFilePath() + { + var exeDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var settingsPath = @"Microsoft\PowerToys\experimentation.json"; + var filePath = Path.Combine(exeDir, settingsPath); + return filePath; + } + } +} diff --git a/src/common/AllExperiments/Logger.cs b/src/common/AllExperiments/Logger.cs new file mode 100644 index 0000000000..7604618bdf --- /dev/null +++ b/src/common/AllExperiments/Logger.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.Diagnostics; +using System.Globalization; +using System.IO.Abstractions; + +namespace AllExperiments +{ + public static class Logger + { + private static readonly IFileSystem FileSystem = new FileSystem(); + private static readonly IPath Path = FileSystem.Path; + private static readonly IDirectory Directory = FileSystem.Directory; + + private static readonly string ApplicationLogPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\PowerToys\\Settings Logs\\Experimentation"); + + static Logger() + { + if (!Directory.Exists(ApplicationLogPath)) + { + Directory.CreateDirectory(ApplicationLogPath); + } + + // Using InvariantCulture since this is used for a log file name + var logFilePath = Path.Combine(ApplicationLogPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".txt"); + + Trace.Listeners.Add(new TextWriterTraceListener(logFilePath)); + + Trace.AutoFlush = true; + } + + public static void LogInfo(string message) + { + Log(message, "INFO"); + } + + public static void LogError(string message) + { + Log(message, "ERROR"); +#if DEBUG + Debugger.Break(); +#endif + } + + public static void LogError(string message, Exception e) + { + Log( + message + Environment.NewLine + + e?.Message + Environment.NewLine + + "Inner exception: " + Environment.NewLine + + e?.InnerException?.Message + Environment.NewLine + + "Stack trace: " + Environment.NewLine + + e?.StackTrace, + "ERROR"); +#if DEBUG + Debugger.Break(); +#endif + } + + private static void Log(string message, string type) + { + Trace.WriteLine(type + ": " + DateTime.Now.TimeOfDay); + Trace.Indent(); + Trace.WriteLine(GetCallerInfo()); + Trace.WriteLine(message); + Trace.Unindent(); + } + + private static string GetCallerInfo() + { + StackTrace stackTrace = new StackTrace(); + + var methodName = stackTrace.GetFrame(3)?.GetMethod(); + var className = methodName?.DeclaringType?.Name; + return "[Method]: " + methodName?.Name + " [Class]: " + className; + } + } +} diff --git a/src/common/AllExperiments/Microsoft.VariantAssignment/Client/VariantAssignmentClientExtensionMethods.cs b/src/common/AllExperiments/Microsoft.VariantAssignment/Client/VariantAssignmentClientExtensionMethods.cs new file mode 100644 index 0000000000..ee08acd718 --- /dev/null +++ b/src/common/AllExperiments/Microsoft.VariantAssignment/Client/VariantAssignmentClientExtensionMethods.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.VariantAssignment.Contract; + +// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects +namespace Microsoft.VariantAssignment.Client +{ +#pragma warning disable SA1200 // Using directives should be placed correctly + using TreatmentAssignmentServiceClient = VariantAssignmentServiceClient; +#pragma warning restore SA1200 // Using directives should be placed correctly + + public static class VariantAssignmentClientExtensionMethods + { + public static IVariantAssignmentProvider GetTreatmentAssignmentServiceClient(this VariantAssignmentClientSettings settings) + { + return new TreatmentAssignmentServiceClient(); + } + } +} diff --git a/src/common/AllExperiments/Microsoft.VariantAssignment/Client/VariantAssignmentServiceClient.cs b/src/common/AllExperiments/Microsoft.VariantAssignment/Client/VariantAssignmentServiceClient.cs new file mode 100644 index 0000000000..f90952859a --- /dev/null +++ b/src/common/AllExperiments/Microsoft.VariantAssignment/Client/VariantAssignmentServiceClient.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.VariantAssignment.Contract; + +// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects +namespace Microsoft.VariantAssignment.Client +{ + internal partial class VariantAssignmentServiceClient : IVariantAssignmentProvider, IDisposable + where TServerResponse : VariantAssignmentServiceResponse + { + public void Dispose() + { + throw new NotImplementedException(); + } + + public Task GetVariantAssignmentsAsync(IVariantAssignmentRequest request, CancellationToken ct = default) + { + return Task.FromResult(EmptyVariantAssignmentResponse.Instance); + } + } +} diff --git a/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/EmptyVariantAssignmentResponse.cs b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/EmptyVariantAssignmentResponse.cs new file mode 100644 index 0000000000..90c797155e --- /dev/null +++ b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/EmptyVariantAssignmentResponse.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects +namespace Microsoft.VariantAssignment.Contract +{ + public class EmptyVariantAssignmentResponse : IVariantAssignmentResponse + { + /// + /// Singleton instance of . + /// + public static readonly IVariantAssignmentResponse Instance = new EmptyVariantAssignmentResponse(); + + public EmptyVariantAssignmentResponse() + { + } + + public long DataVersion => 0; + + public string Thumbprint => string.Empty; + + /// + public IReadOnlyCollection AssignedVariants => Array.Empty(); + + /// +#pragma warning disable CS8603 // Possible null reference return. + public IFeatureVariable GetFeatureVariable(IReadOnlyList path) => null; +#pragma warning restore CS8603 // Possible null reference return. + + /// + public IReadOnlyList GetFeatureVariables(IReadOnlyList prefix) => Array.Empty(); + + void IDisposable.Dispose() + { + } + + string IVariantAssignmentResponse.GetAssignmentContext() + { + throw new NotImplementedException(); + } + + IReadOnlyList IVariantAssignmentResponse.GetFeatureVariables() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/IAssignedVariant.cs b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/IAssignedVariant.cs new file mode 100644 index 0000000000..6c9a31e8ce --- /dev/null +++ b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/IAssignedVariant.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects +namespace Microsoft.VariantAssignment.Contract +{ + public interface IAssignedVariant + { + } +} diff --git a/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/IFeatureVariable.cs b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/IFeatureVariable.cs new file mode 100644 index 0000000000..fc9193ed0d --- /dev/null +++ b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/IFeatureVariable.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. + +// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects +namespace Microsoft.VariantAssignment.Contract +{ + public interface IFeatureVariable + { + /// + /// Gets the variable's value as a string. + /// + /// String value of the variable. + string GetStringValue(); + } +} diff --git a/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/IVariantAssignmentProvider.cs b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/IVariantAssignmentProvider.cs new file mode 100644 index 0000000000..dad9d39038 --- /dev/null +++ b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/IVariantAssignmentProvider.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects +namespace Microsoft.VariantAssignment.Contract +{ + public interface IVariantAssignmentProvider : IDisposable + { + /// + /// Computes variant assignments based on data. + /// + /// Variant assignment parameters. + /// Propagates notification that operations should be canceled. + /// An awaitable task that returns a . + Task GetVariantAssignmentsAsync(IVariantAssignmentRequest request, CancellationToken ct = default); + } +} diff --git a/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/IVariantAssignmentRequest.cs b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/IVariantAssignmentRequest.cs new file mode 100644 index 0000000000..9639a3a58d --- /dev/null +++ b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/IVariantAssignmentRequest.cs @@ -0,0 +1,15 @@ +// 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. + +// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects +namespace Microsoft.VariantAssignment.Contract +{ + public interface IVariantAssignmentRequest + { + /// + /// Gets inputs used for evaluating filters, assignment units, etc. + /// + IReadOnlyCollection<(string Key, string Value)> Parameters { get; } + } +} diff --git a/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/IVariantAssignmentResponse.cs b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/IVariantAssignmentResponse.cs new file mode 100644 index 0000000000..29ee2209de --- /dev/null +++ b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/IVariantAssignmentResponse.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects +namespace Microsoft.VariantAssignment.Contract +{ + /// + /// Snapshot of variant assignments. + /// + public interface IVariantAssignmentResponse : IDisposable + { + ///// + ///// Gets the serial number of variant assignment configuration snapshot used when assigning variants. + ///// + long DataVersion { get; } + + ///// + ///// Get a hash of the response suitable for caching. + ///// + // string Thumbprint { get; } + + /// + /// Gets the variants assigned based on request parameters and a variant configuration snapshot. + /// + IReadOnlyCollection AssignedVariants { get; } + + /// + /// Gets feature variables assigned by variants in this response. + /// + /// (Optional) Filter feature variables where contains the . + /// Range of matching feature variables. + IReadOnlyList GetFeatureVariables(IReadOnlyList prefix); + + // this actually part of the interface but gets the job done + IReadOnlyList GetFeatureVariables(); + + // this actually part of the interface but gets the job done + string GetAssignmentContext(); + + /// + /// Gets a single feature variable assigned by variants in this response. + /// + /// Exact feature variable path. + /// Matching feature variable or null. + IFeatureVariable GetFeatureVariable(IReadOnlyList path); + } +} diff --git a/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/TreatmentAssignmentServiceResponse.cs b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/TreatmentAssignmentServiceResponse.cs new file mode 100644 index 0000000000..3b082549f7 --- /dev/null +++ b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/TreatmentAssignmentServiceResponse.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects +namespace Microsoft.VariantAssignment.Contract +{ + internal class TreatmentAssignmentServiceResponse : VariantAssignmentServiceResponse + { + } +} diff --git a/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/VariantAssignmentClientSettings.cs b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/VariantAssignmentClientSettings.cs new file mode 100644 index 0000000000..1ea295bfda --- /dev/null +++ b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/VariantAssignmentClientSettings.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.ComponentModel.DataAnnotations; + +// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects +namespace Microsoft.VariantAssignment.Contract +{ + /// + /// Configuration for variant assignment service client. + /// + public class VariantAssignmentClientSettings + { + /// + /// Gets or sets the variant assignment service endpoint URL. + /// + [Required] + public Uri? Endpoint { get; set; } + + /// + /// Gets or sets a value indicating whether gets or sets a value whether client side request caching should be enabled. + /// + public bool EnableCaching { get; set; } + + /// + /// Gets or sets the the maximum time a cached variant assignment response may be used without re-validating. + /// + public TimeSpan ResponseCacheTime { get; set; } + } +} diff --git a/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/VariantAssignmentRequest.cs b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/VariantAssignmentRequest.cs new file mode 100644 index 0000000000..976ce53531 --- /dev/null +++ b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/VariantAssignmentRequest.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Specialized; + +// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects +namespace Microsoft.VariantAssignment.Contract +{ + public class VariantAssignmentRequest : IVariantAssignmentRequest + { + private NameValueCollection _parameters = new NameValueCollection(); + + /// + /// Gets or sets mutable . + /// + public NameValueCollection Parameters { get => _parameters; set => _parameters = value; } + + IReadOnlyCollection<(string Key, string Value)> IVariantAssignmentRequest.Parameters => (IReadOnlyCollection<(string Key, string Value)>)_parameters; + } +} diff --git a/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/VariantAssignmentServiceResponse.cs b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/VariantAssignmentServiceResponse.cs new file mode 100644 index 0000000000..e87425f4d3 --- /dev/null +++ b/src/common/AllExperiments/Microsoft.VariantAssignment/Contract/VariantAssignmentServiceResponse.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects +namespace Microsoft.VariantAssignment.Contract +{ + /// + /// Mutable implementation of for (de)serialization. + /// + internal class VariantAssignmentServiceResponse : IVariantAssignmentResponse, IDisposable + { + /// + public virtual long DataVersion { get; set; } + + public virtual IReadOnlyCollection AssignedVariants { get; set; } = Array.Empty(); + + public IFeatureVariable GetFeatureVariable(IReadOnlyList path) + { + throw new NotImplementedException(); + } + + public IReadOnlyList GetFeatureVariables(IReadOnlyList prefix) + { + throw new NotImplementedException(); + } + + protected virtual void Dispose(bool disposing) + { + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public IReadOnlyList GetFeatureVariables() + { + throw new NotImplementedException(); + } + + public string GetAssignmentContext() + { + return string.Empty; + } + } +} diff --git a/src/runner/general_settings.cpp b/src/runner/general_settings.cpp index 60f69eccdf..de72d9aa2d 100644 --- a/src/runner/general_settings.cpp +++ b/src/runner/general_settings.cpp @@ -16,6 +16,7 @@ static std::wstring settings_theme = L"system"; static bool run_as_elevated = false; static bool download_updates_automatically = true; +static bool enable_experimentation = true; json::JsonObject GeneralSettings::to_json() { @@ -37,6 +38,7 @@ json::JsonObject GeneralSettings::to_json() result.SetNamedValue(L"is_elevated", json::value(isElevated)); result.SetNamedValue(L"run_elevated", json::value(isRunElevated)); result.SetNamedValue(L"download_updates_automatically", json::value(downloadUpdatesAutomatically)); + result.SetNamedValue(L"enable_experimentation", json::value(enableExperimentation)); result.SetNamedValue(L"is_admin", json::value(isAdmin)); result.SetNamedValue(L"theme", json::value(theme)); result.SetNamedValue(L"system_theme", json::value(systemTheme)); @@ -55,6 +57,7 @@ json::JsonObject load_general_settings() } run_as_elevated = loaded.GetNamedBoolean(L"run_elevated", false); download_updates_automatically = loaded.GetNamedBoolean(L"download_updates_automatically", true) && check_user_is_admin(); + enable_experimentation = loaded.GetNamedBoolean(L"enable_experimentation",true); return loaded; } @@ -67,6 +70,7 @@ GeneralSettings get_general_settings() .isRunElevated = run_as_elevated, .isAdmin = is_user_admin, .downloadUpdatesAutomatically = download_updates_automatically && is_user_admin, + .enableExperimentation = enable_experimentation, .theme = settings_theme, .systemTheme = WindowsColors::is_dark_mode() ? L"dark" : L"light", .powerToysVersion = get_product_version() @@ -89,6 +93,8 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save) download_updates_automatically = general_configs.GetNamedBoolean(L"download_updates_automatically", true); + enable_experimentation = general_configs.GetNamedBoolean(L"enable_experimentation", true); + if (json::has(general_configs, L"startup", json::JsonValueType::Boolean)) { const bool startup = general_configs.GetNamedBoolean(L"startup"); diff --git a/src/runner/general_settings.h b/src/runner/general_settings.h index bc7f59cae6..4f87ffd884 100644 --- a/src/runner/general_settings.h +++ b/src/runner/general_settings.h @@ -11,6 +11,7 @@ struct GeneralSettings bool isRunElevated; bool isAdmin; bool downloadUpdatesAutomatically; + bool enableExperimentation; std::wstring theme; std::wstring systemTheme; std::wstring powerToysVersion; diff --git a/src/runner/trace.cpp b/src/runner/trace.cpp index 5e5bdcbf38..82522bd0f4 100644 --- a/src/runner/trace.cpp +++ b/src/runner/trace.cpp @@ -56,6 +56,7 @@ void Trace::SettingsChanged(const GeneralSettings& settings) TraceLoggingWideString(enabledModules.c_str(), "ModulesEnabled"), TraceLoggingBoolean(settings.isRunElevated, "AlwaysRunElevated"), TraceLoggingBoolean(settings.downloadUpdatesAutomatically, "DownloadUpdatesAutomatically"), + TraceLoggingBoolean(settings.enableExperimentation, "EnableExperimentation"), TraceLoggingWideString(settings.theme.c_str(), "Theme"), ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"), diff --git a/src/settings-ui/Settings.UI.Library/GeneralSettings.cs b/src/settings-ui/Settings.UI.Library/GeneralSettings.cs index 25bd1b2a07..e59e685b76 100644 --- a/src/settings-ui/Settings.UI.Library/GeneralSettings.cs +++ b/src/settings-ui/Settings.UI.Library/GeneralSettings.cs @@ -49,12 +49,16 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("download_updates_automatically")] public bool AutoDownloadUpdates { get; set; } + [JsonPropertyName("enable_experimentation")] + public bool EnableExperimentation { get; set; } + public GeneralSettings() { Startup = false; IsAdmin = false; IsElevated = false; AutoDownloadUpdates = false; + EnableExperimentation = true; Theme = "system"; SystemTheme = "light"; try diff --git a/src/settings-ui/Settings.UI.Library/Telemetry/Events/OobeVariantAssignmentEvent.cs b/src/settings-ui/Settings.UI.Library/Telemetry/Events/OobeVariantAssignmentEvent.cs new file mode 100644 index 0000000000..8f17b58dc3 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/Telemetry/Events/OobeVariantAssignmentEvent.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.Tracing; +using Microsoft.PowerToys.Telemetry; +using Microsoft.PowerToys.Telemetry.Events; + +namespace Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events +{ + [EventData] + public class OobeVariantAssignmentEvent : EventBase, IEvent + { + public string AssignmentContext { get; set; } + + public string ClientID { get; set; } + + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + } +} diff --git a/src/settings-ui/Settings.UI.UnitTests/Settings.UI.UnitTests.csproj b/src/settings-ui/Settings.UI.UnitTests/Settings.UI.UnitTests.csproj index b083e176cc..495d233dec 100644 --- a/src/settings-ui/Settings.UI.UnitTests/Settings.UI.UnitTests.csproj +++ b/src/settings-ui/Settings.UI.UnitTests/Settings.UI.UnitTests.csproj @@ -3,7 +3,7 @@ net7.0-windows10.0.19041.0 false - + win10-x64;win10-arm64 $(Version).0 diff --git a/src/settings-ui/Settings.UI/Assets/FluentIcons/FluentIconsExperimentation.png b/src/settings-ui/Settings.UI/Assets/FluentIcons/FluentIconsExperimentation.png new file mode 100644 index 0000000000..dc5e0c3169 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/FluentIcons/FluentIconsExperimentation.png differ diff --git a/src/settings-ui/Settings.UI/Controls/OOBEPageControl/OOBEPageControl.xaml b/src/settings-ui/Settings.UI/Controls/OOBEPageControl/OOBEPageControl.xaml index 391026cee4..98877bd01c 100644 --- a/src/settings-ui/Settings.UI/Controls/OOBEPageControl/OOBEPageControl.xaml +++ b/src/settings-ui/Settings.UI/Controls/OOBEPageControl/OOBEPageControl.xaml @@ -10,12 +10,13 @@ - + @@ -23,9 +24,7 @@ Grid.Row="1" Padding="32,24,32,24" VerticalScrollBarVisibility="Auto"> - + - + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/Controls/OOBEPageControl/OOBEPageControl.xaml.cs b/src/settings-ui/Settings.UI/Controls/OOBEPageControl/OOBEPageControl.xaml.cs index abc73ee3e2..72abab90d6 100644 --- a/src/settings-ui/Settings.UI/Controls/OOBEPageControl/OOBEPageControl.xaml.cs +++ b/src/settings-ui/Settings.UI/Controls/OOBEPageControl/OOBEPageControl.xaml.cs @@ -32,6 +32,12 @@ namespace Microsoft.PowerToys.Settings.UI.Controls set => SetValue(HeroImageProperty, value); } + public double HeroImageHeight + { + get { return (double)GetValue(HeroImageHeightProperty); } + set { SetValue(HeroImageHeightProperty, value); } + } + public object PageContent { get { return (object)GetValue(PageContentProperty); } @@ -42,5 +48,6 @@ namespace Microsoft.PowerToys.Settings.UI.Controls public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register("Description", typeof(string), typeof(SettingsPageControl), new PropertyMetadata(default(string))); public static readonly DependencyProperty HeroImageProperty = DependencyProperty.Register("HeroImage", typeof(string), typeof(SettingsPageControl), new PropertyMetadata(default(string))); public static readonly DependencyProperty PageContentProperty = DependencyProperty.Register("PageContent", typeof(object), typeof(SettingsPageControl), new PropertyMetadata(new Grid())); + public static readonly DependencyProperty HeroImageHeightProperty = DependencyProperty.Register("HeroImageHeight", typeof(double), typeof(SettingsPageControl), new PropertyMetadata(280.0)); } } diff --git a/src/settings-ui/Settings.UI/OOBE/Views/OobeOverviewAlternate.xaml b/src/settings-ui/Settings.UI/OOBE/Views/OobeOverviewAlternate.xaml new file mode 100644 index 0000000000..ab32d44c5b --- /dev/null +++ b/src/settings-ui/Settings.UI/OOBE/Views/OobeOverviewAlternate.xaml @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/OOBE/Views/OobeOverviewAlternate.xaml.cs b/src/settings-ui/Settings.UI/OOBE/Views/OobeOverviewAlternate.xaml.cs new file mode 100644 index 0000000000..c5171f4159 --- /dev/null +++ b/src/settings-ui/Settings.UI/OOBE/Views/OobeOverviewAlternate.xaml.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.OOBE.Enums; +using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel; +using Microsoft.PowerToys.Settings.UI.Views; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace Microsoft.PowerToys.Settings.UI.OOBE.Views +{ + public sealed partial class OobeOverviewAlternate : Page + { + public OobePowerToysModule ViewModel { get; set; } + + public OobeOverviewAlternate() + { + this.InitializeComponent(); + ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.Overview]); + DataContext = ViewModel; + + FancyZonesHotkeyControl.Keys = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.FancyzonesEditorHotkey.Value.GetKeysList(); + RunHotkeyControl.Keys = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.OpenPowerLauncher.GetKeysList(); + ColorPickerHotkeyControl.Keys = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.ActivationShortcut.GetKeysList(); + AlwaysOnTopHotkeyControl.Keys = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.Hotkey.Value.GetKeysList(); + } + + private void SettingsLaunchButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + if (OobeShellPage.OpenMainWindowCallback != null) + { + OobeShellPage.OpenMainWindowCallback(typeof(GeneralPage)); + } + + ViewModel.LogOpeningSettingsEvent(); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + ViewModel.LogOpeningModuleEvent(); + } + + protected override void OnNavigatedFrom(NavigationEventArgs e) + { + ViewModel.LogClosingModuleEvent(); + } + } +} diff --git a/src/settings-ui/Settings.UI/OOBE/Views/OobeOverviewPlaceholder.xaml b/src/settings-ui/Settings.UI/OOBE/Views/OobeOverviewPlaceholder.xaml new file mode 100644 index 0000000000..3c2a5abb45 --- /dev/null +++ b/src/settings-ui/Settings.UI/OOBE/Views/OobeOverviewPlaceholder.xaml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/OOBE/Views/OobeOverviewPlaceholder.xaml.cs b/src/settings-ui/Settings.UI/OOBE/Views/OobeOverviewPlaceholder.xaml.cs new file mode 100644 index 0000000000..40dcb56aa2 --- /dev/null +++ b/src/settings-ui/Settings.UI/OOBE/Views/OobeOverviewPlaceholder.xaml.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; +using AllExperiments; +using Microsoft.PowerToys.Settings.UI.OOBE.Enums; +using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel; +using Microsoft.PowerToys.Settings.UI.Services; +using Microsoft.PowerToys.Settings.UI.Views; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace Microsoft.PowerToys.Settings.UI.OOBE.Views +{ + public sealed partial class OobeOverviewPlaceholder : Page + { + public OobePowerToysModule ViewModel { get; set; } + + public OobeOverviewPlaceholder() + { + this.InitializeComponent(); + ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.Overview]); + DataContext = ViewModel; + } + + private static async Task GetIsExperiment() + { + Experiments landingPageExp = new Experiments(); + var experimentEnabled = await landingPageExp.EnableLandingPageExperimentAsync(); + return experimentEnabled; + } + + private async void Reload() + { + var isExperiment = await GetIsExperiment(); + + if (isExperiment) + { + this.Frame.Navigate(typeof(OobeOverviewAlternate)); + } + else + { + this.Frame.Navigate(typeof(OobeOverview)); + } + } + + private void Page_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + Reload(); + } + + private void SettingsLaunchButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + if (OobeShellPage.OpenMainWindowCallback != null) + { + OobeShellPage.OpenMainWindowCallback(typeof(GeneralPage)); + } + + ViewModel.LogOpeningSettingsEvent(); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + ViewModel.LogOpeningModuleEvent(); + } + + protected override void OnNavigatedFrom(NavigationEventArgs e) + { + ViewModel.LogClosingModuleEvent(); + } + } +} diff --git a/src/settings-ui/Settings.UI/OOBE/Views/OobeShellPage.xaml.cs b/src/settings-ui/Settings.UI/OOBE/Views/OobeShellPage.xaml.cs index dcd099285c..05498bd208 100644 --- a/src/settings-ui/Settings.UI/OOBE/Views/OobeShellPage.xaml.cs +++ b/src/settings-ui/Settings.UI/OOBE/Views/OobeShellPage.xaml.cs @@ -4,6 +4,8 @@ using System; using System.Collections.ObjectModel; +using System.Globalization; +using AllExperiments; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.OOBE.Enums; using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel; @@ -47,10 +49,16 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views public ObservableCollection Modules { get; } + private static ISettingsUtils settingsUtils = new SettingsUtils(); + + private bool ExperimentationToggleSwitchEnabled { get; set; } = true; + public OobeShellPage() { InitializeComponent(); + ExperimentationToggleSwitchEnabled = SettingsRepository.GetInstance(settingsUtils).SettingsConfig.EnableExperimentation; + DataContext = ViewModel; OobeShellHandler = this; UpdateUITheme(); @@ -186,7 +194,27 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views { switch (selectedItem.Tag) { - case "Overview": NavigationFrame.Navigate(typeof(OobeOverview)); break; + case "Overview": + if (ExperimentationToggleSwitchEnabled) + { + switch (AllExperiments.Experiments.LandingPageExperiment) + { + case Experiments.ExperimentState.Enabled: + NavigationFrame.Navigate(typeof(OobeOverviewAlternate)); break; + case Experiments.ExperimentState.Disabled: + NavigationFrame.Navigate(typeof(OobeOverview)); break; + case Experiments.ExperimentState.NotLoaded: + NavigationFrame.Navigate(typeof(OobeOverviewPlaceholder)); break; + } + + break; + } + else + { + NavigationFrame.Navigate(typeof(OobeOverview)); + break; + } + case "WhatsNew": NavigationFrame.Navigate(typeof(OobeWhatsNew)); break; case "AlwaysOnTop": NavigationFrame.Navigate(typeof(OobeAlwaysOnTop)); break; case "Awake": NavigationFrame.Navigate(typeof(OobeAwake)); break; diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj index 90b499c799..7a5f19c1c2 100644 --- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj +++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj @@ -55,6 +55,9 @@ https://pkgs.dev.azure.com/dotnet/CommunityToolkit/_packaging/CommunityToolkit-Labs/nuget/v3/index.json + + + @@ -91,15 +94,19 @@ + + + + MSBuild:Compile @@ -122,10 +129,17 @@ Always + + MSBuild:Compile + MSBuild:Compile + + $(DefaultXamlRuntime) + + $(DefaultXamlRuntime) diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index a08afd5429..c397cbcbd8 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -2830,6 +2830,10 @@ Activate by holding the key for the character you want to add an accent to, then Launch Host File Editor "Host File Editor" is a product name + + Launch Host File Editor + "Host File Editor" is a product name + Position of additional content @@ -2891,6 +2895,39 @@ Activate by holding the key for the character you want to add an accent to, then Preferred language + + Pin a window so that: + + + Always On Top + + + To pick a color: + + + Color Picker + + + Here are a few shortcuts to get you started: + + + To open the FancyZones editor, press: + + + FancyZones + + + Get access to your files and more: + + + PowerToys Run + + + Only affects Windows Insider builds + + + Enable experimentation + All apps diff --git a/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs index e7447ed1c9..babf4e0158 100644 --- a/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs @@ -125,6 +125,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels _startup = GeneralSettingsConfig.Startup; _autoDownloadUpdates = GeneralSettingsConfig.AutoDownloadUpdates; + _enableExperimentation = GeneralSettingsConfig.EnableExperimentation; _isElevated = isElevated; _runElevated = GeneralSettingsConfig.RunElevated; @@ -152,6 +153,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private int _themeIndex; private bool _autoDownloadUpdates; + private bool _enableExperimentation; private UpdatingSettings.UpdatingState _updatingState = UpdatingSettings.UpdatingState.UpToDate; private string _newAvailableVersion = string.Empty; @@ -284,6 +286,24 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public bool EnableExperimentation + { + get + { + return _enableExperimentation; + } + + set + { + if (_enableExperimentation != value) + { + _enableExperimentation = value; + GeneralSettingsConfig.EnableExperimentation = value; + NotifyPropertyChanged(); + } + } + } + public static bool AutoUpdatesEnabled { get diff --git a/src/settings-ui/Settings.UI/Views/GeneralPage.xaml b/src/settings-ui/Settings.UI/Views/GeneralPage.xaml index fdb0bcaefd..7ac95c8537 100644 --- a/src/settings-ui/Settings.UI/Views/GeneralPage.xaml +++ b/src/settings-ui/Settings.UI/Views/GeneralPage.xaml @@ -369,8 +369,13 @@ + + + -