Compare commits

...

6 Commits

Author SHA1 Message Date
Gleb Khmyznikov
53c5e66cce [UI Tests][Light Switch] Fix tests: LS, Hosts, Worspaces, Mouse (#44754)
This pull request refactors the LightSwitch module to introduce a new
shared static library, `LightSwitchLib`, which centralizes theme
management logic and enables code sharing between the service and module
interface. The changes move theme-related code from the service and
module interface into this new library, update project references, and
clean up now-redundant files and includes.

**LightSwitchLib introduction and code deduplication:**

- Added a new static library project, `LightSwitchLib`, containing all
theme management logic (`SetSystemTheme`, `SetAppsTheme`,
`GetCurrentSystemTheme`, `GetCurrentAppsTheme`, `IsNightLightEnabled`)
and related files (`ThemeHelper.cpp`, `ThemeHelper.h`, `pch.h`,
`pch.cpp`). This code was previously duplicated in both the service and
module interface.
(`src/modules/LightSwitch/LightSwitchLib/LightSwitchLib.vcxproj`
[[1]](diffhunk://#diff-c38e95060ad294c9ed5c2bb769616bb52032a4330af7e268ad63d81a99dc1cadR1-R123)
`LightSwitchLib.vcxproj.filters`
[[2]](diffhunk://#diff-fcfc49f1628c274cd9a40aca385e03a1937f9e42958298f36155ad16a267ba9aR1-R33)
`ThemeHelper.cpp`
[[3]](diffhunk://#diff-f5ab83c022406172501172ee88e21294c7aba2a87fb30334d7c4d4fc9d736a56L1-R3)
`ThemeHelper.h`
[[4]](diffhunk://#diff-6609a7fc7abc61d4d0029f0fb605a9f4732511642af6e12851e86c234108169aR1-R10)
`pch.h`
[[5]](diffhunk://#diff-57e4d6ddad1d356a24555bce4d6cbb0d6a93386515254abf95573324454c94c2R1-R5)
`pch.cpp`
[[6]](diffhunk://#diff-87fbf215a559e7833ec06ff32aa7f8109fdf86d92b360fe44302fc16f1784d52R1)

- Updated the solution file and relevant project files to add and
reference `LightSwitchLib` from both `LightSwitchService` and
`LightSwitchModuleInterface`, ensuring both components use the shared
implementation. (`PowerToys.slnx`
[[1]](diffhunk://#diff-40c552fef4118125c3ccd6b156db518acec74b11150b193b31f18a2cc17a531eR668)
`LightSwitchService.vcxproj`
[[2]](diffhunk://#diff-51f54bd015aa96b38ddf4e96134ea542fac4b648566a23c2c86fe91a2b5a6bdaR58)
[[3]](diffhunk://#diff-51f54bd015aa96b38ddf4e96134ea542fac4b648566a23c2c86fe91a2b5a6bdaR113-R115)
`LightSwitchModuleInterface.vcxproj`
[[4]](diffhunk://#diff-72e859ee44b3f0087018e55708e850fb5040c5b8f72449d1cac30e8efb28e2c2R205-R207)
[[5]](diffhunk://#diff-72e859ee44b3f0087018e55708e850fb5040c5b8f72449d1cac30e8efb28e2c2L169-R179)

**Cleanup and removal of redundant code:**

- Removed old theme management code and headers from
`LightSwitchService` and `LightSwitchModuleInterface` now that logic
resides in `LightSwitchLib`. (`ThemeHelper.cpp`
[[1]](diffhunk://#diff-3e2766504c1cf989390508c613b2177cd5de14fb9de46df3b416f95f955338bfL1-L106)
`ThemeHelper.h`
[[2]](diffhunk://#diff-0e8540cace398ec3eebca416ca38d81262b689eca76a004584e686a605b7a242L1-L5)
`LightSwitchService.vcxproj`
[[3]](diffhunk://#diff-51f54bd015aa96b38ddf4e96134ea542fac4b648566a23c2c86fe91a2b5a6bdaL81)
[[4]](diffhunk://#diff-51f54bd015aa96b38ddf4e96134ea542fac4b648566a23c2c86fe91a2b5a6bdaL96)
`LightSwitchModuleInterface.vcxproj`
[[5]](diffhunk://#diff-72e859ee44b3f0087018e55708e850fb5040c5b8f72449d1cac30e8efb28e2c2L190)

- Removed duplicated registry path constants from `SettingsConstants.h`,
as they are now defined in the shared header. (`SettingsConstants.h`
[src/modules/LightSwitch/LightSwitchService/SettingsConstants.hL15-L17](diffhunk://#diff-e74db005ffb8b881a08c4dae1c1ead9dc732928a69cafb4c9e0bae8b86d4e24aL15-L17))

**Module interface improvements:**

- Added `ExportedFunctions.cpp` to the module interface, exposing theme
management functions as exports and using the shared library
implementation. (`ExportedFunctions.cpp`
[[1]](diffhunk://#diff-48acf3b77a8b6ac6fd1129afe1a677b34447ce39454e86ea04f1a1181a23b546R1-R22)
`LightSwitchModuleInterface.vcxproj`
[[2]](diffhunk://#diff-72e859ee44b3f0087018e55708e850fb5040c5b8f72449d1cac30e8efb28e2c2L169-R179)

**Minor test and logging adjustments:**

- Fixed a UI test to use the correct toggle name for the Hosts File
Editor. (`HostsSettingTests.cs`
[src/modules/Hosts/Hosts.UITests/HostsSettingTests.csL116-R116](diffhunk://#diff-3782109c99cd66a2c1b870a83d1f9d9807422479c89e03799b311ef5f13a2098L116-R116))
- Updated a log message to refer to `LightSwitchLib` instead of
`LightSwitchService` for clarity. (`ThemeHelper.cpp`
[src/modules/LightSwitch/LightSwitchLib/ThemeHelper.cppL66-R63](diffhunk://#diff-f5ab83c022406172501172ee88e21294c7aba2a87fb30334d7c4d4fc9d736a56L66-R63))
2026-01-16 13:58:51 -08:00
moooyo
4cde968c9b chore(imageresizer): fix log folder path formatting in App.xaml.cs (#44761)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

Co-authored-by: Yu Leng <yuleng@microsoft.com>
2026-01-16 17:10:41 +08:00
Kai Tao
e148a89288 Powertoys Extension: Trigger latest layout/monitor refresh for fancyzone in cmdpal extension (#44756)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Best effort trigger refresh for the monitor/layout refresh for
fancyzones, user may experience error for first time run but will get
correct result in next visit.

This bring better experience than the always-stale-state, while still
remain performant command.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Validated locally that second time visit will bring correct monitor when
plug/unplug monitor
2026-01-16 10:40:32 +08:00
Niels Laute
1dddf9fa2c "What's new" improvements (#44638)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

TO DO: Upgrade to the latest version of MarkdownTextBlock:
https://github.com/CommunityToolkit/Labs-Windows/pull/771

This PR introduces the following changes:
**Removed the custom titlebars on the OOBE Window, and replaced it with
the inbox WinUI `TitleBar` control.**

**New "What's new" experience following the VS Code release notes
experience**
- Created a new SCOOBE Windows that is a standalone window to better
visualize release notes.
- Adding a nav menu on the left to easily switch between release notes,
instead of a long page.
- Point releases are combined with the latest main release.. e.g. 0.96.1
is rendered above 0.96.0.
- The 'hero image' on main release notes will automatically be rendered
at the top of the page.
- Improved markdown styling for better readability.
- Pull requests links can now be clicked.
- Upgraded `CommunityToolkit.Labs.MarkdownTextblock` to the latest
version as it includes much needed bugfixes.

<img width="1234" height="819" alt="image"
src="https://github.com/user-attachments/assets/447b3136-306b-4f24-bc7a-c022a99e8e51"
/>

Note: the blurry image shown above will be replaced in new releases by
an image that fits the right dimensions.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
2026-01-15 16:37:08 +08:00
Tokenicrat 词元
0d59b9f790 Chore: Fix broken links in README.md (#44658)
The repository README.md has several formatting issues in [Installation
section](https://github.com/microsoft/PowerToys#-installation),
resulting in invalid link references. It's mainly because Markdown
references won't be rendered in HTML blocks.

This trivial PR just fixes that, nothing fancy. Have a nice day!

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
2026-01-15 09:21:12 +01:00
Shawn Yuan
e314485e85 Improve module enable/disable IPC and sorting reliability (#44734)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

- Refactored the runner logic for handling individual module
enable/disable updates. Instead of receiving the entire settings.json
via IPC, it now processes only single-module state updates, which avoids
race conditions and fixes a bug where modules could end up being
skipped.
- Fixed an issue where the sort order option could be deselected — it is
now enforced as a mutually exclusive choice.
- Fixed a potential race condition when updating the AppList control’s
sorting.


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #44697
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-15 15:29:46 +08:00
54 changed files with 1462 additions and 975 deletions

View File

@@ -26,7 +26,7 @@
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.251002-build.2316" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260107-build.2454" />
<PackageVersion Include="ControlzEx" Version="6.0.0" />
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />

View File

@@ -665,6 +665,7 @@
</Project>
</Folder>
<Folder Name="/modules/LightSwitch/">
<Project Path="src/modules/LightSwitch/LightSwitchLib/LightSwitchLib.vcxproj" Id="79267138-2895-4346-9021-21408d65379f" />
<Project Path="src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj" Id="38177d56-6ad1-4adf-88c9-2843a7932166" />
<Project Path="src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj" Id="08e71c67-6a7e-4ca1-b04e-2fb336410bac" />
</Folder>

View File

@@ -48,7 +48,7 @@ But to get started quickly, choose one of the installation methods below:
<details open>
<summary><strong>Download .exe from GitHub</strong></summary>
<br/>
Go to the [PowerToys GitHub releases][github-release-link], click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
Go to the <a href="https://aka.ms/installPowerToys">PowerToys GitHub releases</a>, click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
<!-- items that need to be updated release to release -->
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.97%22
@@ -83,7 +83,7 @@ You can easily install PowerToys from the Microsoft Store:
<details>
<summary><strong>WinGet</strong></summary>
<br/>
Download PowerToys from [WinGet][winget-link]. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
Download PowerToys from <a href="https://github.com/microsoft/winget-cli#installing-the-client">WinGet</a>. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
*User scope installer [default]*
```powershell
@@ -99,7 +99,7 @@ winget install --scope machine Microsoft.PowerToys -s winget
<details>
<summary><strong>Other methods</strong></summary>
<br/>
There are [community driven install methods](./doc/unofficialInstallMethods.md) such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
There are <a href="https://learn.microsoft.com/windows/powertoys/install#community-driven-install-tools">community driven install methods</a> such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
</details>
## ✨ What's new

View File

@@ -113,7 +113,7 @@ namespace Hosts.UITests
this.Find<NavigationViewItem>("Hosts File Editor").Click();
this.Find<ToggleSwitch>("Enable Hosts File Editor").Toggle(true);
this.Find<ToggleSwitch>("Hosts File Editor").Toggle(true);
this.Find<ToggleSwitch>("Launch as administrator").Toggle(launchAsAdmin);
this.Find<ToggleSwitch>("Show a warning at startup").Toggle(showWarning);

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{79267138-2895-4346-9021-21408d65379f}</ProjectGuid>
<RootNamespace>LightSwitchLib</RootNamespace>
<WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
<ProjectName>LightSwitchLib</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<AdditionalIncludeDirectories>
./;
..\..\..\common;
..\..\..\common\logger;
..\..\..\common\utils;
..\..\..\..\deps\spdlog\include;
%(AdditionalIncludeDirectories)
</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="ThemeHelper.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="ThemeHelper.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
</Project>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ThemeHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ThemeHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>

View File

@@ -1,9 +1,6 @@
#include <windows.h>
#include <logger/logger_settings.h>
#include <logger/logger.h>
#include <utils/logger_helper.h>
#include "pch.h"
#include "ThemeHelper.h"
#include <SettingsConstants.h>
#include <logger/logger.h>
// Controls changing the themes.
@@ -63,7 +60,7 @@ void SetSystemTheme(bool mode)
if (mode) // if are changing to light mode
{
ResetColorPrevalence();
Logger::info(L"[LightSwitchService] Reset ColorPrevalence to default when switching to light mode.");
Logger::info(L"[LightSwitchLib] Reset ColorPrevalence to default when switching to light mode.");
}
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
@@ -136,4 +133,4 @@ bool IsNightLightEnabled()
RegCloseKey(hKey);
return data[23] == 0x10 && data[24] == 0x00;
}
}

View File

@@ -0,0 +1,10 @@
#pragma once
inline constexpr wchar_t PERSONALIZATION_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
inline 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";
void SetSystemTheme(bool isLight);
void SetAppsTheme(bool isLight);
bool GetCurrentSystemTheme();
bool GetCurrentAppsTheme();
bool IsNightLightEnabled();

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,5 @@
#pragma once
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
#include <windows.h>
#include <vector>

View File

@@ -0,0 +1,22 @@
#include "pch.h"
#include "ThemeHelper.h"
extern "C" __declspec(dllexport) void __cdecl LightSwitch_SetSystemTheme(bool isLight)
{
SetSystemTheme(isLight);
}
extern "C" __declspec(dllexport) void __cdecl LightSwitch_SetAppsTheme(bool isLight)
{
SetAppsTheme(isLight);
}
extern "C" __declspec(dllexport) bool __cdecl LightSwitch_GetCurrentSystemTheme()
{
return GetCurrentSystemTheme();
}
extern "C" __declspec(dllexport) bool __cdecl LightSwitch_GetCurrentAppsTheme()
{
return GetCurrentAppsTheme();
}

View File

@@ -166,17 +166,17 @@
</ItemDefinitionGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>..\LightSwitchLib;..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="ThemeHelper.h" />
<ClInclude Include="trace.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="ExportedFunctions.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
@@ -187,7 +187,6 @@
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">pch.h</PrecompiledHeaderFile>
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">pch.h</PrecompiledHeaderFile>
</ClCompile>
<ClCompile Include="ThemeHelper.cpp" />
<ClCompile Include="trace.cpp" />
</ItemGroup>
<ItemGroup>
@@ -203,6 +202,9 @@
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
<ProjectReference Include="..\LightSwitchLib\LightSwitchLib.vcxproj">
<Project>{79267138-2895-4346-9021-21408d65379f}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@@ -1,106 +0,0 @@
#include "pch.h"
#include <windows.h>
#include "ThemeHelper.h"
// Controls changing the themes.
static void ResetColorPrevalence()
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = 0; // back to default value
RegSetValueEx(hKey, L"ColorPrevalence", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(hKey);
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_DWMCOLORIZATIONCOLORCHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
void SetAppsTheme(bool mode)
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = mode;
RegSetValueEx(hKey, L"AppsUseLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(hKey);
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
void SetSystemTheme(bool mode)
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = mode;
RegSetValueEx(hKey, L"SystemUsesLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(hKey);
if (mode) // if are changing to light mode
{
ResetColorPrevalence();
}
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
bool GetCurrentSystemTheme()
{
HKEY hKey;
DWORD value = 1; // default = light
DWORD size = sizeof(value);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
{
RegQueryValueEx(hKey, L"SystemUsesLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size);
RegCloseKey(hKey);
}
return value == 1; // true = light, false = dark
}
bool GetCurrentAppsTheme()
{
HKEY hKey;
DWORD value = 1;
DWORD size = sizeof(value);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
{
RegQueryValueEx(hKey, L"AppsUseLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size);
RegCloseKey(hKey);
}
return value == 1; // true = light, false = dark
}

View File

@@ -1,5 +0,0 @@
#pragma once
void SetSystemTheme(bool dark);
void SetAppsTheme(bool dark);
bool GetCurrentSystemTheme();
bool GetCurrentAppsTheme();

View File

@@ -55,6 +55,7 @@
<PreprocessorDefinitions>%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>
./../;
..\LightSwitchLib;
..\..\..\common;
..\..\..\common\logger;
..\..\..\common\utils;
@@ -78,7 +79,6 @@
<ClCompile Include="LightSwitchStateManager.cpp" />
<ClCompile Include="NightLightRegistryObserver.cpp" />
<ClCompile Include="SettingsConstants.cpp" />
<ClCompile Include="ThemeHelper.cpp" />
<ClCompile Include="ThemeScheduler.cpp" />
<ClCompile Include="trace.cpp" />
<ClCompile Include="WinHookEventIDs.cpp" />
@@ -93,7 +93,6 @@
<ClInclude Include="NightLightRegistryObserver.h" />
<ClInclude Include="SettingsConstants.h" />
<ClInclude Include="SettingsObserver.h" />
<ClInclude Include="ThemeHelper.h" />
<ClInclude Include="ThemeScheduler.h" />
<ClInclude Include="trace.h" />
<ClInclude Include="WinHookEventIDs.h" />
@@ -111,6 +110,9 @@
<ProjectReference Include="..\..\..\common\Telemetry\EtwTrace\EtwTrace.vcxproj">
<Project>{8f021b46-362b-485c-bfba-ccf83e820cbd}</Project>
</ProjectReference>
<ProjectReference Include="..\LightSwitchLib\LightSwitchLib.vcxproj">
<Project>{79267138-2895-4346-9021-21408d65379f}</Project>
</ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
@@ -118,4 +120,4 @@
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
</Project>
</Project>

View File

@@ -12,6 +12,3 @@ enum class SettingId
ChangeSystem,
ChangeApps
};
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";

View File

@@ -1,6 +0,0 @@
#pragma once
void SetSystemTheme(bool dark);
void SetAppsTheme(bool dark);
bool GetCurrentSystemTheme();
bool GetCurrentAppsTheme();
bool IsNightLightEnabled();

View File

@@ -19,4 +19,9 @@
<PackageReference Include="MSTest" />
<ProjectReference Include="..\..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
</ItemGroup>
<Target Name="CopyNativeDll" AfterTargets="Build">
<Copy SourceFiles="$(SolutionDir)$(Platform)\$(Configuration)\PowerToys.LightSwitchModuleInterface.dll" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" />
<Copy SourceFiles="$(SolutionDir)$(Platform)\$(Configuration)\LightSwitchLib\LightSwitchLib.lib" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" Condition="Exists('$(SolutionDir)$(Platform)\$(Configuration)\LightSwitchLib\LightSwitchLib.lib')" ContinueOnError="true" />
</Target>
</Project>

View File

@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.PowerToys.UITest;
@@ -17,6 +18,20 @@ namespace LightSwitch.UITests
{
private static readonly string[] ShortcutSeparators = { " + ", "+", " " };
[DllImport("PowerToys.LightSwitchModuleInterface.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void LightSwitch_SetSystemTheme(bool isLight);
[DllImport("PowerToys.LightSwitchModuleInterface.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void LightSwitch_SetAppsTheme(bool isLight);
[DllImport("PowerToys.LightSwitchModuleInterface.dll", CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
private static extern bool LightSwitch_GetCurrentSystemTheme();
[DllImport("PowerToys.LightSwitchModuleInterface.dll", CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
private static extern bool LightSwitch_GetCurrentAppsTheme();
/// <summary>
/// Performs common test initialization: navigate to settings, enable toggle, verify shortcut
/// </summary>
@@ -127,8 +142,7 @@ namespace LightSwitch.UITests
/// <param name="testBase">The test base instance</param>
public static void CleanupTest(UITestBase testBase)
{
// TODO: Make sure the task kills?
// CloseLightSwitch(testBase);
CloseLightSwitch(testBase);
// Ensure we're attached to settings after cleanup
try
@@ -141,6 +155,51 @@ namespace LightSwitch.UITests
}
}
/// <summary>
/// Switch to white/light theme for both system and apps
/// </summary>
/// <param name="testBase">The test base instance</param>
public static void CloseLightSwitch(UITestBase testBase)
{
// Kill LightSwitch process before setting themes
KillLightSwitchProcess();
// Set both themes to light (white)
SetSystemTheme(true);
SetAppsTheme(true);
}
/// <summary>
/// Kill the LightSwitch service process if it's running
/// </summary>
private static void KillLightSwitchProcess()
{
try
{
var processes = System.Diagnostics.Process.GetProcessesByName("PowerToys.LightSwitchService");
foreach (var process in processes)
{
try
{
process.Kill();
process.WaitForExit(2000);
}
catch
{
// Ignore errors killing individual processes
}
finally
{
process.Dispose();
}
}
}
catch
{
// Ignore errors enumerating processes
}
}
/// <summary>
/// Perform a update time test operation
/// </summary>
@@ -408,24 +467,22 @@ namespace LightSwitch.UITests
/* Helpers */
private static int GetSystemTheme()
{
using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
if (key is null)
{
return 1;
}
return (int)key.GetValue("SystemUsesLightTheme", 1);
return LightSwitch_GetCurrentSystemTheme() ? 1 : 0;
}
private static int GetAppsTheme()
{
using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
if (key is null)
{
return 1;
}
return LightSwitch_GetCurrentAppsTheme() ? 1 : 0;
}
return (int)key.GetValue("AppsUseLightTheme", 1);
private static void SetSystemTheme(bool isLight)
{
LightSwitch_SetSystemTheme(isLight);
}
private static void SetAppsTheme(bool isLight)
{
LightSwitch_SetAppsTheme(isLight);
}
private static string GetHelpTextValue(string helpText, string key)

View File

@@ -456,10 +456,11 @@ namespace MouseUtils.UITests
var groupAppearanceBehavior = foundCustom.Find(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseAppearanceBehavior));
if (groupAppearanceBehavior != null)
{
// groupAppearanceBehavior.Click();
if (foundCustom.FindAll(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseOverlayOpacity)).Count == 0)
var expandState = groupAppearanceBehavior.Selected;
if (!expandState)
{
groupAppearanceBehavior.Click();
Task.Delay(500).Wait();
}
// Set the BackGround color
@@ -541,15 +542,6 @@ namespace MouseUtils.UITests
Task.Delay(500).Wait();
spotlightColorButton.Click(false, 500, 1500);
// Set the overlay opacity to overlayOpacity%
var overlayOpacitySlider = foundCustom.Find<Slider>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseOverlayOpacity));
Assert.IsNotNull(overlayOpacitySlider);
Assert.IsNotNull(settings.OverlayOpacity);
int overlayOpacityValue = int.Parse(settings.OverlayOpacity, CultureInfo.InvariantCulture);
overlayOpacitySlider.QuickSetValue(overlayOpacityValue);
Assert.AreEqual(settings.OverlayOpacity, overlayOpacitySlider.Text);
Task.Delay(1000).Wait();
// Set the Fade Initial zoom to 0
var spotlightInitialZoomSlider = foundCustom.Find<Slider>(By.AccessibilityId(MouseUtilsSettings.AccessibilityIds.FindMyMouseSpotlightZoom));
Assert.IsNotNull(spotlightInitialZoomSlider);
@@ -592,7 +584,7 @@ namespace MouseUtils.UITests
// Assert.IsNull(animationDisabledWarning);
if (foundElements.Count != 0)
{
var openSettingsLink = foundCustom.Find<Element>("Open settings");
var openSettingsLink = foundCustom.Find<Element>("Open animation settings");
Assert.IsNotNull(openSettingsLink);
openSettingsLink.Click(false, 500, 3000);

View File

@@ -32,7 +32,6 @@ namespace MouseUtils.UITests
public const string FindMyMouseExcludedApps = "MouseUtils_FindMyMouseExcludedAppsId";
public const string FindMyMouseBackgroundColor = "MouseUtils_FindMyMouseBackgroundColorId";
public const string FindMyMouseSpotlightColor = "MouseUtils_FindMyMouseSpotlightColorId";
public const string FindMyMouseOverlayOpacity = "MouseUtils_FindMyMouseOverlayOpacityId";
public const string FindMyMouseSpotlightZoom = "MouseUtils_FindMyMouseSpotlightZoomId";
public const string FindMyMouseSpotlightRadius = "MouseUtils_FindMyMouseSpotlightRadiusId";
public const string FindMyMouseAnimationDuration = "MouseUtils_FindMyMouseAnimationDurationId";
@@ -72,10 +71,10 @@ namespace MouseUtils.UITests
private static readonly Dictionary<MouseUtils, string> MouseUtilUIToggleMap = new()
{
[MouseUtils.MouseHighlighter] = @"Enable Mouse Highlighter",
[MouseUtils.FindMyMouse] = @"Enable Find My Mouse",
[MouseUtils.MousePointerCrosshairs] = @"Enable Mouse Pointer Crosshairs",
[MouseUtils.MouseJump] = @"Enable Mouse Jump",
[MouseUtils.MouseHighlighter] = @"Mouse Highlighter",
[MouseUtils.FindMyMouse] = @"Find My Mouse",
[MouseUtils.MousePointerCrosshairs] = @"Mouse Pointer Crosshairs",
[MouseUtils.MouseJump] = @"Mouse Jump",
};
public static string GetMouseUtilUIName(MouseUtils element)

View File

@@ -57,7 +57,7 @@ public class WorkspacesSettingsTests : UITestBase
GoToSettingsPageAndEnable();
// Find the enable toggle
var enableToggle = Find<ToggleSwitch>("Enable Workspaces");
var enableToggle = Find<ToggleSwitch>("Workspaces");
Assert.IsNotNull(enableToggle, "Enable Workspaces toggle should exist");
Assert.IsTrue(enableToggle.IsOn, "Enable Workspaces toggle should be in the 'on' state");
@@ -80,7 +80,7 @@ public class WorkspacesSettingsTests : UITestBase
public void TestLaunchEditorByActivationShortcut()
{
// Ensure module is enabled
var enableToggle = Find<ToggleSwitch>("Enable Workspaces");
var enableToggle = Find<ToggleSwitch>("Workspaces");
if (!enableToggle.IsOn)
{
enableToggle.Click();
@@ -109,7 +109,7 @@ public class WorkspacesSettingsTests : UITestBase
public void TestDisabledModuleDoesNotLaunchByShortcut()
{
// Disable the module
var enableToggle = Find<ToggleSwitch>("Enable Workspaces");
var enableToggle = Find<ToggleSwitch>("Workspaces");
if (enableToggle.IsOn)
{
enableToggle.Click();
@@ -131,7 +131,7 @@ public class WorkspacesSettingsTests : UITestBase
RestartScopeExe();
NavigateToWorkspacesSettings();
enableToggle = Find<ToggleSwitch>("Enable Workspaces");
enableToggle = Find<ToggleSwitch>("Workspaces");
if (!enableToggle.IsOn)
{
enableToggle.Click();
@@ -174,7 +174,7 @@ public class WorkspacesSettingsTests : UITestBase
this.Find<NavigationViewItem>("Workspaces").Click();
var enableButton = this.Find<ToggleSwitch>("Enable Workspaces");
var enableButton = this.Find<ToggleSwitch>("Workspaces");
Assert.IsNotNull(enableButton, "Enable Workspaces toggle should exist");
if (!enableButton.IsOn)

View File

@@ -41,16 +41,13 @@ internal static class FancyZonesDataService
try
{
if (!File.Exists(FZPaths.EditorParameters))
{
error = Resources.FancyZones_MonitorDataNotFound;
Logger.LogWarning($"TryGetMonitors: File not found. Path={FZPaths.EditorParameters}");
return false;
}
Logger.LogInfo($"TryGetMonitors: File exists, reading...");
var editorParams = FancyZonesDataIO.ReadEditorParameters();
Logger.LogInfo($"TryGetMonitors: ReadEditorParameters returned. Monitors={editorParams.Monitors?.Count ?? -1}");
// Request FancyZones to save current monitor configuration.
// The editor-parameters.json file is only written when:
// 1. Opening the FancyZones Editor
// 2. Receiving the WM_PRIV_SAVE_EDITOR_PARAMETERS message
// Without this, monitor changes (plug/unplug) won't be reflected in the file.
var editorParams = ReadEditorParametersWithRefresh();
Logger.LogInfo($"TryGetMonitors: ReadEditorParametersWithRefreshWithRefresh returned. Monitors={editorParams.Monitors?.Count ?? -1}");
var editorMonitors = editorParams.Monitors;
if (editorMonitors is null || editorMonitors.Count == 0)
@@ -74,6 +71,23 @@ internal static class FancyZonesDataService
}
}
/// <summary>
/// Requests FancyZones to save the current monitor configuration and reads the file.
/// This is a best-effort approach for performance: we send the save request and immediately
/// read the file without waiting. If the file hasn't been updated yet, the next call will
/// see the updated data since FancyZones processes the message asynchronously.
/// </summary>
private static EditorParameters.ParamsWrapper ReadEditorParametersWithRefresh()
{
// Request FancyZones to save the current monitor configuration.
// This is fire-and-forget for performance - we don't wait for the save to complete.
// If this is the first call after a monitor change, we may read stale data, but the
// next call will see the updated file since FancyZones will have processed the message.
FancyZonesNotifier.NotifySaveEditorParameters();
return FancyZonesDataIO.ReadEditorParameters();
}
public static IReadOnlyList<FancyZonesLayoutDescriptor> GetLayouts()
{
Logger.LogInfo($"GetLayouts: Starting. LayoutTemplatesPath={FZPaths.LayoutTemplates} CustomLayoutsPath={FZPaths.CustomLayouts}");

View File

@@ -10,13 +10,25 @@ namespace PowerToysExtension.Helpers;
internal static class FancyZonesNotifier
{
private const string AppliedLayoutsFileUpdateMessage = "{2ef2c8a7-e0d5-4f31-9ede-52aade2d284d}";
private const string SaveEditorParametersMessage = "{d8f9c0e3-5d77-4e83-8a4f-7c704c2bfb4a}";
private static readonly uint WmPrivAppliedLayoutsFileUpdate = RegisterWindowMessageW(AppliedLayoutsFileUpdateMessage);
private static readonly uint WmPrivSaveEditorParameters = RegisterWindowMessageW(SaveEditorParametersMessage);
public static void NotifyAppliedLayoutsChanged()
{
_ = PostMessageW(new IntPtr(0xFFFF), WmPrivAppliedLayoutsFileUpdate, UIntPtr.Zero, IntPtr.Zero);
}
/// <summary>
/// Notifies FancyZones to save the current monitor configuration to editor-parameters.json.
/// This is needed because FancyZones only writes this file when opening the editor or when explicitly requested.
/// </summary>
public static void NotifySaveEditorParameters()
{
_ = PostMessageW(new IntPtr(0xFFFF), WmPrivSaveEditorParameters, UIntPtr.Zero, IntPtr.Zero);
}
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern uint RegisterWindowMessageW(string lpString);

View File

@@ -584,7 +584,7 @@ namespace UITests_FancyZones
}
windowingElement.Find<Element>("FancyZones").Click();
this.Find<ToggleSwitch>("Enable FancyZones").Toggle(true);
Find<ToggleSwitch>(By.AccessibilityId("EnableFancyZonesToggleSwitch")).Toggle(true);
if (isMax == true)
{
this.Find<Button>("Maximize").Click(); // maximize the window
@@ -661,7 +661,7 @@ namespace UITests_FancyZones
this.Find<NavigationViewItem>("Hosts File Editor").Click();
Task.Delay(1000).Wait();
this.Find<ToggleSwitch>("Enable Hosts File Editor").Toggle(true);
this.Find<ToggleSwitch>("Hosts File Editor").Toggle(true);
this.Find<ToggleSwitch>("Launch as administrator").Toggle(launchAsAdmin);
this.Find<ToggleSwitch>("Show a warning at startup").Toggle(showWarning);

View File

@@ -261,7 +261,7 @@ namespace UITests_FancyZones
}
this.Find<NavigationViewItem>("FancyZones").Click();
this.Find<ToggleSwitch>("Enable FancyZones").Toggle(true);
Find<ToggleSwitch>(By.AccessibilityId("EnableFancyZonesToggleSwitch")).Toggle(true);
this.Session.SetMainWindowSize(WindowSize.Large);
// fixed settings
@@ -322,7 +322,7 @@ namespace UITests_FancyZones
this.Find<NavigationViewItem>("Hosts File Editor").Click();
Task.Delay(1000).Wait();
this.Find<ToggleSwitch>("Enable Hosts File Editor").Toggle(true);
this.Find<ToggleSwitch>("Hosts File Editor").Toggle(true);
this.Find<ToggleSwitch>("Open as administrator").Toggle(launchAsAdmin);
this.Find<ToggleSwitch>("Show a warning at startup").Toggle(showWarning);

View File

@@ -20,7 +20,7 @@ namespace ImageResizer
{
public partial class App : Application, IDisposable
{
private const string LogSubFolder = "\\ImageResizer\\Logs";
private const string LogSubFolder = "\\Image Resizer\\Logs";
/// <summary>
/// Gets cached AI availability state, checked at app startup.

View File

@@ -193,6 +193,102 @@ GeneralSettings get_general_settings()
return settings;
}
void apply_module_status_update(const json::JsonObject& module_config, bool save)
{
Logger::info(L"apply_module_status_update: {}", std::wstring{ module_config.ToString() });
// Expected format: {"ModuleName": true/false} - only one module per update
auto iter = module_config.First();
if (!iter.HasCurrent())
{
Logger::warn(L"apply_module_status_update: Empty module config");
return;
}
const auto& element = iter.Current();
const auto value = element.Value();
if (value.ValueType() != json::JsonValueType::Boolean)
{
Logger::warn(L"apply_module_status_update: Invalid value type for module status");
return;
}
const std::wstring name{ element.Key().c_str() };
if (modules().find(name) == modules().end())
{
Logger::warn(L"apply_module_status_update: Module {} not found", name);
return;
}
PowertoyModule& powertoy = modules().at(name);
const bool module_inst_enabled = powertoy->is_enabled();
bool target_enabled = value.GetBoolean();
auto gpo_rule = powertoy->gpo_policy_enabled_configuration();
if (gpo_rule == powertoys_gpo::gpo_rule_configured_enabled || gpo_rule == powertoys_gpo::gpo_rule_configured_disabled)
{
// Apply the GPO Rule.
target_enabled = gpo_rule == powertoys_gpo::gpo_rule_configured_enabled;
}
if (module_inst_enabled == target_enabled)
{
Logger::info(L"apply_module_status_update: Module {} already in target state {}", name, target_enabled);
return;
}
if (target_enabled)
{
Logger::info(L"apply_module_status_update: Enabling powertoy {}", name);
powertoy->enable();
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
hkmng.EnableHotkeyByModule(name);
// Trigger AI capability detection when ImageResizer is enabled
if (name == L"Image Resizer")
{
Logger::info(L"ImageResizer enabled, triggering AI capability detection");
DetectAiCapabilitiesAsync(true); // Skip settings check since we know it's being enabled
}
}
else
{
Logger::info(L"apply_module_status_update: Disabling powertoy {}", name);
powertoy->disable();
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
hkmng.DisableHotkeyByModule(name);
}
// Sync the hotkey state with the module state, so it can be removed for disabled modules.
powertoy.UpdateHotkeyEx();
if (save)
{
// Load existing settings and only update the specific module's enabled state
json::JsonObject current_settings = PTSettingsHelper::load_general_settings();
json::JsonObject enabled;
if (current_settings.HasKey(L"enabled"))
{
enabled = current_settings.GetNamedObject(L"enabled");
}
// Check if the saved state is different from the requested state
bool current_saved = enabled.HasKey(name) ? enabled.GetNamedBoolean(name, true) : true;
if (current_saved != target_enabled)
{
// Update only this module's enabled state
enabled.SetNamedValue(name, json::value(target_enabled));
current_settings.SetNamedValue(L"enabled", enabled);
PTSettingsHelper::save_general_settings(current_settings);
GeneralSettings settings_for_trace = get_general_settings();
Trace::SettingsChanged(settings_for_trace);
}
}
}
void apply_general_settings(const json::JsonObject& general_configs, bool save)
{
std::wstring old_settings_json_string;

View File

@@ -38,4 +38,5 @@ struct GeneralSettings
json::JsonObject load_general_settings();
GeneralSettings get_general_settings();
void apply_general_settings(const json::JsonObject& general_configs, bool save = true);
void apply_module_status_update(const json::JsonObject& module_config, bool save = true);
void start_enabled_powertoys();

View File

@@ -215,6 +215,12 @@ void dispatch_received_json(const std::wstring& json_to_parse)
// current_settings_ipc->send(settings_string);
// }
}
else if (name == L"module_status")
{
// Handle single module enable/disable update
// Expected format: {"module_status": {"ModuleName": true/false}}
apply_module_status_update(value.GetObjectW());
}
else if (name == L"powertoys")
{
dispatch_json_config_to_modules(value.GetObjectW());

View File

@@ -51,6 +51,7 @@ public sealed partial class AppsListPage : Page
if (ViewModel != null)
{
ViewModel.DashboardSortOrder = DashboardSortOrder.Alphabetical;
((ToggleMenuFlyoutItem)sender).IsChecked = true;
}
}
@@ -59,6 +60,7 @@ public sealed partial class AppsListPage : Page
if (ViewModel != null)
{
ViewModel.DashboardSortOrder = DashboardSortOrder.ByStatus;
((ToggleMenuFlyoutItem)sender).IsChecked = true;
}
}
}

View File

@@ -3,7 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
namespace Microsoft.PowerToys.QuickAccess.Services;
@@ -19,10 +21,10 @@ public interface IQuickAccessCoordinator
Task<bool> ShowDocumentationAsync();
void NotifyUserSettingsInteraction();
bool UpdateModuleEnabled(ModuleType moduleType, bool isEnabled);
void SendSortOrderUpdate(GeneralSettings generalSettings);
void ReportBug();
void OnModuleLaunched(ModuleType moduleType);

View File

@@ -55,37 +55,8 @@ internal sealed class QuickAccessCoordinator : IQuickAccessCoordinator, IDisposa
return Task.FromResult(false);
}
public void NotifyUserSettingsInteraction()
{
Logger.LogDebug("QuickAccessCoordinator.NotifyUserSettingsInteraction invoked.");
}
public bool UpdateModuleEnabled(ModuleType moduleType, bool isEnabled)
{
GeneralSettings? updatedSettings = null;
lock (_generalSettingsLock)
{
var repository = SettingsRepository<GeneralSettings>.GetInstance(_settingsUtils);
var generalSettings = repository.SettingsConfig;
var current = ModuleHelper.GetIsModuleEnabled(generalSettings, moduleType);
if (current == isEnabled)
{
return false;
}
ModuleHelper.SetIsModuleEnabled(generalSettings, moduleType, isEnabled);
_settingsUtils.SaveSettings(generalSettings.ToJsonString());
Logger.LogInfo($"QuickAccess updated module '{moduleType}' enabled state to {isEnabled}.");
updatedSettings = generalSettings;
}
if (updatedSettings != null)
{
SendGeneralSettingsUpdate(updatedSettings);
}
return true;
}
=> TrySendIpcMessage($"{{\"module_status\": {{\"{ModuleHelper.GetModuleKey(moduleType)}\": {isEnabled.ToString().ToLowerInvariant()}}}}}", "module status update");
public void ReportBug()
{
@@ -131,20 +102,10 @@ internal sealed class QuickAccessCoordinator : IQuickAccessCoordinator, IDisposa
Logger.LogDebug($"QuickAccessCoordinator received IPC payload: {message}");
}
private void SendGeneralSettingsUpdate(GeneralSettings updatedSettings)
public void SendSortOrderUpdate(GeneralSettings generalSettings)
{
string payload;
try
{
payload = new OutGoingGeneralSettings(updatedSettings).ToString();
}
catch (Exception ex)
{
Logger.LogError("QuickAccessCoordinator: failed to serialize general settings payload.", ex);
return;
}
TrySendIpcMessage(payload, "general settings update");
var outgoing = new OutGoingGeneralSettings(generalSettings);
TrySendIpcMessage(outgoing.ToString(), "sort order update");
}
private bool TrySendIpcMessage(string payload, string operationDescription)

View File

@@ -22,6 +22,7 @@ namespace Microsoft.PowerToys.QuickAccess.ViewModels;
public sealed class AllAppsViewModel : Observable
{
private readonly object _sortLock = new object();
private readonly IQuickAccessCoordinator _coordinator;
private readonly ISettingsRepository<GeneralSettings> _settingsRepository;
private readonly SettingsUtils _settingsUtils;
@@ -30,6 +31,9 @@ public sealed class AllAppsViewModel : Observable
private readonly List<FlyoutMenuItem> _allFlyoutMenuItems = new();
private GeneralSettings _generalSettings;
// Flag to prevent toggle operations during sorting to avoid race conditions.
private bool _isSorting;
public ObservableCollection<FlyoutMenuItem> FlyoutMenuItems { get; }
public DashboardSortOrder DashboardSortOrder
@@ -40,9 +44,9 @@ public sealed class AllAppsViewModel : Observable
if (_generalSettings.DashboardSortOrder != value)
{
_generalSettings.DashboardSortOrder = value;
_settingsUtils.SaveSettings(_generalSettings.ToJsonString(), _generalSettings.GetModuleName());
_coordinator.SendSortOrderUpdate(_generalSettings);
OnPropertyChanged();
RefreshFlyoutMenuItems();
SortFlyoutMenuItems();
}
}
}
@@ -54,7 +58,6 @@ public sealed class AllAppsViewModel : Observable
_settingsUtils = SettingsUtils.Default;
_settingsRepository = SettingsRepository<GeneralSettings>.GetInstance(_settingsUtils);
_generalSettings = _settingsRepository.SettingsConfig;
_generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
_settingsRepository.SettingsChanged += OnSettingsChanged;
_resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
@@ -87,7 +90,6 @@ public sealed class AllAppsViewModel : Observable
_dispatcherQueue.TryEnqueue(() =>
{
_generalSettings = newSettings;
_generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
OnPropertyChanged(nameof(DashboardSortOrder));
RefreshFlyoutMenuItems();
});
@@ -120,30 +122,55 @@ public sealed class AllAppsViewModel : Observable
}
}
var sortedItems = DashboardSortOrder switch
{
DashboardSortOrder.ByStatus => _allFlyoutMenuItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label).ToList(),
_ => _allFlyoutMenuItems.OrderBy(x => x.Label).ToList(),
};
SortFlyoutMenuItems();
}
if (FlyoutMenuItems.Count == 0)
private void SortFlyoutMenuItems()
{
if (_isSorting)
{
foreach (var item in sortedItems)
{
FlyoutMenuItems.Add(item);
}
return;
}
for (int i = 0; i < sortedItems.Count; i++)
lock (_sortLock)
{
var item = sortedItems[i];
var oldIndex = FlyoutMenuItems.IndexOf(item);
if (oldIndex != -1 && oldIndex != i)
_isSorting = true;
try
{
FlyoutMenuItems.Move(oldIndex, i);
var sortedItems = DashboardSortOrder switch
{
DashboardSortOrder.ByStatus => _allFlyoutMenuItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label).ToList(),
_ => _allFlyoutMenuItems.OrderBy(x => x.Label).ToList(),
};
if (FlyoutMenuItems.Count == 0)
{
foreach (var item in sortedItems)
{
FlyoutMenuItems.Add(item);
}
return;
}
for (int i = 0; i < sortedItems.Count; i++)
{
var item = sortedItems[i];
var oldIndex = FlyoutMenuItems.IndexOf(item);
if (oldIndex != -1 && oldIndex != i)
{
FlyoutMenuItems.Move(oldIndex, i);
}
}
}
finally
{
// Use dispatcher to reset flag after UI updates complete
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
_isSorting = false;
});
}
}
}
@@ -151,17 +178,17 @@ public sealed class AllAppsViewModel : Observable
private void EnabledChangedOnUI(ModuleListItem item)
{
var flyoutItem = (FlyoutMenuItem)item;
if (_coordinator.UpdateModuleEnabled(flyoutItem.Tag, flyoutItem.IsEnabled))
var isEnabled = flyoutItem.IsEnabled;
// Ignore toggle operations during sorting to prevent race conditions.
// Revert the toggle state since UI already changed due to TwoWay binding.
if (_isSorting)
{
_coordinator.NotifyUserSettingsInteraction();
// Trigger re-sort immediately when status changes on UI
RefreshFlyoutMenuItems();
flyoutItem.UpdateStatus(!isEnabled);
return;
}
}
private void ModuleEnabledChangedOnSettingsPage()
{
RefreshFlyoutMenuItems();
_coordinator.UpdateModuleEnabled(flyoutItem.Tag, flyoutItem.IsEnabled);
SortFlyoutMenuItems();
}
}

View File

@@ -117,5 +117,47 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
case ModuleType.GeneralSettings: generalSettingsConfig.EnableQuickAccess = isEnabled; break;
}
}
/// <summary>
/// Gets the module key name used in IPC messages and settings JSON.
/// These names match the JsonPropertyName attributes in EnabledModules class.
/// </summary>
public static string GetModuleKey(ModuleType moduleType)
{
return moduleType switch
{
ModuleType.AdvancedPaste => AdvancedPasteSettings.ModuleName,
ModuleType.AlwaysOnTop => AlwaysOnTopSettings.ModuleName,
ModuleType.Awake => AwakeSettings.ModuleName,
ModuleType.CmdPal => "CmdPal", // No dedicated settings class
ModuleType.ColorPicker => ColorPickerSettings.ModuleName,
ModuleType.CropAndLock => CropAndLockSettings.ModuleName,
ModuleType.CursorWrap => CursorWrapSettings.ModuleName,
ModuleType.EnvironmentVariables => EnvironmentVariablesSettings.ModuleName,
ModuleType.FancyZones => FancyZonesSettings.ModuleName,
ModuleType.FileLocksmith => FileLocksmithSettings.ModuleName,
ModuleType.FindMyMouse => FindMyMouseSettings.ModuleName,
ModuleType.Hosts => HostsSettings.ModuleName,
ModuleType.ImageResizer => ImageResizerSettings.ModuleName,
ModuleType.KeyboardManager => KeyboardManagerSettings.ModuleName,
ModuleType.LightSwitch => LightSwitchSettings.ModuleName,
ModuleType.MouseHighlighter => MouseHighlighterSettings.ModuleName,
ModuleType.MouseJump => MouseJumpSettings.ModuleName,
ModuleType.MousePointerCrosshairs => MousePointerCrosshairsSettings.ModuleName,
ModuleType.MouseWithoutBorders => MouseWithoutBordersSettings.ModuleName,
ModuleType.NewPlus => NewPlusSettings.ModuleName,
ModuleType.Peek => PeekSettings.ModuleName,
ModuleType.PowerRename => PowerRenameSettings.ModuleName,
ModuleType.PowerLauncher => PowerLauncherSettings.ModuleName,
ModuleType.PowerAccent => PowerAccentSettings.ModuleName,
ModuleType.RegistryPreview => RegistryPreviewSettings.ModuleName,
ModuleType.MeasureTool => MeasureToolSettings.ModuleName,
ModuleType.ShortcutGuide => ShortcutGuideSettings.ModuleName,
ModuleType.PowerOCR => PowerOcrSettings.ModuleName,
ModuleType.Workspaces => WorkspacesSettings.ModuleName,
ModuleType.ZoomIt => ZoomItSettings.ModuleName,
_ => moduleType.ToString(),
};
}
}
}

View File

@@ -32,7 +32,6 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
MeasureTool,
Hosts,
Workspaces,
WhatsNew,
RegistryPreview,
NewPlus,
ZoomIt,

View File

@@ -9,7 +9,6 @@ using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
@@ -227,7 +226,6 @@ namespace Microsoft.PowerToys.Settings.UI
{
settingsWindow = new MainWindow();
settingsWindow.Activate();
settingsWindow.ExtendsContentIntoTitleBar = true;
settingsWindow.NavigateToSection(StartupPage);
// https://github.com/microsoft/microsoft-ui-xaml/issues/7595 - Activate doesn't bring window to the foreground
@@ -257,11 +255,10 @@ namespace Microsoft.PowerToys.Settings.UI
else if (ShowScoobe)
{
PowerToysTelemetry.Log.WriteEvent(new ScoobeStartedEvent());
OobeWindow scoobeWindow = new OobeWindow(OOBE.Enums.PowerToysModules.WhatsNew);
scoobeWindow.Activate();
scoobeWindow.ExtendsContentIntoTitleBar = true;
ScoobeWindow newScoobeWindow = new ScoobeWindow();
newScoobeWindow.Activate();
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(WindowNative.GetWindowHandle(settingsWindow));
SetOobeWindow(scoobeWindow);
SetScoobeWindow(newScoobeWindow);
}
}
}
@@ -339,6 +336,7 @@ namespace Microsoft.PowerToys.Settings.UI
private static MainWindow settingsWindow;
private static OobeWindow oobeWindow;
private static ScoobeWindow scoobeWindow;
public static void ClearSettingsWindow()
{
@@ -365,6 +363,21 @@ namespace Microsoft.PowerToys.Settings.UI
oobeWindow = null;
}
public static ScoobeWindow GetScoobeWindow()
{
return scoobeWindow;
}
public static void SetScoobeWindow(ScoobeWindow window)
{
scoobeWindow = window;
}
public static void ClearScoobeWindow()
{
scoobeWindow = null;
}
public static Type GetPage(string settingWindow)
{
switch (settingWindow)

View File

@@ -20,9 +20,6 @@ using WinUIEx;
namespace Microsoft.PowerToys.Settings.UI
{
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainWindow : WindowEx
{
public MainWindow(bool createHidden = false)
@@ -35,10 +32,12 @@ namespace Microsoft.PowerToys.Settings.UI
App.ThemeService.ThemeChanged += OnThemeChanged;
App.ThemeService.ApplyTheme();
this.ExtendsContentIntoTitleBar = true;
ShellPage.SetElevationStatus(App.IsElevated);
ShellPage.SetIsUserAnAdmin(App.IsUserAnAdmin);
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
var hWnd = WindowNative.GetWindowHandle(this);
var placement = WindowHelper.DeserializePlacementOrDefault(hWnd);
if (createHidden)
{
@@ -121,16 +120,12 @@ namespace Microsoft.PowerToys.Settings.UI
// open whats new window
ShellPage.SetOpenWhatIsNewCallback(() =>
{
if (App.GetOobeWindow() == null)
if (App.GetScoobeWindow() == null)
{
App.SetOobeWindow(new OobeWindow(OOBE.Enums.PowerToysModules.WhatsNew));
}
else
{
App.GetOobeWindow().SetAppWindow(OOBE.Enums.PowerToysModules.WhatsNew);
App.SetScoobeWindow(new ScoobeWindow());
}
App.GetOobeWindow().Activate();
App.GetScoobeWindow().Activate();
});
this.InitializeComponent();
@@ -187,7 +182,7 @@ namespace Microsoft.PowerToys.Settings.UI
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
WindowHelper.SerializePlacement(hWnd);
if (App.GetOobeWindow() == null)
if (App.GetOobeWindow() == null && App.GetScoobeWindow() == null)
{
App.ClearSettingsWindow();
}

View File

@@ -1,60 +1,44 @@
<UserControl
<Page
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.OobeShellPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
HighContrastAdjustment="None"
Loaded="ShellPage_Loaded"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="48" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Button
x:Name="PaneToggleBtn"
Width="48"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Click="PaneToggleBtn_Click"
Style="{StaticResource PaneToggleButtonStyle}" />
<Grid
<TitleBar
x:Name="AppTitleBar"
Height="48"
Margin="48,0,0,0"
VerticalAlignment="Center"
IsHitTestVisible="True">
<animations:Implicit.Animations>
<animations:OffsetAnimation Duration="0:0:0.3" />
</animations:Implicit.Animations>
<StackPanel Orientation="Horizontal">
<Image
Width="16"
x:Uid="OobeWindow_TitleTxt"
IsBackButtonVisible="False"
IsPaneToggleButtonVisible="False"
PaneToggleRequested="TitleBar_PaneButtonClick">
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
<TitleBar.LeftHeader>
<ImageIcon
x:Name="TitleBarIcon"
Height="16"
Margin="16,0,0,0"
Source="/Assets/Settings/icon.ico" />
<TextBlock
x:Name="AppTitleBarText"
x:Uid="OobeWindow_TitleTxt"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
TextWrapping="NoWrap" />
</StackPanel>
</Grid>
</TitleBar.LeftHeader>
</TitleBar>
<NavigationView
x:Name="navigationView"
Grid.Row="1"
CompactModeThresholdWidth="1007"
DisplayModeChanged="NavigationView_DisplayModeChanged"
ExpandedModeThresholdWidth="1007"
IsBackButtonVisible="Collapsed"
IsPaneOpen="True"
IsPaneToggleButtonVisible="False"
IsSettingsVisible="False"
OpenPaneLength="296"
PaneDisplayMode="Left"
SelectionChanged="NavigationView_SelectionChanged">
<NavigationView.MenuItems>
<NavigationViewItem
@@ -174,34 +158,16 @@
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/ZoomIt.png}"
Tag="ZoomIt" />
</NavigationView.MenuItems>
<NavigationView.FooterMenuItems>
<NavigationView.PaneFooter>
<NavigationViewItem
x:Uid="Shell_WhatsNew"
AutomationProperties.AutomationId="WhatIsNewNavItem"
Icon="{ui:FontIcon Glyph=&#xE789;}"
Tag="WhatsNew" />
</NavigationView.FooterMenuItems>
Tapped="WhatIsNewItem_Tapped" />
</NavigationView.PaneFooter>
<NavigationView.Content>
<Frame x:Name="NavigationFrame" />
</NavigationView.Content>
</NavigationView>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="LayoutVisualStates">
<VisualState x:Name="WideLayout">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="720" />
</VisualState.StateTriggers>
</VisualState>
<VisualState x:Name="SmallLayout">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="600" />
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="navigationView.PaneDisplayMode" Value="LeftMinimal" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>
</Page>

View File

@@ -5,18 +5,17 @@
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using WinRT.Interop;
using Microsoft.UI.Xaml.Input;
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
public sealed partial class OobeShellPage : UserControl
public sealed partial class OobeShellPage : Page
{
public static Func<string> RunSharedEventCallback { get; set; }
@@ -63,7 +62,6 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
// NOTE: Experimentation for OOBE is currently turned off on server side. Keeping this code in a comment to allow future experiments.
// ExperimentationToggleSwitchEnabled = SettingsRepository<GeneralSettings>.GetInstance(settingsUtils).SettingsConfig.EnableExperimentation;
SetTitleBar();
DataContext = ViewModel;
OobeShellHandler = this;
Modules = new ObservableCollection<OobePowerToysModule>();
@@ -202,12 +200,6 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
IsNew = true,
});
Modules.Insert((int)PowerToysModules.WhatsNew, new OobePowerToysModule()
{
ModuleName = "WhatsNew",
IsNew = false,
});
Modules.Insert((int)PowerToysModules.RegistryPreview, new OobePowerToysModule()
{
ModuleName = "RegistryPreview",
@@ -229,7 +221,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
public void OnClosing()
{
Microsoft.UI.Xaml.Controls.NavigationViewItem selectedItem = this.navigationView.SelectedItem as Microsoft.UI.Xaml.Controls.NavigationViewItem;
NavigationViewItem selectedItem = this.navigationView.SelectedItem as NavigationViewItem;
if (selectedItem != null)
{
Modules[(int)(PowerToysModules)Enum.Parse(typeof(PowerToysModules), (string)selectedItem.Tag, true)].LogClosingModuleEvent();
@@ -238,19 +230,22 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
public void NavigateToModule(PowerToysModules selectedModule)
{
if (selectedModule == PowerToysModules.WhatsNew)
{
navigationView.SelectedItem = navigationView.FooterMenuItems[0];
}
else
{
navigationView.SelectedItem = navigationView.MenuItems[(int)selectedModule];
}
navigationView.SelectedItem = navigationView.MenuItems[(int)selectedModule];
}
private void NavigationView_SelectionChanged(Microsoft.UI.Xaml.Controls.NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewSelectionChangedEventArgs args)
private static void OpenScoobeWindow()
{
Microsoft.UI.Xaml.Controls.NavigationViewItem selectedItem = args.SelectedItem as Microsoft.UI.Xaml.Controls.NavigationViewItem;
if (App.GetScoobeWindow() == null)
{
App.SetScoobeWindow(new ScoobeWindow());
}
App.GetScoobeWindow().Activate();
}
private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
NavigationViewItem selectedItem = args.SelectedItem as NavigationViewItem;
if (selectedItem != null)
{
@@ -278,7 +273,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
break;
}
*/
case "WhatsNew": NavigationFrame.Navigate(typeof(OobeWhatsNew)); break;
case "AdvancedPaste": NavigationFrame.Navigate(typeof(OobeAdvancedPaste)); break;
case "AlwaysOnTop": NavigationFrame.Navigate(typeof(OobeAlwaysOnTop)); break;
case "Awake": NavigationFrame.Navigate(typeof(OobeAwake)); break;
@@ -311,43 +306,37 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
}
}
private void SetTitleBar()
{
var u = App.GetOobeWindow();
if (u != null)
{
// A custom title bar is required for full window theme and Mica support.
// https://docs.microsoft.com/windows/apps/develop/title-bar?tabs=winui3#full-customization
u.ExtendsContentIntoTitleBar = true;
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(WindowNative.GetWindowHandle(u));
u.SetTitleBar(AppTitleBar);
}
}
private void ShellPage_Loaded(object sender, RoutedEventArgs e)
{
SetTitleBar();
// Select the first module by default
if (navigationView.MenuItems.Count > 0)
{
navigationView.SelectedItem = navigationView.MenuItems[0];
}
}
private void NavigationView_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
{
if (args.DisplayMode == NavigationViewDisplayMode.Compact || args.DisplayMode == NavigationViewDisplayMode.Minimal)
{
PaneToggleBtn.Visibility = Visibility.Visible;
AppTitleBar.Margin = new Thickness(48, 0, 0, 0);
AppTitleBarText.Margin = new Thickness(12, 0, 0, 0);
TitleBarIcon.Margin = new Thickness(0, 0, 8, 0); // Workaround, see XAML comment
AppTitleBar.IsPaneToggleButtonVisible = true;
}
else
{
PaneToggleBtn.Visibility = Visibility.Collapsed;
AppTitleBar.Margin = new Thickness(16, 0, 0, 0);
AppTitleBarText.Margin = new Thickness(16, 0, 0, 0);
TitleBarIcon.Margin = new Thickness(16, 0, 0, 0); // Workaround, see XAML comment
AppTitleBar.IsPaneToggleButtonVisible = false;
}
}
private void PaneToggleBtn_Click(object sender, RoutedEventArgs e)
private void TitleBar_PaneButtonClick(TitleBar sender, object args)
{
navigationView.IsPaneOpen = !navigationView.IsPaneOpen;
}
private void WhatIsNewItem_Tapped(object sender, TappedRoutedEventArgs e)
{
OpenScoobeWindow();
}
}
}

View File

@@ -1,191 +0,0 @@
<Page
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.OobeWhatsNew"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
Loaded="Page_Loaded"
mc:Ignorable="d">
<Page.Resources>
<tkcontrols:MarkdownThemes
x:Key="ReleaseNotesMarkdownThemeConfig"
H1FontSize="22"
H1FontWeight="SemiBold"
H1Margin="0, 36, 0, 8"
H2FontSize="16"
H2FontWeight="SemiBold"
H2Margin="0, 16, 0, 4"
H3FontSize="16"
H3FontWeight="SemiBold"
H3Margin="0, 16, 0, 4" />
<tkcontrols:MarkdownConfig x:Key="ReleaseNotesMarkdownConfig" Themes="{StaticResource ReleaseNotesMarkdownThemeConfig}" />
</Page.Resources>
<!-- Main layout container -->
<Grid>
<!-- Main content grid -->
<Grid Margin="0,24,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Compact Header overlay that covers both InfoBar and Title sections -->
<Border
x:Name="HeaderOverlay"
Grid.Row="0"
Grid.RowSpan="2"
Margin="0,-24,0,0"
VerticalAlignment="Top"
BorderThickness="0"
Canvas.ZIndex="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<tkcontrols:SettingsCard
x:Name="WhatsNewDataDiagnosticsInfoBar"
x:Uid="Oobe_WhatsNew_DataDiagnostics_InfoBar"
Grid.Row="0"
Padding="12,8,12,8"
Background="{ThemeResource InfoBarInformationalSeverityBackgroundBrush}"
IsTabStop="{x:Bind ShowDataDiagnosticsInfoBar, Mode=OneWay}"
Visibility="{x:Bind ShowDataDiagnosticsInfoBar, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<tkcontrols:SettingsCard.HeaderIcon>
<FontIcon Foreground="{ThemeResource InfoBarInformationalSeverityIconBackground}" Glyph="&#xF167;" />
</tkcontrols:SettingsCard.HeaderIcon>
<tkcontrols:SettingsCard.Description>
<StackPanel>
<TextBlock x:Name="WhatsNewDataDiagnosticsInfoBarDescText">
<Hyperlink NavigateUri="https://aka.ms/powertoys-data-and-privacy-documentation">
<Run x:Uid="Oobe_WhatsNew_DataDiagnostics_InfoBar_Desc" />
</Hyperlink>
</TextBlock>
<TextBlock x:Name="WhatsNewDataDiagnosticsInfoBarDescTextYesClicked" Visibility="Collapsed">
<Run x:Uid="Oobe_WhatsNew_DataDiagnostics_Yes_Click_InfoBar_Desc" />
<Hyperlink Click="DataDiagnostics_OpenSettings_Click">
<Run x:Uid="Oobe_WhatsNew_DataDiagnostics_Yes_Click_OpenSettings_Text" />
</Hyperlink>
</TextBlock>
</StackPanel>
</tkcontrols:SettingsCard.Description>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button
x:Name="DataDiagnosticsButtonYes"
x:Uid="Oobe_WhatsNew_DataDiagnostics_Button_Yes"
Click="DataDiagnostics_InfoBar_YesNo_Click"
CommandParameter="Yes" />
<HyperlinkButton
x:Name="DataDiagnosticsButtonNo"
x:Uid="Oobe_WhatsNew_DataDiagnostics_Button_No"
Click="DataDiagnostics_InfoBar_YesNo_Click"
CommandParameter="No" />
<Button
Margin="16,0,0,0"
Click="DataDiagnostics_InfoBar_Close_Click"
Content="{ui:FontIcon Glyph=&#xE894;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}" />
</StackPanel>
</tkcontrols:SettingsCard>
<Grid Grid.Row="1" Margin="16,12,0,12">
<StackPanel
VerticalAlignment="Top"
Orientation="Vertical"
Spacing="4">
<TextBlock
x:Uid="Oobe_WhatsNew"
AutomationProperties.HeadingLevel="Level1"
Style="{StaticResource TitleTextBlockStyle}" />
<HyperlinkButton NavigateUri="https://github.com/microsoft/PowerToys/releases" Style="{StaticResource TextButtonStyle}">
<TextBlock x:Uid="Oobe_WhatsNew_DetailedReleaseNotesLink" TextWrapping="Wrap" />
</HyperlinkButton>
</StackPanel>
<!-- ShortcutConflictControl positioned at the right side -->
<controls:ShortcutConflictControl
Grid.RowSpan="2"
Margin="0,0,16,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
AllHotkeyConflictsData="{x:Bind AllHotkeyConflictsData, Mode=OneWay}"
Visibility="{x:Bind HasConflicts, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
</Grid>
</Grid>
</Border>
<!-- Reduced spacer for the compact header overlay -->
<Grid Grid.Row="0" Height="0" />
<Grid Grid.Row="1" Height="80" />
<InfoBar
x:Name="ErrorInfoBar"
x:Uid="Oobe_WhatsNew_LoadingError"
Grid.Row="2"
VerticalAlignment="Top"
IsClosable="False"
IsTabStop="False"
Severity="Error">
<InfoBar.ActionButton>
<Button
x:Uid="RetryBtn"
HorizontalAlignment="Right"
Click="LoadReleaseNotes_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE72C;" />
<TextBlock x:Uid="RetryLabel" />
</StackPanel>
</Button>
</InfoBar.ActionButton>
</InfoBar>
<InfoBar
x:Name="ProxyWarningInfoBar"
x:Uid="Oobe_WhatsNew_ProxyAuthenticationWarning"
Grid.Row="2"
VerticalAlignment="Top"
IsClosable="False"
IsTabStop="False"
Severity="Warning">
<InfoBar.ActionButton>
<Button
x:Uid="RetryBtn"
HorizontalAlignment="Right"
Click="LoadReleaseNotes_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE72C;" />
<TextBlock x:Uid="RetryLabel" />
</StackPanel>
</Button>
</InfoBar.ActionButton>
</InfoBar>
<ScrollViewer Grid.Row="3" VerticalScrollBarVisibility="Auto">
<Grid Margin="32,16,32,24">
<ProgressRing
x:Name="LoadingProgressRing"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsIndeterminate="True"
Visibility="Visible" />
<tkcontrols:MarkdownTextBlock
x:Name="ReleaseNotesMarkdown"
Config="{StaticResource ReleaseNotesMarkdownConfig}"
UseAutoLinks="True"
UseEmphasisExtras="True"
UseListExtras="True"
UsePipeTables="True"
UseTaskLists="True" />
</Grid>
</ScrollViewer>
</Grid>
</Grid>
</Page>

View File

@@ -1,359 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CommunityToolkit.WinUI.Controls;
using global::PowerToys.GPOWrapper;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Microsoft.PowerToys.Settings.UI.Services;
using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
public sealed partial class OobeWhatsNew : Page, INotifyPropertyChanged
{
public OobePowerToysModule ViewModel { get; set; }
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
public bool ShowDataDiagnosticsInfoBar => GetShowDataDiagnosticsInfoBar();
private int _conflictCount;
public AllHotkeyConflictsData AllHotkeyConflictsData
{
get => _allHotkeyConflictsData;
set
{
if (_allHotkeyConflictsData != value)
{
_allHotkeyConflictsData = value;
UpdateConflictCount();
OnPropertyChanged(nameof(AllHotkeyConflictsData));
OnPropertyChanged(nameof(HasConflicts));
}
}
}
public bool HasConflicts => _conflictCount > 0;
private void UpdateConflictCount()
{
int count = 0;
if (AllHotkeyConflictsData == null)
{
_conflictCount = count;
}
if (AllHotkeyConflictsData.InAppConflicts != null)
{
foreach (var inAppConflict in AllHotkeyConflictsData.InAppConflicts)
{
if (!inAppConflict.ConflictIgnored)
{
count++;
}
}
}
if (AllHotkeyConflictsData.SystemConflicts != null)
{
foreach (var systemConflict in AllHotkeyConflictsData.SystemConflicts)
{
if (!systemConflict.ConflictIgnored)
{
count++;
}
}
}
_conflictCount = count;
}
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Initializes a new instance of the <see cref="OobeWhatsNew"/> class.
/// </summary>
public OobeWhatsNew()
{
this.InitializeComponent();
ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.WhatsNew]);
DataContext = this;
// Subscribe to hotkey conflict updates
if (GlobalHotkeyConflictManager.Instance != null)
{
GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated;
GlobalHotkeyConflictManager.Instance.RequestAllConflicts();
}
}
private void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
{
this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
{
var allConflictData = e.Conflicts;
foreach (var inAppConflict in allConflictData.InAppConflicts)
{
var hotkey = inAppConflict.Hotkey;
var hotkeySetting = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key);
inAppConflict.ConflictIgnored = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySetting);
}
foreach (var systemConflict in allConflictData.SystemConflicts)
{
var hotkey = systemConflict.Hotkey;
var hotkeySetting = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key);
systemConflict.ConflictIgnored = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySetting);
}
AllHotkeyConflictsData = e.Conflicts ?? new AllHotkeyConflictsData();
});
}
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private bool GetShowDataDiagnosticsInfoBar()
{
var isDataDiagnosticsGpoDisallowed = GPOWrapper.GetAllowDataDiagnosticsValue() == GpoRuleConfigured.Disabled;
if (isDataDiagnosticsGpoDisallowed)
{
return false;
}
bool userActed = DataDiagnosticsSettings.GetUserActionValue();
if (userActed)
{
return false;
}
bool registryValue = DataDiagnosticsSettings.GetEnabledValue();
bool isFirstRunAfterUpdate = (App.Current as Microsoft.PowerToys.Settings.UI.App).ShowScoobe;
if (isFirstRunAfterUpdate && registryValue == false)
{
return true;
}
return false;
}
/// <summary>
/// Regex to remove installer hash sections from the release notes.
/// </summary>
private const string RemoveInstallerHashesRegex = @"(\r\n)+## Installer Hashes(\r\n.*)+## Highlights";
private const string RemoveHotFixInstallerHashesRegex = @"(\r\n)+## Installer Hashes(\r\n.*)+$";
private const RegexOptions RemoveInstallerHashesRegexOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
private bool _loadingReleaseNotes;
private static async Task<string> GetReleaseNotesMarkdown()
{
string releaseNotesJSON = string.Empty;
// Let's use system proxy
using var proxyClientHandler = new HttpClientHandler
{
DefaultProxyCredentials = CredentialCache.DefaultCredentials,
Proxy = WebRequest.GetSystemWebProxy(),
PreAuthenticate = true,
};
using var getReleaseInfoClient = new HttpClient(proxyClientHandler);
// GitHub APIs require sending an user agent
// https://docs.github.com/rest/overview/resources-in-the-rest-api#user-agent-required
getReleaseInfoClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "PowerToys");
releaseNotesJSON = await getReleaseInfoClient.GetStringAsync("https://api.github.com/repos/microsoft/PowerToys/releases");
IList<PowerToysReleaseInfo> releases = JsonSerializer.Deserialize<IList<PowerToysReleaseInfo>>(releaseNotesJSON, SourceGenerationContextContext.Default.IListPowerToysReleaseInfo);
// Get the latest releases
var latestReleases = releases.OrderByDescending(release => release.PublishedDate).Take(5);
StringBuilder releaseNotesHtmlBuilder = new StringBuilder(string.Empty);
// Regex to remove installer hash sections from the release notes.
Regex removeHashRegex = new Regex(RemoveInstallerHashesRegex, RemoveInstallerHashesRegexOptions);
// Regex to remove installer hash sections from the release notes, since there'll be no Highlights section for hotfix releases.
Regex removeHotfixHashRegex = new Regex(RemoveHotFixInstallerHashesRegex, RemoveInstallerHashesRegexOptions);
int counter = 0;
foreach (var release in latestReleases)
{
releaseNotesHtmlBuilder.AppendLine("# " + release.Name);
var notes = removeHashRegex.Replace(release.ReleaseNotes, "\r\n### Highlights");
// Add a unique counter to [github-current-release-work] to distinguish each release,
// since this variable is used for all latest releases when they are merged.
notes = notes.Replace("[github-current-release-work]", $"[github-current-release-work{++counter}]");
notes = removeHotfixHashRegex.Replace(notes, string.Empty);
releaseNotesHtmlBuilder.AppendLine(notes);
releaseNotesHtmlBuilder.AppendLine("&nbsp;");
}
return releaseNotesHtmlBuilder.ToString();
}
private async Task Reload()
{
if (_loadingReleaseNotes)
{
return;
}
try
{
_loadingReleaseNotes = true;
ReleaseNotesMarkdown.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
LoadingProgressRing.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
string releaseNotesMarkdown = await GetReleaseNotesMarkdown();
ProxyWarningInfoBar.IsOpen = false;
ErrorInfoBar.IsOpen = false;
ReleaseNotesMarkdown.Text = releaseNotesMarkdown;
ReleaseNotesMarkdown.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
LoadingProgressRing.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
}
catch (HttpRequestException httpEx)
{
Logger.LogError("Exception when loading the release notes", httpEx);
if (httpEx.Message.Contains("407", StringComparison.CurrentCulture))
{
ProxyWarningInfoBar.IsOpen = true;
}
else
{
ErrorInfoBar.IsOpen = true;
}
}
catch (Exception ex)
{
Logger.LogError("Exception when loading the release notes", ex);
ErrorInfoBar.IsOpen = true;
}
finally
{
LoadingProgressRing.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
_loadingReleaseNotes = false;
}
}
private async void Page_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
await Reload();
}
/// <inheritdoc/>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
ViewModel.LogOpeningModuleEvent();
}
/// <inheritdoc/>
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
ViewModel.LogClosingModuleEvent();
// Unsubscribe from conflict updates when leaving the page
if (GlobalHotkeyConflictManager.Instance != null)
{
GlobalHotkeyConflictManager.Instance.ConflictsUpdated -= OnConflictsUpdated;
}
}
private void DataDiagnostics_InfoBar_YesNo_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
string commandArg = string.Empty;
if (sender is Button senderBtn)
{
commandArg = senderBtn.CommandParameter.ToString();
}
else if (sender is HyperlinkButton senderLink)
{
commandArg = senderLink.CommandParameter.ToString();
}
if (string.IsNullOrEmpty(commandArg))
{
return;
}
// Update UI
if (commandArg == "Yes")
{
WhatsNewDataDiagnosticsInfoBar.Header = ResourceLoaderInstance.ResourceLoader.GetString("Oobe_WhatsNew_DataDiagnostics_Yes_Click_InfoBar_Title");
}
else
{
WhatsNewDataDiagnosticsInfoBar.Header = ResourceLoaderInstance.ResourceLoader.GetString("Oobe_WhatsNew_DataDiagnostics_No_Click_InfoBar_Title");
}
WhatsNewDataDiagnosticsInfoBarDescText.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
WhatsNewDataDiagnosticsInfoBarDescTextYesClicked.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
DataDiagnosticsButtonYes.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
DataDiagnosticsButtonNo.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
// Set Data Diagnostics registry values
if (commandArg == "Yes")
{
DataDiagnosticsSettings.SetEnabledValue(true);
}
else
{
DataDiagnosticsSettings.SetEnabledValue(false);
}
DataDiagnosticsSettings.SetUserActionValue(true);
this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
{
ShellPage.ShellHandler?.SignalGeneralDataUpdate();
});
}
private void DataDiagnostics_InfoBar_Close_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
WhatsNewDataDiagnosticsInfoBar.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
}
private void DataDiagnostics_OpenSettings_Click(Microsoft.UI.Xaml.Documents.Hyperlink sender, Microsoft.UI.Xaml.Documents.HyperlinkClickEventArgs args)
{
Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Overview);
}
private async void LoadReleaseNotes_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
await Reload();
}
}
}

View File

@@ -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 System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.PowerToys.Settings.UI.Helpers;
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
/// <summary>
/// View model for a group of releases (grouped by major.minor version).
/// </summary>
public class ScoobeReleaseGroupViewModel
{
/// <summary>
/// Gets the list of releases in this group.
/// </summary>
public IList<PowerToysReleaseInfo> Releases { get; }
/// <summary>
/// Gets the version text to display (e.g., "0.96.0").
/// </summary>
public string VersionText { get; }
/// <summary>
/// Gets the date text to display (e.g., "December 2025").
/// </summary>
public string DateText { get; }
public ScoobeReleaseGroupViewModel(IList<PowerToysReleaseInfo> releases)
{
Releases = releases ?? throw new ArgumentNullException(nameof(releases));
if (releases.Count > 0)
{
var latestRelease = releases[0];
VersionText = GetVersionFromRelease(latestRelease);
DateText = latestRelease.PublishedDate.ToString("MMMM yyyy", CultureInfo.CurrentCulture);
}
else
{
VersionText = "Unknown";
DateText = string.Empty;
}
}
private static string GetVersionFromRelease(PowerToysReleaseInfo release)
{
// TagName is typically like "v0.96.0", Name might be "Release v0.96.0"
string version = release.TagName ?? release.Name ?? "Unknown";
if (version.StartsWith("v", StringComparison.OrdinalIgnoreCase))
{
version = version.Substring(1);
}
return version;
}
}
}

View File

@@ -0,0 +1,63 @@
<Page
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.ScoobeReleaseNotesPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
Loaded="Page_Loaded"
mc:Ignorable="d">
<Page.Resources>
<tkcontrols:MarkdownThemes
x:Key="ReleaseNotesMarkdownThemeConfig"
BoldFontWeight="SemiBold"
H1FontSize="28"
H1FontWeight="SemiBold"
H1Margin="0, 36, 0, 8"
H2FontSize="20"
H2FontWeight="SemiBold"
H2Margin="0, 16, 0, 4"
H3FontSize="16"
H3FontWeight="SemiBold"
H3Margin="0, 16, 0, 4"
HorizontalRuleBrush="{StaticResource DividerStrokeColorDefaultBrush}"
HorizontalRuleThickness="1"
ImageStretch="Uniform"
ListBulletSpacing="1"
ListGutterWidth="10" />
<tkcontrols:MarkdownConfig x:Key="ReleaseNotesMarkdownConfig" Themes="{StaticResource ReleaseNotesMarkdownThemeConfig}" />
</Page.Resources>
<!-- Main layout container -->
<Grid MaxWidth="1000">
<ScrollViewer Padding="0,0,0,0" VerticalScrollBarVisibility="Auto">
<Grid Margin="0,0,0,24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Image
x:Name="HeroImageHolder"
Height="186"
HorizontalAlignment="Left"
Stretch="UniformToFill" />
<Grid Grid.Row="1" Margin="24,16,24,24">
<ProgressRing
x:Name="LoadingProgressRing"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsIndeterminate="True"
Visibility="Visible" />
<tkcontrols:MarkdownTextBlock
x:Name="ReleaseNotesMarkdown"
Config="{StaticResource ReleaseNotesMarkdownConfig}"
UseAutoLinks="True"
UseEmphasisExtras="True"
UseListExtras="True"
UsePipeTables="True"
UseTaskLists="True" />
</Grid>
</Grid>
</ScrollViewer>
</Grid>
</Page>

View File

@@ -0,0 +1,165 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
using Microsoft.UI.Xaml.Navigation;
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
public sealed partial class ScoobeReleaseNotesPage : Page
{
private IList<PowerToysReleaseInfo> _currentReleases;
/// <summary>
/// Initializes a new instance of the <see cref="ScoobeReleaseNotesPage"/> class.
/// </summary>
public ScoobeReleaseNotesPage()
{
this.InitializeComponent();
}
/// <summary>
/// Regex to remove installer hash sections from the release notes.
/// </summary>
private const string RemoveInstallerHashesRegex = @"(\r\n)+## Installer Hashes(\r\n.*)+## Highlights";
private const string RemoveHotFixInstallerHashesRegex = @"(\r\n)+## Installer Hashes(\r\n.*)+$";
private const RegexOptions RemoveInstallerHashesRegexOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
/// <summary>
/// Regex to match markdown images with 'Hero' in the alt text.
/// Matches: ![...Hero...](url)
/// </summary>
private static readonly Regex HeroImageRegex = new Regex(
@"!\[([^\]]*Hero[^\]]*)\]\(([^)]+)\)",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>
/// Regex to match GitHub PR/Issue references (e.g., #41029).
/// Only matches # followed by digits that are not already part of a markdown link.
/// </summary>
private static readonly Regex GitHubPrReferenceRegex = new Regex(
@"(?<!\[)#(\d+)(?!\])",
RegexOptions.Compiled);
private static readonly CompositeFormat GitHubPrLinkTemplate = CompositeFormat.Parse("[#{0}](https://github.com/microsoft/PowerToys/pull/{0})");
private static readonly CompositeFormat GitHubReleaseLinkTemplate = CompositeFormat.Parse("https://github.com/microsoft/PowerToys/releases/tag/{0}");
private static (string Markdown, string HeroImageUrl) ProcessReleaseNotesMarkdown(IList<PowerToysReleaseInfo> releases)
{
if (releases == null || releases.Count == 0)
{
return (string.Empty, null);
}
StringBuilder releaseNotesHtmlBuilder = new StringBuilder(string.Empty);
// Regex to remove installer hash sections from the release notes.
Regex removeHashRegex = new Regex(RemoveInstallerHashesRegex, RemoveInstallerHashesRegexOptions);
// Regex to remove installer hash sections from the release notes, since there'll be no Highlights section for hotfix releases.
Regex removeHotfixHashRegex = new Regex(RemoveHotFixInstallerHashesRegex, RemoveInstallerHashesRegexOptions);
string lastHeroImageUrl = null;
int counter = 0;
bool isFirst = true;
foreach (var release in releases)
{
// Add separator between releases
if (!isFirst)
{
releaseNotesHtmlBuilder.AppendLine("---");
releaseNotesHtmlBuilder.AppendLine();
}
isFirst = false;
var releaseUrl = string.Format(CultureInfo.InvariantCulture, GitHubReleaseLinkTemplate, release.TagName);
releaseNotesHtmlBuilder.AppendLine(CultureInfo.InvariantCulture, $"# {release.Name}");
releaseNotesHtmlBuilder.AppendLine(CultureInfo.InvariantCulture, $"{release.PublishedDate.ToString("MMMM d, yyyy", CultureInfo.CurrentCulture)} <20> [View on GitHub]({releaseUrl})");
releaseNotesHtmlBuilder.AppendLine();
releaseNotesHtmlBuilder.AppendLine("&nbsp;");
releaseNotesHtmlBuilder.AppendLine();
var notes = removeHashRegex.Replace(release.ReleaseNotes, "\r\n## Highlights");
notes = notes.Replace("[github-current-release-work]", $"[github-current-release-work{++counter}]");
notes = removeHotfixHashRegex.Replace(notes, string.Empty);
// Find all Hero images and keep track of the last one
var heroMatches = HeroImageRegex.Matches(notes);
foreach (Match match in heroMatches)
{
lastHeroImageUrl = match.Groups[2].Value;
}
// Remove Hero images from the markdown
notes = HeroImageRegex.Replace(notes, string.Empty);
// Convert GitHub PR/Issue references to hyperlinks
notes = GitHubPrReferenceRegex.Replace(notes, match =>
string.Format(CultureInfo.InvariantCulture, GitHubPrLinkTemplate, match.Groups[1].Value));
releaseNotesHtmlBuilder.AppendLine(notes);
releaseNotesHtmlBuilder.AppendLine("&nbsp;");
}
return (releaseNotesHtmlBuilder.ToString(), lastHeroImageUrl);
}
private void DisplayReleaseNotes()
{
if (_currentReleases == null || _currentReleases.Count == 0)
{
ReleaseNotesMarkdown.Visibility = Visibility.Collapsed;
return;
}
try
{
LoadingProgressRing.Visibility = Visibility.Collapsed;
var (releaseNotesMarkdown, heroImageUrl) = ProcessReleaseNotesMarkdown(_currentReleases);
// Set the Hero image if found
if (!string.IsNullOrEmpty(heroImageUrl))
{
HeroImageHolder.Source = new BitmapImage(new Uri(heroImageUrl));
HeroImageHolder.Visibility = Visibility.Visible;
}
else
{
HeroImageHolder.Visibility = Visibility.Collapsed;
}
ReleaseNotesMarkdown.Text = releaseNotesMarkdown;
ReleaseNotesMarkdown.Visibility = Visibility.Visible;
}
catch (Exception ex)
{
Logger.LogError("Exception when displaying the release notes", ex);
}
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
DisplayReleaseNotes();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (e.Parameter is IList<PowerToysReleaseInfo> releases)
{
_currentReleases = releases;
}
}
}
}

View File

@@ -0,0 +1,96 @@
<Page
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.ScoobeShellPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.OOBE.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
HighContrastAdjustment="None"
Loaded="ShellPage_Loaded"
mc:Ignorable="d">
<Page.Resources>
<!-- Template for NavigationViewItem content with version and date -->
<DataTemplate x:Key="ReleaseNavItemTemplate" x:DataType="local:ScoobeReleaseGroupViewModel">
<StackPanel
Margin="0,8,0,8"
Orientation="Vertical"
Spacing="4">
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{x:Bind DateText}" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind VersionText}" />
</StackPanel>
</DataTemplate>
</Page.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TitleBar
x:Name="AppTitleBar"
x:Uid="ScoobeWindow_TitleTxt"
IsBackButtonVisible="False"
IsPaneToggleButtonVisible="False"
PaneToggleRequested="TitleBar_PaneButtonClick">
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
<TitleBar.LeftHeader>
<ImageIcon
x:Name="TitleBarIcon"
Height="16"
Margin="16,0,0,0"
Source="/Assets/Settings/icon.ico" />
</TitleBar.LeftHeader>
</TitleBar>
<NavigationView
x:Name="navigationView"
Grid.Row="1"
CompactModeThresholdWidth="1007"
DisplayModeChanged="NavigationView_DisplayModeChanged"
ExpandedModeThresholdWidth="1007"
IsBackButtonVisible="Collapsed"
IsPaneOpen="True"
IsPaneToggleButtonVisible="False"
IsSettingsVisible="False"
MenuItemTemplate="{StaticResource ReleaseNavItemTemplate}"
OpenPaneLength="186"
SelectionChanged="NavigationView_SelectionChanged">
<NavigationView.MenuItems>
<!-- Items are added dynamically -->
</NavigationView.MenuItems>
<NavigationView.Content>
<Grid>
<ProgressRing
x:Name="LoadingProgressRing"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsIndeterminate="True"
Visibility="Collapsed" />
<InfoBar
x:Name="ErrorInfoBar"
x:Uid="Oobe_WhatsNew_LoadingError"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsClosable="False"
IsOpen="False"
Severity="Error">
<InfoBar.ActionButton>
<Button
x:Uid="RetryBtn"
HorizontalAlignment="Right"
Click="RetryButton_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE72C;" />
<TextBlock x:Uid="RetryLabel" />
</StackPanel>
</Button>
</InfoBar.ActionButton>
</InfoBar>
<Frame x:Name="NavigationFrame" />
</Grid>
</NavigationView.Content>
</NavigationView>
</Grid>
</Page>

View File

@@ -0,0 +1,194 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
public sealed partial class ScoobeShellPage : Page
{
public static Action<Type> OpenMainWindowCallback { get; set; }
public static void SetOpenMainWindowCallback(Action<Type> implementation)
{
OpenMainWindowCallback = implementation;
}
/// <summary>
/// Gets or sets a shell handler to be used to update contents of the shell dynamically from page within the frame.
/// </summary>
public static ScoobeShellPage ScoobeShellHandler { get; set; }
/// <summary>
/// Gets the list of release groups loaded from GitHub (grouped by major.minor version).
/// </summary>
public IList<IList<PowerToysReleaseInfo>> ReleaseGroups { get; private set; }
private bool _isLoading;
public ScoobeShellPage()
{
InitializeComponent();
ScoobeShellHandler = this;
}
private async void ShellPage_Loaded(object sender, RoutedEventArgs e)
{
SetTitleBar();
await LoadReleasesAsync();
}
private async Task LoadReleasesAsync()
{
if (_isLoading)
{
return;
}
_isLoading = true;
LoadingProgressRing.Visibility = Visibility.Visible;
ErrorInfoBar.IsOpen = false;
navigationView.MenuItems.Clear();
try
{
var releases = await FetchReleasesFromGitHubAsync();
ReleaseGroups = GroupReleasesByMajorMinor(releases);
PopulateNavigationItems();
}
catch (Exception ex)
{
Logger.LogError("Failed to load releases", ex);
ErrorInfoBar.IsOpen = true;
}
finally
{
LoadingProgressRing.Visibility = Visibility.Collapsed;
_isLoading = false;
}
}
private static async Task<IList<PowerToysReleaseInfo>> FetchReleasesFromGitHubAsync()
{
using var proxyClientHandler = new HttpClientHandler
{
DefaultProxyCredentials = CredentialCache.DefaultCredentials,
Proxy = WebRequest.GetSystemWebProxy(),
PreAuthenticate = true,
};
using var httpClient = new HttpClient(proxyClientHandler);
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "PowerToys");
string json = await httpClient.GetStringAsync("https://api.github.com/repos/microsoft/PowerToys/releases?per_page=20");
var allReleases = JsonSerializer.Deserialize<IList<PowerToysReleaseInfo>>(json, SourceGenerationContextContext.Default.IListPowerToysReleaseInfo);
return allReleases
.OrderByDescending(r => r.PublishedDate)
.ToList();
}
private static IList<IList<PowerToysReleaseInfo>> GroupReleasesByMajorMinor(IList<PowerToysReleaseInfo> releases)
{
return releases
.GroupBy(r => GetMajorMinorVersion(r))
.Select(g => g.OrderByDescending(r => r.PublishedDate).ToList() as IList<PowerToysReleaseInfo>)
.ToList();
}
private static string GetMajorMinorVersion(PowerToysReleaseInfo release)
{
string version = GetVersionFromRelease(release);
var parts = version.Split('.');
if (parts.Length >= 2)
{
return $"{parts[0]}.{parts[1]}";
}
return version;
}
private static string GetVersionFromRelease(PowerToysReleaseInfo release)
{
// TagName is typically like "v0.96.0", Name might be "Release v0.96.0"
string version = release.TagName ?? release.Name ?? "Unknown";
if (version.StartsWith("v", StringComparison.OrdinalIgnoreCase))
{
version = version.Substring(1);
}
return version;
}
private void PopulateNavigationItems()
{
if (ReleaseGroups == null || ReleaseGroups.Count == 0)
{
return;
}
foreach (var releaseGroup in ReleaseGroups)
{
var viewModel = new ScoobeReleaseGroupViewModel(releaseGroup);
navigationView.MenuItems.Add(viewModel);
}
// Select the first item to trigger navigation
navigationView.SelectedItem = navigationView.MenuItems[0];
}
private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
if (args.SelectedItem is ScoobeReleaseGroupViewModel viewModel)
{
NavigationFrame.Navigate(typeof(ScoobeReleaseNotesPage), viewModel.Releases);
}
}
private async void RetryButton_Click(object sender, RoutedEventArgs e)
{
await LoadReleasesAsync();
}
private void SetTitleBar()
{
var window = App.GetScoobeWindow();
if (window != null)
{
window.ExtendsContentIntoTitleBar = true;
window.SetTitleBar(AppTitleBar);
}
}
private void NavigationView_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
{
if (args.DisplayMode == NavigationViewDisplayMode.Compact || args.DisplayMode == NavigationViewDisplayMode.Minimal)
{
TitleBarIcon.Margin = new Thickness(0, 0, 8, 0); // Workaround, see XAML comment
AppTitleBar.IsPaneToggleButtonVisible = true;
}
else
{
TitleBarIcon.Margin = new Thickness(16, 0, 0, 0); // Workaround, see XAML comment
AppTitleBar.IsPaneToggleButtonVisible = false;
}
}
private void TitleBar_PaneButtonClick(TitleBar sender, object args)
{
navigationView.IsPaneOpen = !navigationView.IsPaneOpen;
}
}
}

View File

@@ -44,6 +44,7 @@ namespace Microsoft.PowerToys.Settings.UI
_windowId = Win32Interop.GetWindowIdFromWindow(_hWnd);
_appWindow = AppWindow.GetFromWindowId(_windowId);
this.Activated += Window_Activated_SetIcon;
this.ExtendsContentIntoTitleBar = true;
var dpi = NativeMethods.GetDpiForWindow(_hWnd);
_currentDPI = dpi;
@@ -60,7 +61,7 @@ namespace Microsoft.PowerToys.Settings.UI
this.SizeChanged += OobeWindow_SizeChanged;
var loader = Helpers.ResourceLoaderInstance.ResourceLoader;
var loader = ResourceLoaderInstance.ResourceLoader;
Title = loader.GetString("OobeWindow_Title");
if (shellPage != null)

View File

@@ -0,0 +1,17 @@
<winuiex:WindowEx
x:Class="Microsoft.PowerToys.Settings.UI.ScoobeWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.OOBE.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:winuiex="using:WinUIEx"
MinWidth="480"
MinHeight="480"
Closed="Window_Closed"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<local:ScoobeShellPage x:Name="shellPage" />
</winuiex:WindowEx>

View File

@@ -0,0 +1,121 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.OOBE.Views;
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using PowerToys.Interop;
using Windows.Graphics;
using WinUIEx;
using WinUIEx.Messaging;
namespace Microsoft.PowerToys.Settings.UI
{
public sealed partial class ScoobeWindow : WindowEx, IDisposable
{
private const int ExpectedWidth = 1100;
private const int ExpectedHeight = 700;
private const int DefaultDPI = 96;
private int _currentDPI;
private WindowId _windowId;
private IntPtr _hWnd;
private AppWindow _appWindow;
private bool disposedValue;
public ScoobeWindow()
{
App.ThemeService.ThemeChanged += OnThemeChanged;
App.ThemeService.ApplyTheme();
this.InitializeComponent();
_hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
_windowId = Win32Interop.GetWindowIdFromWindow(_hWnd);
_appWindow = AppWindow.GetFromWindowId(_windowId);
this.Activated += Window_Activated_SetIcon;
this.ExtendsContentIntoTitleBar = true;
var dpi = NativeMethods.GetDpiForWindow(_hWnd);
_currentDPI = dpi;
float scalingFactor = (float)dpi / DefaultDPI;
int width = (int)(ExpectedWidth * scalingFactor);
int height = (int)(ExpectedHeight * scalingFactor);
SizeInt32 size;
size.Width = width;
size.Height = height;
_appWindow.Resize(size);
this.SizeChanged += ScoobeWindow_SizeChanged;
var loader = Helpers.ResourceLoaderInstance.ResourceLoader;
Title = loader.GetString("ScoobeWindow_Title");
ScoobeShellPage.SetOpenMainWindowCallback((Type type) =>
{
App.OpenSettingsWindow(type);
});
}
private void Window_Activated_SetIcon(object sender, WindowActivatedEventArgs args)
{
// Set window icon
_appWindow.SetIcon("Assets\\Settings\\icon.ico");
}
private void ScoobeWindow_SizeChanged(object sender, WindowSizeChangedEventArgs args)
{
var dpi = NativeMethods.GetDpiForWindow(_hWnd);
if (_currentDPI != dpi)
{
// Reacting to a DPI change. Should not cause a resize -> sizeChanged loop.
_currentDPI = dpi;
float scalingFactor = (float)dpi / DefaultDPI;
int width = (int)(ExpectedWidth * scalingFactor);
int height = (int)(ExpectedHeight * scalingFactor);
SizeInt32 size;
size.Width = width;
size.Height = height;
_appWindow.Resize(size);
}
}
private void Window_Closed(object sender, WindowEventArgs args)
{
App.ClearScoobeWindow();
var mainWindow = App.GetSettingsWindow();
if (mainWindow != null)
{
mainWindow.CloseHiddenWindow();
}
App.ThemeService.ThemeChanged -= OnThemeChanged;
}
private void OnThemeChanged(object sender, ElementTheme theme)
{
WindowHelper.SetTheme(this, theme);
}
private void Dispose(bool disposing)
{
if (!disposedValue)
{
disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -8,8 +8,6 @@ using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.Views;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -50,26 +48,30 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void WhatsNewButton_Click(object sender, RoutedEventArgs e)
{
if (App.GetOobeWindow() == null)
if (App.GetScoobeWindow() == null)
{
App.SetOobeWindow(new OobeWindow(PowerToysModules.WhatsNew));
}
else
{
App.GetOobeWindow().SetAppWindow(PowerToysModules.WhatsNew);
App.SetScoobeWindow(new ScoobeWindow());
}
App.GetOobeWindow().Activate();
App.GetScoobeWindow().Activate();
}
private void SortAlphabetical_Click(object sender, RoutedEventArgs e)
{
ViewModel.DashboardSortOrder = DashboardSortOrder.Alphabetical;
if (sender is ToggleMenuFlyoutItem item)
{
item.IsChecked = true;
}
}
private void SortByStatus_Click(object sender, RoutedEventArgs e)
{
ViewModel.DashboardSortOrder = DashboardSortOrder.ByStatus;
if (sender is ToggleMenuFlyoutItem item)
{
item.IsChecked = true;
}
}
}
}

View File

@@ -145,10 +145,16 @@
<tkcontrols:SettingsExpander.Items>
<!-- Overlay opacity removed; alpha now encoded in colors -->
<tkcontrols:SettingsCard Name="MouseUtilsFindMyMouseBackgroundColor" x:Uid="MouseUtils_FindMyMouse_BackgroundColor">
<controls:ColorPickerButton IsAlphaEnabled="True" SelectedColor="{x:Bind Path=ViewModel.FindMyMouseBackgroundColor, Mode=TwoWay}" />
<controls:ColorPickerButton
AutomationProperties.AutomationId="MouseUtils_FindMyMouseBackgroundColorId"
IsAlphaEnabled="True"
SelectedColor="{x:Bind Path=ViewModel.FindMyMouseBackgroundColor, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="MouseUtilsFindMyMouseSpotlightColor" x:Uid="MouseUtils_FindMyMouse_SpotlightColor">
<controls:ColorPickerButton IsAlphaEnabled="True" SelectedColor="{x:Bind Path=ViewModel.FindMyMouseSpotlightColor, Mode=TwoWay}" />
<controls:ColorPickerButton
AutomationProperties.AutomationId="MouseUtils_FindMyMouseSpotlightColorId"
IsAlphaEnabled="True"
SelectedColor="{x:Bind Path=ViewModel.FindMyMouseSpotlightColor, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="MouseUtilsFindMyMouseSpotlightRadius" x:Uid="MouseUtils_FindMyMouse_SpotlightRadius">
<NumberBox

View File

@@ -2419,9 +2419,15 @@ From there, simply click on one of the supported files in the File Explorer and
<data name="OobeWindow_Title" xml:space="preserve">
<value>Welcome to PowerToys</value>
</data>
<data name="OobeWindow_TitleTxt.Text" xml:space="preserve">
<data name="OobeWindow_TitleTxt.Title" xml:space="preserve">
<value>Welcome to PowerToys</value>
</data>
<data name="ScoobeWindow_Title" xml:space="preserve">
<value>What's new in PowerToys</value>
</data>
<data name="ScoobeWindow_TitleTxt.Title" xml:space="preserve">
<value>What's new in PowerToys</value>
</data>
<data name="SettingsWindow_Title" xml:space="preserve">
<value>PowerToys Settings</value>
<comment>Title of the settings window when running as user</comment>
@@ -5080,6 +5086,9 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="OpenSettings.Content" xml:space="preserve">
<value>Open settings</value>
</data>
<data name="OpenAnimationsSettings.Content" xml:space="preserve">
<value>Open animation settings</value>
</data>
<data name="LanguageHeader.Header" xml:space="preserve">
<value>Language</value>
</data>

View File

@@ -29,6 +29,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
public partial class DashboardViewModel : PageViewModelBase
{
private readonly object _sortLock = new object();
protected override string ModuleName => "Dashboard";
private Dispatcher dispatcher;
@@ -51,6 +53,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// Flag to prevent circular updates when a UI toggle triggers settings changes.
private bool _isUpdatingFromUI;
// Flag to prevent toggle operations during sorting to avoid race conditions.
private bool _isSorting;
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
public AllHotkeyConflictsData AllHotkeyConflictsData
@@ -80,15 +85,17 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
get => generalSettingsConfig.DashboardSortOrder;
set
{
if (Set(ref _dashboardSortOrder, value))
if (_dashboardSortOrder != value)
{
_dashboardSortOrder = value;
generalSettingsConfig.DashboardSortOrder = value;
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(generalSettingsConfig);
// Save settings to file
SettingsUtils.Default.SaveSettings(generalSettingsConfig.ToJsonString());
SendConfigMSG(outgoing.ToString());
// Notify UI before sorting so menu updates its checked state
OnPropertyChanged(nameof(DashboardSortOrder));
SortModuleList();
}
}
@@ -103,7 +110,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
dispatcher = Dispatcher.CurrentDispatcher;
_settingsRepository = settingsRepository;
generalSettingsConfig = settingsRepository.SettingsConfig;
generalSettingsConfig.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
_settingsRepository.SettingsChanged += OnSettingsChanged;
// Initialize dashboard sort order from settings
@@ -128,7 +135,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
dispatcher.BeginInvoke(() =>
{
generalSettingsConfig = newSettings;
generalSettingsConfig.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
// Update local field and notify UI if sort order changed
if (_dashboardSortOrder != generalSettingsConfig.DashboardSortOrder)
{
_dashboardSortOrder = generalSettingsConfig.DashboardSortOrder;
OnPropertyChanged(nameof(DashboardSortOrder));
}
ModuleEnabledChangedOnSettingsPage();
});
}
@@ -198,40 +212,58 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
/// Sorts the module list according to the current sort order and updates the AllModules collection.
/// On first call, populates AllModules. On subsequent calls, uses Move() to reorder items in-place
/// to avoid destroying and recreating UI elements.
/// Temporarily disables interaction on all items during sorting to prevent race conditions.
/// </summary>
private void SortModuleList()
{
var sortedItems = (DashboardSortOrder switch
if (_isSorting)
{
DashboardSortOrder.ByStatus => _moduleItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label),
_ => _moduleItems.OrderBy(x => x.Label), // Default alphabetical
}).ToList();
// If AllModules is empty (first load), just populate it.
if (AllModules.Count == 0)
{
foreach (var item in sortedItems)
{
AllModules.Add(item);
}
return;
}
// Otherwise, update the collection in place using Move to avoid UI glitches.
for (int i = 0; i < sortedItems.Count; i++)
lock (_sortLock)
{
var currentItem = sortedItems[i];
var currentIndex = AllModules.IndexOf(currentItem);
if (currentIndex != -1 && currentIndex != i)
_isSorting = true;
try
{
AllModules.Move(currentIndex, i);
var sortedItems = (DashboardSortOrder switch
{
DashboardSortOrder.ByStatus => _moduleItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label),
_ => _moduleItems.OrderBy(x => x.Label), // Default alphabetical
}).ToList();
// If AllModules is empty (first load), just populate it.
if (AllModules.Count == 0)
{
foreach (var item in sortedItems)
{
AllModules.Add(item);
}
return;
}
// Otherwise, update the collection in place using Move to avoid UI glitches.
for (int i = 0; i < sortedItems.Count; i++)
{
var currentItem = sortedItems[i];
var currentIndex = AllModules.IndexOf(currentItem);
if (currentIndex != -1 && currentIndex != i)
{
AllModules.Move(currentIndex, i);
}
}
}
finally
{
// Use dispatcher to reset flag after UI updates complete
dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background, () =>
{
_isSorting = false;
});
}
}
// Notify that DashboardSortOrder changed so the menu updates its checked state.
OnPropertyChanged(nameof(DashboardSortOrder));
}
/// <summary>
@@ -279,10 +311,25 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
var dashboardListItem = (DashboardListItem)item;
var isEnabled = dashboardListItem.IsEnabled;
// Ignore toggle operations during sorting to prevent race conditions.
// Revert the toggle state since UI already changed due to TwoWay binding.
if (_isSorting)
{
dashboardListItem.UpdateStatus(!isEnabled);
return;
}
_isUpdatingFromUI = true;
try
{
Views.ShellPage.UpdateGeneralSettingsCallback(dashboardListItem.Tag, isEnabled);
// Send optimized IPC message with only the module status update
// Format: {"module_status": {"ModuleName": true/false}}
string moduleKey = ModuleHelper.GetModuleKey(dashboardListItem.Tag);
string moduleStatusJson = $"{{\"module_status\": {{\"{moduleKey}\": {isEnabled.ToString().ToLowerInvariant()}}}}}";
SendConfigMSG(moduleStatusJson);
// Update local settings config to keep UI in sync
ModuleHelper.SetIsModuleEnabled(generalSettingsConfig, dashboardListItem.Tag, isEnabled);
if (dashboardListItem.Tag == ModuleType.NewPlus && isEnabled == true)
{