mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-17 01:27:48 +01:00
Compare commits
6 Commits
leilzh/fix
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53c5e66cce | ||
|
|
4cde968c9b | ||
|
|
e148a89288 | ||
|
|
1dddf9fa2c | ||
|
|
0d59b9f790 | ||
|
|
e314485e85 |
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
123
src/modules/LightSwitch/LightSwitchLib/LightSwitchLib.vcxproj
Normal file
123
src/modules/LightSwitch/LightSwitchLib/LightSwitchLib.vcxproj
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
10
src/modules/LightSwitch/LightSwitchLib/ThemeHelper.h
Normal file
10
src/modules/LightSwitch/LightSwitchLib/ThemeHelper.h
Normal 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();
|
||||
1
src/modules/LightSwitch/LightSwitchLib/pch.cpp
Normal file
1
src/modules/LightSwitch/LightSwitchLib/pch.cpp
Normal file
@@ -0,0 +1 @@
|
||||
#include "pch.h"
|
||||
5
src/modules/LightSwitch/LightSwitchLib/pch.h
Normal file
5
src/modules/LightSwitch/LightSwitchLib/pch.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
|
||||
#include <windows.h>
|
||||
#include <vector>
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
void SetSystemTheme(bool dark);
|
||||
void SetAppsTheme(bool dark);
|
||||
bool GetCurrentSystemTheme();
|
||||
bool GetCurrentAppsTheme();
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
void SetSystemTheme(bool dark);
|
||||
void SetAppsTheme(bool dark);
|
||||
bool GetCurrentSystemTheme();
|
||||
bool GetCurrentAppsTheme();
|
||||
bool IsNightLightEnabled();
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
|
||||
MeasureTool,
|
||||
Hosts,
|
||||
Workspaces,
|
||||
WhatsNew,
|
||||
RegistryPreview,
|
||||
NewPlus,
|
||||
ZoomIt,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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=}"
|
||||
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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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="" />
|
||||
</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=,
|
||||
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="" />
|
||||
<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="" />
|
||||
<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>
|
||||
@@ -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(" ");
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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: 
|
||||
/// </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(" ");
|
||||
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(" ");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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="" />
|
||||
<TextBlock x:Uid="RetryLabel" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</InfoBar.ActionButton>
|
||||
</InfoBar>
|
||||
<Frame x:Name="NavigationFrame" />
|
||||
</Grid>
|
||||
</NavigationView.Content>
|
||||
</NavigationView>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
17
src/settings-ui/Settings.UI/SettingsXAML/ScoobeWindow.xaml
Normal file
17
src/settings-ui/Settings.UI/SettingsXAML/ScoobeWindow.xaml
Normal 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>
|
||||
121
src/settings-ui/Settings.UI/SettingsXAML/ScoobeWindow.xaml.cs
Normal file
121
src/settings-ui/Settings.UI/SettingsXAML/ScoobeWindow.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user