diff --git a/.github/actions/spell-check/allow/code.txt b/.github/actions/spell-check/allow/code.txt index 41a53d33ed..c655bb1b55 100644 --- a/.github/actions/spell-check/allow/code.txt +++ b/.github/actions/spell-check/allow/code.txt @@ -95,6 +95,7 @@ OTP Yubi Yubico Perplexity +Groq svgl # KEYS @@ -328,3 +329,9 @@ FFF HHH riday YYY + +# GitHub issue/PR commands +azp +feedbackhub +needinfo +reportbug diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 77385974f4..f839c5976c 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -141,8 +141,11 @@ BITSPIXEL bla BLACKFRAME BLENDFUNCTION +blittable Blockquotes blt +bluelightreduction +bluelightreductionstate BLURBEHIND BLURREGION bmi @@ -221,6 +224,7 @@ clientside CLIPBOARDUPDATE CLIPCHILDREN CLIPSIBLINGS +CLITo closesocket clp CLSCTX @@ -249,6 +253,7 @@ colorformat colorhistory colorhistorylimit COLORKEY +colorref comctl comdlg comexp @@ -728,9 +733,9 @@ HWNDPARENT HWNDPREV hyjiacan IAI -icf ICONERROR ICONLOCATION +icf IDCANCEL IDD idk @@ -1073,6 +1078,7 @@ MVVMTK MWBEx MYICON NAMECHANGE +Notavailable namespaceanddescendants nao NCACTIVATE @@ -1111,6 +1117,7 @@ NEWPLUSSHELLEXTENSIONWIN newrow nicksnettravels NIF +nightlight NLog NLSTEXT NMAKE @@ -1481,6 +1488,7 @@ rgh rgn rgs rguid +rhk RIDEV RIGHTSCROLLBAR riid @@ -1586,6 +1594,7 @@ SHGDNF SHGFI SHIL shinfo +shk shlwapi shobjidl SHORTCUTATLEAST @@ -1796,6 +1805,7 @@ tlbimp tlc tmain TNP +toolgood Toolhelp toolwindow TOPDOWNDIB @@ -1855,8 +1865,10 @@ Uniquifies unitconverter unittests UNLEN +Uninitializes UNORM unremapped +Unsubscribes unvirtualized unwide unzoom diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index 83289fa102..e3ebffc20c 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -60,6 +60,8 @@ "PowerToys.FancyZonesEditorCommon.dll", "PowerToys.FancyZonesModuleInterface.dll", "PowerToys.FancyZones.exe", + "FancyZonesCLI.exe", + "FancyZonesCLI.dll", "PowerToys.GcodePreviewHandler.dll", "PowerToys.GcodePreviewHandler.exe", @@ -351,6 +353,11 @@ "Microsoft.SemanticKernel.Connectors.Ollama.dll", "OllamaSharp.dll", + "boost_regex-vc143-mt-gd-x32-1_87.dll", + "boost_regex-vc143-mt-gd-x64-1_87.dll", + "boost_regex-vc143-mt-x32-1_87.dll", + "boost_regex-vc143-mt-x64-1_87.dll", + "UnitsNet.dll", "UtfUnknown.dll", "Wpf.Ui.dll" diff --git a/.pipelines/versionAndSignCheck.ps1 b/.pipelines/versionAndSignCheck.ps1 index 1bb271300d..f90e59afd6 100644 --- a/.pipelines/versionAndSignCheck.ps1 +++ b/.pipelines/versionAndSignCheck.ps1 @@ -52,7 +52,12 @@ $nullVersionExceptions = @( "System.Diagnostics.EventLog.Messages.dll", "Microsoft.Windows.Widgets.dll", "AdaptiveCards.ObjectModel.WinUI3.dll", - "AdaptiveCards.Rendering.WinUI3.dll") -join '|'; + "AdaptiveCards.Rendering.WinUI3.dll", + "boost_regex_vc143_mt_gd_x32_1_87.dll", + "boost_regex_vc143_mt_gd_x64_1_87.dll", + "boost_regex_vc143_mt_x32_1_87.dll", + "boost_regex_vc143_mt_x64_1_87.dll" + ) -join '|'; $totalFailure = 0; Write-Host $DirPath; diff --git a/COMMUNITY.md b/COMMUNITY.md index d145cafd57..c18bacc8c9 100644 --- a/COMMUNITY.md +++ b/COMMUNITY.md @@ -121,6 +121,9 @@ PowerToys Awake is a tool to keep your computer awake. Randy contributed Registry Preview and some very early conversations about keyboard remapping. +### [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon +Kayla was a former lead for PowerToys and helped create multiple utilities, maintained the GitHub repo, and collaborated with the community to improve the overall product + ### [@oldnewthing](https://github.com/oldnewthing) - Raymond Chen Find My Mouse is based on Raymond Chen's SuperSonar. @@ -180,7 +183,6 @@ ZoomIt source code was originally implemented by [Sysinternals](https://sysinter ## PowerToys core team -- [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon - Lead - [@craigloewen-msft](https://github.com/craigloewen-msft) - Craig Loewen - Product Manager - [@niels9001](https://github.com/niels9001/) - Niels Laute - Product Manager - [@dhowett](https://github.com/dhowett) - Dustin Howett - Dev Lead @@ -209,6 +211,7 @@ ZoomIt source code was originally implemented by [Sysinternals](https://sysinter ## Former PowerToys core team members - [@indierawk2k2](https://github.com/indierawk2k2) - Mike Harsh - Product Manager +- [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon - Product Manager - [@ethanfangg](https://github.com/ethanfangg) - Ethan Fang - Product Manager - [@plante-msft](https://github.com/plante-msft) - Connor Plante - Product Manager - [@joadoumie](https://github.com/joadoumie) - Jordi Adoumie - Product Manager diff --git a/Cpp.Build.props b/Cpp.Build.props index febed67a2b..c1f32b2f9c 100644 --- a/Cpp.Build.props +++ b/Cpp.Build.props @@ -42,6 +42,11 @@ + + true + TurnOffAllWarnings + true + true Use pch.h @@ -115,13 +120,11 @@ - + true true - + false true false diff --git a/Directory.Packages.props b/Directory.Packages.props index c4412d2db9..bca3e568e9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,6 +7,8 @@ + + @@ -38,6 +40,7 @@ + @@ -69,10 +72,12 @@ This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail. --> + - - - + + + + @@ -111,12 +116,14 @@ + + diff --git a/NOTICE.md b/NOTICE.md index 6ca3cbfceb..23efb64864 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -75,6 +75,37 @@ OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to ``` +### ToolGood.Words.Pinyin + +We use the ToolGood.Words.Pinyin NuGet package for converting Chinese characters to pinyin. + +**Source**: [https://github.com/toolgood/ToolGood.Words.Pinyin](https://github.com/toolgood/ToolGood.Words.Pinyin) + +``` +MIT License + +Copyright (c) 2020 ToolGood + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + + ## Utility: Command Palette Built-in Extensions ### Calculator @@ -1532,6 +1563,7 @@ SOFTWARE. - SkiaSharp.Views.WinUI - StreamJsonRpc - StyleCop.Analyzers +- ToolGood.Words.Pinyin - UnicodeInformation - UnitsNet - UTF.Unknown diff --git a/PowerToys.slnx b/PowerToys.slnx index c946514fb5..1884b2d58b 100644 --- a/PowerToys.slnx +++ b/PowerToys.slnx @@ -370,6 +370,10 @@ + + + + diff --git a/doc/devdocs/commands.md b/doc/devdocs/commands.md new file mode 100644 index 0000000000..811625284e --- /dev/null +++ b/doc/devdocs/commands.md @@ -0,0 +1,34 @@ +# Issue/PR commands + +The PowerToys repository uses some special keywords to help manage issues and pull requests. Here is a list of the most important commands you can use in issue and PR descriptions or comments. + +| Command | Description | +|---------|-------------| +| `/azp run` | Triggers the Azure Pipelines CI build for the current PR. Useful if you want to re-run the build without creating a new commit. | +| `/bugreport` / `/reportbug` | Adds a comment with a manual for the Bug Report Tool, which helps users collect logs and system information for debugging purposes. It requests to upload this file and adds the `Needs-Author-Feedback` label. | +| `/feedbackhub` | Adds a comment with a link to the Feedback Hub app on Windows, where users can submit feedback about PowerToys. Closes the issue and adds the `Resolution-Please File on Feedback Hub` label. | +| `/dup #...` / `/duplicate #...` / `/dup https://...` / `/duplicate https://...` | Marks the current issue as a duplicate of another issue. It closes the current issue and applies the `Resolution-Duplicate` label. Replace `#...` with the issue number or a link to the issue. | +| `/needinfo` | Adds the `Needs-Author-Feedback` label to the issue or PR, indicating that more information is needed from the author. | +| `/helped` | Closes the issue and adds the `Resolution-Helped User` label. Furthermore a comment is added with a link to the PowerToys user documentation. | +| `/loc` | Adds a comment informing the user that the issue was forwarded to the localization team and will soon be fixed. It adds the `Loc-Sent To Team` label. | + +## Defining new commands + +Most of these commands are using the [Microsoft GitHub Policy Service](https://github.com/apps/microsoft-github-policy-service) bot. Its commands are defined in the [PowerToys policy configuration file](/.github/policies/resourceManagement.yml). + +## Other automated tasks + +### Automatic labeling + +The bot can automatically apply the correct `product-...` label for any opened issue. + +> [!NOTE] +> This feature is currently only available for the Workspaces module as a test. + +### The `Needs-Author-Feedback` label + +If an issue has this label and had no activity for 5 days, the bot will post a comment reminding the author to provide the needed information. It also adds the `Status-No recent activity` label. If no further activity occurs for another 5 days, the bot will close the issue. + +### Filtering users that want to contribute + +If a user utters their intention to contribute (e.g., by using the phrase "I want to contribute" in an issue or PR), the bot will add a comment with a link to the ["Would you like to contribute to PowerToys?" thread](https://github.com/microsoft/PowerToys/issues/28769). diff --git a/doc/devdocs/core/settings/settings-implementation.md b/doc/devdocs/core/settings/settings-implementation.md index defe59a3fa..65d0d27c73 100644 --- a/doc/devdocs/core/settings/settings-implementation.md +++ b/doc/devdocs/core/settings/settings-implementation.md @@ -38,7 +38,7 @@ For C# modules, the settings are accessed through the `SettingsUtils` class in t using Microsoft.PowerToys.Settings.UI.Library; // Read settings -var settings = SettingsUtils.GetSettings("ModuleName"); +var settings = SettingsUtils.Default.GetSettings("ModuleName"); bool enabled = settings.Enabled; ``` @@ -49,7 +49,7 @@ using Microsoft.PowerToys.Settings.UI.Library; // Write settings settings.Enabled = true; -SettingsUtils.SaveSettings(settings.ToJsonString(), "ModuleName"); +SettingsUtils.Default.SaveSettings(settings.ToJsonString(), "ModuleName"); ``` ## Settings Handling in Modules diff --git a/doc/devdocs/style.md b/doc/devdocs/development/style.md similarity index 100% rename from doc/devdocs/style.md rename to doc/devdocs/development/style.md diff --git a/doc/devdocs/localization.md b/doc/devdocs/localization.md deleted file mode 100644 index f9e7e50a67..0000000000 --- a/doc/devdocs/localization.md +++ /dev/null @@ -1,165 +0,0 @@ -# Localization - -> **NOTE**: THIS DOCUMENT IS OUTDATED. -> Follow [issue 15243](https://github.com/microsoft/PowerToys/issues/15243) for updates. - -## Table of Contents -1. [Localization on the pipeline (CDPX)](#localization-on-the-pipeline-cdpx) - 1. [UWP Special case](#uwp-special-case) -2. [Enabling localization on a new project](#enabling-localization-on-a-new-project) - 1. [C++](#c) - 2. [C#](#c-1) - 3. [UWP](#uwp) -3. [Lcl Files](#lcl-files) -4. [Possible Issues in localization PRs (LEGO)](#possible-issues-in-localization-prs-lego) -5. [Enabling localized MSI for a new project](#enabling-localized-msi-for-a-new-project) - -## Localization on the pipeline (CDPX) -[The localization step](https://github.com/microsoft/PowerToys/blob/86d77103e9c69686c297490acb04775d43ef8b76/.pipelines/pipeline.user.windows.yml#L45-L52) is run on the pipeline before the solution is built. This step runs the [build-localization](https://github.com/microsoft/PowerToys/blob/main/.pipelines/build-localization.cmd) script, which generates resx files for all the projects with localization enabled using the `Localization.XLoc` package. - -The [`Localization.XLoc`](https://github.com/microsoft/PowerToys/blob/86d77103e9c69686c297490acb04775d43ef8b76/.pipelines/build-localization.cmd#L24-L25) tool is run on the repo root, and it checks for all occurrences of `LocProject.json`. Each localized project has a `LocProject.json` file in the project root, which contains the location of the English resx file, list of languages for localization, and the output path where the localized resx files are to be copied to. In addition to this, some other parameters can be set, such as whether the language ID should be added as a folder in the file path or in the file name. When the CDPX pipeline is run, the localization team is notified of changes in the English resx files. For each project with localization enabled, a `loc` folder (see [this](https://github.com/microsoft/PowerToys/tree/main/src/modules/launcher/Microsoft.Launcher/loc) for example) is created in the same directory as the `LocProject.json` file. The folder contains language specific folders which in turn have a nested folder path equivalent to `OutputPath` in the `LocProject.json`. Each of these folders contain one `lcl` file. The `lcl` files contain the English resources along with their translation for that language. These are described in more detail in the [Lcl files section](#lcl-files). Once the `.resx` files are generated, they will be used during the `Build PowerToys` step for localized versions of the modules. - -Since the localization script requires certain nuget packages, the [`restore-localization`](https://github.com/microsoft/PowerToys/blob/main/.pipelines/restore-localization.cmd) script is run before running `build-localization` to install all the required packages. This script must [run in the `restore` step](https://github.com/microsoft/PowerToys/blob/86d77103e9c69686c297490acb04775d43ef8b76/.pipelines/pipeline.user.windows.yml#L37-L39) of pipeline because [the host is network isolated](https://onebranch.visualstudio.com/Pipeline/_wiki/wikis/Pipeline.wiki/2066/Consuming-Packages-in-a-CDPx-Pipeline?anchor=overview) at the `build` step. The [Toolset package source](https://github.com/microsoft/PowerToys/blob/86d77103e9c69686c297490acb04775d43ef8b76/.pipelines/pipeline.user.windows.yml#L23) is used for this. - -The process and variables that can be tweaked on the pipeline are described in more detail on [onebranch (account required) under Localization](https://onebranch.visualstudio.com/Pipeline/_wiki/wikis/Pipeline.wiki/290/Localization). - -The localized resource dlls for C# projects are added to the MSI only for build on the pipeline. This is done by checking if the [`IsPipeline` variable is defined](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/installer/PowerToysSetup/Product.wxs#L804-L805), which gets defined before [building the installer on the pipeline](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/.pipelines/build-installer.cmd#L4). This is done because the localized resx files are only present on the pipeline, and not having this check would result in the installer project failing to build locally. - -## Enabling localization on a new project -To enable localization on a new project, the first step is to create a file `LocProject.json` in the project root. - -For example, for a project in the folder `src\path` where the resx file is present in `resources\Resources.resx`, the LocProject.json file will contain the following: -``` -{ - "Projects": [ - { - "LanguageSet": "Azure_Languages", - "LocItems": [ - { - "SourceFile": "src\\path\\resources\\Resources.resx", - "CopyOption": "LangIDOnName", - "OutputPath": "src\\path\\resources" - } - ] - } - ] -} -``` -The rest of the steps depend on the project type and are covered in the sections below. The steps to add the localized files to the MSI can be found in [Enabling localized MSI for a new project](#Enabling-localized-MSI-for-a-new-project). - -### C++ -C++ projects do not support `resx` files, and instead use `rc` files along with `resource.h` files. The CDPX pipeline however doesn't support localizing `rc` files and the other alternative they support is directly translating the resources from the binary which makes it harder to maintain resources. To avoid this, a custom script has been added which expects a resx file and converts the entries to an rc file with a string table and adds resource declarations to a resource.h file so that the resources can be compiled with the C++ project. - -If you already have a .rc file, copy the string table to a separate txt file and run the [convert-stringtable-to-resx.ps1](https://github.com/microsoft/PowerToys/blob/main/tools/build/convert-stringtable-to-resx.ps1) script on it. This script is not very robust to input, and requires the data in a specific format, where `IDS_ResName L"ResourceValue"` and any number of spaces can be present in between. The script converts this file to the format expected by [`resgen`](https://learn.microsoft.com/dotnet/framework/tools/resgen-exe-resource-file-generator#Convert), which will convert it to resx. The resource names are changed from all uppercase to title case, and the `IDS_` prefix is removed. Escape characters might have to be manually replaced, for example .rc files would have escaped double quotes as `""`, so this should be replaced with just `"` before converting to the resx files. - -After generating the resx file, rename the existing rc and h files to ProjName.base.rc and resource.base.h. In the rc file remove the string table which is to be localized and in the .h file remove all `#define`s corresponding to localized resources. In the vcxproj of the C++ project, add the following build event: -``` - - - -``` - -This event runs a script which generates a resource.h and ProjName.rc in the `Generated Files` folder using the strings in all the resx files along with the existing information in resource.base.h and ProjName.base.rc. The script is [convert-resx-to-rc.ps1](https://github.com/microsoft/PowerToys/blob/main/tools/build/convert-resx-to-rc.ps1). The script uses [`resgen`](https://learn.microsoft.com/dotnet/framework/tools/resgen-exe-resource-file-generator#Convert) to convert the resx file to a string table expected in the .rc file format. When the resources are added to the rc file the `IDS_` prefix is added and resource names are in upper case (as it was originally). Any occurrences of `"` in the string resource is escaped as `""` to prevent build errors. The string tables are added to the rc file in the following format: -``` -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US - -STRINGTABLE -BEGIN - strings -END - -#endif -``` -Since there is no API to identify the `AFX_TARG_*`, `LANG_*` or `SUBLANG_*` values from each langId from the pipeline, these are hardcoded in the script (for each language) as done in [lines 50-77 of `convert-resx-to-rc.ps1`](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/tools/build/convert-resx-to-rc.ps1#L50-L77). **If any other languages are added in the future, this script will have to be updated.** In order to determine what are the language codes, you can open the rc file in Resource View, right click the string table and press `Insert Copy` and choose the corresponding language. This autogenerates the required code and can be used to figure out the language codes. The files also add the resource declarations to a resource.h file, starting from 101 by default(this can be changed by an optional argument). Since the output files will be generated in `Generated Files`, any includes in these two files will require an additional `..\` and wherever resource.h is used, it will have to be included as `Generated Files\resource.h`. While adding `resource.base.h` and `ProjName.base.rc` to the vcxproj, these should be modified to not participate in the build to avoid build errors: -``` - -``` - -Some rc/resource.h files might be used in multiple projects (for example, KBM). To ensure the projects build for these cases, the build event can be added to the entire directory so that the rc files are generated before any project is built. See [Directory.Build.targets](https://github.com/microsoft/PowerToys/blob/main/src/modules/keyboardmanager/Directory.Build.targets) for an example. - -Check [this PR](https://github.com/microsoft/PowerToys/pull/6104) for an example for making these changes for a C++ project. - -### C# -Since C# projects natively support `resx` files, the only step required here is to include all the resx files in the build. For .NET Core projects this is done automatically and the .csproj does not need to be modified. For other projects, the following line needs to be added: -``` - -``` - -**Note:** Building with localized resources may cause a build warning `Referenced assembly 'mscorlib.dll' targets a different processor` which is a VS bug. More details can be found in [PowerToys issue #7269](https://github.com/microsoft/PowerToys/issues/7269). - -**Note:** If a project needs to be migrated from XAML resources to resx, the easiest way to convert the resources would be to change to format to `=` separates resources by either manually (by Ctrl+H on a text editor), or by a script, and then running [`resgen`](https://learn.microsoft.com/dotnet/framework/tools/resgen-exe-resource-file-generator#Convert) on `Developer Command Prompt for VS` to convert it to resx format. -``` -Calculator -Allows to do mathematical calculations.(Try 5*3-2 in Wox) -Not a number (NaN) -``` -to -``` -wox_plugin_calculator_plugin_name=Calculator -wox_plugin_calculator_plugin_description=Allows to do mathematical calculations.(Try 5*3-2 in Wox) -wox_plugin_calculator_not_a_number=Not a number (NaN) -``` -After adding the resx file to the project along with the resource generator, references to the strings will have to be replaced with `Properties.Resources.resName` rather than the custom APIs. Check [this PR](https://github.com/microsoft/PowerToys/pull/6165) for an example of the changes required. - -### UWP -UWP projects expect `resw` files rather than `resx` (the format is almost the same). Unlike other C# projects, the files are expected in the format `fullLangId\Resources.resw`. To include these files in the build, replace the following line in the csproj: -``` - -``` -to -``` - -``` - -## Lcl Files -Lcl files contain all the resources that are present in the English resx file, along with a translation if it has been added. - -For example, an entry for a resource in the lcl file looks like this: -``` - - - - - - - - - -``` -The `` element would not be present in the initial commits of the lcl files, as only the English version of the string would be present. - -**Note:** The CDPX Localization system has a fail-safe check on the lcl files, where if the English string value which is present inside `` does not match the value present in the English Resources.resx file then the translated value will not be copied to the localized resx file. This is present so that obsolete translations would not be loaded when the English resource has changed, and the English string will be used rather than the obsolete translation. - -## Possible Issues in localization PRs (LEGO) -Since the LEGO PRs update some of the strings in LCL files at a time, there can be multiple PRs which modify the same files, leading to merge conflicts. In most cases this would show up on GitHub as a merge conflict, but sometimes a bad git merge may occur, and the file could end up with incorrect formatting, such as two `` elements for a single resource. These can be fixed by ensuring the elements follow the format described in [this section](#lcl-files). To catch such errors, the build farm should be run for every LEGO PR and if any error occurs in the localization step, we should check the corresponding resx/lcl files for conflicts. - -## Enabling localized MSI for a new project -For C++ and UWP projects no additional files are generated with localization that need to be added to the MSI. For C++ projects all the resources are added to the dll/exe, while for UWP projects they are added to the `resources.pri` file (which is present even for an unlocalized project). To verify if the localized resources are added to the `resources.pri` file the following steps can be done: -- Open `Developer Command Prompt for VS` -- After navigating to the folder containing the pri file, run the following command: - - makepri.exe dump /if .\resources.pri -- Check the contents of the `resources.pri.xml` file that is generated from the command. The last section of the file will contain the resources with the strings in all the languages: -``` - - - Running as administrator - - - Running as administrator - - -``` - -For C# projects, satellite dlls are generated when the project is built. For a project named `ProjName`, files are created in the format `langId\ProjName.resources.dll` where `langId` is in the same format as the lcl files. The satellite dlls need to be included with the MSI, but they must be added only if the solution is built from the build farm, as the localized resx files will not be present on local machines (and that could cause local builds of the installer to fail). -This can be done by adding the directory name of the project to [Product.wxs near line 806](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/installer/PowerToysSetup/Product.wxs#L806) and a resource component for the project can be created in [Product.wxs near lines 845-847](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/installer/PowerToysSetup/Product.wxs#L845-L847) in this format: -``` - - - -``` - -We should also ensure the new dlls are signed by the pipeline. Currently all dlls of the form [`*.resources.dll` are signed](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/.pipelines/pipeline.user.windows.yml#L68). - -**Note:** The resource dlls should be added to the MSI project only after the initial commit with the lcl files has been done by the Localization team. Otherwise, the pipeline will fail as there wouldn't be any resx files to generate the dlls. diff --git a/doc/devdocs/readme.md b/doc/devdocs/readme.md index 35afb1da89..38df894d1a 100644 --- a/doc/devdocs/readme.md +++ b/doc/devdocs/readme.md @@ -38,6 +38,11 @@ Welcome to the PowerToys developer documentation. This documentation provides in - [Update Process](processes/update-process.md) - How PowerToys updates work - [GPO Implementation](processes/gpo.md) - Group Policy Objects implementation details +## Other Resources + +- [aka.ms links](akaLinks.md) - List of short links +- [Issue/PR commands](commands.md) - Special commands for managing issues and pull requests + ## Fork, Clone, Branch and Create your PR Once you've discussed your proposed feature/fix/etc. with a team member, and an approach or a spec has been written and approved, it's time to start development: diff --git a/doc/thirdPartyRunPlugins.md b/doc/thirdPartyRunPlugins.md index a15cb542a8..9cfdc505ff 100644 --- a/doc/thirdPartyRunPlugins.md +++ b/doc/thirdPartyRunPlugins.md @@ -51,6 +51,7 @@ Contact the developers of a plugin directly for assistance with a specific plugi | [RandomGen](https://github.com/ruslanlap/PowerToysRun-RandomGen) | [ruslanlap](https://github.com/ruslanlap) | 🎲 Generate random data instantly with a single keystroke. Perfect for developers, testers, designers, and anyone who needs quick access to random data. Features include secure passwords, PINs, names, business data, dates, numbers, GUIDs, color codes, and more. Especially useful for designers who need random color codes and placeholder content. | | [Open With Cursor](https://github.com/VictorNoxx/PowerToys-Run-Cursor/) | [VictorNoxx](https://github.com/VictorNoxx) | Open Visual Studio, VS Code recents with Cursor AI | | [CheatSheets](https://github.com/ruslanlap/PowerToysRun-CheatSheets) | [ruslanlap](https://github.com/ruslanlap) | 📚 Find cheat sheets and command examples instantly from tldr pages, cheat.sh, and devhints.io. Features include favorites system, categories, offline mode, and smart caching. | +| [QuickAI](https://github.com/ruslanlap/PowerToysRun-QuickAi) | [ruslanlap](https://github.com/ruslanlap) | AI-powered assistance with instant, smart responses from multiple providers (Groq, Together, Fireworks, OpenRouter, Cohere) | ## Extending software plugins diff --git a/src/common/ManagedCsWin32/CLSID.cs b/src/common/ManagedCsWin32/CLSID.cs index 6087ba575b..00315fe737 100644 --- a/src/common/ManagedCsWin32/CLSID.cs +++ b/src/common/ManagedCsWin32/CLSID.cs @@ -16,4 +16,5 @@ public static partial class CLSID public static readonly Guid CollatorDataSource = new Guid("9E175B8B-F52A-11D8-B9A5-505054503030"); public static readonly Guid ApplicationActivationManager = new Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C"); public static readonly Guid VirtualDesktopManager = new("aa509086-5ca9-4c25-8f95-589d3c07b48a"); + public static readonly Guid DesktopWallpaper = new("C2CF3110-460E-4FC1-B9D0-8A1C0C9CC4BD"); } diff --git a/src/common/ManagedCsWin32/Ole32.cs b/src/common/ManagedCsWin32/Ole32.cs index 20181f3626..cf56c80373 100644 --- a/src/common/ManagedCsWin32/Ole32.cs +++ b/src/common/ManagedCsWin32/Ole32.cs @@ -16,6 +16,12 @@ public static partial class Ole32 CLSCTX dwClsContext, ref Guid riid, out IntPtr rReturnedComObject); + + [LibraryImport("ole32.dll")] + internal static partial int CoInitializeEx(nint pvReserved, uint dwCoInit); + + [LibraryImport("ole32.dll")] + internal static partial void CoUninitialize(); } [Flags] diff --git a/src/common/UITestAutomation/SettingsConfigHelper.cs b/src/common/UITestAutomation/SettingsConfigHelper.cs index 833ec4f19d..0a01891dc4 100644 --- a/src/common/UITestAutomation/SettingsConfigHelper.cs +++ b/src/common/UITestAutomation/SettingsConfigHelper.cs @@ -21,7 +21,7 @@ namespace Microsoft.PowerToys.UITest public class SettingsConfigHelper { private static readonly JsonSerializerOptions IndentedJsonOptions = new() { WriteIndented = true }; - private static readonly SettingsUtils SettingsUtils = new SettingsUtils(); + private static readonly SettingsUtils SettingsUtils = SettingsUtils.Default; /// /// Configures global PowerToys settings to enable only specified modules and disable all others. diff --git a/src/common/utils/registry.h b/src/common/utils/registry.h index 059589352d..c9770bbea3 100644 --- a/src/common/utils/registry.h +++ b/src/common/utils/registry.h @@ -16,9 +16,54 @@ namespace registry { + namespace detail + { + struct on_exit + { + std::function f; + + on_exit(std::function f) : + f{ std::move(f) } {} + ~on_exit() { f(); } + }; + + template + struct overloaded : Ts... + { + using Ts::operator()...; + }; + + template + overloaded(Ts...) -> overloaded; + + inline const wchar_t* getScopeName(HKEY scope) + { + if (scope == HKEY_LOCAL_MACHINE) + { + return L"HKLM"; + } + else if (scope == HKEY_CURRENT_USER) + { + return L"HKCU"; + } + else if (scope == HKEY_CLASSES_ROOT) + { + return L"HKCR"; + } + else + { + return L"HK??"; + } + } + } + namespace install_scope { const wchar_t INSTALL_SCOPE_REG_KEY[] = L"Software\\Classes\\powertoys\\"; + const wchar_t UNINSTALL_REG_KEY[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall"; + + // Bundle UpgradeCode from PowerToys.wxs (with braces as stored in registry) + const wchar_t BUNDLE_UPGRADE_CODE[] = L"{6341382D-C0A9-4238-9188-BE9607E3FAB2}"; enum class InstallScope { @@ -26,8 +71,67 @@ namespace registry PerUser, }; + // Helper function to find PowerToys bundle in Windows Uninstall registry by BundleUpgradeCode + inline bool find_powertoys_bundle_in_uninstall_registry(HKEY rootKey) + { + HKEY uninstallKey{}; + if (RegOpenKeyExW(rootKey, UNINSTALL_REG_KEY, 0, KEY_READ, &uninstallKey) != ERROR_SUCCESS) + { + return false; + } + detail::on_exit closeUninstallKey{ [uninstallKey] { RegCloseKey(uninstallKey); } }; + + DWORD index = 0; + wchar_t subKeyName[256]; + + // Enumerate all subkeys under Uninstall + while (RegEnumKeyW(uninstallKey, index++, subKeyName, 256) == ERROR_SUCCESS) + { + HKEY productKey{}; + if (RegOpenKeyExW(uninstallKey, subKeyName, 0, KEY_READ, &productKey) != ERROR_SUCCESS) + { + continue; + } + detail::on_exit closeProductKey{ [productKey] { RegCloseKey(productKey); } }; + + // Check BundleUpgradeCode value (specific to WiX Bundle installations) + wchar_t bundleUpgradeCode[256]{}; + DWORD bundleUpgradeCodeSize = sizeof(bundleUpgradeCode); + + if (RegQueryValueExW(productKey, L"BundleUpgradeCode", nullptr, nullptr, + reinterpret_cast(bundleUpgradeCode), &bundleUpgradeCodeSize) == ERROR_SUCCESS) + { + if (_wcsicmp(bundleUpgradeCode, BUNDLE_UPGRADE_CODE) == 0) + { + return true; + } + } + } + + return false; + } + inline const InstallScope get_current_install_scope() { + // 1. Check HKCU Uninstall registry first (user-level bundle) + // Note: MSI components are always in HKLM regardless of install scope, + // but the Bundle entry will be in HKCU for per-user installations + if (find_powertoys_bundle_in_uninstall_registry(HKEY_CURRENT_USER)) + { + Logger::info(L"Found user-level PowerToys bundle via BundleUpgradeCode in HKCU"); + return InstallScope::PerUser; + } + + // 2. Check HKLM Uninstall registry (machine-level bundle) + if (find_powertoys_bundle_in_uninstall_registry(HKEY_LOCAL_MACHINE)) + { + Logger::info(L"Found machine-level PowerToys bundle via BundleUpgradeCode in HKLM"); + return InstallScope::PerMachine; + } + + // 3. Fallback to legacy custom registry key detection + Logger::info(L"PowerToys bundle not found in Uninstall registry, falling back to legacy detection"); + // Open HKLM key HKEY perMachineKey{}; if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, @@ -45,6 +149,7 @@ namespace registry &perUserKey) != ERROR_SUCCESS) { // both keys are missing + Logger::warn(L"No PowerToys installation detected, defaulting to PerMachine"); return InstallScope::PerMachine; } else @@ -96,47 +201,6 @@ namespace registry template inline constexpr bool always_false_v = false; - namespace detail - { - struct on_exit - { - std::function f; - - on_exit(std::function f) : - f{ std::move(f) } {} - ~on_exit() { f(); } - }; - - template - struct overloaded : Ts... - { - using Ts::operator()...; - }; - - template - overloaded(Ts...) -> overloaded; - - inline const wchar_t* getScopeName(HKEY scope) - { - if (scope == HKEY_LOCAL_MACHINE) - { - return L"HKLM"; - } - else if (scope == HKEY_CURRENT_USER) - { - return L"HKCU"; - } - else if (scope == HKEY_CLASSES_ROOT) - { - return L"HKCR"; - } - else - { - return L"HK??"; - } - } - } - struct ValueChange { using value_t = std::variant; diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceModuleTest`1.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceModuleTest`1.cs index ad7eb1d200..8d43f48a77 100644 --- a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceModuleTest`1.cs +++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceModuleTest`1.cs @@ -18,7 +18,7 @@ namespace PowerToys.DSC.UnitTests.SettingsResourceTests; public abstract class SettingsResourceModuleTest : BaseDscTest where TSettingsConfig : ISettingsConfig, new() { - private readonly SettingsUtils _settingsUtils = new(); + private readonly SettingsUtils _settingsUtils = SettingsUtils.Default; private TSettingsConfig _originalSettings; protected TSettingsConfig DefaultSettings => new(); diff --git a/src/dsc/v3/PowerToys.DSC/Models/FunctionData/SettingsFunctionData`1.cs b/src/dsc/v3/PowerToys.DSC/Models/FunctionData/SettingsFunctionData`1.cs index 7fcce03d33..9d87b1e773 100644 --- a/src/dsc/v3/PowerToys.DSC/Models/FunctionData/SettingsFunctionData`1.cs +++ b/src/dsc/v3/PowerToys.DSC/Models/FunctionData/SettingsFunctionData`1.cs @@ -18,7 +18,7 @@ namespace PowerToys.DSC.Models.FunctionData; public sealed class SettingsFunctionData : BaseFunctionData, ISettingsFunctionData where TSettingsConfig : ISettingsConfig, new() { - private static readonly SettingsUtils _settingsUtils = new(); + private static readonly SettingsUtils _settingsUtils = SettingsUtils.Default; private static readonly TSettingsConfig _settingsConfig = new(); private readonly SettingsResourceObject _input; diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ConvertersTests/HexColorToColorConverterTests.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ConvertersTests/HexColorToColorConverterTests.cs new file mode 100644 index 0000000000..b8915f278e --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ConvertersTests/HexColorToColorConverterTests.cs @@ -0,0 +1,56 @@ +// 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 AdvancedPaste.Converters; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Windows.UI; + +namespace AdvancedPaste.UnitTests.ConvertersTests; + +[TestClass] +public sealed class HexColorToColorConverterTests +{ + [TestMethod] + public void TestConvert_ValidSixDigitHex_ReturnsColor() + { + Color? result = HexColorConverterHelper.ConvertHexColorToRgb("#FFBFAB"); + Assert.IsNotNull(result); + + var color = (Windows.UI.Color)result; + Assert.AreEqual(255, color.R); + Assert.AreEqual(191, color.G); + Assert.AreEqual(171, color.B); + Assert.AreEqual(255, color.A); + } + + [TestMethod] + public void TestConvert_ValidThreeDigitHex_ReturnsColor() + { + Color? result = HexColorConverterHelper.ConvertHexColorToRgb("#abc"); + Assert.IsNotNull(result); + + var color = (Windows.UI.Color)result; + + // #abc should expand to #aabbcc + Assert.AreEqual(170, color.R); // 0xaa + Assert.AreEqual(187, color.G); // 0xbb + Assert.AreEqual(204, color.B); // 0xcc + Assert.AreEqual(255, color.A); + } + + [TestMethod] + public void TestConvert_NullOrEmpty_ReturnsNull() + { + Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb(null)); + Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb(string.Empty)); + Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb(" ")); + } + + [TestMethod] + public void TestConvert_InvalidHex_ReturnsNull() + { + Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb("#GGGGGG")); + Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb("#12345")); + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/HelpersTests/ClipboardItemHelperTests.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/HelpersTests/ClipboardItemHelperTests.cs new file mode 100644 index 0000000000..2b2a2c7595 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/HelpersTests/ClipboardItemHelperTests.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using AdvancedPaste.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace AdvancedPaste.UnitTests.HelpersTests; + +[TestClass] +public sealed class ClipboardItemHelperTests +{ + [TestMethod] + [DataRow("#FFBFAB", true)] + [DataRow("#000000", true)] + [DataRow("#FFFFFF", true)] + [DataRow("#fff", true)] + [DataRow("#abc", true)] + [DataRow("#123456", true)] + [DataRow("#AbCdEf", true)] + [DataRow("FFBFAB", false)] // Missing # + [DataRow("#GGGGGG", false)] // Invalid hex characters + [DataRow("#12345", false)] // Wrong length + [DataRow("#1234567", false)] // Too long + [DataRow("", false)] + [DataRow(null, false)] + [DataRow(" #FFF ", true)] // Whitespace should be trimmed + [DataRow("Not a color", false)] + [DataRow("#", false)] + [DataRow("##FFFFFF", false)] + public void TestIsRgbHexColor(string input, bool expected) + { + bool result = ClipboardItemHelper.IsRgbHexColor(input); + Assert.AreEqual(expected, result, $"IsRgbHexColor(\"{input}\") should return {expected}"); + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml index e008d35a9a..996f0c5b4f 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml @@ -11,6 +11,7 @@ mc:Ignorable="d"> + @@ -25,6 +26,26 @@ Source="{x:Bind ClipboardItem.Image, Mode=OneWay}" Stretch="UniformToFill" Visibility="{x:Bind HasImage, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" /> + + + + + + + + + ContentImage is not null; - public bool HasText => !string.IsNullOrEmpty(ContentText) && !HasImage; + public bool HasText => !string.IsNullOrEmpty(ContentText) && !HasImage && !HasColor; - public bool HasGlyph => !HasImage && !HasText && !string.IsNullOrEmpty(IconGlyph); + public bool HasGlyph => !HasImage && !HasText && !HasColor && !string.IsNullOrEmpty(IconGlyph); + + public bool HasColor => ClipboardItemHelper.IsRgbHexColor(ContentText); public ClipboardHistoryItemPreviewControl() { diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/HexColorConverterHelper.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/HexColorConverterHelper.cs new file mode 100644 index 0000000000..388b4d3340 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/HexColorConverterHelper.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace AdvancedPaste.Converters +{ + public static class HexColorConverterHelper + { + public static Windows.UI.Color? ConvertHexColorToRgb(string hexColor) + { + try + { + // Remove # if present + var cleanHex = hexColor.TrimStart('#'); + + // Expand 3-digit hex to 6-digit (#ABC -> #AABBCC) + if (cleanHex.Length == 3) + { + cleanHex = $"{cleanHex[0]}{cleanHex[0]}{cleanHex[1]}{cleanHex[1]}{cleanHex[2]}{cleanHex[2]}"; + } + + if (cleanHex.Length == 6) + { + var r = System.Convert.ToByte(cleanHex.Substring(0, 2), 16); + var g = System.Convert.ToByte(cleanHex.Substring(2, 2), 16); + var b = System.Convert.ToByte(cleanHex.Substring(4, 2), 16); + + return Windows.UI.Color.FromArgb(255, r, g, b); + } + } + catch + { + // Invalid color format - return null + } + + return null; + } + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/HexColorToBrushConverter.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/HexColorToBrushConverter.cs new file mode 100644 index 0000000000..436217462a --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/HexColorToBrushConverter.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Media; + +namespace AdvancedPaste.Converters +{ + public sealed partial class HexColorToBrushConverter : IValueConverter + { + public object ConvertBack(object value, Type targetType, object parameter, string language) + => throw new NotSupportedException(); + + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is not string hexColor || string.IsNullOrWhiteSpace(hexColor)) + { + return null; + } + + Windows.UI.Color? color = HexColorConverterHelper.ConvertHexColorToRgb(hexColor); + + return color != null ? new SolidColorBrush((Windows.UI.Color)color) : null; + } + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ClipboardItemHelper.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ClipboardItemHelper.cs index 9f824d3399..e4e18338c9 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ClipboardItemHelper.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ClipboardItemHelper.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Text.RegularExpressions; using System.Threading.Tasks; using AdvancedPaste.Models; using Microsoft.UI.Xaml.Media.Imaging; @@ -10,8 +11,11 @@ using Windows.ApplicationModel.DataTransfer; namespace AdvancedPaste.Helpers { - internal static class ClipboardItemHelper + internal static partial class ClipboardItemHelper { + // Compiled regex for better performance when checking multiple clipboard items + private static readonly Regex HexColorRegex = HexColorCompiledRegex(); + /// /// Creates a ClipboardItem from current clipboard data. /// @@ -55,6 +59,31 @@ namespace AdvancedPaste.Helpers return clipboardItem; } + /// + /// Checks if text is a valid RGB hex color (e.g., #FFBFAB or #fff). + /// + public static bool IsRgbHexColor(string text) + { + if (text == null) + { + return false; + } + + string trimmedText = text.Trim(); + if (trimmedText.Length > 7) + { + return false; + } + + if (string.IsNullOrWhiteSpace(trimmedText)) + { + return false; + } + + // Match #RGB or #RRGGBB format (case-insensitive) + return HexColorRegex.IsMatch(trimmedText); + } + /// /// Creates a BitmapImage from clipboard data. /// @@ -80,5 +109,8 @@ namespace AdvancedPaste.Helpers return null; } + + [GeneratedRegex(@"^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$")] + private static partial Regex HexColorCompiledRegex(); } } diff --git a/src/modules/Hosts/Hosts/Settings/UserSettings.cs b/src/modules/Hosts/Hosts/Settings/UserSettings.cs index 038823f0e2..bd69a336eb 100644 --- a/src/modules/Hosts/Hosts/Settings/UserSettings.cs +++ b/src/modules/Hosts/Hosts/Settings/UserSettings.cs @@ -62,7 +62,7 @@ namespace Hosts.Settings public UserSettings() { - _settingsUtils = new SettingsUtils(); + _settingsUtils = SettingsUtils.Default; var defaultSettings = new HostsProperties(); ShowStartupWarning = defaultSettings.ShowStartupWarning; LoopbackDuplicates = defaultSettings.LoopbackDuplicates; diff --git a/src/modules/LightSwitch/LightSwitchModuleInterface/dllmain.cpp b/src/modules/LightSwitch/LightSwitchModuleInterface/dllmain.cpp index 170dde5b0a..a5973a396f 100644 --- a/src/modules/LightSwitch/LightSwitchModuleInterface/dllmain.cpp +++ b/src/modules/LightSwitch/LightSwitchModuleInterface/dllmain.cpp @@ -50,6 +50,7 @@ enum class ScheduleMode Off, FixedHours, SunsetToSunrise, + FollowNightLight, // add more later }; @@ -61,6 +62,8 @@ inline std::wstring ToString(ScheduleMode mode) return L"SunsetToSunrise"; case ScheduleMode::FixedHours: return L"FixedHours"; + case ScheduleMode::FollowNightLight: + return L"FollowNightLight"; default: return L"Off"; } @@ -72,6 +75,8 @@ inline ScheduleMode FromString(const std::wstring& str) return ScheduleMode::SunsetToSunrise; if (str == L"FixedHours") return ScheduleMode::FixedHours; + if (str == L"FollowNightLight") + return ScheduleMode::FollowNightLight; return ScheduleMode::Off; } @@ -167,7 +172,9 @@ public: ToString(g_settings.m_scheduleMode), { { L"Off", L"Disable the schedule" }, { L"FixedHours", L"Set hours manually" }, - { L"SunsetToSunrise", L"Use sunrise/sunset times" } }); + { L"SunsetToSunrise", L"Use sunrise/sunset times" }, + { L"FollowNightLight", L"Follow Windows Night Light state" } + }); // Integer spinners settings.add_int_spinner( diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp index 845e24fa93..b6684da54e 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp @@ -13,10 +13,12 @@ #include #include "LightSwitchStateManager.h" #include +#include SERVICE_STATUS g_ServiceStatus = {}; SERVICE_STATUS_HANDLE g_StatusHandle = nullptr; HANDLE g_ServiceStopEvent = nullptr; +static LightSwitchStateManager* g_stateManagerPtr = nullptr; VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv); VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl); @@ -168,7 +170,15 @@ static void DetectAndHandleExternalThemeChange(LightSwitchStateManager& stateMan } // Use shared helper (handles wraparound logic) - bool shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark); + bool shouldBeLight = false; + if (s.scheduleMode == ScheduleMode::FollowNightLight) + { + shouldBeLight = !IsNightLightEnabled(); + } + else + { + shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark); + } // Compare current system/apps theme bool currentSystemLight = GetCurrentSystemTheme(); @@ -199,15 +209,40 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) // Initialization // ──────────────────────────────────────────────────────────────── static LightSwitchStateManager stateManager; + g_stateManagerPtr = &stateManager; LightSwitchSettings::instance().InitFileWatcher(); HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); HANDLE hSettingsChanged = LightSwitchSettings::instance().GetSettingsChangedEvent(); + static std::unique_ptr g_nightLightWatcher; + LightSwitchSettings::instance().LoadSettings(); const auto& settings = LightSwitchSettings::instance().settings(); + // after loading settings: + bool nightLightNeeded = (settings.scheduleMode == ScheduleMode::FollowNightLight); + + if (nightLightNeeded && !g_nightLightWatcher) + { + Logger::info(L"[LightSwitchService] Starting Night Light registry watcher..."); + + g_nightLightWatcher = std::make_unique( + HKEY_CURRENT_USER, + NIGHT_LIGHT_REGISTRY_PATH, + []() { + if (g_stateManagerPtr) + g_stateManagerPtr->OnNightLightChange(); + }); + } + else if (!nightLightNeeded && g_nightLightWatcher) + { + Logger::info(L"[LightSwitchService] Stopping Night Light registry watcher..."); + g_nightLightWatcher->Stop(); + g_nightLightWatcher.reset(); + } + SYSTEMTIME st; GetLocalTime(&st); int nowMinutes = st.wHour * 60 + st.wMinute; @@ -274,6 +309,31 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) ResetEvent(hSettingsChanged); LightSwitchSettings::instance().LoadSettings(); stateManager.OnSettingsChanged(); + + const auto& settings = LightSwitchSettings::instance().settings(); + bool nightLightNeeded = (settings.scheduleMode == ScheduleMode::FollowNightLight); + + if (nightLightNeeded && !g_nightLightWatcher) + { + Logger::info(L"[LightSwitchService] Starting Night Light registry watcher..."); + + g_nightLightWatcher = std::make_unique( + HKEY_CURRENT_USER, + NIGHT_LIGHT_REGISTRY_PATH, + []() { + if (g_stateManagerPtr) + g_stateManagerPtr->OnNightLightChange(); + }); + + stateManager.OnNightLightChange(); + } + else if (!nightLightNeeded && g_nightLightWatcher) + { + Logger::info(L"[LightSwitchService] Stopping Night Light registry watcher..."); + g_nightLightWatcher->Stop(); + g_nightLightWatcher.reset(); + } + continue; } } @@ -285,6 +345,11 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) CloseHandle(hManualOverride); if (hParent) CloseHandle(hParent); + if (g_nightLightWatcher) + { + g_nightLightWatcher->Stop(); + g_nightLightWatcher.reset(); + } Logger::info(L"[LightSwitchService] Worker thread exiting cleanly."); return 0; diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj index dde955cc25..96022b454e 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj @@ -76,6 +76,7 @@ + @@ -88,6 +89,7 @@ + diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters index 795df99aba..55c7bde39b 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters @@ -36,6 +36,9 @@ Source Files + + Source Files + @@ -62,6 +65,9 @@ Header Files + + Header Files + diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h index d4029d072d..1d1c7953fe 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h @@ -19,7 +19,8 @@ enum class ScheduleMode { Off, FixedHours, - SunsetToSunrise + SunsetToSunrise, + FollowNightLight, // Add more in the future }; @@ -31,6 +32,8 @@ inline std::wstring ToString(ScheduleMode mode) return L"FixedHours"; case ScheduleMode::SunsetToSunrise: return L"SunsetToSunrise"; + case ScheduleMode::FollowNightLight: + return L"FollowNightLight"; default: return L"Off"; } @@ -42,6 +45,8 @@ inline ScheduleMode FromString(const std::wstring& str) return ScheduleMode::SunsetToSunrise; if (str == L"FixedHours") return ScheduleMode::FixedHours; + if (str == L"FollowNightLight") + return ScheduleMode::FollowNightLight; else return ScheduleMode::Off; } diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp index 4fba4ae9a6..f562d38c41 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp @@ -31,7 +31,10 @@ void LightSwitchStateManager::OnSettingsChanged() void LightSwitchStateManager::OnTick(int currentMinutes) { std::lock_guard lock(_stateMutex); - EvaluateAndApplyIfNeeded(); + if (_state.lastAppliedMode != ScheduleMode::FollowNightLight) + { + EvaluateAndApplyIfNeeded(); + } } // Called when manual override is triggered @@ -49,8 +52,38 @@ void LightSwitchStateManager::OnManualOverride() _state.isAppsLightActive = GetCurrentAppsTheme(); Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).", - (_state.isSystemLightActive ? L"light" : L"dark"), - (_state.isAppsLightActive ? L"light" : L"dark")); + (_state.isSystemLightActive ? L"light" : L"dark"), + (_state.isAppsLightActive ? L"light" : L"dark")); + } + + EvaluateAndApplyIfNeeded(); +} + +// Runs with the registry observer detects a change in Night Light settings. +void LightSwitchStateManager::OnNightLightChange() +{ + std::lock_guard lock(_stateMutex); + + bool newNightLightState = IsNightLightEnabled(); + + // In Follow Night Light mode, treat a Night Light toggle as a boundary + if (_state.lastAppliedMode == ScheduleMode::FollowNightLight && _state.isManualOverride) + { + Logger::info(L"[LightSwitchStateManager] Night Light changed while manual override active; " + L"treating as a boundary and clearing manual override."); + _state.isManualOverride = false; + } + + if (newNightLightState != _state.isNightLightActive) + { + Logger::info(L"[LightSwitchStateManager] Night Light toggled to {}", + newNightLightState ? L"ON" : L"OFF"); + + _state.isNightLightActive = newNightLightState; + } + else + { + Logger::debug(L"[LightSwitchStateManager] Night Light change event fired, but no actual change."); } EvaluateAndApplyIfNeeded(); @@ -77,9 +110,9 @@ void LightSwitchStateManager::SyncInitialThemeState() _state.isSystemLightActive = GetCurrentSystemTheme(); _state.isAppsLightActive = GetCurrentAppsTheme(); Logger::debug(L"[LightSwitchStateManager] Synced initial state to current system theme ({})", - _state.isSystemLightActive ? L"light" : L"dark"); + _state.isSystemLightActive ? L"light" : L"dark"); Logger::debug(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})", - _state.isAppsLightActive ? L"light" : L"dark"); + _state.isAppsLightActive ? L"light" : L"dark"); } static std::pair update_sun_times(auto& settings) @@ -194,7 +227,15 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded() _state.lastAppliedMode = _currentSettings.scheduleMode; - bool shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes); + bool shouldBeLight = false; + if (_currentSettings.scheduleMode == ScheduleMode::FollowNightLight) + { + shouldBeLight = !_state.isNightLightActive; + } + else + { + shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes); + } bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight); bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight); @@ -227,6 +268,3 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded() _state.lastTickMinutes = now; } - - - diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h index 5c9bcc6e25..c4f39a2e9a 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h @@ -9,6 +9,7 @@ struct LightSwitchState bool isManualOverride = false; bool isSystemLightActive = false; bool isAppsLightActive = false; + bool isNightLightActive = false; int lastEvaluatedDay = -1; int lastTickMinutes = -1; @@ -32,6 +33,9 @@ public: // Called when manual override is toggled (via shortcut or system change). void OnManualOverride(); + // Called when night light changes in windows settings + void OnNightLightChange(); + // Initial sync at startup to align internal state with system theme void SyncInitialThemeState(); diff --git a/src/modules/LightSwitch/LightSwitchService/NightLightRegistryObserver.cpp b/src/modules/LightSwitch/LightSwitchService/NightLightRegistryObserver.cpp new file mode 100644 index 0000000000..8da19c6595 --- /dev/null +++ b/src/modules/LightSwitch/LightSwitchService/NightLightRegistryObserver.cpp @@ -0,0 +1 @@ +#include "NightLightRegistryObserver.h" diff --git a/src/modules/LightSwitch/LightSwitchService/NightLightRegistryObserver.h b/src/modules/LightSwitch/LightSwitchService/NightLightRegistryObserver.h new file mode 100644 index 0000000000..2806c28316 --- /dev/null +++ b/src/modules/LightSwitch/LightSwitchService/NightLightRegistryObserver.h @@ -0,0 +1,134 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +class NightLightRegistryObserver +{ +public: + NightLightRegistryObserver(HKEY root, const std::wstring& subkey, std::function callback) : + _root(root), _subkey(subkey), _callback(std::move(callback)), _stop(false) + { + _thread = std::thread([this]() { this->Run(); }); + } + + ~NightLightRegistryObserver() + { + Stop(); + } + + void Stop() + { + _stop = true; + + { + std::lock_guard lock(_mutex); + if (_event) + SetEvent(_event); + } + + if (_thread.joinable()) + _thread.join(); + + std::lock_guard lock(_mutex); + if (_hKey) + { + RegCloseKey(_hKey); + _hKey = nullptr; + } + + if (_event) + { + CloseHandle(_event); + _event = nullptr; + } + } + + +private: + void Run() + { + { + std::lock_guard lock(_mutex); + if (RegOpenKeyExW(_root, _subkey.c_str(), 0, KEY_NOTIFY, &_hKey) != ERROR_SUCCESS) + return; + + _event = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (!_event) + { + RegCloseKey(_hKey); + _hKey = nullptr; + return; + } + } + + while (!_stop) + { + HKEY hKeyLocal = nullptr; + HANDLE eventLocal = nullptr; + + { + std::lock_guard lock(_mutex); + if (_stop) + break; + + hKeyLocal = _hKey; + eventLocal = _event; + } + + if (!hKeyLocal || !eventLocal) + break; + + if (_stop) + break; + + if (RegNotifyChangeKeyValue(hKeyLocal, FALSE, REG_NOTIFY_CHANGE_LAST_SET, eventLocal, TRUE) != ERROR_SUCCESS) + break; + + DWORD wait = WaitForSingleObject(eventLocal, INFINITE); + if (_stop || wait == WAIT_FAILED) + break; + + ResetEvent(eventLocal); + + if (!_stop && _callback) + { + try + { + _callback(); + } + catch (...) + { + } + } + } + + { + std::lock_guard lock(_mutex); + if (_hKey) + { + RegCloseKey(_hKey); + _hKey = nullptr; + } + + if (_event) + { + CloseHandle(_event); + _event = nullptr; + } + } + } + + + HKEY _root; + std::wstring _subkey; + std::function _callback; + HANDLE _event = nullptr; + HKEY _hKey = nullptr; + std::thread _thread; + std::atomic _stop; + std::mutex _mutex; +}; \ No newline at end of file diff --git a/src/modules/LightSwitch/LightSwitchService/SettingsConstants.h b/src/modules/LightSwitch/LightSwitchService/SettingsConstants.h index 4872864eff..8015c9b3e6 100644 --- a/src/modules/LightSwitch/LightSwitchService/SettingsConstants.h +++ b/src/modules/LightSwitch/LightSwitchService/SettingsConstants.h @@ -11,4 +11,7 @@ enum class SettingId Sunset_Offset, ChangeSystem, ChangeApps -}; \ No newline at end of file +}; + +constexpr wchar_t PERSONALIZATION_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr wchar_t NIGHT_LIGHT_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\CloudStore\\Store\\DefaultAccount\\Current\\default$windows.data.bluelightreduction.bluelightreductionstate\\windows.data.bluelightreduction.bluelightreductionstate"; diff --git a/src/modules/LightSwitch/LightSwitchService/ThemeHelper.cpp b/src/modules/LightSwitch/LightSwitchService/ThemeHelper.cpp index 9633ab2fde..cfa858c636 100644 --- a/src/modules/LightSwitch/LightSwitchService/ThemeHelper.cpp +++ b/src/modules/LightSwitch/LightSwitchService/ThemeHelper.cpp @@ -3,6 +3,7 @@ #include #include #include "ThemeHelper.h" +#include // Controls changing the themes. @@ -10,7 +11,7 @@ static void ResetColorPrevalence() { HKEY hKey; if (RegOpenKeyEx(HKEY_CURRENT_USER, - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", + PERSONALIZATION_REGISTRY_PATH, 0, KEY_SET_VALUE, &hKey) == ERROR_SUCCESS) @@ -31,7 +32,7 @@ void SetAppsTheme(bool mode) { HKEY hKey; if (RegOpenKeyEx(HKEY_CURRENT_USER, - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", + PERSONALIZATION_REGISTRY_PATH, 0, KEY_SET_VALUE, &hKey) == ERROR_SUCCESS) @@ -50,7 +51,7 @@ void SetSystemTheme(bool mode) { HKEY hKey; if (RegOpenKeyEx(HKEY_CURRENT_USER, - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", + PERSONALIZATION_REGISTRY_PATH, 0, KEY_SET_VALUE, &hKey) == ERROR_SUCCESS) @@ -79,7 +80,7 @@ bool GetCurrentSystemTheme() DWORD size = sizeof(value); if (RegOpenKeyEx(HKEY_CURRENT_USER, - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", + PERSONALIZATION_REGISTRY_PATH, 0, KEY_READ, &hKey) == ERROR_SUCCESS) @@ -98,7 +99,7 @@ bool GetCurrentAppsTheme() DWORD size = sizeof(value); if (RegOpenKeyEx(HKEY_CURRENT_USER, - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", + PERSONALIZATION_REGISTRY_PATH, 0, KEY_READ, &hKey) == ERROR_SUCCESS) @@ -109,3 +110,30 @@ bool GetCurrentAppsTheme() return value == 1; // true = light, false = dark } + +bool IsNightLightEnabled() +{ + HKEY hKey; + const wchar_t* path = NIGHT_LIGHT_REGISTRY_PATH; + + if (RegOpenKeyExW(HKEY_CURRENT_USER, path, 0, KEY_READ, &hKey) != ERROR_SUCCESS) + return false; + + // RegGetValueW will set size to the size of the data and we expect that to be at least 25 bytes (we need to access bytes 23 and 24) + DWORD size = 0; + if (RegGetValueW(hKey, nullptr, L"Data", RRF_RT_REG_BINARY, nullptr, nullptr, &size) != ERROR_SUCCESS || size < 25) + { + RegCloseKey(hKey); + return false; + } + + std::vector data(size); + if (RegGetValueW(hKey, nullptr, L"Data", RRF_RT_REG_BINARY, nullptr, data.data(), &size) != ERROR_SUCCESS) + { + RegCloseKey(hKey); + return false; + } + + RegCloseKey(hKey); + return data[23] == 0x10 && data[24] == 0x00; +} \ No newline at end of file diff --git a/src/modules/LightSwitch/LightSwitchService/ThemeHelper.h b/src/modules/LightSwitch/LightSwitchService/ThemeHelper.h index 5985fd95c8..e8d45e9c2a 100644 --- a/src/modules/LightSwitch/LightSwitchService/ThemeHelper.h +++ b/src/modules/LightSwitch/LightSwitchService/ThemeHelper.h @@ -3,3 +3,4 @@ void SetSystemTheme(bool dark); void SetAppsTheme(bool dark); bool GetCurrentSystemTheme(); bool GetCurrentAppsTheme(); +bool IsNightLightEnabled(); \ No newline at end of file diff --git a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj index 266fbcace5..c71c81acec 100644 --- a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj +++ b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj @@ -1,14 +1,16 @@  - - - - - - - - - + + + PackageReference + + + native,Version=v0.0 + + + Windows + $(WindowsTargetPlatformVersion) + true true @@ -31,6 +33,11 @@ true true + + + + + DynamicLibrary @@ -38,7 +45,6 @@ true - @@ -118,9 +124,6 @@ true - - - {caba8dfb-823b-4bf2-93ac-3f31984150d9} @@ -142,42 +145,5 @@ - - - - - - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/modules/MeasureTool/MeasureToolCore/packages.config b/src/modules/MeasureTool/MeasureToolCore/packages.config deleted file mode 100644 index d53be50eb7..0000000000 --- a/src/modules/MeasureTool/MeasureToolCore/packages.config +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj b/src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj index 434ff088b2..3e92bd42f3 100644 --- a/src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj +++ b/src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj @@ -73,6 +73,13 @@ - + + false + true + + + + PreserveNewest + diff --git a/src/modules/MeasureTool/MeasureToolUI/Settings.cs b/src/modules/MeasureTool/MeasureToolUI/Settings.cs index 4e8cd99b18..ac48339ad6 100644 --- a/src/modules/MeasureTool/MeasureToolUI/Settings.cs +++ b/src/modules/MeasureTool/MeasureToolUI/Settings.cs @@ -11,7 +11,7 @@ namespace MeasureToolUI { public sealed class Settings { - private static readonly SettingsUtils ModuleSettings = new(); + private static readonly SettingsUtils ModuleSettings = SettingsUtils.Default; public MeasureToolMeasureStyle DefaultMeasureStyle { diff --git a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj index 158ba57ad6..afa4461400 100644 --- a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj +++ b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj @@ -1,13 +1,16 @@ - - - - - - - - + + + PackageReference + + + native,Version=v0.0 + + + Windows + $(WindowsTargetPlatformVersion) + 15.0 {e94fd11c-0591-456f-899f-efc0ca548336} @@ -20,9 +23,12 @@ false true false - - packages.config + + + + + DynamicLibrary @@ -127,18 +133,18 @@ - - - - - - - - - NotUsing - - + + + + + + + + NotUsing + + + <_ToDelete Include="$(OutDir)Microsoft.Web.WebView2.Core.dll" /> @@ -148,38 +154,4 @@ - - - - - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/modules/MouseUtils/FindMyMouse/packages.config b/src/modules/MouseUtils/FindMyMouse/packages.config deleted file mode 100644 index abb20b3a83..0000000000 --- a/src/modules/MouseUtils/FindMyMouse/packages.config +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs b/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs index efe721e873..6e19043547 100644 --- a/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs @@ -53,7 +53,7 @@ internal sealed class SettingsHelper lock (this.LockObject) { { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; // set this to 1 to disable retries var remainingRetries = 5; diff --git a/src/modules/MouseWithoutBorders/App/Class/Setting.cs b/src/modules/MouseWithoutBorders/App/Class/Setting.cs index c526c70976..7d440a6125 100644 --- a/src/modules/MouseWithoutBorders/App/Class/Setting.cs +++ b/src/modules/MouseWithoutBorders/App/Class/Setting.cs @@ -192,7 +192,7 @@ namespace MouseWithoutBorders.Class internal Settings() { - _settingsUtils = new SettingsUtils(); + _settingsUtils = SettingsUtils.Default; _watcher = SettingsHelper.GetFileWatcher("MouseWithoutBorders", "settings.json", () => { diff --git a/src/modules/PowerOCR/PowerOCR/Settings/UserSettings.cs b/src/modules/PowerOCR/PowerOCR/Settings/UserSettings.cs index 0c57171f3a..053f3b5f30 100644 --- a/src/modules/PowerOCR/PowerOCR/Settings/UserSettings.cs +++ b/src/modules/PowerOCR/PowerOCR/Settings/UserSettings.cs @@ -29,7 +29,7 @@ namespace PowerOCR.Settings [ImportingConstructor] public UserSettings(Helpers.IThrottledActionInvoker throttledActionInvoker) { - _settingsUtils = new SettingsUtils(); + _settingsUtils = SettingsUtils.Default; ActivationShortcut = new SettingItem(DefaultActivationShortcut); PreferredLanguage = new SettingItem(string.Empty); diff --git a/src/modules/Workspaces/WorkspacesEditor/Utils/Settings.cs b/src/modules/Workspaces/WorkspacesEditor/Utils/Settings.cs index 29dd65d56f..0955e4019e 100644 --- a/src/modules/Workspaces/WorkspacesEditor/Utils/Settings.cs +++ b/src/modules/Workspaces/WorkspacesEditor/Utils/Settings.cs @@ -9,7 +9,7 @@ namespace WorkspacesEditor.Utils public class Settings { private const string WorkspacesModuleName = "Workspaces"; - private static readonly SettingsUtils _settingsUtils = new(); + private static readonly SettingsUtils _settingsUtils = SettingsUtils.Default; public static WorkspacesSettings ReadSettings() { diff --git a/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs b/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs index d05498d62e..30b28f4cd8 100644 --- a/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs +++ b/src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs @@ -133,7 +133,7 @@ namespace WorkspacesEditor.ViewModels _orderByIndex = value; OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView))); settings.Properties.SortBy = (WorkspacesProperties.SortByProperty)value; - settings.Save(new SettingsUtils()); + settings.Save(SettingsUtils.Default); } } diff --git a/src/modules/awake/Awake/Core/Manager.cs b/src/modules/awake/Awake/Core/Manager.cs index c6aa1c2efb..cc3e461b20 100644 --- a/src/modules/awake/Awake/Core/Manager.cs +++ b/src/modules/awake/Awake/Core/Manager.cs @@ -60,7 +60,7 @@ namespace Awake.Core { _tokenSource = new CancellationTokenSource(); _stateQueue = []; - ModuleSettings = new SettingsUtils(); + ModuleSettings = SettingsUtils.Default; } internal static void StartMonitor() diff --git a/src/modules/awake/Awake/Program.cs b/src/modules/awake/Awake/Program.cs index 4d6d20bc96..b5c3102ba0 100644 --- a/src/modules/awake/Awake/Program.cs +++ b/src/modules/awake/Awake/Program.cs @@ -51,7 +51,7 @@ namespace Awake private static async Task Main(string[] args) { - _settingsUtils = new SettingsUtils(); + _settingsUtils = SettingsUtils.Default; LockMutex = new Mutex(true, Core.Constants.AppName, out bool instantiated); diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/DismissMessage.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/DismissMessage.cs index 98a5b34562..8150ee6584 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/DismissMessage.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/DismissMessage.cs @@ -4,6 +4,4 @@ namespace Microsoft.CmdPal.Core.ViewModels.Messages; -public record DismissMessage() -{ -} +public record DismissMessage(bool ForceGoHome = false); diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/NavigateBackMessage.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/NavigateBackMessage.cs index 3e085184ed..e6152b75d5 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/NavigateBackMessage.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/NavigateBackMessage.cs @@ -4,6 +4,4 @@ namespace Microsoft.CmdPal.Core.ViewModels.Messages; -public record NavigateBackMessage(bool FromBackspace = false) -{ -} +public record NavigateBackMessage(bool FromBackspace = false); diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/NavigateLeftCommand.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/NavigateLeftCommand.cs new file mode 100644 index 0000000000..d352b552cf --- /dev/null +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/NavigateLeftCommand.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Core.ViewModels.Messages; + +/// +/// Used to navigate left in a grid view when pressing the Left arrow key in the SearchBox. +/// +public record NavigateLeftCommand; diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/NavigateRightCommand.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/NavigateRightCommand.cs new file mode 100644 index 0000000000..3cfb05913d --- /dev/null +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/NavigateRightCommand.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Core.ViewModels.Messages; + +/// +/// Used to navigate right in a grid view when pressing the Right arrow key in the SearchBox. +/// +public record NavigateRightCommand; diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs index 046d7ea336..16ca5b1fca 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs @@ -14,6 +14,7 @@ using Microsoft.CommandPalette.Extensions; namespace Microsoft.CmdPal.Core.ViewModels; public partial class ShellViewModel : ObservableObject, + IDisposable, IRecipient, IRecipient { @@ -378,7 +379,7 @@ public partial class ShellViewModel : ObservableObject, { // Reset the palette to the main page and dismiss GoHome(withAnimation: false, focusSearch: false); - WeakReferenceMessenger.Default.Send(); + WeakReferenceMessenger.Default.Send(new DismissMessage()); break; } @@ -398,7 +399,7 @@ public partial class ShellViewModel : ObservableObject, case CommandResultKind.Hide: { // Keep this page open, but hide the palette. - WeakReferenceMessenger.Default.Send(); + WeakReferenceMessenger.Default.Send(new DismissMessage()); break; } @@ -460,4 +461,12 @@ public partial class ShellViewModel : ObservableObject, { _navigationCts?.Cancel(); } + + public void Dispose() + { + _handleInvokeTask?.Dispose(); + _navigationCts?.Dispose(); + + GC.SuppressFinalize(this); + } } diff --git a/src/modules/cmdpal/CoreCommonProps.props b/src/modules/cmdpal/CoreCommonProps.props index aa091d435e..438d044e2a 100644 --- a/src/modules/cmdpal/CoreCommonProps.props +++ b/src/modules/cmdpal/CoreCommonProps.props @@ -6,12 +6,12 @@ preview - $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\ + ..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\ false false $(RootNamespace).pri - + SA1313; @@ -42,5 +42,5 @@ Resources.Designer.cs - + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppearanceSettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppearanceSettingsViewModel.cs new file mode 100644 index 0000000000..71e150a7d2 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppearanceSettingsViewModel.cs @@ -0,0 +1,390 @@ +// 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.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.WinUI; +using Microsoft.CmdPal.UI.ViewModels.Services; +using Microsoft.UI; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; +using Windows.UI; +using Windows.UI.ViewManagement; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDisposable +{ + private static readonly ObservableCollection WindowsColorSwatches = [ + + // row 0 + Color.FromArgb(255, 255, 185, 0), // #ffb900 + Color.FromArgb(255, 255, 140, 0), // #ff8c00 + Color.FromArgb(255, 247, 99, 12), // #f7630c + Color.FromArgb(255, 202, 80, 16), // #ca5010 + Color.FromArgb(255, 218, 59, 1), // #da3b01 + Color.FromArgb(255, 239, 105, 80), // #ef6950 + + // row 1 + Color.FromArgb(255, 209, 52, 56), // #d13438 + Color.FromArgb(255, 255, 67, 67), // #ff4343 + Color.FromArgb(255, 231, 72, 86), // #e74856 + Color.FromArgb(255, 232, 17, 35), // #e81123 + Color.FromArgb(255, 234, 0, 94), // #ea005e + Color.FromArgb(255, 195, 0, 82), // #c30052 + + // row 2 + Color.FromArgb(255, 227, 0, 140), // #e3008c + Color.FromArgb(255, 191, 0, 119), // #bf0077 + Color.FromArgb(255, 194, 57, 179), // #c239b3 + Color.FromArgb(255, 154, 0, 137), // #9a0089 + Color.FromArgb(255, 0, 120, 212), // #0078d4 + Color.FromArgb(255, 0, 99, 177), // #0063b1 + + // row 3 + Color.FromArgb(255, 142, 140, 216), // #8e8cd8 + Color.FromArgb(255, 107, 105, 214), // #6b69d6 + Color.FromArgb(255, 135, 100, 184), // #8764b8 + Color.FromArgb(255, 116, 77, 169), // #744da9 + Color.FromArgb(255, 177, 70, 194), // #b146c2 + Color.FromArgb(255, 136, 23, 152), // #881798 + + // row 4 + Color.FromArgb(255, 0, 153, 188), // #0099bc + Color.FromArgb(255, 45, 125, 154), // #2d7d9a + Color.FromArgb(255, 0, 183, 195), // #00b7c3 + Color.FromArgb(255, 3, 131, 135), // #038387 + Color.FromArgb(255, 0, 178, 148), // #00b294 + Color.FromArgb(255, 1, 133, 116), // #018574 + + // row 5 + Color.FromArgb(255, 0, 204, 106), // #00cc6a + Color.FromArgb(255, 16, 137, 62), // #10893e + Color.FromArgb(255, 122, 117, 116), // #7a7574 + Color.FromArgb(255, 93, 90, 88), // #5d5a58 + Color.FromArgb(255, 104, 118, 138), // #68768a + Color.FromArgb(255, 81, 92, 107), // #515c6b + + // row 6 + Color.FromArgb(255, 86, 124, 115), // #567c73 + Color.FromArgb(255, 72, 104, 96), // #486860 + Color.FromArgb(255, 73, 130, 5), // #498205 + Color.FromArgb(255, 16, 124, 16), // #107c10 + Color.FromArgb(255, 118, 118, 118), // #767676 + Color.FromArgb(255, 76, 74, 72), // #4c4a48 + + // row 7 + Color.FromArgb(255, 105, 121, 126), // #69797e + Color.FromArgb(255, 74, 84, 89), // #4a5459 + Color.FromArgb(255, 100, 124, 100), // #647c64 + Color.FromArgb(255, 82, 94, 84), // #525e54 + Color.FromArgb(255, 132, 117, 69), // #847545 + Color.FromArgb(255, 126, 115, 95), // #7e735f + ]; + + private readonly SettingsModel _settings; + private readonly UISettings _uiSettings; + private readonly IThemeService _themeService; + private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer(); + private readonly DispatcherQueue _uiDispatcher = DispatcherQueue.GetForCurrentThread(); + + private ElementTheme? _elementThemeOverride; + private Color _currentSystemAccentColor; + + public ObservableCollection Swatches => WindowsColorSwatches; + + public int ThemeIndex + { + get => (int)_settings.Theme; + set => Theme = (UserTheme)value; + } + + public UserTheme Theme + { + get => _settings.Theme; + set + { + if (_settings.Theme != value) + { + _settings.Theme = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ThemeIndex)); + Save(); + } + } + } + + public ColorizationMode ColorizationMode + { + get => _settings.ColorizationMode; + set + { + if (_settings.ColorizationMode != value) + { + _settings.ColorizationMode = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ColorizationModeIndex)); + OnPropertyChanged(nameof(IsCustomTintVisible)); + OnPropertyChanged(nameof(IsCustomTintIntensityVisible)); + OnPropertyChanged(nameof(IsBackgroundControlsVisible)); + OnPropertyChanged(nameof(IsNoBackgroundVisible)); + OnPropertyChanged(nameof(IsAccentColorControlsVisible)); + + if (value == ColorizationMode.WindowsAccentColor) + { + ThemeColor = _currentSystemAccentColor; + } + + IsColorizationDetailsExpanded = value != ColorizationMode.None; + + Save(); + } + } + } + + public int ColorizationModeIndex + { + get => (int)_settings.ColorizationMode; + set => ColorizationMode = (ColorizationMode)value; + } + + public Color ThemeColor + { + get => _settings.CustomThemeColor; + set + { + if (_settings.CustomThemeColor != value) + { + _settings.CustomThemeColor = value; + + OnPropertyChanged(); + + if (ColorIntensity == 0) + { + ColorIntensity = 100; + } + + Save(); + } + } + } + + public int ColorIntensity + { + get => _settings.CustomThemeColorIntensity; + set + { + _settings.CustomThemeColorIntensity = value; + OnPropertyChanged(); + Save(); + } + } + + public string BackgroundImagePath + { + get => _settings.BackgroundImagePath ?? string.Empty; + set + { + if (_settings.BackgroundImagePath != value) + { + _settings.BackgroundImagePath = value; + OnPropertyChanged(); + + if (BackgroundImageOpacity == 0) + { + BackgroundImageOpacity = 100; + } + + Save(); + } + } + } + + public int BackgroundImageOpacity + { + get => _settings.BackgroundImageOpacity; + set + { + if (_settings.BackgroundImageOpacity != value) + { + _settings.BackgroundImageOpacity = value; + OnPropertyChanged(); + Save(); + } + } + } + + public int BackgroundImageBrightness + { + get => _settings.BackgroundImageBrightness; + set + { + if (_settings.BackgroundImageBrightness != value) + { + _settings.BackgroundImageBrightness = value; + OnPropertyChanged(); + Save(); + } + } + } + + public int BackgroundImageBlurAmount + { + get => _settings.BackgroundImageBlurAmount; + set + { + if (_settings.BackgroundImageBlurAmount != value) + { + _settings.BackgroundImageBlurAmount = value; + OnPropertyChanged(); + Save(); + } + } + } + + public BackgroundImageFit BackgroundImageFit + { + get => _settings.BackgroundImageFit; + set + { + if (_settings.BackgroundImageFit != value) + { + _settings.BackgroundImageFit = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(BackgroundImageFitIndex)); + Save(); + } + } + } + + public int BackgroundImageFitIndex + { + // Naming between UI facing string and enum is a bit confusing, but the enum fields + // are based on XAML Stretch enum values. So I'm choosing to keep the confusion here, close + // to the UI. + // - BackgroundImageFit.Fill corresponds to "Stretch" + // - BackgroundImageFit.UniformToFill corresponds to "Fill" + get => BackgroundImageFit switch + { + BackgroundImageFit.Fill => 1, + _ => 0, + }; + set => BackgroundImageFit = value switch + { + 1 => BackgroundImageFit.Fill, + _ => BackgroundImageFit.UniformToFill, + }; + } + + [ObservableProperty] + public partial bool IsColorizationDetailsExpanded { get; set; } + + public bool IsCustomTintVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image; + + public bool IsCustomTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image; + + public bool IsBackgroundControlsVisible => _settings.ColorizationMode is ColorizationMode.Image; + + public bool IsNoBackgroundVisible => _settings.ColorizationMode is ColorizationMode.None; + + public bool IsAccentColorControlsVisible => _settings.ColorizationMode is ColorizationMode.WindowsAccentColor; + + public AcrylicBackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f); + + public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme; + + public Color EffectiveThemeColor => ColorizationMode switch + { + ColorizationMode.WindowsAccentColor => _currentSystemAccentColor, + ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor, + _ => Colors.Transparent, + }; + + // Since the blur amount is absolute, we need to scale it down for the preview (which is smaller than full screen). + public int EffectiveBackgroundImageBlurAmount => (int)Math.Round(BackgroundImageBlurAmount / 4f); + + public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0; + + public ImageSource? EffectiveBackgroundImageSource => + ColorizationMode is ColorizationMode.Image + && !string.IsNullOrWhiteSpace(BackgroundImagePath) + && Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri) + ? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri) + : null; + + public AppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings) + { + _themeService = themeService; + _themeService.ThemeChanged += ThemeServiceOnThemeChanged; + _settings = settings; + + _uiSettings = new UISettings(); + _uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged; + UpdateAccentColor(_uiSettings); + + Reapply(); + + IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None; + } + + private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender)); + + private void UpdateAccentColor(UISettings sender) + { + _currentSystemAccentColor = sender.GetColorValue(UIColorType.Accent); + if (ColorizationMode == ColorizationMode.WindowsAccentColor) + { + ThemeColor = _currentSystemAccentColor; + } + } + + private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e) + { + _saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200)); + } + + private void Save() + { + SettingsModel.SaveSettings(_settings); + _saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200)); + } + + private void Reapply() + { + // Theme services recalculates effective color and opacity based on current settings. + EffectiveBackdrop = _themeService.Current.BackdropParameters; + OnPropertyChanged(nameof(EffectiveBackdrop)); + OnPropertyChanged(nameof(EffectiveBackgroundImageBrightness)); + OnPropertyChanged(nameof(EffectiveBackgroundImageSource)); + OnPropertyChanged(nameof(EffectiveThemeColor)); + OnPropertyChanged(nameof(EffectiveBackgroundImageBlurAmount)); + + // LOAD BEARING: + // We need to cycle through the EffectiveTheme property to force reload of resources. + _elementThemeOverride = ElementTheme.Light; + OnPropertyChanged(nameof(EffectiveTheme)); + _elementThemeOverride = ElementTheme.Dark; + OnPropertyChanged(nameof(EffectiveTheme)); + _elementThemeOverride = null; + OnPropertyChanged(nameof(EffectiveTheme)); + } + + [RelayCommand] + private void ResetBackgroundImageProperties() + { + BackgroundImageBrightness = 0; + BackgroundImageBlurAmount = 0; + BackgroundImageFit = BackgroundImageFit.UniformToFill; + BackgroundImageOpacity = 100; + ColorIntensity = 0; + } + + public void Dispose() + { + _uiSettings.ColorValuesChanged -= UiSettingsOnColorValuesChanged; + _themeService.ThemeChanged -= ThemeServiceOnThemeChanged; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/BackgroundImageFit.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/BackgroundImageFit.cs new file mode 100644 index 0000000000..52102df30a --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/BackgroundImageFit.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.UI.ViewModels; + +public enum BackgroundImageFit +{ + Fill, + UniformToFill, +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ColorizationMode.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ColorizationMode.cs new file mode 100644 index 0000000000..57a65f1882 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ColorizationMode.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.UI.ViewModels; + +public enum ColorizationMode +{ + None, + WindowsAccentColor, + CustomColor, + Image, +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs index 003a0bfb9e..4118ac64db 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs @@ -12,6 +12,7 @@ using Microsoft.CmdPal.Core.ViewModels.Messages; using Microsoft.CmdPal.Ext.Apps; using Microsoft.CmdPal.Ext.Apps.Programs; using Microsoft.CmdPal.Ext.Apps.State; +using Microsoft.CmdPal.UI.ViewModels.Commands; using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Properties; using Microsoft.CommandPalette.Extensions; @@ -44,6 +45,9 @@ public partial class MainListPage : DynamicListPage, private List>? _filteredItems; private List>? _filteredApps; private List>? _fallbackItems; + + // Keep as IEnumerable for deferred execution. Fallback item titles are updated + // asynchronously, so scoring must happen lazily when GetItems is called. private IEnumerable>? _scoredFallbackItems; private bool _includeApps; private bool _filteredItemsIncludesApps; @@ -155,42 +159,18 @@ public partial class MainListPage : DynamicListPage, public override IListItem[] GetItems() { - if (string.IsNullOrEmpty(SearchText)) + lock (_tlcManager.TopLevelCommands) { - lock (_tlcManager.TopLevelCommands) - { - return _tlcManager - .TopLevelCommands - .Where(tlc => !tlc.IsFallback && !string.IsNullOrEmpty(tlc.Title)) - .ToArray(); - } - } - else - { - lock (_tlcManager.TopLevelCommands) - { - var limitedApps = new List>(); - - // Fuzzy matching can produce a lot of results, so we want to limit the - // number of apps we show at once if it's a large set. - if (_filteredApps?.Count > 0) - { - limitedApps = _filteredApps.OrderByDescending(s => s.Score).Take(_appResultLimit).ToList(); - } - - var items = Enumerable.Empty>() - .Concat(_filteredItems is not null ? _filteredItems : []) - .Concat(_scoredFallbackItems is not null ? _scoredFallbackItems : []) - .Concat(limitedApps) - .OrderByDescending(o => o.Score) - - // Add fallback items post-sort so they are always at the end of the list - // and eventually ordered based on user preference - .Concat(_fallbackItems is not null ? _fallbackItems.Where(w => !string.IsNullOrEmpty(w.Item.Title)) : []) - .Select(s => s.Item) - .ToArray(); - return items; - } + // Either return the top-level commands (no search text), or the merged and + // filtered results. + return string.IsNullOrEmpty(SearchText) + ? _tlcManager.TopLevelCommands.Where(tlc => !tlc.IsFallback && !string.IsNullOrEmpty(tlc.Title)).ToArray() + : MainListPageResultFactory.Create( + _filteredItems, + _scoredFallbackItems?.ToList(), + _filteredApps, + _fallbackItems, + _appResultLimit); } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPageResultFactory.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPageResultFactory.cs new file mode 100644 index 0000000000..f1bddf5197 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPageResultFactory.cs @@ -0,0 +1,156 @@ +// 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. + +#pragma warning disable IDE0007 // Use implicit type + +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.UI.ViewModels.Commands; + +internal static class MainListPageResultFactory +{ + /// + /// Creates a merged and ordered array of results from multiple scored input lists, + /// applying an application result limit and filtering fallback items as needed. + /// + public static IListItem[] Create( + IList>? filteredItems, + IList>? scoredFallbackItems, + IList>? filteredApps, + IList>? fallbackItems, + int appResultLimit) + { + if (appResultLimit < 0) + { + throw new ArgumentOutOfRangeException( + nameof(appResultLimit), "App result limit must be non-negative."); + } + + int len1 = filteredItems?.Count ?? 0; + int len2 = scoredFallbackItems?.Count ?? 0; + + // Apps are pre-sorted, so we just need to take the top N, limited by appResultLimit. + int len3 = Math.Min(filteredApps?.Count ?? 0, appResultLimit); + + // Allocate the exact size of the result array. + int totalCount = len1 + len2 + len3 + GetNonEmptyFallbackItemsCount(fallbackItems); + var result = new IListItem[totalCount]; + + // Three-way stable merge of already-sorted lists. + int idx1 = 0, idx2 = 0, idx3 = 0; + int writePos = 0; + + // Merge while all three lists have items. To maintain a stable sort, the + // priority is: list1 > list2 > list3 when scores are equal. + while (idx1 < len1 && idx2 < len2 && idx3 < len3) + { + // Using null-forgiving operator as we have already checked against lengths. + int score1 = filteredItems![idx1].Score; + int score2 = scoredFallbackItems![idx2].Score; + int score3 = filteredApps![idx3].Score; + + if (score1 >= score2 && score1 >= score3) + { + result[writePos++] = filteredItems[idx1++].Item; + } + else if (score2 >= score3) + { + result[writePos++] = scoredFallbackItems[idx2++].Item; + } + else + { + result[writePos++] = filteredApps[idx3++].Item; + } + } + + // Two-way merges for remaining pairs. + while (idx1 < len1 && idx2 < len2) + { + if (filteredItems![idx1].Score >= scoredFallbackItems![idx2].Score) + { + result[writePos++] = filteredItems[idx1++].Item; + } + else + { + result[writePos++] = scoredFallbackItems[idx2++].Item; + } + } + + while (idx1 < len1 && idx3 < len3) + { + if (filteredItems![idx1].Score >= filteredApps![idx3].Score) + { + result[writePos++] = filteredItems[idx1++].Item; + } + else + { + result[writePos++] = filteredApps[idx3++].Item; + } + } + + while (idx2 < len2 && idx3 < len3) + { + if (scoredFallbackItems![idx2].Score >= filteredApps![idx3].Score) + { + result[writePos++] = scoredFallbackItems[idx2++].Item; + } + else + { + result[writePos++] = filteredApps[idx3++].Item; + } + } + + // Drain remaining items from a non-empty list. + while (idx1 < len1) + { + result[writePos++] = filteredItems![idx1++].Item; + } + + while (idx2 < len2) + { + result[writePos++] = scoredFallbackItems![idx2++].Item; + } + + while (idx3 < len3) + { + result[writePos++] = filteredApps![idx3++].Item; + } + + // Append filtered fallback items. Fallback items are added post-sort so they are + // always at the end of the list and eventually ordered based on user preference. + if (fallbackItems is not null) + { + for (int i = 0; i < fallbackItems.Count; i++) + { + var item = fallbackItems[i].Item; + if (!string.IsNullOrEmpty(item.Title)) + { + result[writePos++] = item; + } + } + } + + return result; + } + + private static int GetNonEmptyFallbackItemsCount(IList>? fallbackItems) + { + int fallbackItemsCount = 0; + + if (fallbackItems is not null) + { + for (int i = 0; i < fallbackItems.Count; i++) + { + if (!string.IsNullOrEmpty(fallbackItems[i].Item.Title)) + { + fallbackItemsCount++; + } + } + } + + return fallbackItemsCount; + } +} +#pragma warning restore IDE0007 // Use implicit type diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/MainWindowViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000000..140811c784 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/MainWindowViewModel.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CmdPal.UI.ViewModels.Services; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml.Media; +using Windows.UI; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public partial class MainWindowViewModel : ObservableObject, IDisposable +{ + private readonly IThemeService _themeService; + private readonly DispatcherQueue _uiDispatcherQueue = DispatcherQueue.GetForCurrentThread()!; + + [ObservableProperty] + public partial ImageSource? BackgroundImageSource { get; private set; } + + [ObservableProperty] + public partial Stretch BackgroundImageStretch { get; private set; } = Stretch.Fill; + + [ObservableProperty] + public partial double BackgroundImageOpacity { get; private set; } + + [ObservableProperty] + public partial Color BackgroundImageTint { get; private set; } + + [ObservableProperty] + public partial double BackgroundImageTintIntensity { get; private set; } + + [ObservableProperty] + public partial int BackgroundImageBlurAmount { get; private set; } + + [ObservableProperty] + public partial double BackgroundImageBrightness { get; private set; } + + [ObservableProperty] + public partial bool ShowBackgroundImage { get; private set; } + + public MainWindowViewModel(IThemeService themeService) + { + _themeService = themeService; + _themeService.ThemeChanged += ThemeService_ThemeChanged; + } + + private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e) + { + _uiDispatcherQueue.TryEnqueue(() => + { + BackgroundImageSource = _themeService.Current.BackgroundImageSource; + BackgroundImageStretch = _themeService.Current.BackgroundImageStretch; + BackgroundImageOpacity = _themeService.Current.BackgroundImageOpacity; + + BackgroundImageBrightness = _themeService.Current.BackgroundBrightness; + BackgroundImageTint = _themeService.Current.Tint; + BackgroundImageTintIntensity = _themeService.Current.TintIntensity; + BackgroundImageBlurAmount = _themeService.Current.BlurAmount; + + ShowBackgroundImage = BackgroundImageSource != null; + }); + } + + public void Dispose() + { + _themeService.ThemeChanged -= ThemeService_ThemeChanged; + GC.SuppressFinalize(this); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj index 6b1b018273..1c85aa939b 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj @@ -23,11 +23,12 @@ + compile - + compile diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs index be9d103b2d..8bc2a42a92 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class Resources { @@ -411,6 +411,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties { } } + /// + /// Looks up a localized string similar to Pick background image. + /// + public static string builtin_settings_appearance_pick_background_image_title { + get { + return ResourceManager.GetString("builtin_settings_appearance_pick_background_image_title", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0} extensions found. /// diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx index 9a658e38f1..bb7637e133 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx @@ -239,4 +239,7 @@ {0} extensions installed + + Pick background image + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/AcrylicBackdropParameters.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/AcrylicBackdropParameters.cs new file mode 100644 index 0000000000..efb7ca1fa1 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/AcrylicBackdropParameters.cs @@ -0,0 +1,9 @@ +// 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 Windows.UI; + +namespace Microsoft.CmdPal.UI.ViewModels.Services; + +public sealed record AcrylicBackdropParameters(Color TintColor, Color FallbackColor, float TintOpacity, float LuminosityOpacity); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/IThemeService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/IThemeService.cs new file mode 100644 index 0000000000..546742b8f4 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/IThemeService.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.UI.ViewModels.Services; + +/// +/// Provides theme-related values for the Command Palette and notifies listeners about +/// changes that affect visual appearance (theme, tint, background image, and backdrop). +/// +/// +/// Implementations are expected to monitor system/app theme changes and raise +/// accordingly. Consumers should call +/// once to hook required sources and then query properties/methods for the current visuals. +/// +public interface IThemeService +{ + /// + /// Occurs when the effective theme or any visual-affecting setting changes. + /// + /// + /// Triggered for changes such as app theme (light/dark/default), background image, + /// tint/accent, or backdrop parameters that would require UI to refresh styling. + /// + event EventHandler? ThemeChanged; + + /// + /// Initializes the theme service and starts listening for theme-related changes. + /// + /// + /// Safe to call once during application startup before consuming the service. + /// + void Initialize(); + + /// + /// Gets the current theme settings. + /// + ThemeSnapshot Current { get; } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ThemeChangedEventArgs.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ThemeChangedEventArgs.cs new file mode 100644 index 0000000000..96197dc376 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ThemeChangedEventArgs.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.UI.ViewModels.Services; + +/// +/// Event arguments for theme-related changes. +public class ThemeChangedEventArgs : EventArgs; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ThemeSnapshot.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ThemeSnapshot.cs new file mode 100644 index 0000000000..244fd41fba --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Services/ThemeSnapshot.cs @@ -0,0 +1,62 @@ +// 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.UI.Xaml; +using Microsoft.UI.Xaml.Media; +using Windows.UI; + +namespace Microsoft.CmdPal.UI.ViewModels.Services; + +/// +/// Represents a snapshot of theme-related visual settings, including accent color, theme preference, and background +/// image configuration, for use in rendering the Command Palette UI. +/// +public sealed class ThemeSnapshot +{ + /// + /// Gets the accent tint color used by the Command Palette visuals. + /// + public required Color Tint { get; init; } + + /// + /// Gets the accent tint color used by the Command Palette visuals. + /// + public required float TintIntensity { get; init; } + + /// + /// Gets the configured application theme preference. + /// + public required ElementTheme Theme { get; init; } + + /// + /// Gets the image source to render as the background, if any. + /// + /// + /// Returns when no background image is configured. + /// + public required ImageSource? BackgroundImageSource { get; init; } + + /// + /// Gets the stretch mode used to lay out the background image. + /// + public required Stretch BackgroundImageStretch { get; init; } + + /// + /// Gets the opacity applied to the background image. + /// + /// + /// A value in the range [0, 1], where 0 is fully transparent and 1 is fully opaque. + /// + public required double BackgroundImageOpacity { get; init; } + + /// + /// Gets the effective acrylic backdrop parameters based on current settings and theme. + /// + /// The resolved AcrylicBackdropParameters to apply. + public required AcrylicBackdropParameters BackdropParameters { get; init; } + + public required int BlurAmount { get; init; } + + public required float BackgroundBrightness { get; init; } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs index aee23ef0ca..e210359f76 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs @@ -11,7 +11,9 @@ using CommunityToolkit.Mvvm.ComponentModel; using ManagedCommon; using Microsoft.CmdPal.UI.ViewModels.Settings; using Microsoft.CommandPalette.Extensions.Toolkit; +using Microsoft.UI; using Windows.Foundation; +using Windows.UI; namespace Microsoft.CmdPal.UI.ViewModels; @@ -60,6 +62,26 @@ public partial class SettingsModel : ObservableObject public TimeSpan AutoGoHomeInterval { get; set; } = Timeout.InfiniteTimeSpan; + public EscapeKeyBehavior EscapeKeyBehaviorSetting { get; set; } = EscapeKeyBehavior.ClearSearchFirstThenGoBack; + + public UserTheme Theme { get; set; } = UserTheme.Default; + + public ColorizationMode ColorizationMode { get; set; } + + public Color CustomThemeColor { get; set; } = Colors.Transparent; + + public int CustomThemeColorIntensity { get; set; } = 100; + + public int BackgroundImageOpacity { get; set; } = 20; + + public int BackgroundImageBlurAmount { get; set; } + + public int BackgroundImageBrightness { get; set; } + + public BackgroundImageFit BackgroundImageFit { get; set; } + + public string? BackgroundImagePath { get; set; } + // END SETTINGS /////////////////////////////////////////////////////////////////////////// @@ -282,3 +304,11 @@ public enum MonitorBehavior InPlace = 3, ToLast = 4, } + +public enum EscapeKeyBehavior +{ + ClearSearchFirstThenGoBack = 0, + AlwaysGoBack = 1, + AlwaysDismiss = 2, + AlwaysHide = 3, +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs index 4d44db7d8a..6ac9acacc4 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs @@ -4,6 +4,8 @@ using System.Collections.ObjectModel; using System.ComponentModel; +using Microsoft.CmdPal.Core.Common.Services; +using Microsoft.CmdPal.UI.ViewModels.Services; using Microsoft.CmdPal.UI.ViewModels.Settings; using Microsoft.Extensions.DependencyInjection; @@ -29,6 +31,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged public event PropertyChangedEventHandler? PropertyChanged; + public AppearanceSettingsViewModel Appearance { get; } + public HotkeySettings? Hotkey { get => _settings.Hotkey; @@ -160,6 +164,16 @@ public partial class SettingsViewModel : INotifyPropertyChanged } } + public int EscapeKeyBehaviorIndex + { + get => (int)_settings.EscapeKeyBehaviorSetting; + set + { + _settings.EscapeKeyBehaviorSetting = (EscapeKeyBehavior)value; + Save(); + } + } + public ObservableCollection CommandProviders { get; } = []; public SettingsExtensionsViewModel Extensions { get; } @@ -169,6 +183,9 @@ public partial class SettingsViewModel : INotifyPropertyChanged _settings = settings; _serviceProvider = serviceProvider; + var themeService = serviceProvider.GetRequiredService(); + Appearance = new AppearanceSettingsViewModel(themeService, _settings); + var activeProviders = GetCommandProviders(); var allProviderSettings = _settings.ProviderSettings; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/UserTheme.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/UserTheme.cs new file mode 100644 index 0000000000..290668f3f5 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/UserTheme.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.UI.ViewModels; + +public enum UserTheme +{ + Default, + Light, + Dark, +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml index f9a9e37ea1..d8d4655291 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml @@ -4,19 +4,23 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="using:Microsoft.CmdPal.UI.Controls" - xmlns:local="using:Microsoft.CmdPal.UI"> + xmlns:local="using:Microsoft.CmdPal.UI" + xmlns:services="using:Microsoft.CmdPal.UI.Services"> - - - - - - - + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs index 53f47286b2..a44682218f 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs @@ -24,9 +24,11 @@ using Microsoft.CmdPal.Ext.WindowsTerminal; using Microsoft.CmdPal.Ext.WindowWalker; using Microsoft.CmdPal.Ext.WinGet; using Microsoft.CmdPal.UI.Helpers; +using Microsoft.CmdPal.UI.Services; using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; using Microsoft.CmdPal.UI.ViewModels.Models; +using Microsoft.CmdPal.UI.ViewModels.Services; using Microsoft.CommandPalette.Extensions; using Microsoft.Extensions.DependencyInjection; using Microsoft.PowerToys.Telemetry; @@ -112,6 +114,17 @@ public partial class App : Application // Root services services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext()); + AddBuiltInCommands(services); + + AddCoreServices(services); + + AddUIServices(services); + + return services.BuildServiceProvider(); + } + + private static void AddBuiltInCommands(ServiceCollection services) + { // Built-in Commands. Order matters - this is the order they'll be presented by default. var allApps = new AllAppsCommandProvider(); var files = new IndexerCommandsProvider(); @@ -154,17 +167,32 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + } + private static void AddUIServices(ServiceCollection services) + { // Models - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); var sm = SettingsModel.LoadSettings(); services.AddSingleton(sm); var state = AppStateModel.LoadState(); services.AddSingleton(state); - services.AddSingleton(); + + // Services + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + } + + private static void AddCoreServices(ServiceCollection services) + { + // Core services + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -174,7 +202,5 @@ public partial class App : Application // ViewModels services.AddSingleton(); services.AddSingleton(); - - return services.BuildServiceProvider(); } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/CmdPal.Branding.props b/src/modules/cmdpal/Microsoft.CmdPal.UI/CmdPal.Branding.props index d99688c081..298bcbd787 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/CmdPal.Branding.props +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/CmdPal.Branding.props @@ -24,7 +24,7 @@ - + true Assets\%(RecursiveDir)%(FileName)%(Extension) @@ -35,14 +35,10 @@ - - - - + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/CmdPal.pre.props b/src/modules/cmdpal/Microsoft.CmdPal.UI/CmdPal.pre.props index 21c2e7d8d1..d65b4a2cc2 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/CmdPal.pre.props +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/CmdPal.pre.props @@ -7,7 +7,7 @@ - $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + ..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\CmdPal $(OutputPath)\AppPackages\ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/BlurImageControl.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/BlurImageControl.cs new file mode 100644 index 0000000000..743e68d690 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/BlurImageControl.cs @@ -0,0 +1,412 @@ +// 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 ManagedCommon; +using Microsoft.Graphics.Canvas.Effects; +using Microsoft.UI; +using Microsoft.UI.Composition; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Hosting; +using Microsoft.UI.Xaml.Media; +using Windows.UI; + +namespace Microsoft.CmdPal.UI.Controls; + +internal sealed partial class BlurImageControl : Control +{ + private const string ImageSourceParameterName = "ImageSource"; + + private const string BrightnessEffectName = "Brightness"; + private const string BrightnessOverlayEffectName = "BrightnessOverlay"; + private const string BlurEffectName = "Blur"; + private const string TintBlendEffectName = "TintBlend"; + private const string TintEffectName = "Tint"; + +#pragma warning disable CA1507 // Use nameof to express symbol names ... some of these refer to effect properties that are separate from the class properties + private static readonly string BrightnessSource1AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source1Amount"); + private static readonly string BrightnessSource2AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source2Amount"); + private static readonly string BrightnessOverlayColorEffectProperty = GetPropertyName(BrightnessOverlayEffectName, "Color"); + private static readonly string BlurBlurAmountEffectProperty = GetPropertyName(BlurEffectName, "BlurAmount"); + private static readonly string TintColorEffectProperty = GetPropertyName(TintEffectName, "Color"); +#pragma warning restore CA1507 + + private static readonly string[] AnimatableProperties = [ + BrightnessSource1AmountEffectProperty, + BrightnessSource2AmountEffectProperty, + BrightnessOverlayColorEffectProperty, + BlurBlurAmountEffectProperty, + TintColorEffectProperty + ]; + + public static readonly DependencyProperty ImageSourceProperty = + DependencyProperty.Register( + nameof(ImageSource), + typeof(ImageSource), + typeof(BlurImageControl), + new PropertyMetadata(null, OnImageChanged)); + + public static readonly DependencyProperty ImageStretchProperty = + DependencyProperty.Register( + nameof(ImageStretch), + typeof(Stretch), + typeof(BlurImageControl), + new PropertyMetadata(Stretch.UniformToFill, OnImageStretchChanged)); + + public static readonly DependencyProperty ImageOpacityProperty = + DependencyProperty.Register( + nameof(ImageOpacity), + typeof(double), + typeof(BlurImageControl), + new PropertyMetadata(1.0, OnOpacityChanged)); + + public static readonly DependencyProperty ImageBrightnessProperty = + DependencyProperty.Register( + nameof(ImageBrightness), + typeof(double), + typeof(BlurImageControl), + new PropertyMetadata(1.0, OnBrightnessChanged)); + + public static readonly DependencyProperty BlurAmountProperty = + DependencyProperty.Register( + nameof(BlurAmount), + typeof(double), + typeof(BlurImageControl), + new PropertyMetadata(0.0, OnBlurAmountChanged)); + + public static readonly DependencyProperty TintColorProperty = + DependencyProperty.Register( + nameof(TintColor), + typeof(Color), + typeof(BlurImageControl), + new PropertyMetadata(Colors.Transparent, OnVisualPropertyChanged)); + + public static readonly DependencyProperty TintIntensityProperty = + DependencyProperty.Register( + nameof(TintIntensity), + typeof(double), + typeof(BlurImageControl), + new PropertyMetadata(0.0, OnVisualPropertyChanged)); + + private Compositor? _compositor; + private SpriteVisual? _effectVisual; + private CompositionEffectBrush? _effectBrush; + private CompositionSurfaceBrush? _imageBrush; + + public BlurImageControl() + { + this.DefaultStyleKey = typeof(BlurImageControl); + this.Loaded += OnLoaded; + this.SizeChanged += OnSizeChanged; + } + + public ImageSource ImageSource + { + get => (ImageSource)GetValue(ImageSourceProperty); + set => SetValue(ImageSourceProperty, value); + } + + public Stretch ImageStretch + { + get => (Stretch)GetValue(ImageStretchProperty); + set => SetValue(ImageStretchProperty, value); + } + + public double ImageOpacity + { + get => (double)GetValue(ImageOpacityProperty); + set => SetValue(ImageOpacityProperty, value); + } + + public double ImageBrightness + { + get => (double)GetValue(ImageBrightnessProperty); + set => SetValue(ImageBrightnessProperty, Math.Clamp(value, -1, 1)); + } + + public double BlurAmount + { + get => (double)GetValue(BlurAmountProperty); + set => SetValue(BlurAmountProperty, value); + } + + public Color TintColor + { + get => (Color)GetValue(TintColorProperty); + set => SetValue(TintColorProperty, value); + } + + public double TintIntensity + { + get => (double)GetValue(TintIntensityProperty); + set => SetValue(TintIntensityProperty, value); + } + + private static void OnImageStretchChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is BlurImageControl control && control._imageBrush != null) + { + control._imageBrush.Stretch = ConvertStretch((Stretch)e.NewValue); + } + } + + private static void OnVisualPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is BlurImageControl control && control._compositor != null) + { + control.UpdateEffect(); + } + } + + private static void OnOpacityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is BlurImageControl control && control._effectVisual != null) + { + control._effectVisual.Opacity = (float)(double)e.NewValue; + } + } + + private static void OnBlurAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is BlurImageControl control && control._effectBrush != null) + { + control.UpdateEffect(); + } + } + + private static void OnBrightnessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is BlurImageControl control && control._effectBrush != null) + { + control.UpdateEffect(); + } + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + InitializeComposition(); + } + + private void OnSizeChanged(object sender, SizeChangedEventArgs e) + { + if (_effectVisual != null) + { + _effectVisual.Size = new Vector2( + (float)Math.Max(1, e.NewSize.Width), + (float)Math.Max(1, e.NewSize.Height)); + } + } + + private static void OnImageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not BlurImageControl control) + { + return; + } + + control.EnsureEffect(force: true); + control.UpdateEffect(); + } + + private void InitializeComposition() + { + var visual = ElementCompositionPreview.GetElementVisual(this); + _compositor = visual.Compositor; + + _effectVisual = _compositor.CreateSpriteVisual(); + _effectVisual.Size = new Vector2( + (float)Math.Max(1, ActualWidth), + (float)Math.Max(1, ActualHeight)); + _effectVisual.Opacity = (float)ImageOpacity; + + ElementCompositionPreview.SetElementChildVisual(this, _effectVisual); + + UpdateEffect(); + } + + private void EnsureEffect(bool force = false) + { + if (_compositor is null) + { + return; + } + + if (_effectBrush is not null && !force) + { + return; + } + + var imageSource = new CompositionEffectSourceParameter(ImageSourceParameterName); + + // 1) Brightness via ArithmeticCompositeEffect + // We blend between the original image and either black or white, + // depending on whether we want to darken or brighten. BrightnessEffect isn't supported + // in the composition graph. + var brightnessEffect = new ArithmeticCompositeEffect + { + Name = BrightnessEffectName, + Source1 = imageSource, // original image + Source2 = new ColorSourceEffect + { + Name = BrightnessOverlayEffectName, + Color = Colors.Black, // we'll swap black/white via properties + }, + + MultiplyAmount = 0.0f, + Source1Amount = 1.0f, // original + Source2Amount = 0.0f, // overlay + Offset = 0.0f, + }; + + // 2) Blur + var blurEffect = new GaussianBlurEffect + { + Name = BlurEffectName, + BlurAmount = 0.0f, + BorderMode = EffectBorderMode.Hard, + Optimization = EffectOptimization.Balanced, + Source = brightnessEffect, + }; + + // 3) Tint (always in the chain; intensity via alpha) + var tintEffect = new BlendEffect + { + Name = TintBlendEffectName, + Background = blurEffect, + Foreground = new ColorSourceEffect + { + Name = TintEffectName, + Color = Colors.Transparent, + }, + Mode = BlendEffectMode.Multiply, + }; + + var effectFactory = _compositor.CreateEffectFactory(tintEffect, AnimatableProperties); + + _effectBrush?.Dispose(); + _effectBrush = effectFactory.CreateBrush(); + + // Set initial source + if (ImageSource is not null) + { + _imageBrush ??= _compositor.CreateSurfaceBrush(); + LoadImageAsync(ImageSource); + _effectBrush.SetSourceParameter(ImageSourceParameterName, _imageBrush); + } + else + { + _effectBrush.SetSourceParameter(ImageSourceParameterName, _compositor.CreateBackdropBrush()); + } + + if (_effectVisual is not null) + { + _effectVisual.Brush = _effectBrush; + } + } + + private void UpdateEffect() + { + if (_compositor is null) + { + return; + } + + EnsureEffect(); + if (_effectBrush is null) + { + return; + } + + var props = _effectBrush.Properties; + + // Brightness + var b = (float)Math.Clamp(ImageBrightness, -1.0, 1.0); + + float source1Amount; + float source2Amount; + Color overlayColor; + + if (b >= 0) + { + // Brighten: blend towards white + overlayColor = Colors.White; + source1Amount = 1.0f - b; // original image contribution + source2Amount = b; // white overlay contribution + } + else + { + // Darken: blend towards black + overlayColor = Colors.Black; + var t = -b; // 0..1 + source1Amount = 1.0f - t; // original image + source2Amount = t; // black overlay + } + + props.InsertScalar(BrightnessSource1AmountEffectProperty, source1Amount); + props.InsertScalar(BrightnessSource2AmountEffectProperty, source2Amount); + props.InsertColor(BrightnessOverlayColorEffectProperty, overlayColor); + + // Blur + props.InsertScalar(BlurBlurAmountEffectProperty, (float)BlurAmount); + + // Tint + var tintColor = TintColor; + var clampedIntensity = (float)Math.Clamp(TintIntensity, 0.0, 1.0); + + var adjustedColor = Color.FromArgb( + (byte)(clampedIntensity * 255), + tintColor.R, + tintColor.G, + tintColor.B); + + props.InsertColor(TintColorEffectProperty, adjustedColor); + } + + private void LoadImageAsync(ImageSource imageSource) + { + try + { + if (imageSource is Microsoft.UI.Xaml.Media.Imaging.BitmapImage bitmapImage) + { + _imageBrush ??= _compositor?.CreateSurfaceBrush(); + if (_imageBrush is null) + { + return; + } + + var loadedSurface = LoadedImageSurface.StartLoadFromUri(bitmapImage.UriSource); + loadedSurface.LoadCompleted += (_, _) => + { + if (_imageBrush is not null) + { + _imageBrush.Surface = loadedSurface; + _imageBrush.Stretch = ConvertStretch(ImageStretch); + _imageBrush.BitmapInterpolationMode = CompositionBitmapInterpolationMode.Linear; + } + }; + + _effectBrush?.SetSourceParameter(ImageSourceParameterName, _imageBrush); + } + } + catch (Exception ex) + { + Logger.LogError("Failed to load image for BlurImageControl: {0}", ex); + } + } + + private static CompositionStretch ConvertStretch(Stretch stretch) + { + return stretch switch + { + Stretch.None => CompositionStretch.None, + Stretch.Fill => CompositionStretch.Fill, + Stretch.Uniform => CompositionStretch.Uniform, + Stretch.UniformToFill => CompositionStretch.UniformToFill, + _ => CompositionStretch.UniformToFill, + }; + } + + private static string GetPropertyName(string effectName, string propertyName) => $"{effectName}.{propertyName}"; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPalette.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPalette.xaml new file mode 100644 index 0000000000..105010bbd2 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPalette.xaml @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPalette.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPalette.xaml.cs new file mode 100644 index 0000000000..7267e894fa --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPalette.xaml.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Windows.UI; + +namespace Microsoft.CmdPal.UI.Controls; + +public sealed partial class ColorPalette : UserControl +{ + public static readonly DependencyProperty PaletteColorsProperty = DependencyProperty.Register(nameof(PaletteColors), typeof(ObservableCollection), typeof(ColorPalette), null!)!; + + public static readonly DependencyProperty CustomPaletteColumnCountProperty = DependencyProperty.Register(nameof(CustomPaletteColumnCount), typeof(int), typeof(ColorPalette), new PropertyMetadata(10))!; + + public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(ColorPalette), new PropertyMetadata(null!))!; + + public event EventHandler? SelectedColorChanged; + + private Color? _selectedColor; + + public Color? SelectedColor + { + get => _selectedColor; + + set + { + if (_selectedColor != value) + { + _selectedColor = value; + if (value is not null) + { + SetValue(SelectedColorProperty, value); + } + else + { + ClearValue(SelectedColorProperty); + } + } + } + } + + public ObservableCollection PaletteColors + { + get => (ObservableCollection)GetValue(PaletteColorsProperty)!; + set => SetValue(PaletteColorsProperty, value); + } + + public int CustomPaletteColumnCount + { + get => (int)GetValue(CustomPaletteColumnCountProperty); + set => SetValue(CustomPaletteColumnCountProperty, value); + } + + public ColorPalette() + { + PaletteColors = []; + InitializeComponent(); + } + + private void ListViewBase_OnItemClick(object sender, ItemClickEventArgs e) + { + if (e.ClickedItem is Color color) + { + SelectedColor = color; + SelectedColorChanged?.Invoke(this, color); + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPickerButton.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPickerButton.xaml new file mode 100644 index 0000000000..92a556f7a7 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPickerButton.xaml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPickerButton.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPickerButton.xaml.cs new file mode 100644 index 0000000000..ff82fffd4e --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ColorPickerButton.xaml.cs @@ -0,0 +1,146 @@ +// 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.ObjectModel; +using ManagedCommon; +using Microsoft.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Windows.UI; + +namespace Microsoft.CmdPal.UI.Controls; + +public sealed partial class ColorPickerButton : UserControl +{ + public static readonly DependencyProperty PaletteColorsProperty = DependencyProperty.Register(nameof(PaletteColors), typeof(ObservableCollection), typeof(ColorPickerButton), new PropertyMetadata(new ObservableCollection()))!; + + public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(Color), typeof(ColorPickerButton), new PropertyMetadata(Colors.Black))!; + + public static readonly DependencyProperty IsAlphaEnabledProperty = DependencyProperty.Register(nameof(IsAlphaEnabled), typeof(bool), typeof(ColorPickerButton), new PropertyMetadata(defaultValue: false))!; + + public static readonly DependencyProperty IsValueEditorEnabledProperty = DependencyProperty.Register(nameof(IsValueEditorEnabled), typeof(bool), typeof(ColorPickerButton), new PropertyMetadata(false))!; + + public static readonly DependencyProperty HasSelectedColorProperty = DependencyProperty.Register(nameof(HasSelectedColor), typeof(bool), typeof(ColorPickerButton), new PropertyMetadata(false))!; + + private Color _selectedColor; + + public Color SelectedColor + { + get + { + return _selectedColor; + } + + set + { + if (_selectedColor != value) + { + _selectedColor = value; + SetValue(SelectedColorProperty, value); + HasSelectedColor = true; + } + } + } + + public bool HasSelectedColor + { + get { return (bool)GetValue(HasSelectedColorProperty); } + set { SetValue(HasSelectedColorProperty, value); } + } + + public bool IsAlphaEnabled + { + get => (bool)GetValue(IsAlphaEnabledProperty); + set => SetValue(IsAlphaEnabledProperty, value); + } + + public bool IsValueEditorEnabled + { + get { return (bool)GetValue(IsValueEditorEnabledProperty); } + set { SetValue(IsValueEditorEnabledProperty, value); } + } + + public ObservableCollection PaletteColors + { + get { return (ObservableCollection)GetValue(PaletteColorsProperty); } + set { SetValue(PaletteColorsProperty, value); } + } + + public ColorPickerButton() + { + this.InitializeComponent(); + + IsEnabledChanged -= ColorPickerButton_IsEnabledChanged; + SetEnabledState(); + IsEnabledChanged += ColorPickerButton_IsEnabledChanged; + } + + private void ColorPickerButton_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) + { + SetEnabledState(); + } + + private void SetEnabledState() + { + if (this.IsEnabled) + { + ColorPreviewBorder.Opacity = 1; + } + else + { + ColorPreviewBorder.Opacity = 0.2; + } + } + + private void ColorPalette_OnSelectedColorChanged(object? sender, Color? e) + { + if (e.HasValue) + { + HasSelectedColor = true; + SelectedColor = e.Value; + } + } + + private void FlyoutBase_OnOpened(object? sender, object e) + { + if (sender is not Flyout flyout || (flyout.Content as FrameworkElement)?.Parent is not FlyoutPresenter flyoutPresenter) + { + return; + } + + FlyoutRoot!.UpdateLayout(); + flyoutPresenter.UpdateLayout(); + + // Logger.LogInfo($"FlyoutBase_OnOpened: {flyoutPresenter}, {FlyoutRoot!.ActualWidth}"); + flyoutPresenter.MaxWidth = FlyoutRoot!.ActualWidth; + flyoutPresenter.MinWidth = 660; + flyoutPresenter.Width = FlyoutRoot!.ActualWidth; + } + + private void FlyoutRoot_OnSizeChanged(object sender, SizeChangedEventArgs e) + { + if ((ColorPickerFlyout!.Content as FrameworkElement)?.Parent is not FlyoutPresenter flyoutPresenter) + { + return; + } + + FlyoutRoot!.UpdateLayout(); + flyoutPresenter.UpdateLayout(); + + flyoutPresenter.MaxWidth = FlyoutRoot!.ActualWidth; + flyoutPresenter.MinWidth = 660; + flyoutPresenter.Width = FlyoutRoot!.ActualWidth; + } + + private Thickness ToDropDownPadding(bool hasColor) + { + return hasColor ? new Thickness(3, 3, 8, 3) : new Thickness(8, 4, 8, 4); + } + + private void ResetButton_Click(object sender, RoutedEventArgs e) + { + HasSelectedColor = false; + ColorPickerFlyout?.Hide(); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml index 78e234a184..bc78de7e67 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml @@ -142,9 +142,9 @@ FontSize="16" Glyph="" /> + Style="{StaticResource CaptionTextBlockStyle}" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandPalettePreview.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandPalettePreview.xaml.cs new file mode 100644 index 0000000000..96cd5d6aac --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandPalettePreview.xaml.cs @@ -0,0 +1,123 @@ +// 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.CmdPal.UI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Windows.UI; + +namespace Microsoft.CmdPal.UI.Controls; + +public sealed partial class CommandPalettePreview : UserControl +{ + public static readonly DependencyProperty PreviewBackgroundOpacityProperty = DependencyProperty.Register(nameof(PreviewBackgroundOpacity), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d)); + + public static readonly DependencyProperty PreviewBackgroundColorProperty = DependencyProperty.Register(nameof(PreviewBackgroundColor), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color))); + + public static readonly DependencyProperty PreviewBackgroundImageSourceProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageSource), typeof(ImageSource), typeof(CommandPalettePreview), new PropertyMetadata(null, PropertyChangedCallback)); + + public static readonly DependencyProperty PreviewBackgroundImageOpacityProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageOpacity), typeof(int), typeof(CommandPalettePreview), new PropertyMetadata(0)); + + public static readonly DependencyProperty PreviewBackgroundImageFitProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageFit), typeof(BackgroundImageFit), typeof(CommandPalettePreview), new PropertyMetadata(default(BackgroundImageFit))); + + public static readonly DependencyProperty PreviewBackgroundImageBrightnessProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageBrightness), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d)); + + public static readonly DependencyProperty PreviewBackgroundImageBlurAmountProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageBlurAmount), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d)); + + public static readonly DependencyProperty PreviewBackgroundImageTintProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageTint), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color))); + + public static readonly DependencyProperty PreviewBackgroundImageTintIntensityProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageTintIntensity), typeof(int), typeof(CommandPalettePreview), new PropertyMetadata(0)); + + public static readonly DependencyProperty ShowBackgroundImageProperty = DependencyProperty.Register(nameof(ShowBackgroundImage), typeof(Visibility), typeof(CommandPalettePreview), new PropertyMetadata(Visibility.Collapsed)); + + public BackgroundImageFit PreviewBackgroundImageFit + { + get { return (BackgroundImageFit)GetValue(PreviewBackgroundImageFitProperty); } + set { SetValue(PreviewBackgroundImageFitProperty, value); } + } + + public double PreviewBackgroundOpacity + { + get { return (double)GetValue(PreviewBackgroundOpacityProperty); } + set { SetValue(PreviewBackgroundOpacityProperty, value); } + } + + public Color PreviewBackgroundColor + { + get { return (Color)GetValue(PreviewBackgroundColorProperty); } + set { SetValue(PreviewBackgroundColorProperty, value); } + } + + public ImageSource PreviewBackgroundImageSource + { + get { return (ImageSource)GetValue(PreviewBackgroundImageSourceProperty); } + set { SetValue(PreviewBackgroundImageSourceProperty, value); } + } + + public int PreviewBackgroundImageOpacity + { + get { return (int)GetValue(PreviewBackgroundImageOpacityProperty); } + set { SetValue(PreviewBackgroundImageOpacityProperty, value); } + } + + public double PreviewBackgroundImageBrightness + { + get => (double)GetValue(PreviewBackgroundImageBrightnessProperty); + set => SetValue(PreviewBackgroundImageBrightnessProperty, value); + } + + public double PreviewBackgroundImageBlurAmount + { + get => (double)GetValue(PreviewBackgroundImageBlurAmountProperty); + set => SetValue(PreviewBackgroundImageBlurAmountProperty, value); + } + + public Color PreviewBackgroundImageTint + { + get => (Color)GetValue(PreviewBackgroundImageTintProperty); + set => SetValue(PreviewBackgroundImageTintProperty, value); + } + + public int PreviewBackgroundImageTintIntensity + { + get => (int)GetValue(PreviewBackgroundImageTintIntensityProperty); + set => SetValue(PreviewBackgroundImageTintIntensityProperty, value); + } + + public Visibility ShowBackgroundImage + { + get => (Visibility)GetValue(ShowBackgroundImageProperty); + set => SetValue(ShowBackgroundImageProperty, value); + } + + public CommandPalettePreview() + { + InitializeComponent(); + } + + private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not CommandPalettePreview preview) + { + return; + } + + preview.ShowBackgroundImage = e.NewValue is ImageSource ? Visibility.Visible : Visibility.Collapsed; + } + + private double ToOpacity(int value) => value / 100.0; + + private double ToTintIntensity(int value) => value / 100.0; + + private Stretch ToStretch(BackgroundImageFit fit) + { + return fit switch + { + BackgroundImageFit.Fill => Stretch.Fill, + BackgroundImageFit.UniformToFill => Stretch.UniformToFill, + _ => Stretch.None, + }; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ScreenPreview.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ScreenPreview.xaml new file mode 100644 index 0000000000..58c4e890a6 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ScreenPreview.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ScreenPreview.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ScreenPreview.xaml.cs new file mode 100644 index 0000000000..828fa76c74 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ScreenPreview.xaml.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.UI.Helpers; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Markup; +using Microsoft.UI.Xaml.Media; + +namespace Microsoft.CmdPal.UI.Controls; + +[ContentProperty(Name = nameof(PreviewContent))] +public sealed partial class ScreenPreview : UserControl +{ + public static readonly DependencyProperty PreviewContentProperty = + DependencyProperty.Register(nameof(PreviewContent), typeof(object), typeof(ScreenPreview), new PropertyMetadata(null!))!; + + public object PreviewContent + { + get => GetValue(PreviewContentProperty)!; + set => SetValue(PreviewContentProperty, value); + } + + public ScreenPreview() + { + InitializeComponent(); + + var wallpaperHelper = new WallpaperHelper(); + WallpaperImage!.Source = wallpaperHelper.GetWallpaperImage()!; + ScreenBorder!.Background = new SolidColorBrush(wallpaperHelper.GetWallpaperColor()); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml index 80eb1a3ad6..d248c24f89 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml @@ -4,9 +4,8 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:cmdpalUi="using:Microsoft.CmdPal.UI" - xmlns:converters="using:CommunityToolkit.WinUI.Converters" - xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:h="using:Microsoft.CmdPal.UI.Helpers" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> @@ -22,6 +21,7 @@ MinHeight="32" VerticalAlignment="Stretch" VerticalContentAlignment="Stretch" + h:TextBoxCaretColor.SyncWithForeground="True" AutomationProperties.AutomationId="MainSearchBox" KeyDown="FilterBox_KeyDown" PlaceholderText="{x:Bind CurrentPageViewModel.PlaceholderText, Converter={StaticResource PlaceholderTextConverter}, Mode=OneWay}" diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs index f5bac4a286..0d6fd58afa 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs @@ -7,8 +7,10 @@ using CommunityToolkit.WinUI; using Microsoft.CmdPal.Core.ViewModels; using Microsoft.CmdPal.Core.ViewModels.Commands; using Microsoft.CmdPal.Core.ViewModels.Messages; -using Microsoft.CmdPal.UI.Messages; +using Microsoft.CmdPal.Ext.ClipboardHistory.Messages; +using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.Views; +using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Dispatching; using Microsoft.UI.Input; using Microsoft.UI.Xaml; @@ -49,6 +51,8 @@ public sealed partial class SearchBar : UserControl, // 0.6+ suggestions private string? _textToSuggest; + private SettingsModel Settings => App.Current.Services.GetRequiredService(); + public PageViewModel? CurrentPageViewModel { get => (PageViewModel?)GetValue(CurrentPageViewModelProperty); @@ -131,20 +135,39 @@ public sealed partial class SearchBar : UserControl, } else if (e.Key == VirtualKey.Escape) { - if (string.IsNullOrEmpty(FilterBox.Text)) + switch (Settings.EscapeKeyBehaviorSetting) { - WeakReferenceMessenger.Default.Send(new()); - } - else - { - // Clear the search box - FilterBox.Text = string.Empty; + case EscapeKeyBehavior.AlwaysGoBack: + WeakReferenceMessenger.Default.Send(new()); + break; - // hack TODO GH #245 - if (CurrentPageViewModel is not null) - { - CurrentPageViewModel.SearchTextBox = FilterBox.Text; - } + case EscapeKeyBehavior.AlwaysDismiss: + WeakReferenceMessenger.Default.Send(new(ForceGoHome: true)); + break; + + case EscapeKeyBehavior.AlwaysHide: + WeakReferenceMessenger.Default.Send(new()); + break; + + case EscapeKeyBehavior.ClearSearchFirstThenGoBack: + default: + if (string.IsNullOrEmpty(FilterBox.Text)) + { + WeakReferenceMessenger.Default.Send(new()); + } + else + { + // Clear the search box + FilterBox.Text = string.Empty; + + // hack TODO GH #245 + if (CurrentPageViewModel is not null) + { + CurrentPageViewModel.SearchTextBox = FilterBox.Text; + } + } + + break; } e.Handled = true; @@ -185,21 +208,32 @@ public sealed partial class SearchBar : UserControl, e.Handled = true; } + else if (e.Key == VirtualKey.Left) + { + // Check if we're in a grid view, and if so, send grid navigation command + var isGridView = CurrentPageViewModel is ListViewModel { IsGridView: true }; + + // Special handling is required if we're in grid view. + if (isGridView) + { + WeakReferenceMessenger.Default.Send(); + e.Handled = true; + } + } else if (e.Key == VirtualKey.Right) { // Check if the "replace search text with suggestion" feature from 0.4-0.5 is enabled. // If it isn't, then only use the suggestion when the caret is at the end of the input. if (!IsTextToSuggestEnabled) { - if (_textToSuggest != null && + if (!string.IsNullOrEmpty(_textToSuggest) && FilterBox.SelectionStart == FilterBox.Text.Length) { FilterBox.Text = _textToSuggest; FilterBox.Select(_textToSuggest.Length, 0); e.Handled = true; + return; } - - return; } // Here, we're using the "replace search text with suggestion" feature. @@ -209,6 +243,20 @@ public sealed partial class SearchBar : UserControl, _lastText = null; DoFilterBoxUpdate(); } + + // Wouldn't want to perform text completion *and* move the selected item, so only perform this if text suggestion wasn't performed. + if (!e.Handled) + { + // Check if we're in a grid view, and if so, send grid navigation command + var isGridView = CurrentPageViewModel is ListViewModel { IsGridView: true }; + + // Special handling is required if we're in grid view. + if (isGridView) + { + WeakReferenceMessenger.Default.Send(); + e.Handled = true; + } + } } else if (e.Key == VirtualKey.Down) { @@ -251,6 +299,8 @@ public sealed partial class SearchBar : UserControl, e.Key == VirtualKey.Up || e.Key == VirtualKey.Down || + e.Key == VirtualKey.Left || + e.Key == VirtualKey.Right || e.Key == VirtualKey.RightMenu || e.Key == VirtualKey.LeftMenu || diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContrastBrushConverter.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContrastBrushConverter.cs new file mode 100644 index 0000000000..5f54682aaf --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContrastBrushConverter.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Media; +using Windows.UI; + +namespace Microsoft.CmdPal.UI.Converters; + +/// +/// Gets a color, either black or white, depending on the brightness of the supplied color. +/// +public sealed partial class ContrastBrushConverter : IValueConverter +{ + /// + /// Gets or sets the alpha channel threshold below which a default color is used instead of black/white. + /// + public byte AlphaThreshold { get; set; } = 128; + + /// + public object Convert( + object value, + Type targetType, + object parameter, + string language) + { + Color comparisonColor; + Color? defaultColor = null; + + // Get the changing color to compare against + if (value is Color valueColor) + { + comparisonColor = valueColor; + } + else if (value is SolidColorBrush valueBrush) + { + comparisonColor = valueBrush.Color; + } + else + { + // Invalid color value provided + return DependencyProperty.UnsetValue; + } + + // Get the default color when transparency is high + if (parameter is Color parameterColor) + { + defaultColor = parameterColor; + } + else if (parameter is SolidColorBrush parameterBrush) + { + defaultColor = parameterBrush.Color; + } + + if (comparisonColor.A < AlphaThreshold && + defaultColor.HasValue) + { + // If the transparency is less than 50 %, just use the default brush + // This can commonly be something like the TextControlForeground brush + return new SolidColorBrush(defaultColor.Value); + } + else + { + // Chose a white/black brush based on contrast to the base color + return UseLightContrastColor(comparisonColor) + ? new SolidColorBrush(Colors.White) + : new SolidColorBrush(Colors.Black); + } + } + + /// + public object ConvertBack( + object value, + Type targetType, + object parameter, + string language) + { + return DependencyProperty.UnsetValue; + } + + /// + /// Determines whether a light or dark contrast color should be used with the given displayed color. + /// + /// + /// This code is using the WinUI algorithm. + /// + private bool UseLightContrastColor(Color displayedColor) + { + // The selection ellipse should be light if and only if the chosen color + // contrasts more with black than it does with white. + // To find how much something contrasts with white, we use the equation + // for relative luminance, which is given by + // + // L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg + // + // where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise } + // + // If L is closer to 1, then the color is closer to white; if it is closer to 0, + // then the color is closer to black. This is based on the fact that the human + // eye perceives green to be much brighter than red, which in turn is perceived to be + // brighter than blue. + // + // If the third dimension is value, then we won't be updating the spectrum's displayed colors, + // so in that case we should use a value of 1 when considering the backdrop + // for the selection ellipse. + var rg = displayedColor.R <= 10 + ? displayedColor.R / 3294.0 + : Math.Pow((displayedColor.R / 269.0) + 0.0513, 2.4); + var gg = displayedColor.G <= 10 + ? displayedColor.G / 3294.0 + : Math.Pow((displayedColor.G / 269.0) + 0.0513, 2.4); + var bg = displayedColor.B <= 10 + ? displayedColor.B / 3294.0 + : Math.Pow((displayedColor.B / 269.0) + 0.0513, 2.4); + + return (0.2126 * rg) + (0.7152 * gg) + (0.0722 * bg) <= 0.5; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs index a28ae3e133..8957f63ea4 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs @@ -26,6 +26,8 @@ namespace Microsoft.CmdPal.UI; public sealed partial class ListPage : Page, IRecipient, IRecipient, + IRecipient, + IRecipient, IRecipient, IRecipient, IRecipient, @@ -85,6 +87,8 @@ public sealed partial class ListPage : Page, // RegisterAll isn't AOT compatible WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); @@ -99,6 +103,8 @@ public sealed partial class ListPage : Page, WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); + WeakReferenceMessenger.Default.Unregister(this); + WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); @@ -257,25 +263,71 @@ public sealed partial class ListPage : Page, // And then have these commands manipulate that state being bound to the UI instead // We may want to see how other non-list UIs need to behave to make this decision // At least it's decoupled from the SearchBox now :) - if (ItemView.SelectedIndex < ItemView.Items.Count - 1) + if (ViewModel?.IsGridView == true) { - ItemView.SelectedIndex++; + // For grid views, use spatial navigation (down) + HandleGridArrowNavigation(VirtualKey.Down); } else { - ItemView.SelectedIndex = 0; + // For list views, use simple linear navigation + if (ItemView.SelectedIndex < ItemView.Items.Count - 1) + { + ItemView.SelectedIndex++; + } + else + { + ItemView.SelectedIndex = 0; + } } } public void Receive(NavigatePreviousCommand message) { - if (ItemView.SelectedIndex > 0) + if (ViewModel?.IsGridView == true) { - ItemView.SelectedIndex--; + // For grid views, use spatial navigation (up) + HandleGridArrowNavigation(VirtualKey.Up); } else { - ItemView.SelectedIndex = ItemView.Items.Count - 1; + // For list views, use simple linear navigation + if (ItemView.SelectedIndex > 0) + { + ItemView.SelectedIndex--; + } + else + { + ItemView.SelectedIndex = ItemView.Items.Count - 1; + } + } + } + + public void Receive(NavigateLeftCommand message) + { + // For grid views, use spatial navigation. For list views, just move up. + if (ViewModel?.IsGridView == true) + { + HandleGridArrowNavigation(VirtualKey.Left); + } + else + { + // In list view, left arrow doesn't navigate + // This maintains consistency with the SearchBar behavior + } + } + + public void Receive(NavigateRightCommand message) + { + // For grid views, use spatial navigation. For list views, just move down. + if (ViewModel?.IsGridView == true) + { + HandleGridArrowNavigation(VirtualKey.Right); + } + else + { + // In list view, right arrow doesn't navigate + // This maintains consistency with the SearchBar behavior } } @@ -514,6 +566,130 @@ public sealed partial class ListPage : Page, return null; } + // Find a logical neighbor in the requested direction using containers' positions. + private void HandleGridArrowNavigation(VirtualKey key) + { + if (ItemView.Items.Count == 0) + { + // No items, goodbye. + return; + } + + var currentIndex = ItemView.SelectedIndex; + if (currentIndex < 0) + { + // -1 is a valid value (no item currently selected) + currentIndex = 0; + ItemView.SelectedIndex = 0; + } + + try + { + // Try to compute using container positions; if not available, fall back to simple +/-1. + var currentContainer = ItemView.ContainerFromIndex(currentIndex) as FrameworkElement; + if (currentContainer is not null && currentContainer.ActualWidth != 0 && currentContainer.ActualHeight != 0) + { + // Use center of current container as reference + var curPoint = currentContainer.TransformToVisual(ItemView).TransformPoint(new Point(0, 0)); + var curCenterX = curPoint.X + (currentContainer.ActualWidth / 2.0); + var curCenterY = curPoint.Y + (currentContainer.ActualHeight / 2.0); + + var bestScore = double.MaxValue; + var bestIndex = currentIndex; + + for (var i = 0; i < ItemView.Items.Count; i++) + { + if (i == currentIndex) + { + continue; + } + + if (ItemView.ContainerFromIndex(i) is FrameworkElement c && c.ActualWidth > 0 && c.ActualHeight > 0) + { + var p = c.TransformToVisual(ItemView).TransformPoint(new Point(0, 0)); + var centerX = p.X + (c.ActualWidth / 2.0); + var centerY = p.Y + (c.ActualHeight / 2.0); + + var dx = centerX - curCenterX; + var dy = centerY - curCenterY; + + var candidate = false; + var score = double.MaxValue; + + switch (key) + { + case VirtualKey.Left: + if (dx < 0) + { + candidate = true; + score = Math.Abs(dy) + (Math.Abs(dx) * 0.7); + } + + break; + case VirtualKey.Right: + if (dx > 0) + { + candidate = true; + score = Math.Abs(dy) + (Math.Abs(dx) * 0.7); + } + + break; + case VirtualKey.Up: + if (dy < 0) + { + candidate = true; + score = Math.Abs(dx) + (Math.Abs(dy) * 0.7); + } + + break; + case VirtualKey.Down: + if (dy > 0) + { + candidate = true; + score = Math.Abs(dx) + (Math.Abs(dy) * 0.7); + } + + break; + } + + if (candidate && score < bestScore) + { + bestScore = score; + bestIndex = i; + } + } + } + + if (bestIndex != currentIndex) + { + ItemView.SelectedIndex = bestIndex; + ItemView.ScrollIntoView(ItemView.SelectedItem); + } + + return; + } + } + catch + { + // ignore transform errors and fall back + } + + // fallback linear behavior + var fallback = key switch + { + VirtualKey.Left => Math.Max(0, currentIndex - 1), + VirtualKey.Right => Math.Min(ItemView.Items.Count - 1, currentIndex + 1), + VirtualKey.Up => Math.Max(0, currentIndex - 1), + VirtualKey.Down => Math.Min(ItemView.Items.Count - 1, currentIndex + 1), + _ => currentIndex, + }; + if (fallback != currentIndex) + { + ItemView.SelectedIndex = fallback; + ItemView.ScrollIntoView(ItemView.SelectedItem); + } + } + private void Items_OnContextRequested(UIElement sender, ContextRequestedEventArgs e) { var (item, element) = e.OriginalSource switch @@ -564,9 +740,27 @@ public sealed partial class ListPage : Page, private void Items_PreviewKeyDown(object sender, KeyRoutedEventArgs e) { + // Track keyboard as the last input source for activation logic. if (e.Key is VirtualKey.Enter or VirtualKey.Space) { _lastInputSource = InputSource.Keyboard; + return; + } + + // Handle arrow navigation when we're showing a grid. + if (ViewModel?.IsGridView == true) + { + switch (e.Key) + { + case VirtualKey.Left: + case VirtualKey.Right: + case VirtualKey.Up: + case VirtualKey.Down: + _lastInputSource = InputSource.Keyboard; + HandleGridArrowNavigation(e.Key); + e.Handled = true; + break; + } } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs index 012e8dc789..f00c230da5 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs @@ -10,6 +10,8 @@ internal static class BindTransformers { public static bool Negate(bool value) => !value; + public static Visibility NegateVisibility(Visibility value) => value == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible; + public static Visibility EmptyToCollapsed(string? input) => string.IsNullOrEmpty(input) ? Visibility.Collapsed : Visibility.Visible; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/ColorExtensions.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/ColorExtensions.cs new file mode 100644 index 0000000000..2492f7f7c9 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/ColorExtensions.cs @@ -0,0 +1,132 @@ +// 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 CommunityToolkit.WinUI.Helpers; +using Windows.UI; + +namespace Microsoft.CmdPal.UI.Helpers; + +/// +/// Extension methods for . +/// +internal static class ColorExtensions +{ + /// Input color. + public static double CalculateBrightness(this Color color) + { + return color.ToHsv().V; + } + + /// + /// Allows to change the brightness by a factor based on the HSV color space. + /// + /// Input color. + /// The brightness adjustment factor, ranging from -1 to 1. + /// Updated color. + public static Color UpdateBrightness(this Color color, double brightnessFactor) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan(brightnessFactor, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(brightnessFactor, -1); + + var hsvColor = color.ToHsv(); + return ColorHelper.FromHsv(hsvColor.H, hsvColor.S, Math.Clamp(hsvColor.V + brightnessFactor, 0, 1), hsvColor.A); + } + + /// + /// Updates the color by adjusting brightness, saturation, and luminance factors. + /// + /// Input color. + /// The brightness adjustment factor, ranging from -1 to 1. + /// The saturation adjustment factor, ranging from -1 to 1. Defaults to 0. + /// The luminance adjustment factor, ranging from -1 to 1. Defaults to 0. + /// Updated color. + public static Color Update(this Color color, double brightnessFactor, double saturationFactor = 0, double luminanceFactor = 0) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan(brightnessFactor, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(brightnessFactor, -1); + + ArgumentOutOfRangeException.ThrowIfGreaterThan(saturationFactor, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(saturationFactor, -1); + + ArgumentOutOfRangeException.ThrowIfGreaterThan(luminanceFactor, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(luminanceFactor, -1); + + var hsv = color.ToHsv(); + + var rgb = ColorHelper.FromHsv( + hsv.H, + Clamp01(hsv.S + saturationFactor), + Clamp01(hsv.V + brightnessFactor)); + + if (luminanceFactor == 0) + { + return rgb; + } + + var hsl = rgb.ToHsl(); + var lightness = Clamp01(hsl.L + luminanceFactor); + return ColorHelper.FromHsl(hsl.H, hsl.S, lightness); + } + + /// + /// Linearly interpolates between two colors in HSV space. + /// Hue is blended along the shortest arc on the color wheel (wrap-aware). + /// Saturation, Value, and Alpha are blended linearly. + /// + /// Start color. + /// End color. + /// Interpolation factor in [0,1]. + /// Interpolated color. + public static Color LerpHsv(this Color a, Color b, double t) + { + t = Clamp01(t); + + // Convert to HSV + var hslA = a.ToHsv(); + var hslB = b.ToHsv(); + + var h1 = hslA.H; + var h2 = hslB.H; + + // Handle near-gray hues (undefined hue) by inheriting the other's hue + const double satEps = 1e-4f; + if (hslA.S < satEps && hslB.S >= satEps) + { + h1 = h2; + } + else if (hslB.S < satEps && hslA.S >= satEps) + { + h2 = h1; + } + + return ColorHelper.FromHsv( + hue: LerpHueDegrees(h1, h2, t), + saturation: Lerp(hslA.S, hslB.S, t), + value: Lerp(hslA.V, hslB.V, t), + alpha: (byte)Math.Round(Lerp(hslA.A, hslB.A, t))); + } + + private static double LerpHueDegrees(double a, double b, double t) + { + a = Mod360(a); + b = Mod360(b); + var delta = ((b - a + 540f) % 360f) - 180f; + return Mod360(a + (delta * t)); + } + + private static double Mod360(double angle) + { + angle %= 360f; + if (angle < 0f) + { + angle += 360f; + } + + return angle; + } + + private static double Lerp(double a, double b, double t) => a + ((b - a) * t); + + private static double Clamp01(double x) => Math.Clamp(x, 0, 1); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TextBoxCaretColor.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TextBoxCaretColor.cs new file mode 100644 index 0000000000..f5103b9efc --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TextBoxCaretColor.cs @@ -0,0 +1,173 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using CommunityToolkit.WinUI; +using Microsoft.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Rectangle = Microsoft.UI.Xaml.Shapes.Rectangle; + +namespace Microsoft.CmdPal.UI.Helpers; + +/// +/// Attached property to color internal caret/overlay rectangles inside a TextBox +/// so they follow the TextBox's actual Foreground brush. +/// +public static class TextBoxCaretColor +{ + public static readonly DependencyProperty SyncWithForegroundProperty = + DependencyProperty.RegisterAttached("SyncWithForeground", typeof(bool), typeof(TextBoxCaretColor), new PropertyMetadata(false, OnSyncCaretRectanglesChanged))!; + + private static readonly ConditionalWeakTable States = []; + + public static void SetSyncWithForeground(DependencyObject obj, bool value) + { + obj.SetValue(SyncWithForegroundProperty, value); + } + + public static bool GetSyncWithForeground(DependencyObject obj) + { + return (bool)obj.GetValue(SyncWithForegroundProperty); + } + + private static void OnSyncCaretRectanglesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not TextBox tb) + { + return; + } + + if ((bool)e.NewValue) + { + Attach(tb); + } + else + { + Detach(tb); + } + } + + private static void Attach(TextBox tb) + { + if (States.TryGetValue(tb, out var st) && st.IsHooked) + { + return; + } + + st ??= new State(); + st.IsHooked = true; + States.Remove(tb); + States.Add(tb, st); + + tb.Loaded += TbOnLoaded; + tb.Unloaded += TbOnUnloaded; + tb.GotFocus += TbOnGotFocus; + + st.ForegroundToken = tb.RegisterPropertyChangedCallback(Control.ForegroundProperty!, (_, _) => Apply(tb)); + + if (tb.IsLoaded) + { + Apply(tb); + } + } + + private static void Detach(TextBox tb) + { + if (!States.TryGetValue(tb, out var st)) + { + return; + } + + tb.Loaded -= TbOnLoaded; + tb.Unloaded -= TbOnUnloaded; + tb.GotFocus -= TbOnGotFocus; + + if (st.ForegroundToken != 0) + { + tb.UnregisterPropertyChangedCallback(Control.ForegroundProperty!, st.ForegroundToken); + st.ForegroundToken = 0; + } + + st.IsHooked = false; + } + + private static void TbOnLoaded(object sender, RoutedEventArgs e) + { + if (sender is TextBox tb) + { + Apply(tb); + } + } + + private static void TbOnUnloaded(object sender, RoutedEventArgs e) + { + if (sender is TextBox tb) + { + Detach(tb); + } + } + + private static void TbOnGotFocus(object sender, RoutedEventArgs e) + { + if (sender is TextBox tb) + { + Apply(tb); + } + } + + private static void Apply(TextBox tb) + { + try + { + ApplyCore(tb); + } + catch (COMException) + { + // ignore + } + } + + private static void ApplyCore(TextBox tb) + { + // Ensure template is realized + tb.ApplyTemplate(); + + // Find the internal ScrollContentPresenter within the TextBox template + var scp = tb.FindDescendant(s => s.Name == "ScrollContentPresenter"); + if (scp is null) + { + return; + } + + var brush = tb.Foreground; // use the actual current foreground brush + if (brush == null) + { + brush = new SolidColorBrush(Colors.Black); + } + + foreach (var rect in scp.FindDescendants().OfType()) + { + try + { + rect.Fill = brush; + rect.CompositeMode = ElementCompositeMode.SourceOver; + rect.Opacity = 0.9; + } + catch + { + // best-effort; some rectangles might be template-owned + } + } + } + + private sealed class State + { + public long ForegroundToken { get; set; } + + public bool IsHooked { get; set; } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WallpaperHelper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WallpaperHelper.cs new file mode 100644 index 0000000000..9772d33b1d --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WallpaperHelper.cs @@ -0,0 +1,178 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using ManagedCommon; +using ManagedCsWin32; +using Microsoft.UI; +using Microsoft.UI.Xaml.Media.Imaging; +using Windows.UI; + +namespace Microsoft.CmdPal.UI.Helpers; + +/// +/// Lightweight helper to access wallpaper information. +/// +internal sealed partial class WallpaperHelper +{ + private readonly IDesktopWallpaper? _desktopWallpaper; + + public WallpaperHelper() + { + try + { + var desktopWallpaper = ComHelper.CreateComInstance( + ref Unsafe.AsRef(in CLSID.DesktopWallpaper), + CLSCTX.ALL); + + _desktopWallpaper = desktopWallpaper; + } + catch (Exception ex) + { + // If COM initialization fails, keep helper usable with safe fallbacks + Logger.LogError("Failed to initialize DesktopWallpaper COM interface", ex); + _desktopWallpaper = null; + } + } + + private string? GetWallpaperPathForFirstMonitor() + { + try + { + if (_desktopWallpaper is null) + { + return null; + } + + _desktopWallpaper.GetMonitorDevicePathCount(out var monitorCount); + + for (uint i = 0; monitorCount != 0 && i < monitorCount; i++) + { + _desktopWallpaper.GetMonitorDevicePathAt(i, out var monitorId); + if (string.IsNullOrEmpty(monitorId)) + { + continue; + } + + _desktopWallpaper.GetWallpaper(monitorId, out var wallpaperPath); + + if (!string.IsNullOrWhiteSpace(wallpaperPath) && File.Exists(wallpaperPath)) + { + return wallpaperPath; + } + } + } + catch (Exception ex) + { + Logger.LogError("Failed to query wallpaper path", ex); + } + + return null; + } + + /// + /// Gets the wallpaper background color. + /// + /// The wallpaper background color, or black if it cannot be determined. + public Color GetWallpaperColor() + { + try + { + if (_desktopWallpaper is null) + { + return Colors.Black; + } + + _desktopWallpaper.GetBackgroundColor(out var colorref); + var r = (byte)(colorref.Value & 0x000000FF); + var g = (byte)((colorref.Value & 0x0000FF00) >> 8); + var b = (byte)((colorref.Value & 0x00FF0000) >> 16); + return Color.FromArgb(255, r, g, b); + } + catch (Exception ex) + { + Logger.LogError("Failed to load wallpaper color", ex); + return Colors.Black; + } + } + + /// + /// Gets the wallpaper image for the primary monitor. + /// + /// The wallpaper image, or null if it cannot be determined. + public BitmapImage? GetWallpaperImage() + { + try + { + var path = GetWallpaperPathForFirstMonitor(); + if (string.IsNullOrWhiteSpace(path)) + { + return null; + } + + var image = new BitmapImage(); + using var stream = File.OpenRead(path); + var randomAccessStream = stream.AsRandomAccessStream(); + if (randomAccessStream == null) + { + Logger.LogError("Failed to convert file stream to RandomAccessStream for wallpaper image."); + return null; + } + + image.SetSource(randomAccessStream); + return image; + } + catch (Exception ex) + { + Logger.LogError("Failed to load wallpaper image", ex); + return null; + } + } + + // blittable type for COM interop + [StructLayout(LayoutKind.Sequential)] + internal readonly partial struct COLORREF + { + internal readonly uint Value; + } + + // blittable type for COM interop + [StructLayout(LayoutKind.Sequential)] + internal readonly partial struct RECT + { + internal readonly int Left; + internal readonly int Top; + internal readonly int Right; + internal readonly int Bottom; + } + + // COM interface for IDesktopWallpaper, GeneratedComInterface to be AOT compatible + [GeneratedComInterface] + [Guid("B92B56A9-8B55-4E14-9A89-0199BBB6F93B")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal partial interface IDesktopWallpaper + { + void SetWallpaper( + [MarshalAs(UnmanagedType.LPWStr)] string? monitorId, + [MarshalAs(UnmanagedType.LPWStr)] string wallpaper); + + void GetWallpaper( + [MarshalAs(UnmanagedType.LPWStr)] string? monitorId, + [MarshalAs(UnmanagedType.LPWStr)] out string wallpaper); + + void GetMonitorDevicePathAt(uint monitorIndex, [MarshalAs(UnmanagedType.LPWStr)] out string monitorId); + + void GetMonitorDevicePathCount(out uint count); + + void GetMonitorRECT([MarshalAs(UnmanagedType.LPWStr)] string? monitorId, out RECT rect); + + void SetBackgroundColor(COLORREF color); + + void GetBackgroundColor(out COLORREF color); + + // Other methods omitted for brevity + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml index c0c0ab811f..32329e17a0 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml @@ -2,6 +2,7 @@ x:Class="Microsoft.CmdPal.UI.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:controls="using:Microsoft.CmdPal.UI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:pages="using:Microsoft.CmdPal.UI.Pages" @@ -15,6 +16,21 @@ Closed="MainWindow_Closed" mc:Ignorable="d"> + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs index 2936f8447e..d9acdb48d9 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs @@ -15,8 +15,10 @@ using Microsoft.CmdPal.UI.Controls; using Microsoft.CmdPal.UI.Events; using Microsoft.CmdPal.UI.Helpers; using Microsoft.CmdPal.UI.Messages; +using Microsoft.CmdPal.UI.Services; using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.PowerToys.Telemetry; using Microsoft.UI; @@ -66,7 +68,10 @@ public sealed partial class MainWindow : WindowEx, private readonly KeyboardListener _keyboardListener; private readonly LocalKeyboardListener _localKeyboardListener; private readonly HiddenOwnerWindowBehavior _hiddenOwnerBehavior = new(); + private readonly IThemeService _themeService; + private readonly WindowThemeSynchronizer _windowThemeSynchronizer; private bool _ignoreHotKeyWhenFullScreen = true; + private bool _themeServiceInitialized; private DesktopAcrylicController? _acrylicController; private SystemBackdropConfiguration? _configurationSource; @@ -74,13 +79,21 @@ public sealed partial class MainWindow : WindowEx, private WindowPosition _currentWindowPosition = new(); + private MainWindowViewModel ViewModel { get; } + public MainWindow() { InitializeComponent(); + ViewModel = App.Current.Services.GetService()!; + _autoGoHomeTimer = new DispatcherTimer(); _autoGoHomeTimer.Tick += OnAutoGoHomeTimerOnTick; + _themeService = App.Current.Services.GetRequiredService(); + _themeService.ThemeChanged += ThemeServiceOnThemeChanged; + _windowThemeSynchronizer = new WindowThemeSynchronizer(_themeService, this); + _hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32()); unsafe @@ -88,6 +101,8 @@ public sealed partial class MainWindow : WindowEx, CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value); } + SetAcrylic(); + _hiddenOwnerBehavior.ShowInTaskbar(this, Debugger.IsAttached); _keyboardListener = new KeyboardListener(); @@ -100,8 +115,6 @@ public sealed partial class MainWindow : WindowEx, RestoreWindowPosition(); UpdateWindowPositionInMemory(); - SetAcrylic(); - WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); @@ -156,6 +169,11 @@ public sealed partial class MainWindow : WindowEx, WeakReferenceMessenger.Default.Send(new GoHomeMessage(WithAnimation: false, FocusSearch: false)); } + private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e) + { + UpdateAcrylic(); + } + private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e) { if (e.Key == VirtualKey.GoBack) @@ -247,8 +265,6 @@ public sealed partial class MainWindow : WindowEx, _autoGoHomeTimer.Interval = _autoGoHomeInterval; } - // We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material - // other Shell surfaces are using, this cannot be set in XAML however. private void SetAcrylic() { if (DesktopAcrylicController.IsSupported()) @@ -265,41 +281,32 @@ public sealed partial class MainWindow : WindowEx, private void UpdateAcrylic() { - if (_acrylicController != null) + try { - _acrylicController.RemoveAllSystemBackdropTargets(); - _acrylicController.Dispose(); - } - - _acrylicController = GetAcrylicConfig(Content); - - // Enable the system backdrop. - // Note: Be sure to have "using WinRT;" to support the Window.As<...>() call. - _acrylicController.AddSystemBackdropTarget(this.As()); - _acrylicController.SetSystemBackdropConfiguration(_configurationSource); - } - - private static DesktopAcrylicController GetAcrylicConfig(UIElement content) - { - var feContent = content as FrameworkElement; - - return feContent?.ActualTheme == ElementTheme.Light - ? new DesktopAcrylicController() + if (_acrylicController != null) { - Kind = DesktopAcrylicKind.Thin, - TintColor = Color.FromArgb(255, 243, 243, 243), - LuminosityOpacity = 0.90f, - TintOpacity = 0.0f, - FallbackColor = Color.FromArgb(255, 238, 238, 238), + _acrylicController.RemoveAllSystemBackdropTargets(); + _acrylicController.Dispose(); } - : new DesktopAcrylicController() + + var backdrop = _themeService.Current.BackdropParameters; + _acrylicController = new DesktopAcrylicController { - Kind = DesktopAcrylicKind.Thin, - TintColor = Color.FromArgb(255, 32, 32, 32), - LuminosityOpacity = 0.96f, - TintOpacity = 0.5f, - FallbackColor = Color.FromArgb(255, 28, 28, 28), + TintColor = backdrop.TintColor, + TintOpacity = backdrop.TintOpacity, + FallbackColor = backdrop.FallbackColor, + LuminosityOpacity = backdrop.LuminosityOpacity, }; + + // Enable the system backdrop. + // Note: Be sure to have "using WinRT;" to support the Window.As<...>() call. + _acrylicController.AddSystemBackdropTarget(this.As()); + _acrylicController.SetSystemBackdropConfiguration(_configurationSource); + } + catch (Exception ex) + { + Logger.LogError("Failed to update backdrop", ex); + } } private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target) @@ -530,6 +537,11 @@ public sealed partial class MainWindow : WindowEx, public void Receive(DismissMessage message) { + if (message.ForceGoHome) + { + WeakReferenceMessenger.Default.Send(new GoHomeMessage(false, false)); + } + // This might come in off the UI thread. Make sure to hop back. DispatcherQueue.TryEnqueue(() => { @@ -706,6 +718,19 @@ public sealed partial class MainWindow : WindowEx, internal void MainWindow_Activated(object sender, WindowActivatedEventArgs args) { + if (!_themeServiceInitialized && args.WindowActivationState != WindowActivationState.Deactivated) + { + try + { + _themeService.Initialize(); + _themeServiceInitialized = true; + } + catch (Exception ex) + { + Logger.LogError("Failed to initialize ThemeService", ex); + } + } + if (args.WindowActivationState == WindowActivationState.Deactivated) { // Save the current window position before hiding the window @@ -999,6 +1024,7 @@ public sealed partial class MainWindow : WindowEx, public void Dispose() { _localKeyboardListener.Dispose(); + _windowThemeSynchronizer.Dispose(); DisposeAcrylic(); } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj index eac3643847..54961a5828 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj @@ -15,7 +15,7 @@ enable enable true - preview + preview $(CmdPalVersion) @@ -23,13 +23,14 @@ false false + true - + - + --> true @@ -45,7 +46,7 @@ - + Microsoft.Terminal.UI;CmdPalKeyboardService $(OutDir) @@ -67,8 +68,11 @@ + + + @@ -77,10 +81,12 @@ + + @@ -92,10 +98,11 @@ + - + all runtime; build; native; contentfiles; analyzers @@ -141,12 +148,16 @@ - - True - True - True + + False + True - + + + + + PreserveNewest + True True @@ -202,6 +213,39 @@ + + + MSBuild:Compile + + + + + + MSBuild:Compile + + + + + + MSBuild:Compile + + + + + + Designer + + + MSBuild:Compile + + + + + + MSBuild:Compile + + + MSBuild:Compile diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml index fe1a29dd97..04b4ca6c16 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml @@ -11,7 +11,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:h="using:Microsoft.CmdPal.UI.Helpers" xmlns:help="using:Microsoft.CmdPal.UI.Helpers" - xmlns:labToolkit="using:CommunityToolkit.Labs.WinUI.MarkdownTextBlock" xmlns:markdownImageProviders="using:Microsoft.CmdPal.UI.Helpers.MarkdownImageProviders" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls" @@ -177,7 +176,7 @@ - + @@ -190,7 +189,7 @@ Padding="0,12,0,12" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}" + BorderBrush="{ThemeResource CmdPal.TopBarBorderBrush}" BorderThickness="0,0,0,1"> @@ -390,7 +389,7 @@ HorizontalAlignment="Stretch" ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" - BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}" + BorderBrush="{ThemeResource CmdPal.DividerStrokeColorDefaultBrush}" BorderThickness="1" CornerRadius="{StaticResource ControlCornerRadius}" Visibility="Collapsed"> @@ -518,7 +517,7 @@ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs index dc12cf142b..7e8dc9eebd 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs @@ -133,7 +133,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, if (!message.FromBackspace) { // If we can't go back then we must be at the top and thus escape again should quit. - WeakReferenceMessenger.Default.Send(); + WeakReferenceMessenger.Default.Send(new DismissMessage()); PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnEsc()); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ColorfulThemeProvider.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ColorfulThemeProvider.cs new file mode 100644 index 0000000000..fe6c8e48e0 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ColorfulThemeProvider.cs @@ -0,0 +1,207 @@ +// 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 CommunityToolkit.WinUI.Helpers; +using Microsoft.CmdPal.UI.Helpers; +using Microsoft.CmdPal.UI.ViewModels.Services; +using Microsoft.UI.Xaml; +using Windows.UI; +using Windows.UI.ViewManagement; + +namespace Microsoft.CmdPal.UI.Services; + +/// +/// Provides theme appropriate for colorful (accented) appearance. +/// +internal sealed class ColorfulThemeProvider : IThemeProvider +{ + // Fluent dark: #202020 + private static readonly Color DarkBaseColor = Color.FromArgb(255, 32, 32, 32); + + // Fluent light: #F3F3F3 + private static readonly Color LightBaseColor = Color.FromArgb(255, 243, 243, 243); + + private readonly UISettings _uiSettings; + + public string ThemeKey => "colorful"; + + public string ResourcePath => "ms-appx:///Styles/Theme.Colorful.xaml"; + + public ColorfulThemeProvider(UISettings uiSettings) + { + ArgumentNullException.ThrowIfNull(uiSettings); + _uiSettings = uiSettings; + } + + public AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context) + { + var isLight = context.Theme == ElementTheme.Light || + (context.Theme == ElementTheme.Default && + _uiSettings.GetColorValue(UIColorType.Background).R > 128); + + var baseColor = isLight ? LightBaseColor : DarkBaseColor; + + // Windows is warping the hue of accent colors and running it through some curves to produce their accent shades. + // This will attempt to mimic that behavior. + var accentShades = AccentShades.Compute(context.Tint.LerpHsv(WindowsAccentHueWarpTransform.Transform(context.Tint), 0.5f)); + var blended = isLight ? accentShades.Light3 : accentShades.Dark2; + var colorIntensityUser = (context.ColorIntensity ?? 100) / 100f; + + // For light theme, we want to reduce intensity a bit, and also we need to keep the color fairly light, + // to avoid issues with text box caret. + var colorIntensity = isLight ? 0.6f * colorIntensityUser : colorIntensityUser; + var effectiveBgColor = ColorBlender.Blend(baseColor, blended, colorIntensity); + + return new AcrylicBackdropParameters(effectiveBgColor, effectiveBgColor, 0.8f, 0.8f); + } + + private static class ColorBlender + { + /// + /// Blends a semitransparent tint color over an opaque base color using alpha compositing. + /// + /// The opaque base color (background) + /// The semitransparent tint color (foreground) + /// The intensity of the tint (0.0 - 1.0) + /// The resulting blended color + public static Color Blend(Color baseColor, Color tintColor, float intensity) + { + // Normalize alpha to 0.0 - 1.0 range + intensity = Math.Clamp(intensity, 0f, 1f); + + // Alpha compositing formula: result = tint * alpha + base * (1 - alpha) + var r = (byte)((tintColor.R * intensity) + (baseColor.R * (1 - intensity))); + var g = (byte)((tintColor.G * intensity) + (baseColor.G * (1 - intensity))); + var b = (byte)((tintColor.B * intensity) + (baseColor.B * (1 - intensity))); + + // Result is fully opaque since base is opaque + return Color.FromArgb(255, r, g, b); + } + } + + private static class WindowsAccentHueWarpTransform + { + private static readonly (double HIn, double HOut)[] HueMap = + [ + (0, 0), + (10, 1), + (20, 6), + (30, 10), + (40, 14), + (50, 19), + (60, 36), + (70, 94), + (80, 112), + (90, 120), + (100, 120), + (110, 120), + (120, 120), + (130, 120), + (140, 120), + (150, 125), + (160, 135), + (170, 142), + (180, 178), + (190, 205), + (200, 220), + (210, 229), + (220, 237), + (230, 241), + (240, 243), + (250, 244), + (260, 245), + (270, 248), + (280, 252), + (290, 276), + (300, 293), + (310, 313), + (320, 330), + (330, 349), + (340, 353), + (350, 357) + ]; + + public static Color Transform(Color input, Options? opt = null) + { + opt ??= new Options(); + var hsv = input.ToHsv(); + return ColorHelper.FromHsv( + RemapHueLut(hsv.H), + Clamp01(Math.Pow(hsv.S, opt.SaturationGamma) * opt.SaturationGain), + Clamp01((opt.ValueScaleA * hsv.V) + opt.ValueBiasB), + input.A); + } + + // Hue LUT remap (piecewise-linear with cyclic wrap) + private static double RemapHueLut(double hDeg) + { + // Normalize to [0,360) + hDeg = Mod(hDeg, 360.0); + + // Handle wrap-around case: hDeg is between last entry (350°) and 360° + var last = HueMap[^1]; + var first = HueMap[0]; + if (hDeg >= last.HIn) + { + // Interpolate between last entry and first entry (wrapped by 360°) + var t = (hDeg - last.HIn) / (first.HIn + 360.0 - last.HIn + 1e-12); + var ho = Lerp(last.HOut, first.HOut + 360.0, t); + return Mod(ho, 360.0); + } + + // Find segment [i, i+1] where HueMap[i].HIn <= hDeg < HueMap[i+1].HIn + for (var i = 0; i < HueMap.Length - 1; i++) + { + var a = HueMap[i]; + var b = HueMap[i + 1]; + + if (hDeg >= a.HIn && hDeg < b.HIn) + { + var t = (hDeg - a.HIn) / (b.HIn - a.HIn + 1e-12); + return Lerp(a.HOut, b.HOut, t); + } + } + + // Fallback (shouldn't happen) + return hDeg; + } + + private static double Lerp(double a, double b, double t) => a + ((b - a) * t); + + private static double Mod(double x, double m) => ((x % m) + m) % m; + + private static double Clamp01(double x) => x < 0 ? 0 : (x > 1 ? 1 : x); + + public sealed class Options + { + // Saturation boost (1.0 = no change). Typical: 1.3–1.8 + public double SaturationGain { get; init; } = 1.0; + + // Optional saturation gamma (1.0 = linear). <1.0 raises low S a bit; >1.0 preserves low S. + public double SaturationGamma { get; init; } = 1.0; + + // Value (V) remap: V' = a*V + b (tone curve; clamp applied) + // Example that lifts blacks & compresses whites slightly: a=0.50, b=0.08 + public double ValueScaleA { get; init; } = 0.6; + + public double ValueBiasB { get; init; } = 0.01; + } + } + + private static class AccentShades + { + public static (Color Light3, Color Light2, Color Light1, Color Dark1, Color Dark2, Color Dark3) Compute(Color accent) + { + var light1 = accent.Update(brightnessFactor: 0.15, saturationFactor: -0.12); + var light2 = accent.Update(brightnessFactor: 0.30, saturationFactor: -0.24); + var light3 = accent.Update(brightnessFactor: 0.45, saturationFactor: -0.36); + + var dark1 = accent.UpdateBrightness(brightnessFactor: -0.05f); + var dark2 = accent.UpdateBrightness(brightnessFactor: -0.01f); + var dark3 = accent.UpdateBrightness(brightnessFactor: -0.015f); + + return (light3, light2, light1, dark1, dark2, dark3); + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/IThemeProvider.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/IThemeProvider.cs new file mode 100644 index 0000000000..a9411c3656 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/IThemeProvider.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.Core.Common.Services; +using Microsoft.CmdPal.UI.ViewModels.Services; + +namespace Microsoft.CmdPal.UI.Services; + +/// +/// Provides theme identification, resource path resolution, and creation of acrylic +/// backdrop parameters based on the current . +/// +/// +/// Implementations should expose a stable and a valid XAML resource +/// dictionary path via . The +/// method computes +/// using the supplied theme context. +/// +internal interface IThemeProvider +{ + /// + /// Gets the unique key identifying this theme provider. + /// + string ThemeKey { get; } + + /// + /// Gets the resource dictionary path for this theme. + /// + string ResourcePath { get; } + + /// + /// Creates acrylic backdrop parameters based on the provided theme context. + /// + /// The current theme context, including theme, tint, and optional background details. + /// The computed for the backdrop. + AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/MutableOverridesDictionary.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/MutableOverridesDictionary.cs new file mode 100644 index 0000000000..8177326259 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/MutableOverridesDictionary.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.UI.Xaml; + +namespace Microsoft.CmdPal.UI.Services; + +/// +/// Dedicated ResourceDictionary for dynamic overrides that win over base theme resources. Since +/// we can't use a key or name to identify the dictionary in Application resources, we use a dedicated type. +/// +internal sealed partial class MutableOverridesDictionary : ResourceDictionary; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/NormalThemeProvider.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/NormalThemeProvider.cs new file mode 100644 index 0000000000..c393894346 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/NormalThemeProvider.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.UI.ViewModels.Services; +using Microsoft.UI.Xaml; +using Windows.UI; +using Windows.UI.ViewManagement; + +namespace Microsoft.CmdPal.UI.Services; + +/// +/// Provides theme resources and acrylic backdrop parameters matching the default Command Palette theme. +/// +internal sealed class NormalThemeProvider : IThemeProvider +{ + private static readonly Color DarkBaseColor = Color.FromArgb(255, 32, 32, 32); + private static readonly Color LightBaseColor = Color.FromArgb(255, 243, 243, 243); + private readonly UISettings _uiSettings; + + public NormalThemeProvider(UISettings uiSettings) + { + ArgumentNullException.ThrowIfNull(uiSettings); + _uiSettings = uiSettings; + } + + public string ThemeKey => "normal"; + + public string ResourcePath => "ms-appx:///Styles/Theme.Normal.xaml"; + + public AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context) + { + var isLight = context.Theme == ElementTheme.Light || + (context.Theme == ElementTheme.Default && + _uiSettings.GetColorValue(UIColorType.Background).R > 128); + + return new AcrylicBackdropParameters( + TintColor: isLight ? LightBaseColor : DarkBaseColor, + FallbackColor: isLight ? LightBaseColor : DarkBaseColor, + TintOpacity: 0.5f, + LuminosityOpacity: isLight ? 0.9f : 0.96f); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ResourceSwapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ResourceSwapper.cs new file mode 100644 index 0000000000..6d0a6f01dd --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ResourceSwapper.cs @@ -0,0 +1,332 @@ +// 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.UI.Xaml; + +namespace Microsoft.CmdPal.UI.Services; + +/// +/// Simple theme switcher that swaps application ResourceDictionaries at runtime. +/// Can also operate in event-only mode for consumers to apply resources themselves. +/// Exposes a dedicated override dictionary that stays merged and is cleared on theme changes. +/// +internal sealed partial class ResourceSwapper +{ + private readonly Lock _resourceSwapGate = new(); + private readonly Dictionary _themeUris = new(StringComparer.OrdinalIgnoreCase); + private ResourceDictionary? _activeDictionary; + private string? _currentThemeName; + private Uri? _currentThemeUri; + + private ResourceDictionary? _overrideDictionary; + + /// + /// Raised after a theme has been activated. + /// + public event EventHandler? ResourcesSwapped; + + /// + /// Gets or sets a value indicating whether when true (default) ResourceSwapper updates Application.Current.Resources. When false, it only raises ResourcesSwapped. + /// + public bool ApplyToAppResources { get; set; } = true; + + /// + /// Gets name of the currently selected theme (if any). + /// + public string? CurrentThemeName + { + get + { + lock (_resourceSwapGate) + { + return _currentThemeName; + } + } + } + + /// + /// Initializes ResourceSwapper by checking Application resources for an already merged theme dictionary. + /// + public void Initialize() + { + // Find merged dictionary in Application resources that matches a registered theme by URI + // This allows ResourceSwapper to pick up an initial theme set in XAML + var app = Application.Current; + var resourcesMergedDictionaries = app?.Resources?.MergedDictionaries; + if (resourcesMergedDictionaries == null) + { + return; + } + + foreach (var dict in resourcesMergedDictionaries) + { + var uri = dict.Source; + if (uri is null) + { + continue; + } + + var name = GetNameForUri(uri); + if (name is null) + { + continue; + } + + lock (_resourceSwapGate) + { + _currentThemeName = name; + _currentThemeUri = uri; + _activeDictionary = dict; + } + + break; + } + } + + /// + /// Gets uri of the currently selected theme dictionary (if any). + /// + public Uri? CurrentThemeUri + { + get + { + lock (_resourceSwapGate) + { + return _currentThemeUri; + } + } + } + + public static ResourceDictionary GetOverrideDictionary(bool clear = false) + { + var app = Application.Current ?? throw new InvalidOperationException("App is null"); + + if (app.Resources == null) + { + throw new InvalidOperationException("Application.Resources is null"); + } + + // (Re)locate the slot – Hot Reload may rebuild Application.Resources. + var slot = app.Resources!.MergedDictionaries! + .OfType() + .FirstOrDefault(); + + if (slot is null) + { + // If the slot vanished (Hot Reload), create it again at the end so it wins precedence. + slot = new MutableOverridesDictionary(); + app.Resources.MergedDictionaries!.Add(slot); + } + + // Ensure the slot has exactly one child RD we can swap safely. + if (slot.MergedDictionaries!.Count == 0) + { + slot.MergedDictionaries.Add(new ResourceDictionary()); + } + else if (slot.MergedDictionaries.Count > 1) + { + // Normalize to a single child to keep semantics predictable. + var keep = slot.MergedDictionaries[^1]; + slot.MergedDictionaries.Clear(); + slot.MergedDictionaries.Add(keep); + } + + if (clear) + { + // Swap the child dictionary instead of Clear() to avoid reentrancy issues. + var fresh = new ResourceDictionary(); + slot.MergedDictionaries[0] = fresh; + return fresh; + } + + return slot.MergedDictionaries[0]!; + } + + /// + /// Registers a theme name mapped to a XAML ResourceDictionary URI (e.g. ms-appx:///Themes/Red.xaml) + /// + public void RegisterTheme(string name, Uri dictionaryUri) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Theme name is required", nameof(name)); + } + + lock (_resourceSwapGate) + { + _themeUris[name] = dictionaryUri ?? throw new ArgumentNullException(nameof(dictionaryUri)); + } + } + + /// + /// Registers a theme with a string URI. + /// + public void RegisterTheme(string name, string dictionaryUri) + { + ArgumentNullException.ThrowIfNull(dictionaryUri); + RegisterTheme(name, new Uri(dictionaryUri)); + } + + /// + /// Removes a previously registered theme. + /// + public bool UnregisterTheme(string name) + { + lock (_resourceSwapGate) + { + return _themeUris.Remove(name); + } + } + + /// + /// Gets the names of all registered themes. + /// + public IEnumerable GetRegisteredThemes() + { + lock (_resourceSwapGate) + { + // return a copy to avoid external mutation + return new List(_themeUris.Keys); + } + } + + /// + /// Activates a theme by name. The dictionary for the given name must be registered first. + /// + public void ActivateTheme(string theme) + { + if (string.IsNullOrWhiteSpace(theme)) + { + throw new ArgumentException("Theme name is required", nameof(theme)); + } + + Uri uri; + lock (_resourceSwapGate) + { + if (!_themeUris.TryGetValue(theme, out uri!)) + { + throw new KeyNotFoundException($"Theme '{theme}' is not registered."); + } + } + + ActivateThemeInternal(theme, uri); + } + + /// + /// Tries to activate a theme by name without throwing. + /// + public bool TryActivateTheme(string theme) + { + if (string.IsNullOrWhiteSpace(theme)) + { + return false; + } + + Uri uri; + lock (_resourceSwapGate) + { + if (!_themeUris.TryGetValue(theme, out uri!)) + { + return false; + } + } + + ActivateThemeInternal(theme, uri); + return true; + } + + /// + /// Activates a theme by URI to a ResourceDictionary. + /// + public void ActivateTheme(Uri dictionaryUri) + { + ArgumentNullException.ThrowIfNull(dictionaryUri); + + ActivateThemeInternal(GetNameForUri(dictionaryUri), dictionaryUri); + } + + /// + /// Clears the currently active theme ResourceDictionary. Also clears the override dictionary. + /// + public void ClearActiveTheme() + { + lock (_resourceSwapGate) + { + var app = Application.Current; + if (app is null) + { + return; + } + + if (_activeDictionary is not null && ApplyToAppResources) + { + _ = app.Resources.MergedDictionaries.Remove(_activeDictionary); + _activeDictionary = null; + } + + // Clear overrides but keep the override dictionary merged for future updates + _overrideDictionary?.Clear(); + + _currentThemeName = null; + _currentThemeUri = null; + } + } + + private void ActivateThemeInternal(string? name, Uri dictionaryUri) + { + lock (_resourceSwapGate) + { + _currentThemeName = name; + _currentThemeUri = dictionaryUri; + } + + if (ApplyToAppResources) + { + ActivateThemeCore(dictionaryUri); + } + + OnResourcesSwapped(new(name, dictionaryUri)); + } + + private void ActivateThemeCore(Uri dictionaryUri) + { + var app = Application.Current ?? throw new InvalidOperationException("Application.Current is null"); + + // Remove previously applied base theme dictionary + if (_activeDictionary is not null) + { + _ = app.Resources.MergedDictionaries.Remove(_activeDictionary); + _activeDictionary = null; + } + + // Load and merge the new base theme dictionary + var newDict = new ResourceDictionary { Source = dictionaryUri }; + app.Resources.MergedDictionaries.Add(newDict); + _activeDictionary = newDict; + + // Ensure override dictionary exists and is merged last, then clear it to avoid leaking stale overrides + _overrideDictionary = GetOverrideDictionary(clear: true); + } + + private string? GetNameForUri(Uri dictionaryUri) + { + lock (_resourceSwapGate) + { + foreach (var (key, value) in _themeUris) + { + if (Uri.Compare(value, dictionaryUri, UriComponents.AbsoluteUri, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) == 0) + { + return key; + } + } + + return null; + } + } + + private void OnResourcesSwapped(ResourcesSwappedEventArgs e) + { + ResourcesSwapped?.Invoke(this, e); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ResourcesSwappedEventArgs.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ResourcesSwappedEventArgs.cs new file mode 100644 index 0000000000..0a5cc15de6 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ResourcesSwappedEventArgs.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.UI.Services; + +public sealed class ResourcesSwappedEventArgs(string? name, Uri dictionaryUri) : EventArgs +{ + public string? Name { get; } = name; + + public Uri DictionaryUri { get; } = dictionaryUri ?? throw new ArgumentNullException(nameof(dictionaryUri)); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ThemeContext.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ThemeContext.cs new file mode 100644 index 0000000000..67432c8748 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ThemeContext.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; +using Windows.UI; + +namespace Microsoft.CmdPal.UI.Services; + +internal sealed record ThemeContext +{ + public ElementTheme Theme { get; init; } + + public Color Tint { get; init; } + + public ImageSource? BackgroundImageSource { get; init; } + + public Stretch BackgroundImageStretch { get; init; } + + public double BackgroundImageOpacity { get; init; } + + public int? ColorIntensity { get; init; } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ThemeService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ThemeService.cs new file mode 100644 index 0000000000..65fbfb24d7 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/ThemeService.cs @@ -0,0 +1,261 @@ +// 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 CommunityToolkit.WinUI; +using ManagedCommon; +using Microsoft.CmdPal.UI.Helpers; +using Microsoft.CmdPal.UI.ViewModels; +using Microsoft.CmdPal.UI.ViewModels.Services; +using Microsoft.UI; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Imaging; +using Windows.UI.ViewManagement; +using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue; + +namespace Microsoft.CmdPal.UI.Services; + +/// +/// ThemeService is a hub that translates user settings and system preferences into concrete +/// theme resources and notifies listeners of changes. +/// +internal sealed partial class ThemeService : IThemeService, IDisposable +{ + private static readonly TimeSpan ReloadDebounceInterval = TimeSpan.FromMilliseconds(500); + + private readonly UISettings _uiSettings; + private readonly SettingsModel _settings; + private readonly ResourceSwapper _resourceSwapper; + private readonly NormalThemeProvider _normalThemeProvider; + private readonly ColorfulThemeProvider _colorfulThemeProvider; + + private DispatcherQueue? _dispatcherQueue; + private DispatcherQueueTimer? _dispatcherQueueTimer; + private bool _isInitialized; + private bool _disposed; + private InternalThemeState _currentState; + + public event EventHandler? ThemeChanged; + + public ThemeSnapshot Current => Volatile.Read(ref _currentState).Snapshot; + + /// + /// Initializes the theme service. Must be called after the application window is activated and on UI thread. + /// + public void Initialize() + { + if (_isInitialized) + { + return; + } + + _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + if (_dispatcherQueue is null) + { + throw new InvalidOperationException("Failed to get DispatcherQueue for the current thread. Ensure Initialize is called on the UI thread after window activation."); + } + + _dispatcherQueueTimer = _dispatcherQueue.CreateTimer(); + + _resourceSwapper.Initialize(); + _isInitialized = true; + Reload(); + } + + private void Reload() + { + if (!_isInitialized) + { + return; + } + + // provider selection + var intensity = Math.Clamp(_settings.CustomThemeColorIntensity, 0, 100); + IThemeProvider provider = intensity > 0 && _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image + ? _colorfulThemeProvider + : _normalThemeProvider; + + // Calculate values + var tint = _settings.ColorizationMode switch + { + ColorizationMode.CustomColor => _settings.CustomThemeColor, + ColorizationMode.WindowsAccentColor => _uiSettings.GetColorValue(UIColorType.Accent), + ColorizationMode.Image => _settings.CustomThemeColor, + _ => Colors.Transparent, + }; + var effectiveTheme = GetElementTheme((ElementTheme)_settings.Theme); + var imageSource = _settings.ColorizationMode == ColorizationMode.Image + ? LoadImageSafe(_settings.BackgroundImagePath) + : null; + var stretch = _settings.BackgroundImageFit switch + { + BackgroundImageFit.Fill => Stretch.Fill, + _ => Stretch.UniformToFill, + }; + var opacity = Math.Clamp(_settings.BackgroundImageOpacity, 0, 100) / 100.0; + + // create context and offload to actual theme provider + var context = new ThemeContext + { + Tint = tint, + ColorIntensity = intensity, + Theme = effectiveTheme, + BackgroundImageSource = imageSource, + BackgroundImageStretch = stretch, + BackgroundImageOpacity = opacity, + }; + var backdrop = provider.GetAcrylicBackdrop(context); + var blur = _settings.BackgroundImageBlurAmount; + var brightness = _settings.BackgroundImageBrightness; + + // Create public snapshot (no provider!) + var snapshot = new ThemeSnapshot + { + Tint = tint, + TintIntensity = intensity / 100f, + Theme = effectiveTheme, + BackgroundImageSource = imageSource, + BackgroundImageStretch = stretch, + BackgroundImageOpacity = opacity, + BackdropParameters = backdrop, + BlurAmount = blur, + BackgroundBrightness = brightness / 100f, + }; + + // Bundle with provider for internal use + var newState = new InternalThemeState + { + Snapshot = snapshot, + Provider = provider, + }; + + // Atomic swap + Interlocked.Exchange(ref _currentState, newState); + + _resourceSwapper.TryActivateTheme(provider.ThemeKey); + ThemeChanged?.Invoke(this, new ThemeChangedEventArgs()); + } + + private static BitmapImage? LoadImageSafe(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return null; + } + + try + { + // If it looks like a file path and exists, prefer absolute file URI + if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uri)) + { + return null; + } + + if (!uri.IsAbsoluteUri && File.Exists(path)) + { + uri = new Uri(Path.GetFullPath(path)); + } + + return new BitmapImage(uri); + } + catch (Exception ex) + { + Logger.LogWarning($"Failed to load background image '{path}'. {ex.Message}"); + return null; + } + } + + public ThemeService(SettingsModel settings, ResourceSwapper resourceSwapper) + { + ArgumentNullException.ThrowIfNull(settings); + ArgumentNullException.ThrowIfNull(resourceSwapper); + + _settings = settings; + _settings.SettingsChanged += SettingsOnSettingsChanged; + + _resourceSwapper = resourceSwapper; + + _uiSettings = new UISettings(); + _uiSettings.ColorValuesChanged += UiSettings_ColorValuesChanged; + + _normalThemeProvider = new NormalThemeProvider(_uiSettings); + _colorfulThemeProvider = new ColorfulThemeProvider(_uiSettings); + List providers = [_normalThemeProvider, _colorfulThemeProvider]; + + foreach (var provider in providers) + { + _resourceSwapper.RegisterTheme(provider.ThemeKey, provider.ResourcePath); + } + + _currentState = new InternalThemeState + { + Snapshot = new ThemeSnapshot + { + Tint = Colors.Transparent, + Theme = ElementTheme.Light, + BackdropParameters = new AcrylicBackdropParameters(Colors.Black, Colors.Black, 0.5f, 0.5f), + BackgroundImageOpacity = 1, + BackgroundImageSource = null, + BackgroundImageStretch = Stretch.Fill, + BlurAmount = 0, + TintIntensity = 1.0f, + BackgroundBrightness = 0, + }, + Provider = _normalThemeProvider, + }; + } + + private void RequestReload() + { + if (!_isInitialized || _dispatcherQueueTimer is null) + { + return; + } + + _dispatcherQueueTimer.Debounce(Reload, ReloadDebounceInterval); + } + + private ElementTheme GetElementTheme(ElementTheme theme) + { + return theme switch + { + ElementTheme.Light => ElementTheme.Light, + ElementTheme.Dark => ElementTheme.Dark, + _ => _uiSettings.GetColorValue(UIColorType.Background).CalculateBrightness() < 0.5 + ? ElementTheme.Dark + : ElementTheme.Light, + }; + } + + private void SettingsOnSettingsChanged(SettingsModel sender, object? args) + { + RequestReload(); + } + + private void UiSettings_ColorValuesChanged(UISettings sender, object args) + { + RequestReload(); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _dispatcherQueueTimer?.Stop(); + _uiSettings.ColorValuesChanged -= UiSettings_ColorValuesChanged; + _settings.SettingsChanged -= SettingsOnSettingsChanged; + } + + private sealed class InternalThemeState + { + public required ThemeSnapshot Snapshot { get; init; } + + public required IThemeProvider Provider { get; init; } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/WindowThemeSynchronizer.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/WindowThemeSynchronizer.cs new file mode 100644 index 0000000000..5c250b94ef --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Services/WindowThemeSynchronizer.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.Core.Common.Services; +using Microsoft.CmdPal.UI.ViewModels.Services; +using Microsoft.UI.Xaml; + +namespace Microsoft.CmdPal.UI.Services; + +/// +/// Synchronizes a window's theme with . +/// +internal sealed partial class WindowThemeSynchronizer : IDisposable +{ + private readonly IThemeService _themeService; + private readonly Window _window; + + /// + /// Initializes a new instance of the class and subscribes to theme changes. + /// + /// The theme service to monitor for changes. + /// The window to synchronize. + /// Thrown when or is null. + public WindowThemeSynchronizer(IThemeService themeService, Window window) + { + _themeService = themeService ?? throw new ArgumentNullException(nameof(themeService)); + _window = window ?? throw new ArgumentNullException(nameof(window)); + _themeService.ThemeChanged += ThemeServiceOnThemeChanged; + } + + /// + /// Unsubscribes from theme change events. + /// + public void Dispose() + { + _themeService.ThemeChanged -= ThemeServiceOnThemeChanged; + } + + /// + /// Applies the current theme to the window when theme changes occur. + /// + private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e) + { + if (_window.Content is not FrameworkElement fe) + { + return; + } + + var dispatcherQueue = fe.DispatcherQueue; + + if (dispatcherQueue is not null && dispatcherQueue.HasThreadAccess) + { + ApplyRequestedTheme(fe); + } + else + { + dispatcherQueue?.TryEnqueue(() => ApplyRequestedTheme(fe)); + } + } + + private void ApplyRequestedTheme(FrameworkElement fe) + { + // LOAD BEARING: Changing the RequestedTheme to Dark then Light then target forces + // a refresh of the theme. + fe.RequestedTheme = ElementTheme.Dark; + fe.RequestedTheme = ElementTheme.Light; + fe.RequestedTheme = _themeService.Current.Theme; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/AppearancePage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/AppearancePage.xaml new file mode 100644 index 0000000000..b9f31d8443 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/AppearancePage.xaml @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + public class Settings { - private static SettingsUtils moduleSettings = new SettingsUtils(); + private static SettingsUtils moduleSettings = SettingsUtils.Default; /// /// Gets a value indicating whether word wrapping should be applied. Set by PT settings. diff --git a/src/modules/previewpane/StlThumbnailProvider/StlThumbnailProvider.cs b/src/modules/previewpane/StlThumbnailProvider/StlThumbnailProvider.cs index 2301c7beb0..d6dbd74251 100644 --- a/src/modules/previewpane/StlThumbnailProvider/StlThumbnailProvider.cs +++ b/src/modules/previewpane/StlThumbnailProvider/StlThumbnailProvider.cs @@ -155,7 +155,7 @@ namespace Microsoft.PowerToys.ThumbnailHandler.Stl { try { - var moduleSettings = new SettingsUtils(); + var moduleSettings = SettingsUtils.Default; var colorString = moduleSettings.GetSettings(PowerPreviewSettings.ModuleName).Properties.StlThumbnailColor.Value; diff --git a/src/modules/previewpane/SvgPreviewHandler/Settings.cs b/src/modules/previewpane/SvgPreviewHandler/Settings.cs index 925da8307a..dfde5bed03 100644 --- a/src/modules/previewpane/SvgPreviewHandler/Settings.cs +++ b/src/modules/previewpane/SvgPreviewHandler/Settings.cs @@ -8,7 +8,7 @@ namespace SvgPreviewHandler { internal sealed class Settings { - private static readonly SettingsUtils ModuleSettings = new SettingsUtils(); + private static readonly SettingsUtils ModuleSettings = SettingsUtils.Default; public int ColorMode { diff --git a/src/runner/packages.config b/src/runner/packages.config deleted file mode 100644 index 974d142cec..0000000000 --- a/src/runner/packages.config +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/runner/runner.vcxproj b/src/runner/runner.vcxproj index 724fd44137..e08a66aae6 100644 --- a/src/runner/runner.vcxproj +++ b/src/runner/runner.vcxproj @@ -1,27 +1,34 @@ - + 81010002 + + + PackageReference + + native,Version=v0.0 + + Windows + $(WindowsTargetPlatformVersion) + 15.0 {9412D5C6-2CF2-4FC2-A601-B55508EA9B27} powertoys runner + + + + + + - - - - - - - - Application @@ -31,10 +38,6 @@ true - - - - @@ -140,39 +143,16 @@ - - - - - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - - - - - - - - - - - - - + + + + + + + + + NotUsing + + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI.Library/SettingsBackupAndRestoreUtils.cs b/src/settings-ui/Settings.UI.Library/SettingsBackupAndRestoreUtils.cs index 10ebf74314..08708009d0 100644 --- a/src/settings-ui/Settings.UI.Library/SettingsBackupAndRestoreUtils.cs +++ b/src/settings-ui/Settings.UI.Library/SettingsBackupAndRestoreUtils.cs @@ -592,7 +592,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library /// public (bool Success, string Message, string Severity, bool LastBackupExists, string OptionalMessage) DryRunBackup() { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; var appBasePath = Path.GetDirectoryName(settingsUtils.GetSettingsFilePath()); string settingsBackupAndRestoreDir = GetSettingsBackupAndRestoreDir(); var results = BackupSettings(appBasePath, settingsBackupAndRestoreDir, true); diff --git a/src/settings-ui/Settings.UI.Library/SettingsUtils.cs b/src/settings-ui/Settings.UI.Library/SettingsUtils.cs index 6109df0646..eb4169f422 100644 --- a/src/settings-ui/Settings.UI.Library/SettingsUtils.cs +++ b/src/settings-ui/Settings.UI.Library/SettingsUtils.cs @@ -22,7 +22,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library private readonly ISettingsPath _settingsPath; private readonly JsonSerializerOptions _serializerOptions; - public SettingsUtils() + /// + /// Gets the default instance of the class for general use. + /// Same as instantiating a new instance with the constructor with a new object as the first argument and null as the second argument. + /// + /// For using in tests, you should use one of the public constructors. + public static SettingsUtils Default { get; } = new SettingsUtils(); + + private SettingsUtils() : this(new FileSystem()) { } @@ -234,7 +241,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public static (bool Success, string Message, string Severity, bool LastBackupExists, string OptionalMessage) BackupSettings() { var settingsBackupAndRestoreUtilsX = SettingsBackupAndRestoreUtils.Instance; - var settingsUtils = new SettingsUtils(); + var settingsUtils = Default; var appBasePath = Path.GetDirectoryName(settingsUtils._settingsPath.GetSettingsPath(string.Empty, string.Empty)); string settingsBackupAndRestoreDir = settingsBackupAndRestoreUtilsX.GetSettingsBackupAndRestoreDir(); @@ -247,7 +254,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public static (bool Success, string Message, string Severity) RestoreSettings() { var settingsBackupAndRestoreUtilsX = SettingsBackupAndRestoreUtils.Instance; - var settingsUtils = new SettingsUtils(); + var settingsUtils = Default; var appBasePath = Path.GetDirectoryName(settingsUtils._settingsPath.GetSettingsPath(string.Empty, string.Empty)); string settingsBackupAndRestoreDir = settingsBackupAndRestoreUtilsX.GetSettingsBackupAndRestoreDir(); return settingsBackupAndRestoreUtilsX.RestoreSettings(appBasePath, settingsBackupAndRestoreDir); diff --git a/src/settings-ui/Settings.UI.Library/Utilities/GetSettingCommandLineCommand.cs b/src/settings-ui/Settings.UI.Library/Utilities/GetSettingCommandLineCommand.cs index a4ad1d1862..5780b95392 100644 --- a/src/settings-ui/Settings.UI.Library/Utilities/GetSettingCommandLineCommand.cs +++ b/src/settings-ui/Settings.UI.Library/Utilities/GetSettingCommandLineCommand.cs @@ -48,7 +48,7 @@ public sealed class GetSettingCommandLineCommand var modulesSettings = new Dictionary>(); var settingsAssembly = CommandLineUtils.GetSettingsAssembly(); - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; var enabledModules = SettingsRepository.GetInstance(settingsUtils).SettingsConfig.Enabled; diff --git a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/ColorPicker.cs b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/ColorPicker.cs index baf46827c4..19e2ca3b67 100644 --- a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/ColorPicker.cs +++ b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/ColorPicker.cs @@ -67,7 +67,7 @@ namespace ViewModelTests using (var viewModel = new ColorPickerViewModel( ISettingsUtilsMocks.GetStubSettingsUtils().Object, SettingsRepository.GetInstance(ISettingsUtilsMocks.GetStubSettingsUtils().Object), - SettingsRepository.GetInstance(new SettingsUtils()), + SettingsRepository.GetInstance(SettingsUtils.Default), ColorPickerIsEnabledByDefaultIPC)) { Assert.IsTrue(viewModel.IsEnabled); diff --git a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/FancyZones.cs b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/FancyZones.cs index de6421785b..bf8f9396c4 100644 --- a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/FancyZones.cs +++ b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/FancyZones.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.IO.Abstractions; using System.Text.Json; using Microsoft.PowerToys.Settings.UI.Library; @@ -120,7 +121,7 @@ namespace ViewModelTests [TestMethod] public void IsEnabledShouldDisableModuleWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); Func sendMockIPCConfigMSG = msg => { @@ -140,7 +141,7 @@ namespace ViewModelTests [TestMethod] public void ShiftDragShouldSetValue2FalseWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -158,7 +159,7 @@ namespace ViewModelTests [TestMethod] public void OverrideSnapHotkeysShouldSetValue2TrueWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -176,7 +177,7 @@ namespace ViewModelTests [TestMethod] public void MoveWindowsAcrossMonitorsShouldSetValue2TrueWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -194,7 +195,7 @@ namespace ViewModelTests [TestMethod] public void MoveWindowsBasedOnPositionShouldSetValue2TrueWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -217,7 +218,7 @@ namespace ViewModelTests [TestMethod] public void QuickLayoutSwitchShouldSetValue2FalseWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -235,7 +236,7 @@ namespace ViewModelTests [TestMethod] public void FlashZonesOnQuickSwitchShouldSetValue2FalseWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -253,7 +254,7 @@ namespace ViewModelTests [TestMethod] public void MakeDraggedWindowsTransparentShouldSetValue2TrueWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -271,7 +272,7 @@ namespace ViewModelTests [TestMethod] public void MouseSwitchShouldSetValue2TrueWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -289,7 +290,7 @@ namespace ViewModelTests [TestMethod] public void DisplayOrWorkAreaChangeMoveWindowsShouldSetValue2FalseWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -307,7 +308,7 @@ namespace ViewModelTests [TestMethod] public void ZoneSetChangeMoveWindowsShouldSetValue2TrueWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -325,7 +326,7 @@ namespace ViewModelTests [TestMethod] public void AppLastZoneMoveWindowsShouldSetValue2TrueWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -343,7 +344,7 @@ namespace ViewModelTests [TestMethod] public void OpenWindowOnActiveMonitorShouldSetValue2TrueWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -361,7 +362,7 @@ namespace ViewModelTests [TestMethod] public void RestoreSizeShouldSetValue2TrueWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -379,7 +380,7 @@ namespace ViewModelTests [TestMethod] public void UseCursorPosEditorStartupScreenShouldSetValue2FalseWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -397,7 +398,7 @@ namespace ViewModelTests [TestMethod] public void ShowOnAllMonitorsShouldSetValue2TrueWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -415,7 +416,7 @@ namespace ViewModelTests [TestMethod] public void SpanZonesAcrossMonitorsShouldSetValue2TrueWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -433,7 +434,7 @@ namespace ViewModelTests [TestMethod] public void OverlappingZonesAlgorithmIndexShouldSetValue2AnotherWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -451,7 +452,7 @@ namespace ViewModelTests [TestMethod] public void AllowChildWindowsToSnapShouldSetValue2TrueWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -469,7 +470,7 @@ namespace ViewModelTests [TestMethod] public void DisableRoundCornersOnSnapShouldSetValue2TrueWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -487,7 +488,7 @@ namespace ViewModelTests [TestMethod] public void ZoneHighlightColorShouldSetColorValue2WhiteWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -505,7 +506,7 @@ namespace ViewModelTests [TestMethod] public void ZoneBorderColorShouldSetColorValue2WhiteWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -523,7 +524,7 @@ namespace ViewModelTests [TestMethod] public void ZoneInActiveColorShouldSetColorValue2WhiteWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -541,7 +542,7 @@ namespace ViewModelTests [TestMethod] public void ExcludedAppsShouldSetColorValue2WhiteWhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); @@ -559,7 +560,7 @@ namespace ViewModelTests [TestMethod] public void HighlightOpacityShouldSetOpacityValueTo60WhenSuccessful() { - Mock mockSettingsUtils = new Mock(); + Mock mockSettingsUtils = new Mock(new FileSystem(), null); // arrange FancyZonesViewModel viewModel = new FancyZonesViewModel(mockSettingsUtils.Object, SettingsRepository.GetInstance(mockGeneralSettingsUtils.Object), SettingsRepository.GetInstance(mockFancyZonesSettingsUtils.Object), sendMockIPCConfigMSG, FancyZonesTestFolderName); diff --git a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/ShortcutGuide.cs b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/ShortcutGuide.cs index 3613d0cfa3..897fc2bec6 100644 --- a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/ShortcutGuide.cs +++ b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/ShortcutGuide.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.IO.Abstractions; using System.Text.Json; using Microsoft.PowerToys.Settings.UI.Library; @@ -69,7 +70,7 @@ namespace ViewModelTests [TestMethod] public void IsEnabledShouldEnableModuleWhenSuccessful() { - var settingsUtilsMock = new Mock(); + var settingsUtilsMock = new Mock(new FileSystem(), null); // Assert // Initialize mock function of sending IPC message. diff --git a/src/settings-ui/Settings.UI/Helpers/HotkeyConflictIgnoreHelper.cs b/src/settings-ui/Settings.UI/Helpers/HotkeyConflictIgnoreHelper.cs index d2e737180a..1397fca0b7 100644 --- a/src/settings-ui/Settings.UI/Helpers/HotkeyConflictIgnoreHelper.cs +++ b/src/settings-ui/Settings.UI/Helpers/HotkeyConflictIgnoreHelper.cs @@ -22,7 +22,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers static HotkeyConflictIgnoreHelper() { - _settingsUtils = new SettingsUtils(); + _settingsUtils = SettingsUtils.Default; _generalSettingsRepository = SettingsRepository.GetInstance(_settingsUtils); } diff --git a/src/settings-ui/Settings.UI/Helpers/StartProcessHelper.cs b/src/settings-ui/Settings.UI/Helpers/StartProcessHelper.cs index ce172b2aa2..05c5d7f66c 100644 --- a/src/settings-ui/Settings.UI/Helpers/StartProcessHelper.cs +++ b/src/settings-ui/Settings.UI/Helpers/StartProcessHelper.cs @@ -11,6 +11,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers { public const string ColorsSettings = "ms-settings:colors"; public const string DiagnosticsAndFeedback = "ms-settings:privacy-feedback"; + public const string NightLightSettings = "ms-settings:nightlight"; public static string AnimationsSettings => OSVersionHelper.IsWindows11() ? "ms-settings:easeofaccess-visualeffects" diff --git a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs index 19cd75b022..3189f15b31 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs @@ -133,7 +133,7 @@ namespace Microsoft.PowerToys.Settings.UI var settingValue = cmdArgs[3]; try { - SetSettingCommandLineCommand.Execute(settingName, settingValue, new SettingsUtils()); + SetSettingCommandLineCommand.Execute(settingName, settingValue, SettingsUtils.Default); } catch (Exception ex) { @@ -151,7 +151,7 @@ namespace Microsoft.PowerToys.Settings.UI { using (var settings = JsonDocument.Parse(File.ReadAllText(ipcFileName))) { - SetAdditionalSettingsCommandLineCommand.Execute(moduleName, settings, new SettingsUtils()); + SetAdditionalSettingsCommandLineCommand.Execute(moduleName, settings, SettingsUtils.Default); } } catch (Exception ex) @@ -357,7 +357,7 @@ namespace Microsoft.PowerToys.Settings.UI return 0; } - private static ISettingsUtils settingsUtils = new SettingsUtils(); + private static ISettingsUtils settingsUtils = SettingsUtils.Default; private static ThemeService themeService = new ThemeService(SettingsRepository.GetInstance(settingsUtils)); public static ThemeService ThemeService => themeService; diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/Card.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/Card.xaml index 34abbe993d..9563bfceb3 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/Card.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/Card.xaml @@ -33,6 +33,7 @@ diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs index b9bee4ff08..19a6cb06ad 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs @@ -26,7 +26,7 @@ namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard public ShortcutConflictWindow() { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new ShortcutConflictViewModel( settingsUtils, SettingsRepository.GetInstance(settingsUtils), diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/AppsListPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/AppsListPage.xaml.cs index b58636f41b..fd034239e1 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/AppsListPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/AppsListPage.xaml.cs @@ -24,7 +24,7 @@ namespace Microsoft.PowerToys.Settings.UI.Flyout { this.InitializeComponent(); - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new AllAppsViewModel(SettingsRepository.GetInstance(settingsUtils), Views.ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml.cs index 51219309e0..f82da1466c 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml.cs @@ -27,7 +27,7 @@ namespace Microsoft.PowerToys.Settings.UI.Flyout public LaunchPage() { this.InitializeComponent(); - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new LauncherViewModel(SettingsRepository.GetInstance(settingsUtils), Views.ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; } @@ -51,7 +51,7 @@ namespace Microsoft.PowerToys.Settings.UI.Flyout break; case ModuleType.EnvironmentVariables: // Launch Environment Variables { - bool launchAdmin = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.LaunchAdministrator; + bool launchAdmin = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.LaunchAdministrator; string eventName = !App.IsElevated && launchAdmin ? Constants.ShowEnvironmentVariablesAdminSharedEvent() : Constants.ShowEnvironmentVariablesSharedEvent(); @@ -74,7 +74,7 @@ namespace Microsoft.PowerToys.Settings.UI.Flyout case ModuleType.Hosts: // Launch Hosts { - bool launchAdmin = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.LaunchAdministrator; + bool launchAdmin = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.LaunchAdministrator; string eventName = !App.IsElevated && launchAdmin ? Constants.ShowHostsAdminSharedEvent() : Constants.ShowHostsSharedEvent(); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs index 90b2577268..27d5456418 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs @@ -81,7 +81,7 @@ namespace Microsoft.PowerToys.Settings.UI // open main window ShellPage.SetUpdatingGeneralSettingsCallback((ModuleType moduleType, bool isEnabled) => { - SettingsRepository repository = SettingsRepository.GetInstance(new SettingsUtils()); + SettingsRepository repository = SettingsRepository.GetInstance(SettingsUtils.Default); GeneralSettings generalSettingsConfig = repository.SettingsConfig; bool needToUpdate = ModuleHelper.GetIsModuleEnabled(generalSettingsConfig, moduleType) != isEnabled; diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeAdvancedPaste.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeAdvancedPaste.xaml.cs index 52cbc6cef2..73e25ed856 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeAdvancedPaste.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeAdvancedPaste.xaml.cs @@ -35,9 +35,9 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views protected override void OnNavigatedTo(NavigationEventArgs e) { ViewModel.LogOpeningModuleEvent(); - AdvancedPasteUIHotkeyControl.Keys = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.AdvancedPasteUIShortcut.GetKeysList(); - PasteAsPlainTextHotkeyControl.Keys = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.PasteAsPlainTextShortcut.GetKeysList(); - PasteAsMarkdownHotkeyControl.Keys = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.PasteAsMarkdownShortcut.GetKeysList(); + AdvancedPasteUIHotkeyControl.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.AdvancedPasteUIShortcut.GetKeysList(); + PasteAsPlainTextHotkeyControl.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.PasteAsPlainTextShortcut.GetKeysList(); + PasteAsMarkdownHotkeyControl.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.PasteAsMarkdownShortcut.GetKeysList(); // TODO(stefan): Check how to remove additional space if item is set to Collapsed. if (PasteAsMarkdownHotkeyControl.Keys.Count > 0) @@ -45,7 +45,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views PasteAsMarkdownHotkeyControl.Visibility = Microsoft.UI.Xaml.Visibility.Visible; } - PasteAsJsonHotkeyControl.Keys = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.PasteAsJsonShortcut.GetKeysList(); + PasteAsJsonHotkeyControl.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.PasteAsJsonShortcut.GetKeysList(); if (PasteAsJsonHotkeyControl.Keys.Count > 0) { PasteAsJsonHotkeyControl.Visibility = Microsoft.UI.Xaml.Visibility.Visible; diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeAlwaysOnTop.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeAlwaysOnTop.xaml.cs index d22392e717..08fb0f8a80 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeAlwaysOnTop.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeAlwaysOnTop.xaml.cs @@ -35,7 +35,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views protected override void OnNavigatedTo(NavigationEventArgs e) { ViewModel.LogOpeningModuleEvent(); - HotkeyControl.Keys = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.Hotkey.Value.GetKeysList(); + HotkeyControl.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.Hotkey.Value.GetKeysList(); } protected override void OnNavigatedFrom(NavigationEventArgs e) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeColorPicker.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeColorPicker.xaml.cs index 444b2d7295..8fd2bfb28d 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeColorPicker.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeColorPicker.xaml.cs @@ -50,7 +50,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views protected override void OnNavigatedTo(NavigationEventArgs e) { ViewModel.LogOpeningModuleEvent(); - ColorPickerSettings settings = new SettingsUtils().GetSettingsOrDefault(ColorPickerSettings.ModuleName, settingsUpgrader: ColorPickerSettings.UpgradeSettings); + ColorPickerSettings settings = SettingsUtils.Default.GetSettingsOrDefault(ColorPickerSettings.ModuleName, settingsUpgrader: ColorPickerSettings.UpgradeSettings); HotkeyControl.Keys = settings.Properties.ActivationShortcut.GetKeysList(); } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCropAndLock.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCropAndLock.xaml.cs index be05925ec3..914dd647f6 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCropAndLock.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCropAndLock.xaml.cs @@ -35,8 +35,8 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views protected override void OnNavigatedTo(NavigationEventArgs e) { ViewModel.LogOpeningModuleEvent(); - ReparentHotkeyControl.Keys = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.ReparentHotkey.Value.GetKeysList(); - ThumbnailHotkeyControl.Keys = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.ThumbnailHotkey.Value.GetKeysList(); + ReparentHotkeyControl.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ReparentHotkey.Value.GetKeysList(); + ThumbnailHotkeyControl.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ThumbnailHotkey.Value.GetKeysList(); } protected override void OnNavigatedFrom(NavigationEventArgs e) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml.cs index af20978b42..3e2b66b9d6 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml.cs @@ -37,7 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views private void Launch_EnvironmentVariables_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) { - bool launchAdmin = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.LaunchAdministrator; + bool launchAdmin = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.LaunchAdministrator; string eventName = !App.IsElevated && launchAdmin ? Constants.ShowEnvironmentVariablesAdminSharedEvent() : Constants.ShowEnvironmentVariablesSharedEvent(); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeFancyZones.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeFancyZones.xaml.cs index ccbc5c8cc7..5308336e16 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeFancyZones.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeFancyZones.xaml.cs @@ -35,7 +35,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views protected override void OnNavigatedTo(NavigationEventArgs e) { ViewModel.LogOpeningModuleEvent(); - HotkeyControl.Keys = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.FancyzonesEditorHotkey.Value.GetKeysList(); + HotkeyControl.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.FancyzonesEditorHotkey.Value.GetKeysList(); } protected override void OnNavigatedFrom(NavigationEventArgs e) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeHosts.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeHosts.xaml.cs index d47af96c7f..0a98df472e 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeHosts.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeHosts.xaml.cs @@ -37,7 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views private void Launch_Hosts_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) { - bool launchAdmin = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.LaunchAdministrator; + bool launchAdmin = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.LaunchAdministrator; string eventName = !App.IsElevated && launchAdmin ? Constants.ShowHostsAdminSharedEvent() : Constants.ShowHostsSharedEvent(); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeMeasureTool.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeMeasureTool.xaml.cs index 7ca7739883..e8a24b9fc5 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeMeasureTool.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeMeasureTool.xaml.cs @@ -38,7 +38,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views protected override void OnNavigatedTo(NavigationEventArgs e) { ViewModel.LogOpeningModuleEvent(); - HotkeyActivation.Keys = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.ActivationShortcut.GetKeysList(); + HotkeyActivation.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.GetKeysList(); } protected override void OnNavigatedFrom(NavigationEventArgs e) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverviewAlternate.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverviewAlternate.xaml.cs index d0ae488347..cbd05fc7ed 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverviewAlternate.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverviewAlternate.xaml.cs @@ -21,10 +21,10 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views 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(); + FancyZonesHotkeyControl.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.FancyzonesEditorHotkey.Value.GetKeysList(); + RunHotkeyControl.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.OpenPowerLauncher.GetKeysList(); + ColorPickerHotkeyControl.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.GetKeysList(); + AlwaysOnTopHotkeyControl.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.Hotkey.Value.GetKeysList(); } private void SettingsLaunchButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobePeek.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobePeek.xaml.cs index 7817b13ca4..6782f4adc7 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobePeek.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobePeek.xaml.cs @@ -38,7 +38,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views protected override void OnNavigatedTo(NavigationEventArgs e) { ViewModel.LogOpeningModuleEvent(); - HotkeyControl.Keys = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.ActivationShortcut.GetKeysList(); + HotkeyControl.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.GetKeysList(); } protected override void OnNavigatedFrom(NavigationEventArgs e) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobePowerOCR.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobePowerOCR.xaml.cs index 7fef84d2b9..cf607d370e 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobePowerOCR.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobePowerOCR.xaml.cs @@ -35,7 +35,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views protected override void OnNavigatedTo(NavigationEventArgs e) { ViewModel.LogOpeningModuleEvent(); - HotkeyControl.Keys = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.ActivationShortcut.GetKeysList(); + HotkeyControl.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.GetKeysList(); } protected override void OnNavigatedFrom(NavigationEventArgs e) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRun.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRun.xaml.cs index 3cb8593922..796e05bdea 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRun.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeRun.xaml.cs @@ -54,7 +54,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views { ViewModel.LogOpeningModuleEvent(); - HotkeyControl.Keys = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.OpenPowerLauncher.GetKeysList(); + HotkeyControl.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.OpenPowerLauncher.GetKeysList(); } protected override void OnNavigatedFrom(NavigationEventArgs e) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShellPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShellPage.xaml.cs index 8b01ab0bd0..7f8531286c 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShellPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShellPage.xaml.cs @@ -51,7 +51,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views public ObservableCollection Modules { get; } - private static ISettingsUtils settingsUtils = new SettingsUtils(); + private static ISettingsUtils settingsUtils = SettingsUtils.Default; /* NOTE: Experimentation for OOBE is currently turned off on server side. Keeping this code in a comment to allow future experiments. private bool ExperimentationToggleSwitchEnabled { get; set; } = true; diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShortcutGuide.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShortcutGuide.xaml.cs index 5702ddcb9f..e721deefd6 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShortcutGuide.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeShortcutGuide.xaml.cs @@ -56,7 +56,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views protected override void OnNavigatedTo(NavigationEventArgs e) { ViewModel.LogOpeningModuleEvent(); - var settingsProperties = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties; + var settingsProperties = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties; if ((bool)settingsProperties.UseLegacyPressWinKeyBehavior.Value) { diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWorkspaces.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWorkspaces.xaml.cs index 772807f735..98a8bccb84 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWorkspaces.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWorkspaces.xaml.cs @@ -38,7 +38,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views protected override void OnNavigatedTo(NavigationEventArgs e) { ViewModel.LogOpeningModuleEvent(); - HotkeyControl.Keys = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.Hotkey.Value.GetKeysList(); + HotkeyControl.Keys = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.Hotkey.Value.GetKeysList(); } protected override void OnNavigatedFrom(NavigationEventArgs e) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Panels/MouseJumpPanel.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Panels/MouseJumpPanel.xaml index 32fa3d19d5..a0725c0149 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Panels/MouseJumpPanel.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Panels/MouseJumpPanel.xaml @@ -48,39 +48,13 @@ HeaderIcon="{ui:FontIcon Glyph=}" IsEnabled="{x:Bind ViewModel.IsMouseJumpEnabled, Mode=OneWay}"> - - - - - - - + + + + + + + .GetInstance(settingsUtils), diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs index c5d99e8e98..95ba8b595f 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs @@ -15,7 +15,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public AlwaysOnTopPage() { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new AlwaysOnTopViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; InitializeComponent(); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AwakePage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/AwakePage.xaml.cs index 91b2b0b781..f52e96fb8f 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AwakePage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AwakePage.xaml.cs @@ -36,7 +36,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views { _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); _fileSystem = new FileSystem(); - _settingsUtils = new SettingsUtils(); + _settingsUtils = SettingsUtils.Default; _sendConfigMsg = ShellPage.SendDefaultIPCMessage; ViewModel = new AwakeViewModel(); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs index ea318100f0..90dec43398 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs @@ -19,7 +19,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public CmdPalPage() { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new CmdPalViewModel( settingsUtils, SettingsRepository.GetInstance(settingsUtils), diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml.cs index 9e0ba6be07..652db49f4b 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml.cs @@ -27,7 +27,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public ColorPickerPage() { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new ColorPickerViewModel( settingsUtils, SettingsRepository.GetInstance(settingsUtils), diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/CropAndLockPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/CropAndLockPage.xaml.cs index 67ef238645..5c174b1f98 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/CropAndLockPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/CropAndLockPage.xaml.cs @@ -15,7 +15,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public CropAndLockPage() { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new CropAndLockViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; InitializeComponent(); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml index 643811d2fc..e5ea54d21b 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml @@ -87,6 +87,7 @@ MaxWidth="{StaticResource PageMaxWidth}" Margin="1,0,0,0" VerticalAlignment="Center" + AutomationProperties.HeadingLevel="1" Style="{StaticResource TitleTextBlockStyle}" /> .GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/EnvironmentVariablesPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/EnvironmentVariablesPage.xaml.cs index 0772869249..a3e17ac491 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/EnvironmentVariablesPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/EnvironmentVariablesPage.xaml.cs @@ -16,7 +16,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public EnvironmentVariablesPage() { InitializeComponent(); - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new EnvironmentVariablesViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage, App.IsElevated); } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs index e1d7f7ea4b..ddf7c1e8ee 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs @@ -16,7 +16,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public FancyZonesPage() { InitializeComponent(); - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new FancyZonesViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; Loaded += (s, e) => ViewModel.OnPageLoaded(); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/FileLocksmithPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/FileLocksmithPage.xaml.cs index 6020917b69..8f78ce2c8c 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/FileLocksmithPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/FileLocksmithPage.xaml.cs @@ -15,7 +15,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public FileLocksmithPage() { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new FileLocksmithViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; InitializeComponent(); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml.cs index 56eb73c6ef..5e84711b86 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml.cs @@ -38,7 +38,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views // Load string resources var loader = Helpers.ResourceLoaderInstance.ResourceLoader; - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; Action stateUpdatingAction = () => { diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml.cs index 19375d90f7..b65f32d520 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml.cs @@ -16,7 +16,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public HostsPage() { InitializeComponent(); - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new HostsViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage, App.IsElevated); BackupsCountInputSettingsCard.Header = ResourceLoaderInstance.ResourceLoader.GetString("Hosts_Backup_CountInput_Header"); BackupsCountInputSettingsCard.Description = ResourceLoaderInstance.ResourceLoader.GetString("Hosts_Backup_CountInput_Description"); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml.cs index 18e1aacc15..6a2068d5a8 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml.cs @@ -21,7 +21,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public ImageResizerPage() { InitializeComponent(); - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; var resourceLoader = ResourceLoaderInstance.ResourceLoader; Func loader = resourceLoader.GetString; diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml.cs index e32f5aa9cb..fce4dfc718 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml.cs @@ -28,7 +28,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public KeyboardManagerPage() { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new KeyboardManagerViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage, FilterRemapKeysList); watcher = Helper.GetFileWatcher( diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml index ec61a0fcd5..a44d482a04 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml @@ -67,6 +67,10 @@ x:Uid="LightSwitch_ModeSunsetToSunrise" AutomationProperties.AutomationId="SunCBItem_LightSwitch" Tag="SunsetToSunrise" /> + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml.cs index 974447a20e..1ee79a4010 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml.cs @@ -40,7 +40,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public LightSwitchPage() { - this.settingsUtils = new SettingsUtils(); + this.settingsUtils = SettingsUtils.Default; this.sendConfigMsg = ShellPage.SendDefaultIPCMessage; this.generalSettingsRepository = SettingsRepository.GetInstance(this.settingsUtils); @@ -355,6 +355,10 @@ namespace Microsoft.PowerToys.Settings.UI.Views VisualStateManager.GoToState(this, "SunsetToSunriseState", true); this.SunriseModeChartState(); break; + case "FollowNightLight": + VisualStateManager.GoToState(this, "FollowNightLightState", true); + TimelineCard.Visibility = Visibility.Collapsed; + break; default: VisualStateManager.GoToState(this, "OffState", true); this.TimelineCard.Visibility = Visibility.Collapsed; @@ -362,6 +366,18 @@ namespace Microsoft.PowerToys.Settings.UI.Views } } + private void OpenNightLightSettings_Click(object sender, RoutedEventArgs e) + { + try + { + Helpers.StartProcessHelper.Start(Helpers.StartProcessHelper.NightLightSettings); + } + catch (Exception ex) + { + Logger.LogError("Error while trying to open the system night light settings", ex); + } + } + private void SunriseModeChartState() { if (this.ViewModel.Latitude != "0.0" && this.ViewModel.Longitude != "0.0") diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/MeasureToolPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/MeasureToolPage.xaml.cs index bf516b557b..8f80f1c13b 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/MeasureToolPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/MeasureToolPage.xaml.cs @@ -17,7 +17,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public MeasureToolPage() { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new MeasureToolViewModel( settingsUtils, SettingsRepository.GetInstance(settingsUtils), diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml index 60d0f5370d..80a00f66e8 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml @@ -23,6 +23,34 @@ + + + + + + + + + + + + + + + + + + + + + IsEnabled="{x:Bind ViewModel.IsFindMyMouseEnabled, Mode=OneWay}"> @@ -114,8 +141,7 @@ x:Uid="Appearance_Behavior" AutomationProperties.AutomationId="MouseUtils_FindMyMouseAppearanceBehaviorId" HeaderIcon="{ui:FontIcon Glyph=}" - IsEnabled="{x:Bind ViewModel.IsFindMyMouseEnabled, Mode=OneWay}" - IsExpanded="False"> + IsEnabled="{x:Bind ViewModel.IsFindMyMouseEnabled, Mode=OneWay}"> @@ -206,8 +232,7 @@ x:Uid="MouseUtils_MouseHighlighter_ActivationShortcut" AutomationProperties.AutomationId="MouseUtils_MouseHighlighterActivationShortcutId" HeaderIcon="{ui:FontIcon Glyph=}" - IsEnabled="{x:Bind ViewModel.IsMouseHighlighterEnabled, Mode=OneWay}" - IsExpanded="True"> + IsEnabled="{x:Bind ViewModel.IsMouseHighlighterEnabled, Mode=OneWay}"> @@ -273,34 +298,6 @@ - - - - - - - - - - - - - - - - - - - + IsEnabled="{x:Bind ViewModel.IsMousePointerCrosshairsEnabled, Mode=OneWay}"> diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml.cs index ac4c7cc71d..fa34ca5293 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml.cs @@ -22,7 +22,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views { // By mistake, the first release of Find My Mouse was saving settings in two places at the same time. // Delete the wrong path for Find My Mouse settings. - var tempSettingsUtils = new SettingsUtils(); + var tempSettingsUtils = SettingsUtils.Default; if (tempSettingsUtils.SettingsExists("Find My Mouse")) { var settingsFilePath = tempSettingsUtils.GetSettingsFilePath("Find My Mouse"); @@ -34,7 +34,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views { } - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new MouseUtilsViewModel( settingsUtils, SettingsRepository.GetInstance(settingsUtils), diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseWithoutBordersPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseWithoutBordersPage.xaml.cs index 296a9a3deb..2db0d7a5d6 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseWithoutBordersPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseWithoutBordersPage.xaml.cs @@ -33,7 +33,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public MouseWithoutBordersPage() { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new MouseWithoutBordersViewModel( settingsUtils, SettingsRepository.GetInstance(settingsUtils), diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml.cs index daa4dca2b5..997f6c7771 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml.cs @@ -16,7 +16,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public NewPlusPage() { InitializeComponent(); - var settings_utils = new SettingsUtils(); + var settings_utils = SettingsUtils.Default; ViewModel = new NewPlusViewModel(settings_utils, SettingsRepository.GetInstance(settings_utils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml.cs index 1a845e25ec..b848ae86dd 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml.cs @@ -15,7 +15,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public PeekPage() { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new PeekViewModel( settingsUtils, SettingsRepository.GetInstance(settingsUtils), diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerAccentPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerAccentPage.xaml.cs index fa666dd242..0f60a8dcd3 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerAccentPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerAccentPage.xaml.cs @@ -17,7 +17,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public PowerAccentPage() { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new PowerAccentViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; this.InitializeComponent(); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml index dd84aa28ec..5bcdf7195c 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerLauncherPage.xaml @@ -548,7 +548,7 @@ x:Uid="PowerLauncher_TitleFontSize" HeaderIcon="{ui:FontIcon Glyph=}"> - - .GetInstance(settingsUtils)?.SettingsConfig; diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerOcrPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerOcrPage.xaml.cs index 27769b92ce..9198d338ef 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerOcrPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerOcrPage.xaml.cs @@ -15,7 +15,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public PowerOcrPage() { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new PowerOcrViewModel( settingsUtils, SettingsRepository.GetInstance(settingsUtils), diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerPreviewPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerPreviewPage.xaml.cs index 9d56c686e3..ecc960651b 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerPreviewPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerPreviewPage.xaml.cs @@ -19,7 +19,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public PowerPreviewPage() { InitializeComponent(); - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new PowerPreviewViewModel(SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerRenamePage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerRenamePage.xaml.cs index d97b83c499..10013b84a3 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerRenamePage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerRenamePage.xaml.cs @@ -16,7 +16,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public PowerRenamePage() { InitializeComponent(); - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new PowerRenameViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/RegistryPreviewPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/RegistryPreviewPage.xaml.cs index 8c1ed319a3..41d814e48b 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/RegistryPreviewPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/RegistryPreviewPage.xaml.cs @@ -14,7 +14,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public RegistryPreviewPage() { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new RegistryPreviewViewModel( SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs index 06f7e222ce..853b66f164 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs @@ -151,7 +151,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views { InitializeComponent(); SetWindowTitle(); - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new ShellViewModel(SettingsRepository.GetInstance(settingsUtils)); DataContext = ViewModel; ShellHandler = this; diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShortcutGuidePage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShortcutGuidePage.xaml.cs index ffca0184c7..feb0e4837d 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShortcutGuidePage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShortcutGuidePage.xaml.cs @@ -17,7 +17,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views { InitializeComponent(); - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new ShortcutGuideViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/WorkspacesPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/WorkspacesPage.xaml.cs index 0b0ca571c4..3028b46ea1 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/WorkspacesPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/WorkspacesPage.xaml.cs @@ -15,7 +15,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public WorkspacesPage() { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new WorkspacesViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; InitializeComponent(); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ZoomItPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/ZoomItPage.xaml.cs index 9018eb64e8..043ca7df8c 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ZoomItPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ZoomItPage.xaml.cs @@ -104,7 +104,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views public ZoomItPage() { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; ViewModel = new ZoomItViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage, PickFileDialog, PickFontDialog); DataContext = ViewModel; InitializeComponent(); 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 7548d9da57..30535804b7 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -1,17 +1,17 @@ - @@ -228,7 +228,7 @@ Adds feet to the end of cross lines - Enable Screen Ruler + Screen Ruler "Screen Ruler" is the name of the utility @@ -299,7 +299,7 @@ Service - Enable Mouse Without Borders + Mouse Without Borders Attribution @@ -540,7 +540,7 @@ opera.exe Product name: Navigation view item name for Shortcut Guide - File Explorer add-ons + File Explorer Add-ons Product name: Navigation view item name for File Explorer. Please use File Explorer as in the context of File Explorer in Windows @@ -564,7 +564,7 @@ opera.exe Product name: Navigation view item name for Mouse Without Borders - Mouse utilities + Mouse Utilities Product name: Navigation view item name for Mouse utilities @@ -584,7 +584,7 @@ opera.exe Keyboard Manager page description - Enable Keyboard Manager + Keyboard Manager Keyboard Manager enable toggle header. Do not loc the Product name. Do you want this feature on / off @@ -624,7 +624,7 @@ opera.exe Disable - Enable Paste with AI + Paste with AI ## Preview Terms @@ -638,11 +638,8 @@ Please review the placeholder content that represents the final terms and usage I have read and accept the information above. - - Enable OpenAI content moderation - - Enable OpenAI content moderation + OpenAI content moderation Use built-in functions to handle complex tasks. Token consumption may increase. @@ -751,7 +748,7 @@ Please review the placeholder content that represents the final terms and usage Quick and simple system-wide color picker. - Enable Color Picker + Color Picker do not loc the Product name. Do you want this feature on / off @@ -761,7 +758,7 @@ Please review the placeholder content that represents the final terms and usage A quick launcher that has additional capabilities without sacrificing performance. - Enable PowerToys Run + PowerToys Run do not loc the Product name. Do you want this feature on / off @@ -886,7 +883,7 @@ Please review the placeholder content that represents the final terms and usage windows refers to application windows - Enable FancyZones + FancyZones {Locked="FancyZones"} @@ -1056,7 +1053,7 @@ Please review the placeholder content that represents the final terms and usage This refers to directly integrating in with Windows - Enable PowerRename + PowerRename do not loc the Product name. Do you want this feature on / off @@ -1238,7 +1235,7 @@ Please review the placeholder content that represents the final terms and usage PowerToys will restart automatically if needed - Enable Shortcut Guide + Shortcut Guide do not loc the Product name. Do you want this feature on / off @@ -1270,7 +1267,7 @@ Please review the placeholder content that represents the final terms and usage Lets you resize images by right-clicking. - Enable Image Resizer + Image Resizer do not loc the Product name. Do you want this feature on / off @@ -2092,9 +2089,6 @@ Made with 💗 by Microsoft and the PowerToys community. Transcode to .mp4 (H.264/AAC) - - OpenAI API key: - Save @@ -2411,7 +2405,7 @@ From there, simply click on one of the supported files in the File Explorer and A convenient way to keep your PC awake on-demand. - Enable Awake + Awake Awake is a product name, do not loc @@ -2676,7 +2670,7 @@ From there, simply click on one of the supported files in the File Explorer and Mouse as in the hardware peripheral. - Enable CursorWrap + CursorWrap CursorWrap @@ -2831,7 +2825,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut."Ctrl" is a keyboard key. "Find My Mouse" is the name of the utility - Enable Find My Mouse + Find My Mouse "Find My Mouse" is the name of the utility. @@ -2920,7 +2914,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut."Mouse Highlighter" is the name of the utility. Mouse is the hardware mouse. - Enable Mouse Highlighter + Mouse Highlighter "Find My Mouse" is the name of the utility. @@ -2965,7 +2959,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut."Mouse Pointer Crosshairs" is the name of the utility. Mouse is the hardware mouse. - Enable Mouse Pointer Crosshairs + Mouse Pointer Crosshairs "Mouse Pointer Crosshairs" is the name of the utility. @@ -3090,7 +3084,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.Activation - Enable Crop And Lock + Crop And Lock "Crop And Lock" is the name of the utility @@ -3146,7 +3140,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.Activation - Enable Always On Top + Always On Top {Locked="Always On Top"} @@ -3247,7 +3241,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.Peek is a quick and easy way to preview files. Select a file in File Explorer and press the shortcut to open the file preview. - Enable Peek + Peek Peek is a product name, do not loc @@ -3324,7 +3318,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.Quick Accent is a product name, do not loc - Enable Quick Accent + Quick Accent Quick Accent @@ -3333,7 +3327,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.Workspaces - Enable Workspaces + Workspaces "Workspaces" is the name of the utility @@ -3607,7 +3601,7 @@ Activate by holding the key for the character you want to add an accent to, then Text Extractor - Enable Text Extractor + Text Extractor Text Extractor can only recognize languages that have the OCR pack installed. @@ -3836,7 +3830,7 @@ Activate by holding the key for the character you want to add an accent to, then Products name: Navigation view item name for Hosts File Editor - Enable Hosts File Editor + Hosts File Editor "Hosts File Editor" is the name of the utility @@ -3901,7 +3895,7 @@ Activate by holding the key for the character you want to add an accent to, then Environment Variables - Enable Environment Variables + Environment Variables Activation @@ -3949,7 +3943,7 @@ Activate by holding the key for the character you want to add an accent to, then Product name: Navigation view item name for FileLocksmith - Enable File Locksmith + File Locksmith File Locksmith is the name of the utility @@ -4029,7 +4023,7 @@ Activate by holding the key for the character you want to add an accent to, then Product name: Navigation view item name for Advanced Paste - A tool to quickly format clipboard content into plain text, Markdown, JSON, and more. An AI-powered option requiring an OpenAI API key is available for advanced formatting. + Formats clipboard content into plain text, Markdown, JSON, and more. Advanced formatting can use an online or local language model endpoint. Advanced Paste @@ -4038,7 +4032,7 @@ Activate by holding the key for the character you want to add an accent to, then cancel - Enable Advanced Paste + Advanced Paste Paste with AI @@ -4053,7 +4047,7 @@ Activate by holding the key for the character you want to add an accent to, then Preview the output of AI formats and Image to text before pasting - Enable Advanced AI + Advanced AI Advanced Paste is a tool to put your clipboard content into any format you need, focused towards developer workflows. It can paste as plain text, markdown, or json directly with the UX or with a direct keystroke invoke. These are fully locally executed. In addition, it has an AI powered option that is 100% opt-in and requires an Open AI key. Note: this will replace the formatted text in your clipboard with the selected format. @@ -4205,7 +4199,7 @@ Activate by holding the key for the character you want to add an accent to, then Product name: Navigation view item name for Registry Preview - Enable Registry Preview + Registry Preview Registry Preview is the name of the utility @@ -4239,7 +4233,7 @@ Activate by holding the key for the character you want to add an accent to, then "Mouse Jump" is the name of the utility. Mouse is the hardware mouse. - Enable Mouse Jump + Mouse Jump "Mouse Jump" is the name of the utility. @@ -4559,18 +4553,9 @@ Activate by holding the key for the character you want to add an accent to, then OpenAI Terms - - Paste with AI allows you to format your clipboard content into any format you need. Learn more about the terms of conditions while using OpenAI and privacy at Microsoft: - • Login into your - - Configure OpenAI key - - - OpenAI API keys overview - • Create a new secret key and paste it in the field below @@ -4588,9 +4573,6 @@ Activate by holding the key for the character you want to add an accent to, then Font size - - • NOTE: You need to have available paid credits in your OpenAI account to use this feature. If you do not have credits you will see an 'API key quota exceeded' error - If you do not have credits you will see an 'API key quota exceeded' error @@ -4626,7 +4608,7 @@ Activate by holding the key for the character you want to add an accent to, then New+ learn more link. Localize product name in accordance with Windows New - Enable New+ + New+ Localize product name in accordance with Windows New @@ -4739,7 +4721,7 @@ Activate by holding the key for the character you want to add an accent to, then {Locked="ZoomIt"} - Enable ZoomIt + ZoomIt {Locked="ZoomIt"} @@ -5160,7 +5142,7 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m System Tools - Enable Command Palette + Command Palette Command Palette is a product name, do not loc @@ -5345,9 +5327,6 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m Light Switch - - Enable Light Switch - Easily switch between light and dark mode - on a schedule, automatically, or with a shortcut. @@ -5361,7 +5340,7 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m Behavior - Enable Light Switch + Light Switch Shortcuts @@ -5781,4 +5760,13 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m A modern UI built with Fluent Design Fluent Design is a product name, do not loc - + + Follow Night Light + + + Personalize your Night Light settings. + + + Following Night Light settings. + + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs index b0520dd38d..1afe76897e 100644 --- a/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs @@ -251,7 +251,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels if (dashboardListItem.Tag == ModuleType.NewPlus && dashboardListItem.IsEnabled == true) { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; var settings = NewPlusViewModel.LoadSettings(settingsUtils); NewPlusViewModel.CopyTemplateExamples(settings.Properties.TemplateLocation.Value); } @@ -390,7 +390,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private ObservableCollection GetModuleItemsAlwaysOnTop() { - ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(new SettingsUtils()); + ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(SettingsUtils.Default); var list = new List { new DashboardModuleShortcutItem() { Label = resourceLoader.GetString("AlwaysOnTop_ShortDescription"), Shortcut = moduleSettingsRepository.SettingsConfig.Properties.Hotkey.Value.GetKeysList() }, @@ -411,7 +411,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private ObservableCollection GetModuleItemsColorPicker() { - ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(new SettingsUtils()); + ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(SettingsUtils.Default); var settings = moduleSettingsRepository.SettingsConfig; var hotkey = settings.Properties.ActivationShortcut; var list = new List @@ -423,7 +423,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private ObservableCollection GetModuleItemsLightSwitch() { - ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(new SettingsUtils()); + ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(SettingsUtils.Default); var settings = moduleSettingsRepository.SettingsConfig; var list = new List { @@ -434,7 +434,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private ObservableCollection GetModuleItemsCropAndLock() { - ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(new SettingsUtils()); + ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(SettingsUtils.Default); var settings = moduleSettingsRepository.SettingsConfig; var list = new List { @@ -455,7 +455,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private ObservableCollection GetModuleItemsFancyZones() { - ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(new SettingsUtils()); + ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(SettingsUtils.Default); var settings = moduleSettingsRepository.SettingsConfig; string activationMode = $"{resourceLoader.GetString(settings.Properties.FancyzonesShiftDrag.Value ? "FancyZones_ActivationShiftDrag" : "FancyZones_ActivationNoShiftDrag")}."; @@ -470,7 +470,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private ObservableCollection GetModuleItemsFindMyMouse() { - ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(new SettingsUtils()); + ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(SettingsUtils.Default); string shortDescription = resourceLoader.GetString("FindMyMouse_ShortDescription"); var settings = moduleSettingsRepository.SettingsConfig; var activationMethod = settings.Properties.ActivationMethod.Value; @@ -508,7 +508,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private ObservableCollection GetModuleItemsMouseHighlighter() { - ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(new SettingsUtils()); + ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(SettingsUtils.Default); var list = new List { new DashboardModuleShortcutItem() { Label = resourceLoader.GetString("MouseHighlighter_ShortDescription"), Shortcut = moduleSettingsRepository.SettingsConfig.Properties.ActivationShortcut.GetKeysList() }, @@ -518,7 +518,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private ObservableCollection GetModuleItemsMouseJump() { - ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(new SettingsUtils()); + ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(SettingsUtils.Default); var list = new List { new DashboardModuleShortcutItem() { Label = resourceLoader.GetString("MouseJump_ShortDescription"), Shortcut = moduleSettingsRepository.SettingsConfig.Properties.ActivationShortcut.GetKeysList() }, @@ -528,7 +528,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private ObservableCollection GetModuleItemsMousePointerCrosshairs() { - ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(new SettingsUtils()); + ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(SettingsUtils.Default); var list = new List { new DashboardModuleShortcutItem() { Label = resourceLoader.GetString("MouseCrosshairs_ShortDescription"), Shortcut = moduleSettingsRepository.SettingsConfig.Properties.ActivationShortcut.GetKeysList() }, @@ -538,7 +538,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private ObservableCollection GetModuleItemsAdvancedPaste() { - ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(new SettingsUtils()); + ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(SettingsUtils.Default); var list = new List { new DashboardModuleShortcutItem() { Label = resourceLoader.GetString("AdvancedPasteUI_Shortcut/Header"), Shortcut = moduleSettingsRepository.SettingsConfig.Properties.AdvancedPasteUIShortcut.GetKeysList() }, @@ -560,7 +560,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private ObservableCollection GetModuleItemsPeek() { - ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(new SettingsUtils()); + ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(SettingsUtils.Default); var list = new List { new DashboardModuleShortcutItem() { Label = resourceLoader.GetString("Peek_ShortDescription"), Shortcut = moduleSettingsRepository.SettingsConfig.Properties.ActivationShortcut.GetKeysList() }, @@ -570,7 +570,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private ObservableCollection GetModuleItemsPowerLauncher() { - ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(new SettingsUtils()); + ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(SettingsUtils.Default); var list = new List { new DashboardModuleShortcutItem() { Label = resourceLoader.GetString("Run_ShortDescription"), Shortcut = moduleSettingsRepository.SettingsConfig.Properties.OpenPowerLauncher.GetKeysList() }, @@ -580,7 +580,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private ObservableCollection GetModuleItemsPowerAccent() { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; PowerAccentSettings moduleSettings = settingsUtils.GetSettingsOrDefault(PowerAccentSettings.ModuleName); var activationMethod = moduleSettings.Properties.ActivationKey; string activation = string.Empty; @@ -600,7 +600,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private ObservableCollection GetModuleItemsWorkspaces() { - ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(new SettingsUtils()); + ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(SettingsUtils.Default); var settings = moduleSettingsRepository.SettingsConfig; var list = new List @@ -622,7 +622,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private ObservableCollection GetModuleItemsMeasureTool() { - ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(new SettingsUtils()); + ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(SettingsUtils.Default); var list = new List { new DashboardModuleShortcutItem() { Label = resourceLoader.GetString("ScreenRuler_ShortDescription"), Shortcut = moduleSettingsRepository.SettingsConfig.Properties.ActivationShortcut.GetKeysList() }, @@ -632,7 +632,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private ObservableCollection GetModuleItemsShortcutGuide() { - ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(new SettingsUtils()); + ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(SettingsUtils.Default); var shortcut = moduleSettingsRepository.SettingsConfig.Properties.UseLegacyPressWinKeyBehavior.Value ? new List { 92 } // Right Windows key code @@ -647,7 +647,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private ObservableCollection GetModuleItemsPowerOCR() { - ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(new SettingsUtils()); + ISettingsRepository moduleSettingsRepository = SettingsRepository.GetInstance(SettingsUtils.Default); var list = new List { new DashboardModuleShortcutItem() { Label = resourceLoader.GetString("PowerOcr_ShortDescription"), Shortcut = moduleSettingsRepository.SettingsConfig.Properties.ActivationShortcut.GetKeysList() }, @@ -662,14 +662,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private void EnvironmentVariablesLaunchClicked(object sender, RoutedEventArgs e) { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; var environmentVariablesViewModel = new EnvironmentVariablesViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage, App.IsElevated); environmentVariablesViewModel.Launch(); } private void HostLaunchClicked(object sender, RoutedEventArgs e) { - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; var hostsViewModel = new HostsViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage, App.IsElevated); hostsViewModel.Launch(); } diff --git a/src/settings-ui/Settings.UI/ViewModels/Flyout/LauncherViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/Flyout/LauncherViewModel.cs index a1db1e2dd0..7adc4bb933 100644 --- a/src/settings-ui/Settings.UI/ViewModels/Flyout/LauncherViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/Flyout/LauncherViewModel.cs @@ -88,12 +88,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { return moduleType switch { - ModuleType.ColorPicker => SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.ActivationShortcut.ToString(), - ModuleType.FancyZones => SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.FancyzonesEditorHotkey.Value.ToString(), - ModuleType.PowerLauncher => SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.OpenPowerLauncher.ToString(), - ModuleType.PowerOCR => SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.ActivationShortcut.ToString(), - ModuleType.Workspaces => SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.Hotkey.Value.ToString(), - ModuleType.MeasureTool => SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.ActivationShortcut.ToString(), + ModuleType.ColorPicker => SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(), + ModuleType.FancyZones => SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.FancyzonesEditorHotkey.Value.ToString(), + ModuleType.PowerLauncher => SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.OpenPowerLauncher.ToString(), + ModuleType.PowerOCR => SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(), + ModuleType.Workspaces => SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.Hotkey.Value.ToString(), + ModuleType.MeasureTool => SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(), ModuleType.ShortcutGuide => GetShortcutGuideToolTip(), _ => string.Empty, }; @@ -111,7 +111,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private string GetShortcutGuideToolTip() { - var shortcutGuideSettings = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig; + var shortcutGuideSettings = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig; return shortcutGuideSettings.Properties.UseLegacyPressWinKeyBehavior.Value ? "Win" : shortcutGuideSettings.Properties.OpenShortcutGuide.ToString(); diff --git a/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs index 7a6a7fc284..25ab4db13a 100644 --- a/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs @@ -1145,7 +1145,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels _settingsBackupMessage = GetResourceString(results.Message) + results.OptionalMessage; // now we do a dry run to get the results for "setting match" - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; var appBasePath = Path.GetDirectoryName(settingsUtils.GetSettingsFilePath()); settingsBackupAndRestoreUtils.BackupSettings(appBasePath, settingsBackupAndRestoreDir, true); diff --git a/src/settings-ui/Settings.UI/ViewModels/KeyboardManagerViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/KeyboardManagerViewModel.cs index 3aa90cbc24..a48742672c 100644 --- a/src/settings-ui/Settings.UI/ViewModels/KeyboardManagerViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/KeyboardManagerViewModel.cs @@ -274,7 +274,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels try { // Check if the experimentation toggle is enabled in the settings - var settingsUtils = new SettingsUtils(); + var settingsUtils = SettingsUtils.Default; bool isExperimentationEnabled = SettingsRepository.GetInstance(settingsUtils).SettingsConfig.EnableExperimentation; // Only read the registry value if the experimentation toggle is enabled diff --git a/src/settings-ui/Settings.UI/ViewModels/LightSwitchViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/LightSwitchViewModel.cs index 621fa91d43..e9e744705f 100644 --- a/src/settings-ui/Settings.UI/ViewModels/LightSwitchViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/LightSwitchViewModel.cs @@ -42,6 +42,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels "Off", "FixedHours", "SunsetToSunrise", + "FollowNightLight", }; _toggleThemeHotkey = _moduleSettings.Properties.ToggleThemeHotkey.Value;