Compare commits

..

2 Commits

Author SHA1 Message Date
Leilei Zhang
50c9a270ed fix help message error 2026-01-12 13:50:31 +08:00
Leilei Zhang
353820e06f fix ps error and add sub help 2026-01-12 13:21:10 +08:00
85 changed files with 1898 additions and 3010 deletions

View File

@@ -22,6 +22,7 @@ ADate
ADDSTRING
ADDUNDORECORD
ADifferent
adjacents
ADMINS
adml
admx
@@ -98,6 +99,7 @@ ASYNCWINDOWPLACEMENT
ASYNCWINDOWPOS
atl
ATRIOX
ATX
aumid
authenticode
AUTOBUDDY
@@ -207,17 +209,14 @@ changecursor
CHILDACTIVATE
CHILDWINDOW
CHOOSEFONT
CIBUILD
cidl
CIELCh
cim
CImage
cla
CLASSDC
classguid
classmethod
CLASSNOTAVAILABLE
claude
CLEARTYPE
clickable
clickonce
@@ -256,6 +255,7 @@ colorhistory
colorhistorylimit
COLORKEY
colorref
Convs
comctl
comdlg
comexp
@@ -276,7 +276,6 @@ CONTEXTHELP
CONTEXTMENUHANDLER
contractversion
CONTROLPARENT
Convs
copiedcolorrepresentation
coppied
copyable
@@ -292,6 +291,7 @@ cpcontrols
cph
cplusplus
CPower
cppcoreguidelines
cpptools
cppvsdbg
cppwinrt
@@ -320,7 +320,7 @@ CURRENTDIR
CURSORINFO
cursorpos
CURSORSHOWING
cursorwrap
CURSORWRAP
customaction
CUSTOMACTIONTEST
CUSTOMFORMATPLACEHOLDER
@@ -343,14 +343,12 @@ datareader
datatracker
dataversion
Dayof
dbcc
DBID
DBLCLKS
DBLEPSILON
DBPROP
DBPROPIDSET
DBPROPSET
DBT
DCBA
DCOM
DComposition
@@ -368,7 +366,8 @@ DEFAULTICON
defaultlib
DEFAULTONLY
DEFAULTSIZE
defaulttonearest
DEFAULTTONEAREST
Defaulttonearest
DEFAULTTONULL
DEFAULTTOPRIMARY
DEFERERASE
@@ -390,19 +389,14 @@ DESKTOPVERTRES
devblogs
devdocs
devenv
DEVICEINTERFACE
devicetype
DEVINTERFACE
devmgmt
DEVMODE
DEVMODEW
DEVNODES
devpal
DEVTYP
dfx
DIALOGEX
diffs
digicert
diffs
DINORMAL
DISABLEASACTIONKEY
DISABLENOSCROLL
@@ -545,6 +539,7 @@ fdx
FErase
fesf
FFFF
FInc
Figma
FILEEXPLORER
fileexploreraddons
@@ -565,7 +560,6 @@ FILESYSPATH
Filetime
FILEVERSION
FILTERMODE
FInc
findfast
findmymouse
FIXEDFILEINFO
@@ -667,14 +661,13 @@ HCRYPTPROV
hcursor
hcwhite
hdc
HDEVNOTIFY
hdr
hdrop
hdwwiz
Helpline
helptext
hgdiobj
HGFE
hgdiobj
hglobal
hhk
HHmmssfff
@@ -750,9 +743,9 @@ HWNDPARENT
HWNDPREV
hyjiacan
IAI
icf
ICONERROR
ICONLOCATION
icf
IDCANCEL
IDD
idk
@@ -843,8 +836,8 @@ jeli
jfif
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
jjw
jobject
JOBOBJECT
jobject
jpe
jpnime
Jsons
@@ -888,7 +881,6 @@ Ldr
LEFTALIGN
LEFTSCROLLBAR
LEFTTEXT
leftclick
LError
LEVELID
LExit
@@ -931,9 +923,9 @@ LOWORD
lparam
LPBITMAPINFOHEADER
LPCFHOOKPROC
lpch
LPCITEMIDLIST
LPCLSID
lpch
lpcmi
LPCMINVOKECOMMANDINFO
LPCREATESTRUCT
@@ -949,7 +941,6 @@ LPMONITORINFO
LPOSVERSIONINFOEXW
LPQUERY
lprc
LPrivate
LPSAFEARRAY
lpstr
lpsz
@@ -959,6 +950,7 @@ lptpm
LPTR
LPTSTR
lpv
LPrivate
LPW
lpwcx
lpwndpl
@@ -1002,19 +994,18 @@ mber
MBM
MBR
Mbuttondown
mcp
MDICHILD
MDL
mdtext
mdtxt
mdwn
meme
mcp
memicmp
MENUITEMINFO
MENUITEMINFOW
MERGECOPY
MERGEPAINT
Metacharacter
metadatamatters
Metadatas
metafile
@@ -1045,7 +1036,6 @@ mmsys
mobileredirect
mockapi
MODALFRAME
modelcontextprotocol
MODESPRUNED
MONITORENUMPROC
MONITORINFO
@@ -1089,9 +1079,9 @@ MSLLHOOKSTRUCT
Mso
msrc
msstore
mstsc
msvcp
MT
mstsc
MTND
MULTIPLEUSE
multizone
@@ -1101,11 +1091,11 @@ muxxc
muxxh
MVPs
mvvm
myorg
myrepo
MVVMTK
MWBEx
MYICON
myorg
myrepo
NAMECHANGE
namespaceanddescendants
nao
@@ -1246,8 +1236,10 @@ opencode
OPENFILENAME
openrdp
opensource
openurl
openxmlformats
ollama
onnx
openurl
OPTIMIZEFORINVOKE
ORPHANEDDIALOGTITLE
ORSCANS
@@ -1463,6 +1455,7 @@ rbhid
Rbuttondown
rclsid
RCZOOMIT
remotedesktop
rdp
RDW
READMODE
@@ -1491,7 +1484,6 @@ remappings
REMAPSUCCESSFUL
REMAPUNSUCCESSFUL
Remotable
remotedesktop
remoteip
Removelnk
renamable
@@ -1525,8 +1517,8 @@ RIGHTSCROLLBAR
riid
RKey
RNumber
rollups
rop
rollups
ROUNDSMALL
ROWSETEXT
rpcrt
@@ -1764,7 +1756,8 @@ SVGIO
svgz
SVSI
SWFO
swp
SWP
Swp
SWPNOSIZE
SWPNOZORDER
SWRESTORE
@@ -1783,7 +1776,8 @@ SYSKEY
syskeydown
SYSKEYUP
SYSLIB
sysmenu
SYSMENU
Sysmenu
systemai
SYSTEMAPPS
SYSTEMMODAL
@@ -1887,9 +1881,9 @@ uitests
UITo
ULONGLONG
Ultrawide
ums
UMax
UMin
ums
uncompilable
UNCPRIORITY
UNDNAME

View File

@@ -504,14 +504,6 @@ jobs:
Remove-Item -Force -Recurse "$(JobOutputDirectory)/_appx" -ErrorAction:Ignore
displayName: Re-pack the new CmdPal package after signing
- pwsh: |
$testsPath = "$(Build.SourcesDirectory)/$(BuildPlatform)/$(BuildConfiguration)/tests"
if (Test-Path $testsPath) {
Remove-Item -Path $testsPath -Recurse -Force
Write-Host "Removed tests folder to reduce signing workload: $testsPath"
}
displayName: Remove tests folder before signing
- template: steps-esrp-signing.yml
parameters:
displayName: Sign Core PowerToys

View File

@@ -10,7 +10,7 @@ parameters:
default: {}
steps:
- task: EsrpCodeSigning@6
- task: EsrpCodeSigning@5
displayName: 🔏 ${{ parameters.displayName }}
inputs:
ConnectedServiceName: ${{ parameters.signingIdentity.serviceName }}

View File

@@ -13,8 +13,6 @@
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
<PackageVersion Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
<!-- Pin the SixLabors.ImageSharp version (a transitive dependency of CoenM.ImageSharp.ImageHash) to restore functionality and apply patches. -->
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.12" />
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.0" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.250402" />
@@ -26,7 +24,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.260116-build.2514" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.251002-build.2316" />
<PackageVersion Include="ControlzEx" Version="6.0.0" />
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />

View File

@@ -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 <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.
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.
<!-- 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 <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:
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:
*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 <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.
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.
</details>
## ✨ What's new

View File

@@ -1549,7 +1549,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
}
processes.resize(bytes / sizeof(processes[0]));
std::array<std::wstring_view, 44> processesToTerminate = {
std::array<std::wstring_view, 42> processesToTerminate = {
L"PowerToys.PowerLauncher.exe",
L"PowerToys.Settings.exe",
L"PowerToys.AdvancedPaste.exe",
@@ -1584,14 +1584,12 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
L"PowerToys.MouseWithoutBordersService.exe",
L"PowerToys.CropAndLock.exe",
L"PowerToys.EnvironmentVariables.exe",
L"PowerToys.QuickAccess.exe",
L"PowerToys.WorkspacesSnapshotTool.exe",
L"PowerToys.WorkspacesLauncher.exe",
L"PowerToys.WorkspacesLauncherUI.exe",
L"PowerToys.WorkspacesEditor.exe",
L"PowerToys.WorkspacesWindowArranger.exe",
L"Microsoft.CmdPal.UI.exe",
L"Microsoft.CmdPal.Ext.PowerToys.exe",
L"PowerToys.ZoomIt.exe",
L"PowerToys.exe",
};

View File

@@ -18,7 +18,6 @@
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="Module_CmdPal" Value="" KeyPath="yes" />
</RegistryKey>
<RemoveFile Id="RemoveOldCmdPalMsix" Name="Microsoft.CmdPal.UI_*.msix" On="install" />
<?if $(sys.BUILDARCH) = x64 ?>
<File Id="Microsoft.CmdPal.UI___var.CmdPalVersion_._x64.msix" Source="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_x64.msix" />
<?else?>

View File

@@ -61,16 +61,6 @@
</RegistryKey>
<File Source="$(var.RepoDir)\Notice.md" Id="Notice.md" />
</Component>
<Directory Id="SvgsFolder" Name="svgs">
<Component Id="svgs_icons" Guid="A9B7C5D3-E1F2-4A6B-8C9D-0E1F2A3B4C5D" Bitness="always64">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="svgs_icons" Value="" KeyPath="yes" />
</RegistryKey>
<File Id="icon.ico" Source="$(var.BinDir)svgs\icon.ico" />
<File Id="PowerToysWhite.ico" Source="$(var.BinDir)svgs\PowerToysWhite.ico" />
<File Id="PowerToysDark.ico" Source="$(var.BinDir)svgs\PowerToysDark.ico" />
</Component>
</Directory>
</DirectoryRef>
<?if $(var.PerUser) = "true" ?>
@@ -122,7 +112,6 @@
<RemoveFolder Id="RemoveBaseApplicationsAssetsFolder" Directory="BaseApplicationsAssetsFolder" On="uninstall" />
<RemoveFolder Id="RemoveWinUI3AppsInstallFolder" Directory="WinUI3AppsInstallFolder" On="uninstall" />
<RemoveFolder Id="RemoveWinUI3AppsAssetsFolder" Directory="WinUI3AppsAssetsFolder" On="uninstall" />
<RemoveFolder Id="RemoveSvgsFolder" Directory="SvgsFolder" On="uninstall" />
<RemoveFolder Id="RemoveINSTALLFOLDER" Directory="INSTALLFOLDER" On="uninstall" />
</Component>
<ComponentRef Id="powertoys_exe" />
@@ -131,7 +120,6 @@
<ComponentRef Id="powertoys_toast_clsid" />
<ComponentRef Id="License_rtf" />
<ComponentRef Id="Notice_md" />
<ComponentRef Id="svgs_icons" />
<ComponentRef Id="DesktopShortcut" />
<?if $(var.PerUser) = "true" ?>
<ComponentRef Id="powertoys_env_path_user" />

View File

@@ -10,7 +10,7 @@ namespace ManagedCommon
{
public static bool IsWindows10()
{
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build < 22000;
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Minor < 22000;
}
public static bool IsWindows11()

View File

@@ -16,50 +16,13 @@ DWORD WINAPI _checkTheme(LPVOID lpParam)
void ThemeListener::AddChangedHandler(THEME_HANDLE handle)
{
std::lock_guard<std::mutex> lock(handlesMutex);
handles.push_back(handle);
}
void ThemeListener::DelChangedHandler(THEME_HANDLE handle)
{
std::lock_guard<std::mutex> lock(handlesMutex);
auto it = std::find(handles.begin(), handles.end(), handle);
if (it != handles.end())
{
handles.erase(it);
}
}
void ThemeListener::AddAppThemeChangedHandler(THEME_HANDLE handle)
{
std::lock_guard<std::mutex> lock(handlesMutex);
appThemeHandles.push_back(handle);
}
void ThemeListener::DelAppThemeChangedHandler(THEME_HANDLE handle)
{
std::lock_guard<std::mutex> lock(handlesMutex);
auto it = std::find(appThemeHandles.begin(), appThemeHandles.end(), handle);
if (it != appThemeHandles.end())
{
appThemeHandles.erase(it);
}
}
void ThemeListener::AddSystemThemeChangedHandler(THEME_HANDLE handle)
{
std::lock_guard<std::mutex> lock(handlesMutex);
systemThemeHandles.push_back(handle);
}
void ThemeListener::DelSystemThemeChangedHandler(THEME_HANDLE handle)
{
std::lock_guard<std::mutex> lock(handlesMutex);
auto it = std::find(systemThemeHandles.begin(), systemThemeHandles.end(), handle);
if (it != systemThemeHandles.end())
{
systemThemeHandles.erase(it);
}
handles.erase(it);
}
void ThemeListener::CheckTheme()
@@ -85,51 +48,13 @@ void ThemeListener::CheckTheme()
WaitForSingleObject(hEvent, INFINITE);
auto _appTheme = ThemeHelpers::GetAppTheme();
auto _systemTheme = ThemeHelpers::GetSystemTheme();
bool appThemeChanged = (AppTheme != _appTheme);
bool systemThemeChanged = (SystemTheme != _systemTheme);
if (appThemeChanged || systemThemeChanged)
auto _theme = ThemeHelpers::GetAppTheme();
if (AppTheme != _theme)
{
AppTheme = _appTheme;
SystemTheme = _systemTheme;
// Copy handlers under lock, then invoke outside lock to avoid deadlock
std::vector<THEME_HANDLE> handlesCopy;
std::vector<THEME_HANDLE> appThemeHandlesCopy;
std::vector<THEME_HANDLE> systemThemeHandlesCopy;
AppTheme = _theme;
for (int i = 0; i < handles.size(); i++)
{
std::lock_guard<std::mutex> lock(handlesMutex);
handlesCopy = handles;
if (appThemeChanged)
{
appThemeHandlesCopy = appThemeHandles;
}
if (systemThemeChanged)
{
systemThemeHandlesCopy = systemThemeHandles;
}
}
// Call generic handlers (backward compatible)
for (const auto& handler : handlesCopy)
{
handler();
}
// Call app theme specific handlers
for (const auto& handler : appThemeHandlesCopy)
{
handler();
}
// Call system theme specific handlers
for (const auto& handler : systemThemeHandlesCopy)
{
handler();
handles[i]();
}
}
}

View File

@@ -3,7 +3,6 @@
#include <windows.h>
#include <iostream>
#include <vector>
#include <mutex>
typedef void (*THEME_HANDLE)();
DWORD WINAPI _checkTheme(LPVOID lpParam);
@@ -15,7 +14,6 @@ public:
ThemeListener()
{
AppTheme = ThemeHelpers::GetAppTheme();
SystemTheme = ThemeHelpers::GetSystemTheme();
dwThreadHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)_checkTheme, this, 0, &dwThreadId);
}
~ThemeListener()
@@ -25,20 +23,12 @@ public:
}
Theme AppTheme;
Theme SystemTheme;
void ThemeListener::AddChangedHandler(THEME_HANDLE handle);
void ThemeListener::DelChangedHandler(THEME_HANDLE handle);
void ThemeListener::AddAppThemeChangedHandler(THEME_HANDLE handle);
void ThemeListener::DelAppThemeChangedHandler(THEME_HANDLE handle);
void ThemeListener::AddSystemThemeChangedHandler(THEME_HANDLE handle);
void ThemeListener::DelSystemThemeChangedHandler(THEME_HANDLE handle);
void CheckTheme();
private:
HANDLE dwThreadHandle;
DWORD dwThreadId;
std::vector<THEME_HANDLE> handles;
std::vector<THEME_HANDLE> appThemeHandles;
std::vector<THEME_HANDLE> systemThemeHandles;
mutable std::mutex handlesMutex;
};

View File

@@ -2,7 +2,6 @@
#include <Windows.h>
#include <algorithm>
#include <appxpackaging.h>
#include <exception>
#include <filesystem>
@@ -338,30 +337,6 @@ namespace package
}
}
}
// Sort by package version in descending order (newest first)
std::sort(matchedFiles.begin(), matchedFiles.end(), [](const std::wstring& a, const std::wstring& b) {
std::wstring nameA, nameB;
PACKAGE_VERSION versionA{}, versionB{};
bool gotA = GetPackageNameAndVersionFromAppx(a, nameA, versionA);
bool gotB = GetPackageNameAndVersionFromAppx(b, nameB, versionB);
// Files that failed to parse go to the end
if (!gotA)
return false;
if (!gotB)
return true;
// Compare versions: Major, Minor, Build, Revision (descending)
if (versionA.Major != versionB.Major)
return versionA.Major > versionB.Major;
if (versionA.Minor != versionB.Minor)
return versionA.Minor > versionB.Minor;
if (versionA.Build != versionB.Build)
return versionA.Build > versionB.Build;
return versionA.Revision > versionB.Revision;
});
}
catch (const std::exception& ex)
{

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) Microsoft Corporation.
Licensed under the MIT License. -->
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.19" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.18" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<policyNamespaces>
<target prefix="powertoys" namespace="Microsoft.Policies.PowerToys" />
</policyNamespaces>
<resources minRequiredRevision="1.19"/><!-- Last changed with PowerToys v0.97.0 -->
<resources minRequiredRevision="1.18"/><!-- Last changed with PowerToys v0.96.0 -->
<supportedOn>
<definitions>
<definition name="SUPPORTED_POWERTOYS_0_64_0" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0)"/>
@@ -27,7 +27,6 @@
<definition name="SUPPORTED_POWERTOYS_0_89_0" displayName="$(string.SUPPORTED_POWERTOYS_0_89_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_90_0" displayName="$(string.SUPPORTED_POWERTOYS_0_90_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_96_0" displayName="$(string.SUPPORTED_POWERTOYS_0_96_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_97_0" displayName="$(string.SUPPORTED_POWERTOYS_0_97_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1)"/>
</definitions>
</supportedOn>
@@ -339,16 +338,6 @@
<decimal value="0" />
</disabledValue>
</policy>
<policy name="ConfigureEnabledUtilityCursorWrap" class="Both" displayName="$(string.ConfigureEnabledUtilityCursorWrap)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityCursorWrap">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_97_0" />
<enabledValue>
<decimal value="1" />
</enabledValue>
<disabledValue>
<decimal value="0" />
</disabledValue>
</policy>
<policy name="ConfigureEnabledUtilityFindMyMouse" class="Both" displayName="$(string.ConfigureEnabledUtilityFindMyMouse)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityFindMyMouse">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_64_0" />

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) Microsoft Corporation.
Licensed under the MIT License. -->
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.19" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.18" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<displayName>PowerToys</displayName>
<description>PowerToys</description>
<resources>
@@ -34,7 +34,6 @@
<string id="SUPPORTED_POWERTOYS_0_89_0">PowerToys version 0.89.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_90_0">PowerToys version 0.90.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_96_0">PowerToys version 0.96.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_97_0">PowerToys version 0.97.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1">From PowerToys version 0.64.0 until PowerToys version 0.87.1</string>
<string id="ConfigureAllUtilityGlobalEnabledStateDescription">This policy configures the enabled state for all PowerToys utilities.
@@ -267,7 +266,6 @@ If you don't configure this policy, the user will be able to control the setting
<string id="ConfigureEnabledUtilityKeyboardManager">Keyboard Manager: Configure enabled state</string>
<string id="ConfigureEnabledUtilityFindMyMouse">Find My Mouse: Configure enabled state</string>
<string id="ConfigureEnabledUtilityMouseHighlighter">Mouse Highlighter: Configure enabled state</string>
<string id="ConfigureEnabledUtilityCursorWrap">CursorWrap: Configure enabled state</string>
<string id="ConfigureEnabledUtilityMouseJump">Mouse Jump: Configure enabled state</string>
<string id="ConfigureEnabledUtilityMousePointerCrosshairs">Mouse Pointer Crosshairs: Configure enabled state</string>
<string id="ConfigureEnabledUtilityMouseWithoutBorders">Mouse Without Borders: Configure enabled state</string>

View File

@@ -466,27 +466,39 @@
TextChanged="EditVariableDialogValueTxtBox_TextChanged"
TextWrapping="Wrap" />
<MenuFlyoutSeparator Visibility="{Binding ShowAsList, Converter={StaticResource BoolToVisibilityConverter}}" />
<ItemsControl
<ListView
x:Name="EditVariableValuesList"
Margin="0,-8,0,12"
HorizontalAlignment="Stretch"
AllowDrop="True"
CanDragItems="True"
CanReorderItems="True"
DragItemsCompleted="EditVariableValuesList_DragItemsCompleted"
ItemsSource="{Binding ValuesList, Mode=TwoWay}"
Visibility="{Binding ShowAsList, Converter={StaticResource BoolToVisibilityConverter}}">
<ItemsControl.ItemTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="40" />
</Grid.ColumnDefinitions>
<FontIcon
Grid.Column="0"
Margin="0,0,8,0"
FontSize="16"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xE759;" />
<TextBox
Grid.Column="1"
Background="Transparent"
BorderBrush="Transparent"
LostFocus="EditVariableValuesListTextBox_LostFocus"
Text="{Binding Text}" />
<Button
x:Uid="More_Options_Button"
Grid.Column="1"
Grid.Column="2"
VerticalAlignment="Center"
Content="&#xE712;"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
@@ -523,8 +535,8 @@
</Button>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</ScrollViewer>
</ContentDialog>

View File

@@ -16,6 +16,8 @@ namespace EnvironmentVariablesUILib
{
public sealed partial class EnvironmentVariablesMainPage : Page
{
private const string ValueListSeparator = ";";
private sealed class RelayCommandParameter
{
public RelayCommandParameter(Variable variable, VariablesSet set)
@@ -440,7 +442,7 @@ namespace EnvironmentVariablesUILib
variable.ValuesList.Move(index, index - 1);
}
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.Text = newValues;
}
@@ -461,7 +463,7 @@ namespace EnvironmentVariablesUILib
variable.ValuesList.Move(index, index + 1);
}
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.Text = newValues;
}
@@ -476,7 +478,7 @@ namespace EnvironmentVariablesUILib
var variable = EditVariableDialog.DataContext as Variable;
variable.ValuesList.Remove(listItem);
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.Text = newValues;
}
@@ -492,7 +494,7 @@ namespace EnvironmentVariablesUILib
var index = variable.ValuesList.IndexOf(listItem);
variable.ValuesList.Insert(index, new Variable.ValuesListItem { Text = string.Empty });
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
EditVariableDialogValueTxtBox.Text = newValues;
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
@@ -510,7 +512,7 @@ namespace EnvironmentVariablesUILib
var index = variable.ValuesList.IndexOf(listItem);
variable.ValuesList.Insert(index + 1, new Variable.ValuesListItem { Text = string.Empty });
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
EditVariableDialogValueTxtBox.Text = newValues;
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
@@ -532,7 +534,7 @@ namespace EnvironmentVariablesUILib
listItem.Text = (sender as TextBox)?.Text;
var variable = EditVariableDialog.DataContext as Variable;
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
EditVariableDialogValueTxtBox.Text = newValues;
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
@@ -548,5 +550,16 @@ namespace EnvironmentVariablesUILib
CancelAddVariable();
ConfirmAddVariableBtn.IsEnabled = false;
}
private void EditVariableValuesList_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
{
if (EditVariableDialog.DataContext is Variable variable && variable.ValuesList != null)
{
var newValues = string.Join(ValueListSeparator, variable.ValuesList.Select(x => x.Text));
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
EditVariableDialogValueTxtBox.Text = newValues;
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
}
}
}
}

View File

@@ -84,17 +84,14 @@
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="CursorWrapCore.h" />
<ClInclude Include="CursorWrapTests.h" />
<ClInclude Include="MonitorTopology.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="trace.h" />
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="CursorWrapCore.cpp" />
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="MonitorTopology.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>

View File

@@ -1,268 +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.
#include "pch.h"
#include "CursorWrapCore.h"
#include "../../../common/logger/logger.h"
#include <sstream>
#include <iomanip>
#include <ctime>
CursorWrapCore::CursorWrapCore()
{
}
#ifdef _DEBUG
std::wstring CursorWrapCore::GenerateTopologyJSON() const
{
std::wostringstream json;
// Get current time
auto now = std::time(nullptr);
std::tm tm{};
localtime_s(&tm, &now);
wchar_t computerName[MAX_COMPUTERNAME_LENGTH + 1] = {0};
DWORD size = MAX_COMPUTERNAME_LENGTH + 1;
GetComputerNameW(computerName, &size);
wchar_t userName[256] = {0};
size = 256;
GetUserNameW(userName, &size);
json << L"{\n";
json << L" \"captured_at\": \"" << std::put_time(&tm, L"%Y-%m-%dT%H:%M:%S%z") << L"\",\n";
json << L" \"computer_name\": \"" << computerName << L"\",\n";
json << L" \"user_name\": \"" << userName << L"\",\n";
json << L" \"monitor_count\": " << m_monitors.size() << L",\n";
json << L" \"monitors\": [\n";
for (size_t i = 0; i < m_monitors.size(); ++i)
{
const auto& monitor = m_monitors[i];
// Get DPI for this monitor
UINT dpiX = 96, dpiY = 96;
POINT center = {
(monitor.rect.left + monitor.rect.right) / 2,
(monitor.rect.top + monitor.rect.bottom) / 2
};
HMONITOR hMon = MonitorFromPoint(center, MONITOR_DEFAULTTONEAREST);
if (hMon)
{
// Try GetDpiForMonitor (requires linking Shcore.lib)
using GetDpiForMonitorFunc = HRESULT (WINAPI *)(HMONITOR, int, UINT*, UINT*);
HMODULE shcore = LoadLibraryW(L"Shcore.dll");
if (shcore)
{
auto getDpi = reinterpret_cast<GetDpiForMonitorFunc>(GetProcAddress(shcore, "GetDpiForMonitor"));
if (getDpi)
{
getDpi(hMon, 0, &dpiX, &dpiY); // MDT_EFFECTIVE_DPI = 0
}
FreeLibrary(shcore);
}
}
int scalingPercent = static_cast<int>((dpiX / 96.0) * 100);
json << L" {\n";
json << L" \"left\": " << monitor.rect.left << L",\n";
json << L" \"top\": " << monitor.rect.top << L",\n";
json << L" \"right\": " << monitor.rect.right << L",\n";
json << L" \"bottom\": " << monitor.rect.bottom << L",\n";
json << L" \"width\": " << (monitor.rect.right - monitor.rect.left) << L",\n";
json << L" \"height\": " << (monitor.rect.bottom - monitor.rect.top) << L",\n";
json << L" \"dpi\": " << dpiX << L",\n";
json << L" \"scaling_percent\": " << scalingPercent << L",\n";
json << L" \"primary\": " << (monitor.isPrimary ? L"true" : L"false") << L",\n";
json << L" \"monitor_id\": " << monitor.monitorId << L"\n";
json << L" }";
if (i < m_monitors.size() - 1)
{
json << L",";
}
json << L"\n";
}
json << L" ]\n";
json << L"}";
return json.str();
}
#endif
void CursorWrapCore::UpdateMonitorInfo()
{
size_t previousMonitorCount = m_monitors.size();
Logger::info(L"======= UPDATE MONITOR INFO START =======");
Logger::info(L"Previous monitor count: {}", previousMonitorCount);
m_monitors.clear();
EnumDisplayMonitors(nullptr, nullptr, [](HMONITOR hMonitor, HDC, LPRECT, LPARAM lParam) -> BOOL {
auto* self = reinterpret_cast<CursorWrapCore*>(lParam);
MONITORINFO mi{};
mi.cbSize = sizeof(MONITORINFO);
if (GetMonitorInfo(hMonitor, &mi))
{
MonitorInfo info{};
info.hMonitor = hMonitor; // Store handle for direct comparison later
info.rect = mi.rcMonitor;
info.isPrimary = (mi.dwFlags & MONITORINFOF_PRIMARY) != 0;
info.monitorId = static_cast<int>(self->m_monitors.size());
self->m_monitors.push_back(info);
Logger::info(L"Enumerated monitor {}: hMonitor={}, rect=({},{},{},{}), primary={}",
info.monitorId, reinterpret_cast<uintptr_t>(hMonitor),
mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom,
info.isPrimary ? L"yes" : L"no");
}
return TRUE;
}, reinterpret_cast<LPARAM>(this));
if (previousMonitorCount != m_monitors.size())
{
Logger::info(L"*** MONITOR CONFIGURATION CHANGED: {} -> {} monitors ***",
previousMonitorCount, m_monitors.size());
}
m_topology.Initialize(m_monitors);
// Log monitor configuration summary
Logger::info(L"Monitor configuration updated: {} monitor(s)", m_monitors.size());
for (size_t i = 0; i < m_monitors.size(); ++i)
{
const auto& m = m_monitors[i];
int width = m.rect.right - m.rect.left;
int height = m.rect.bottom - m.rect.top;
Logger::info(L" Monitor {}: {}x{} at ({}, {}){}",
i, width, height, m.rect.left, m.rect.top,
m.isPrimary ? L" [PRIMARY]" : L"");
}
Logger::info(L" Detected {} outer edges for cursor wrapping", m_topology.GetOuterEdges().size());
// Detect and log monitor gaps
auto gaps = m_topology.DetectMonitorGaps();
if (!gaps.empty())
{
Logger::warn(L"Monitor configuration has coordinate gaps that may prevent wrapping:");
for (const auto& gap : gaps)
{
Logger::warn(L" Gap between Monitor {} and Monitor {}: {}px horizontal gap, {}px vertical overlap",
gap.monitor1Index, gap.monitor2Index, gap.horizontalGap, gap.verticalOverlap);
}
Logger::warn(L" If monitors appear snapped in Display Settings but show gaps here:");
Logger::warn(L" 1. Try dragging monitors apart and snapping them back together");
Logger::warn(L" 2. Update your GPU drivers");
}
Logger::info(L"======= UPDATE MONITOR INFO END =======");
}
POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode)
{
// Check if wrapping should be disabled during drag
if (disableWrapDuringDrag && (GetAsyncKeyState(VK_LBUTTON) & 0x8000))
{
#ifdef _DEBUG
OutputDebugStringW(L"[CursorWrap] [DRAG] Left mouse button down - skipping wrap\n");
#endif
return currentPos;
}
// Convert int wrapMode to WrapMode enum
WrapMode mode = static_cast<WrapMode>(wrapMode);
#ifdef _DEBUG
{
std::wostringstream oss;
oss << L"[CursorWrap] [MOVE] Cursor at (" << currentPos.x << L", " << currentPos.y << L")";
// Get current monitor and identify which one
HMONITOR currentMonitor = MonitorFromPoint(currentPos, MONITOR_DEFAULTTONEAREST);
RECT monitorRect;
if (m_topology.GetMonitorRect(currentMonitor, monitorRect))
{
// Find monitor ID
int monitorId = -1;
for (const auto& monitor : m_monitors)
{
if (monitor.rect.left == monitorRect.left &&
monitor.rect.top == monitorRect.top &&
monitor.rect.right == monitorRect.right &&
monitor.rect.bottom == monitorRect.bottom)
{
monitorId = monitor.monitorId;
break;
}
}
oss << L" on Monitor " << monitorId << L" [" << monitorRect.left << L".." << monitorRect.right
<< L", " << monitorRect.top << L".." << monitorRect.bottom << L"]";
}
else
{
oss << L" (beyond monitor bounds)";
}
oss << L"\n";
OutputDebugStringW(oss.str().c_str());
}
#endif
// Get current monitor
HMONITOR currentMonitor = MonitorFromPoint(currentPos, MONITOR_DEFAULTTONEAREST);
// Check if cursor is on an outer edge (filtered by wrap mode)
EdgeType edgeType;
if (!m_topology.IsOnOuterEdge(currentMonitor, currentPos, edgeType, mode))
{
#ifdef _DEBUG
static bool lastWasNotOuter = false;
if (!lastWasNotOuter)
{
OutputDebugStringW(L"[CursorWrap] [MOVE] Not on outer edge - no wrapping\n");
lastWasNotOuter = true;
}
#endif
return currentPos; // Not on an outer edge
}
#ifdef _DEBUG
{
const wchar_t* edgeStr = L"Unknown";
switch (edgeType)
{
case EdgeType::Left: edgeStr = L"Left"; break;
case EdgeType::Right: edgeStr = L"Right"; break;
case EdgeType::Top: edgeStr = L"Top"; break;
case EdgeType::Bottom: edgeStr = L"Bottom"; break;
}
std::wostringstream oss;
oss << L"[CursorWrap] [EDGE] Detected outer " << edgeStr << L" edge at (" << currentPos.x << L", " << currentPos.y << L")\n";
OutputDebugStringW(oss.str().c_str());
}
#endif
// Calculate wrap destination
POINT newPos = m_topology.GetWrapDestination(currentMonitor, currentPos, edgeType);
#ifdef _DEBUG
if (newPos.x != currentPos.x || newPos.y != currentPos.y)
{
std::wostringstream oss;
oss << L"[CursorWrap] [WRAP] Position change: (" << currentPos.x << L", " << currentPos.y
<< L") -> (" << newPos.x << L", " << newPos.y << L")\n";
oss << L"[CursorWrap] [WRAP] Delta: (" << (newPos.x - currentPos.x) << L", " << (newPos.y - currentPos.y) << L")\n";
OutputDebugStringW(oss.str().c_str());
}
else
{
OutputDebugStringW(L"[CursorWrap] [WRAP] No position change (same-monitor wrap?)\n");
}
#endif
return newPos;
}

View File

@@ -1,33 +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.
#pragma once
#include <windows.h>
#include <vector>
#include <string>
#include "MonitorTopology.h"
// Core cursor wrapping engine
class CursorWrapCore
{
public:
CursorWrapCore();
void UpdateMonitorInfo();
// Handle mouse move with wrap mode filtering
// wrapMode: 0=Both, 1=VerticalOnly, 2=HorizontalOnly
POINT HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode);
const std::vector<MonitorInfo>& GetMonitors() const { return m_monitors; }
const MonitorTopology& GetTopology() const { return m_topology; }
private:
#ifdef _DEBUG
std::wstring GenerateTopologyJSON() const;
#endif
std::vector<MonitorInfo> m_monitors;
MonitorTopology m_topology;
};

View File

@@ -1,546 +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.
#include "pch.h"
#include "MonitorTopology.h"
#include "../../../common/logger/logger.h"
#include <algorithm>
#include <cmath>
void MonitorTopology::Initialize(const std::vector<MonitorInfo>& monitors)
{
Logger::info(L"======= TOPOLOGY INITIALIZATION START =======");
Logger::info(L"Initializing edge-based topology for {} monitors", monitors.size());
m_monitors = monitors;
m_outerEdges.clear();
m_edgeMap.clear();
if (monitors.empty())
{
Logger::warn(L"No monitors provided to Initialize");
return;
}
// Log monitor details
for (size_t i = 0; i < monitors.size(); ++i)
{
const auto& m = monitors[i];
Logger::info(L"Monitor {}: hMonitor={}, rect=({},{},{},{}), primary={}",
i, reinterpret_cast<uintptr_t>(m.hMonitor),
m.rect.left, m.rect.top, m.rect.right, m.rect.bottom,
m.isPrimary ? L"yes" : L"no");
}
BuildEdgeMap();
IdentifyOuterEdges();
Logger::info(L"Found {} outer edges", m_outerEdges.size());
for (const auto& edge : m_outerEdges)
{
const wchar_t* typeStr = L"Unknown";
switch (edge.type)
{
case EdgeType::Left: typeStr = L"Left"; break;
case EdgeType::Right: typeStr = L"Right"; break;
case EdgeType::Top: typeStr = L"Top"; break;
case EdgeType::Bottom: typeStr = L"Bottom"; break;
}
Logger::info(L"Outer edge: Monitor {} {} at position {}, range [{}, {}]",
edge.monitorIndex, typeStr, edge.position, edge.start, edge.end);
}
Logger::info(L"======= TOPOLOGY INITIALIZATION COMPLETE =======");
}
void MonitorTopology::BuildEdgeMap()
{
// Create edges for each monitor using monitor index (not HMONITOR)
// This is important because HMONITOR handles can change when monitors are
// added/removed dynamically, but indices remain stable within a single
// topology configuration
for (size_t idx = 0; idx < m_monitors.size(); ++idx)
{
const auto& monitor = m_monitors[idx];
int monitorIndex = static_cast<int>(idx);
// Left edge
MonitorEdge leftEdge;
leftEdge.monitorIndex = monitorIndex;
leftEdge.type = EdgeType::Left;
leftEdge.position = monitor.rect.left;
leftEdge.start = monitor.rect.top;
leftEdge.end = monitor.rect.bottom;
leftEdge.isOuter = true; // Will be updated in IdentifyOuterEdges
m_edgeMap[{monitorIndex, EdgeType::Left}] = leftEdge;
// Right edge
MonitorEdge rightEdge;
rightEdge.monitorIndex = monitorIndex;
rightEdge.type = EdgeType::Right;
rightEdge.position = monitor.rect.right - 1;
rightEdge.start = monitor.rect.top;
rightEdge.end = monitor.rect.bottom;
rightEdge.isOuter = true;
m_edgeMap[{monitorIndex, EdgeType::Right}] = rightEdge;
// Top edge
MonitorEdge topEdge;
topEdge.monitorIndex = monitorIndex;
topEdge.type = EdgeType::Top;
topEdge.position = monitor.rect.top;
topEdge.start = monitor.rect.left;
topEdge.end = monitor.rect.right;
topEdge.isOuter = true;
m_edgeMap[{monitorIndex, EdgeType::Top}] = topEdge;
// Bottom edge
MonitorEdge bottomEdge;
bottomEdge.monitorIndex = monitorIndex;
bottomEdge.type = EdgeType::Bottom;
bottomEdge.position = monitor.rect.bottom - 1;
bottomEdge.start = monitor.rect.left;
bottomEdge.end = monitor.rect.right;
bottomEdge.isOuter = true;
m_edgeMap[{monitorIndex, EdgeType::Bottom}] = bottomEdge;
}
}
void MonitorTopology::IdentifyOuterEdges()
{
const int tolerance = 50;
// Check each edge against all other edges to find adjacent ones
for (auto& [key1, edge1] : m_edgeMap)
{
for (const auto& [key2, edge2] : m_edgeMap)
{
if (edge1.monitorIndex == edge2.monitorIndex)
{
continue; // Same monitor
}
// Check if edges are adjacent
if (EdgesAreAdjacent(edge1, edge2, tolerance))
{
edge1.isOuter = false;
break; // This edge has an adjacent monitor
}
}
if (edge1.isOuter)
{
m_outerEdges.push_back(edge1);
}
}
}
bool MonitorTopology::EdgesAreAdjacent(const MonitorEdge& edge1, const MonitorEdge& edge2, int tolerance) const
{
// Edges must be opposite types to be adjacent
bool oppositeTypes = false;
if ((edge1.type == EdgeType::Left && edge2.type == EdgeType::Right) ||
(edge1.type == EdgeType::Right && edge2.type == EdgeType::Left) ||
(edge1.type == EdgeType::Top && edge2.type == EdgeType::Bottom) ||
(edge1.type == EdgeType::Bottom && edge2.type == EdgeType::Top))
{
oppositeTypes = true;
}
if (!oppositeTypes)
{
return false;
}
// Check if positions are within tolerance
if (abs(edge1.position - edge2.position) > tolerance)
{
return false;
}
// Check if perpendicular ranges overlap
int overlapStart = max(edge1.start, edge2.start);
int overlapEnd = min(edge1.end, edge2.end);
return overlapEnd > overlapStart + tolerance;
}
bool MonitorTopology::IsOnOuterEdge(HMONITOR monitor, const POINT& cursorPos, EdgeType& outEdgeType, WrapMode wrapMode) const
{
RECT monitorRect;
if (!GetMonitorRect(monitor, monitorRect))
{
Logger::warn(L"IsOnOuterEdge: GetMonitorRect failed for monitor handle {}", reinterpret_cast<uintptr_t>(monitor));
return false;
}
// Get monitor index for edge map lookup
int monitorIndex = GetMonitorIndex(monitor);
if (monitorIndex < 0)
{
Logger::warn(L"IsOnOuterEdge: Monitor index not found for handle {} at cursor ({}, {})",
reinterpret_cast<uintptr_t>(monitor), cursorPos.x, cursorPos.y);
return false; // Monitor not found in our list
}
// Check each edge type
const int edgeThreshold = 1;
// At corners, multiple edges may match - collect all candidates and try each
// to find one with a valid wrap destination
std::vector<EdgeType> candidateEdges;
// Left edge - only if mode allows horizontal wrapping
if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::HorizontalOnly) &&
cursorPos.x <= monitorRect.left + edgeThreshold)
{
auto it = m_edgeMap.find({monitorIndex, EdgeType::Left});
if (it != m_edgeMap.end() && it->second.isOuter)
{
candidateEdges.push_back(EdgeType::Left);
}
}
// Right edge - only if mode allows horizontal wrapping
if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::HorizontalOnly) &&
cursorPos.x >= monitorRect.right - 1 - edgeThreshold)
{
auto it = m_edgeMap.find({monitorIndex, EdgeType::Right});
if (it != m_edgeMap.end())
{
if (it->second.isOuter)
{
candidateEdges.push_back(EdgeType::Right);
}
// Debug: Log why right edge isn't outer
else
{
Logger::trace(L"IsOnOuterEdge: Monitor {} right edge is NOT outer (inner edge)", monitorIndex);
}
}
}
// Top edge - only if mode allows vertical wrapping
if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::VerticalOnly) &&
cursorPos.y <= monitorRect.top + edgeThreshold)
{
auto it = m_edgeMap.find({monitorIndex, EdgeType::Top});
if (it != m_edgeMap.end() && it->second.isOuter)
{
candidateEdges.push_back(EdgeType::Top);
}
}
// Bottom edge - only if mode allows vertical wrapping
if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::VerticalOnly) &&
cursorPos.y >= monitorRect.bottom - 1 - edgeThreshold)
{
auto it = m_edgeMap.find({monitorIndex, EdgeType::Bottom});
if (it != m_edgeMap.end() && it->second.isOuter)
{
candidateEdges.push_back(EdgeType::Bottom);
}
}
if (candidateEdges.empty())
{
return false;
}
// Try each candidate edge and return first with valid wrap destination
for (EdgeType candidate : candidateEdges)
{
MonitorEdge oppositeEdge = FindOppositeOuterEdge(candidate,
(candidate == EdgeType::Left || candidate == EdgeType::Right) ? cursorPos.y : cursorPos.x);
if (oppositeEdge.monitorIndex >= 0)
{
outEdgeType = candidate;
return true;
}
}
return false;
}
POINT MonitorTopology::GetWrapDestination(HMONITOR fromMonitor, const POINT& cursorPos, EdgeType edgeType) const
{
// Get monitor index for edge map lookup
int monitorIndex = GetMonitorIndex(fromMonitor);
if (monitorIndex < 0)
{
return cursorPos; // Monitor not found
}
auto it = m_edgeMap.find({monitorIndex, edgeType});
if (it == m_edgeMap.end())
{
return cursorPos; // Edge not found
}
const MonitorEdge& fromEdge = it->second;
// Calculate relative position on current edge (0.0 to 1.0)
double relativePos = GetRelativePosition(fromEdge,
(edgeType == EdgeType::Left || edgeType == EdgeType::Right) ? cursorPos.y : cursorPos.x);
// Find opposite outer edge
MonitorEdge oppositeEdge = FindOppositeOuterEdge(edgeType,
(edgeType == EdgeType::Left || edgeType == EdgeType::Right) ? cursorPos.y : cursorPos.x);
if (oppositeEdge.monitorIndex < 0)
{
// No opposite edge found, wrap within same monitor
RECT monitorRect;
if (GetMonitorRect(fromMonitor, monitorRect))
{
POINT result = cursorPos;
switch (edgeType)
{
case EdgeType::Left:
result.x = monitorRect.right - 2;
break;
case EdgeType::Right:
result.x = monitorRect.left + 1;
break;
case EdgeType::Top:
result.y = monitorRect.bottom - 2;
break;
case EdgeType::Bottom:
result.y = monitorRect.top + 1;
break;
}
return result;
}
return cursorPos;
}
// Calculate target position on opposite edge
POINT result;
if (edgeType == EdgeType::Left || edgeType == EdgeType::Right)
{
// Horizontal edge -> vertical movement
result.x = oppositeEdge.position;
result.y = GetAbsolutePosition(oppositeEdge, relativePos);
}
else
{
// Vertical edge -> horizontal movement
result.y = oppositeEdge.position;
result.x = GetAbsolutePosition(oppositeEdge, relativePos);
}
return result;
}
MonitorEdge MonitorTopology::FindOppositeOuterEdge(EdgeType fromEdge, int relativePosition) const
{
EdgeType targetType;
bool findMax; // true = find max position, false = find min position
switch (fromEdge)
{
case EdgeType::Left:
targetType = EdgeType::Right;
findMax = true;
break;
case EdgeType::Right:
targetType = EdgeType::Left;
findMax = false;
break;
case EdgeType::Top:
targetType = EdgeType::Bottom;
findMax = true;
break;
case EdgeType::Bottom:
targetType = EdgeType::Top;
findMax = false;
break;
default:
return { .monitorIndex = -1 }; // Invalid edge type
}
MonitorEdge result = { .monitorIndex = -1 }; // -1 indicates not found
int extremePosition = findMax ? INT_MIN : INT_MAX;
for (const auto& edge : m_outerEdges)
{
if (edge.type != targetType)
{
continue;
}
// Check if this edge overlaps with the relative position
if (relativePosition >= edge.start && relativePosition <= edge.end)
{
if ((findMax && edge.position > extremePosition) ||
(!findMax && edge.position < extremePosition))
{
extremePosition = edge.position;
result = edge;
}
}
}
return result;
}
double MonitorTopology::GetRelativePosition(const MonitorEdge& edge, int coordinate) const
{
if (edge.end == edge.start)
{
return 0.5; // Avoid division by zero
}
int clamped = max(edge.start, min(coordinate, edge.end));
// Use int64_t to avoid overflow warning C26451
int64_t numerator = static_cast<int64_t>(clamped) - static_cast<int64_t>(edge.start);
int64_t denominator = static_cast<int64_t>(edge.end) - static_cast<int64_t>(edge.start);
return static_cast<double>(numerator) / static_cast<double>(denominator);
}
int MonitorTopology::GetAbsolutePosition(const MonitorEdge& edge, double relativePosition) const
{
// Use int64_t to prevent arithmetic overflow during subtraction and multiplication
int64_t range = static_cast<int64_t>(edge.end) - static_cast<int64_t>(edge.start);
int64_t offset = static_cast<int64_t>(relativePosition * static_cast<double>(range));
// Clamp result to int range before returning
int64_t result = static_cast<int64_t>(edge.start) + offset;
return static_cast<int>(result);
}
std::vector<MonitorTopology::GapInfo> MonitorTopology::DetectMonitorGaps() const
{
std::vector<GapInfo> gaps;
const int gapThreshold = 50; // Same as ADJACENCY_TOLERANCE
// Check each pair of monitors
for (size_t i = 0; i < m_monitors.size(); ++i)
{
for (size_t j = i + 1; j < m_monitors.size(); ++j)
{
const auto& m1 = m_monitors[i];
const auto& m2 = m_monitors[j];
// Check vertical overlap
int vOverlapStart = max(m1.rect.top, m2.rect.top);
int vOverlapEnd = min(m1.rect.bottom, m2.rect.bottom);
int vOverlap = vOverlapEnd - vOverlapStart;
if (vOverlap <= 0)
{
continue; // No vertical overlap, skip
}
// Check horizontal gap
int hGap = min(abs(m1.rect.right - m2.rect.left), abs(m2.rect.right - m1.rect.left));
if (hGap > gapThreshold)
{
GapInfo gap;
gap.monitor1Index = static_cast<int>(i);
gap.monitor2Index = static_cast<int>(j);
gap.horizontalGap = hGap;
gap.verticalOverlap = vOverlap;
gaps.push_back(gap);
}
}
}
return gaps;
}
HMONITOR MonitorTopology::GetMonitorFromPoint(const POINT& pt) const
{
return MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST);
}
bool MonitorTopology::GetMonitorRect(HMONITOR monitor, RECT& rect) const
{
// First try direct HMONITOR comparison
for (const auto& monitorInfo : m_monitors)
{
if (monitorInfo.hMonitor == monitor)
{
rect = monitorInfo.rect;
return true;
}
}
// Fallback: If direct comparison fails, try matching by current monitor info
MONITORINFO mi{};
mi.cbSize = sizeof(MONITORINFO);
if (GetMonitorInfo(monitor, &mi))
{
for (const auto& monitorInfo : m_monitors)
{
if (monitorInfo.rect.left == mi.rcMonitor.left &&
monitorInfo.rect.top == mi.rcMonitor.top &&
monitorInfo.rect.right == mi.rcMonitor.right &&
monitorInfo.rect.bottom == mi.rcMonitor.bottom)
{
rect = monitorInfo.rect;
return true;
}
}
}
return false;
}
HMONITOR MonitorTopology::GetMonitorFromRect(const RECT& rect) const
{
return MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST);
}
int MonitorTopology::GetMonitorIndex(HMONITOR monitor) const
{
// First try direct HMONITOR comparison (fast and accurate)
for (size_t i = 0; i < m_monitors.size(); ++i)
{
if (m_monitors[i].hMonitor == monitor)
{
return static_cast<int>(i);
}
}
// Fallback: If direct comparison fails (e.g., handle changed after display reconfiguration),
// try matching by position. Get the monitor's current rect and find matching stored rect.
MONITORINFO mi{};
mi.cbSize = sizeof(MONITORINFO);
if (GetMonitorInfo(monitor, &mi))
{
for (size_t i = 0; i < m_monitors.size(); ++i)
{
// Match by rect bounds
if (m_monitors[i].rect.left == mi.rcMonitor.left &&
m_monitors[i].rect.top == mi.rcMonitor.top &&
m_monitors[i].rect.right == mi.rcMonitor.right &&
m_monitors[i].rect.bottom == mi.rcMonitor.bottom)
{
Logger::trace(L"GetMonitorIndex: Found monitor {} via rect fallback (handle changed from {} to {})",
i, reinterpret_cast<uintptr_t>(m_monitors[i].hMonitor), reinterpret_cast<uintptr_t>(monitor));
return static_cast<int>(i);
}
}
// Log all stored monitors vs the requested one for debugging
Logger::warn(L"GetMonitorIndex: No match found. Requested monitor rect=({},{},{},{})",
mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom);
for (size_t i = 0; i < m_monitors.size(); ++i)
{
Logger::warn(L" Stored monitor {}: rect=({},{},{},{})",
i, m_monitors[i].rect.left, m_monitors[i].rect.top,
m_monitors[i].rect.right, m_monitors[i].rect.bottom);
}
}
else
{
Logger::warn(L"GetMonitorIndex: GetMonitorInfo failed for handle {}", reinterpret_cast<uintptr_t>(monitor));
}
return -1; // Not found
}

View File

@@ -1,106 +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.
#pragma once
#include <windows.h>
#include <vector>
#include <map>
// Monitor information structure
struct MonitorInfo
{
HMONITOR hMonitor; // Direct handle for accurate lookup after display changes
RECT rect;
bool isPrimary;
int monitorId;
};
// Edge type enumeration
enum class EdgeType
{
Left = 0,
Right = 1,
Top = 2,
Bottom = 3
};
// Wrap mode enumeration (matches Settings UI dropdown)
enum class WrapMode
{
Both = 0, // Wrap in both directions
VerticalOnly = 1, // Only wrap top/bottom
HorizontalOnly = 2 // Only wrap left/right
};
// Represents a single edge of a monitor
struct MonitorEdge
{
int monitorIndex; // Index into m_monitors (stable across display changes)
EdgeType type;
int start; // For vertical edges: Y start; horizontal: X start
int end; // For vertical edges: Y end; horizontal: X end
int position; // For vertical edges: X coord; horizontal: Y coord
bool isOuter; // True if no adjacent monitor touches this edge
};
// Monitor topology helper - manages edge-based monitor layout
struct MonitorTopology
{
void Initialize(const std::vector<MonitorInfo>& monitors);
// Check if cursor is on an outer edge of the given monitor
// wrapMode filters which edges are considered (Both, VerticalOnly, HorizontalOnly)
bool IsOnOuterEdge(HMONITOR monitor, const POINT& cursorPos, EdgeType& outEdgeType, WrapMode wrapMode) const;
// Get the wrap destination point for a cursor on an outer edge
POINT GetWrapDestination(HMONITOR fromMonitor, const POINT& cursorPos, EdgeType edgeType) const;
// Get monitor at point (helper)
HMONITOR GetMonitorFromPoint(const POINT& pt) const;
// Get monitor rectangle (helper)
bool GetMonitorRect(HMONITOR monitor, RECT& rect) const;
// Get outer edges collection (for debugging)
const std::vector<MonitorEdge>& GetOuterEdges() const { return m_outerEdges; }
// Detect gaps between monitors that should be snapped together
struct GapInfo {
int monitor1Index;
int monitor2Index;
int horizontalGap;
int verticalOverlap;
};
std::vector<GapInfo> DetectMonitorGaps() const;
private:
std::vector<MonitorInfo> m_monitors;
std::vector<MonitorEdge> m_outerEdges;
// Map from (monitor index, edge type) to edge info
// Using monitor index instead of HMONITOR because HMONITOR handles can change
// when monitors are added/removed dynamically
std::map<std::pair<int, EdgeType>, MonitorEdge> m_edgeMap;
// Helper to resolve HMONITOR to monitor index at runtime
int GetMonitorIndex(HMONITOR monitor) const;
// Helper to get consistent HMONITOR from RECT
HMONITOR GetMonitorFromRect(const RECT& rect) const;
void BuildEdgeMap();
void IdentifyOuterEdges();
// Check if two edges are adjacent (within tolerance)
bool EdgesAreAdjacent(const MonitorEdge& edge1, const MonitorEdge& edge2, int tolerance = 50) const;
// Find the opposite outer edge for wrapping
MonitorEdge FindOppositeOuterEdge(EdgeType fromEdge, int relativePosition) const;
// Calculate relative position along an edge (0.0 to 1.0)
double GetRelativePosition(const MonitorEdge& edge, int coordinate) const;
// Convert relative position to absolute coordinate on target edge
int GetAbsolutePosition(const MonitorEdge& edge, double relativePosition) const;
};

File diff suppressed because it is too large Load Diff

View File

@@ -10,10 +10,10 @@ public struct ApplicationWrapper
{
public struct WindowPositionWrapper
{
[JsonPropertyName("X")]
[JsonPropertyName("x")]
public int X { get; set; }
[JsonPropertyName("Y")]
[JsonPropertyName("y")]
public int Y { get; set; }
[JsonPropertyName("width")]

View File

@@ -547,15 +547,6 @@ public partial class MainListPage : DynamicListPage,
// above "git" from "whatever"
max = max + extensionTitleMatch;
// Apply a penalty to fallback items so they rank below direct matches.
// Fallbacks that dynamically match queries (like RDP connections) should
// appear after apps and direct command matches.
if (isFallback && max > 1)
{
// Reduce fallback scores by 50% to prioritize direct matches
max = max * 0.5;
}
var matchSomething = max
+ (isAliasMatch ? 9001 : (isAliasSubstringMatch ? 1 : 0));

View File

@@ -2,6 +2,7 @@
// 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.Reflection;
using System.Runtime.CompilerServices;
namespace Microsoft.CmdPal.UI.Helpers;
@@ -17,41 +18,19 @@ internal static class BuildInfo
// Runtime AOT detection
public static bool IsNativeAot => !RuntimeFeature.IsDynamicCodeSupported;
// build-time values
public static bool PublishTrimmed
{
get
{
#if BUILD_INFO_PUBLISH_TRIMMED
return true;
#else
return false;
#endif
}
}
// From assembly metadata (build-time values)
public static bool PublishTrimmed => GetBoolMetadata("PublishTrimmed", false);
// build-time values
public static bool PublishAot
{
get
{
#if BUILD_INFO_PUBLISH_AOT
return true;
#else
return false;
#endif
}
}
// From assembly metadata (build-time values)
public static bool PublishAot => GetBoolMetadata("PublishAot", false);
public static bool IsCiBuild
{
get
{
#if BUILD_INFO_CIBUILD
return true;
#else
return false;
#endif
}
}
public static bool IsCiBuild => GetBoolMetadata("CIBuild", false);
private static string? GetMetadata(string key) =>
Assembly.GetExecutingAssembly()
.GetCustomAttributes<AssemblyMetadataAttribute>()
.FirstOrDefault(a => a.Key == key)?.Value;
private static bool GetBoolMetadata(string key, bool defaultValue) =>
bool.TryParse(GetMetadata(key), out var result) ? result : defaultValue;
}

View File

@@ -53,7 +53,7 @@
<PropertyGroup>
<!-- This disables the auto-generated main, so we can be single-instanced -->
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>
<DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
</PropertyGroup>
<!-- BODGY: XES Versioning and WinAppSDK get into a fight about the app manifest, which breaks WinAppSDK. -->
@@ -291,15 +291,24 @@
</ItemGroup>
<!-- </AdaptiveCardsWorkaround> -->
<!-- Build information -->
<PropertyGroup Condition=" '$(PublishAot)' == 'true' ">
<DefineConstants>$(DefineConstants);BUILD_INFO_PUBLISH_AOT</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(PublishTrimmed)' == 'true' ">
<DefineConstants>$(DefineConstants);BUILD_INFO_PUBLISH_TRIMMED</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(CIBuild)' == 'true' ">
<DefineConstants>$(DefineConstants);BUILD_INFO_CIBUILD</DefineConstants>
</PropertyGroup>
<!-- Metadata for build information -->
<ItemGroup>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>PublishTrimmed</_Parameter1>
<_Parameter2>$(PublishTrimmed)</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>PublishAot</_Parameter1>
<_Parameter2>$(PublishAot)</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>CIBuild</_Parameter1>
<_Parameter2>$(CIBuild)</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>CommandPaletteBranding</_Parameter1>
<_Parameter2>$(CommandPaletteBranding)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
</Project>

View File

@@ -372,7 +372,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<value>Windows Command Palette</value>
</data>
<data name="Settings_GeneralPage_About_SettingsExpander.Description" xml:space="preserve">
<value>© 2026. All rights reserved.</value>
<value>© 2025. All rights reserved.</value>
</data>
<data name="Settings_GeneralPage_About_GithubLink_Hyperlink.Content" xml:space="preserve">
<value>View GitHub repository</value>

View File

@@ -55,7 +55,7 @@ public class BasicTests : CommandPaletteTestBase
SetTimeAndDaterExtensionSearchBox("year");
Assert.IsNotNull(this.Find<NavigationViewItem>("2026"));
Assert.IsNotNull(this.Find<NavigationViewItem>("2025"));
}
[TestMethod]

View File

@@ -17,25 +17,12 @@ public sealed partial class AppListItem : ListItem
{
private readonly AppCommand _appCommand;
private readonly AppItem _app;
private readonly Lazy<Details> _details;
private readonly Lazy<Task<IconInfo?>> _iconLoadTask;
private readonly Lazy<Task<Details>> _detailsLoadTask;
private InterlockedBoolean _isLoadingIcon;
private InterlockedBoolean _isLoadingDetails;
public override IDetails? Details
{
get
{
if (_isLoadingDetails.Set())
{
_ = LoadDetailsAsync();
}
return base.Details;
}
set => base.Details = value;
}
public override IDetails? Details { get => _details.Value; set => base.Details = value; }
public override IIconInfo? Icon
{
@@ -65,20 +52,14 @@ public sealed partial class AppListItem : ListItem
MoreCommands = AddPinCommands(_app.Commands!, isPinned);
_detailsLoadTask = new Lazy<Task<Details>>(BuildDetails);
_iconLoadTask = new Lazy<Task<IconInfo?>>(async () => await FetchIcon(useThumbnails));
}
_details = new Lazy<Details>(() =>
{
var t = BuildDetails();
t.Wait();
return t.Result;
});
private async Task LoadDetailsAsync()
{
try
{
Details = await _detailsLoadTask.Value;
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to load details for {AppIdentifier}\n{ex}");
}
_iconLoadTask = new Lazy<Task<IconInfo?>>(async () => await FetchIcon(useThumbnails));
}
private async Task LoadIconAsync()
@@ -117,16 +98,18 @@ public sealed partial class AppListItem : ListItem
}
else
{
// do nothing if we fail to load an icon.
// Logging it would be too NOISY, there's really no need.
if (!string.IsNullOrEmpty(_app.IcoPath))
try
{
heroImage = await TryLoadThumbnail(_app.IcoPath, jumbo: true, logOnFailure: false);
var stream = await ThumbnailHelper.GetThumbnail(_app.ExePath, true);
if (stream is not null)
{
heroImage = IconInfo.FromStream(stream);
}
}
if (heroImage == null && !string.IsNullOrEmpty(_app.ExePath))
catch (Exception)
{
heroImage = await TryLoadThumbnail(_app.ExePath, jumbo: true, logOnFailure: false);
// do nothing if we fail to load an icon.
// Logging it would be too NOISY, there's really no need.
}
}
@@ -149,18 +132,25 @@ public sealed partial class AppListItem : ListItem
if (useThumbnails)
{
if (!string.IsNullOrEmpty(_app.IcoPath))
try
{
icon = await TryLoadThumbnail(_app.IcoPath, jumbo: false, logOnFailure: true);
var stream = await ThumbnailHelper.GetThumbnail(_app.ExePath);
if (stream is not null)
{
icon = IconInfo.FromStream(stream);
}
}
catch (Exception ex)
{
Logger.LogDebug($"Failed to load icon for {AppIdentifier}:\n{ex}");
}
if (icon == null && !string.IsNullOrEmpty(_app.ExePath))
{
icon = await TryLoadThumbnail(_app.ExePath, jumbo: false, logOnFailure: true);
}
icon = icon ?? new IconInfo(_app.IcoPath);
}
else
{
icon = new IconInfo(_app.IcoPath);
}
icon ??= new IconInfo(_app.IcoPath);
return icon;
}
@@ -193,25 +183,4 @@ public sealed partial class AppListItem : ListItem
return newCommands.ToArray();
}
private async Task<IconInfo?> TryLoadThumbnail(string path, bool jumbo, bool logOnFailure)
{
try
{
var stream = await ThumbnailHelper.GetThumbnail(path, jumbo);
if (stream is not null)
{
return IconInfo.FromStream(stream);
}
}
catch (Exception ex)
{
if (logOnFailure)
{
Logger.LogDebug($"Failed to load icon {path} for {AppIdentifier}:\n{ex}");
}
}
return null;
}
}

View File

@@ -1052,6 +1052,9 @@ public class Win32Program : IProgram
app.FullPath) :
app.IcoPath;
icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ?
app.FullPath :
icoPath;
return new AppItem()
{
Name = app.Name,

View File

@@ -41,13 +41,16 @@ internal static class FancyZonesDataService
try
{
// 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}");
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}");
var editorMonitors = editorParams.Monitors;
if (editorMonitors is null || editorMonitors.Count == 0)
@@ -71,23 +74,6 @@ internal static class FancyZonesDataService
}
}
/// <summary>
/// Requests FancyZones to save the current monitor configuration and reads the file.
/// This is a best-effort approach for performance: we send the save request and immediately
/// read the file without waiting. If the file hasn't been updated yet, the next call will
/// see the updated data since FancyZones processes the message asynchronously.
/// </summary>
private static EditorParameters.ParamsWrapper ReadEditorParametersWithRefresh()
{
// Request FancyZones to save the current monitor configuration.
// This is fire-and-forget for performance - we don't wait for the save to complete.
// If this is the first call after a monitor change, we may read stale data, but the
// next call will see the updated file since FancyZones will have processed the message.
FancyZonesNotifier.NotifySaveEditorParameters();
return FancyZonesDataIO.ReadEditorParameters();
}
public static IReadOnlyList<FancyZonesLayoutDescriptor> GetLayouts()
{
Logger.LogInfo($"GetLayouts: Starting. LayoutTemplatesPath={FZPaths.LayoutTemplates} CustomLayoutsPath={FZPaths.CustomLayouts}");

View File

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

View File

@@ -7,7 +7,6 @@
<OutputType>WinExe</OutputType>
<RootNamespace>PowerToysExtension</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>..\..\..\..\runner\svgs\icon.ico</ApplicationIcon>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<EnableMsixTooling>false</EnableMsixTooling>
<WindowsPackageType>None</WindowsPackageType>

View File

@@ -69,14 +69,14 @@ internal sealed partial class SampleMarkdownImagesPage : ContentPage
### Web URL
```xml
![painting](https://raw.githubusercontent.com/microsoft/PowerToys/refs/heads/main/doc/images/overview/Original/AdvancedPaste.png)
![painting](https://i.imgur.com/93XJSNh.png)
```
![painting](https://raw.githubusercontent.com/microsoft/PowerToys/refs/heads/main/doc/images/overview/Original/AdvancedPaste.png)
![painting](https://i.imgur.com/93XJSNh.png)
```xml
![painting](https://raw.githubusercontent.com/microsoft/PowerToys/refs/heads/main/doc/images/overview/Original/AdvancedPaste.png?--x-cmdpal-fit=fit)
![painting](https://i.imgur.com/93XJSNh.png?--x-cmdpal-fit=fit)
```
![painting](https://raw.githubusercontent.com/microsoft/PowerToys/refs/heads/main/doc/images/overview/Original/AdvancedPaste.png?--x-cmdpal-fit=fit)
![painting](https://i.imgur.com/93XJSNh.png?--x-cmdpal-fit=fit)
### File URL (PNG):
```xml

View File

@@ -81,7 +81,7 @@ Result:
Result:
![painting](https://raw.githubusercontent.com/microsoft/PowerToys/refs/heads/main/doc/images/overview/Original/AdvancedPaste.png)
![painting](https://i.imgur.com/93XJSNh.png)
### Links

View File

@@ -31,7 +31,7 @@ internal static class Program
Console.InputEncoding = Encoding.Unicode;
// Initialize logger to file (same as other modules)
CliLogger.Initialize("\\Image Resizer\\CLI");
CliLogger.Initialize("\\ImageResizer\\Logs");
CliLogger.Info($"ImageResizerCLI started with {args.Length} argument(s)");
try

View File

@@ -126,10 +126,13 @@ namespace ImageResizer.Properties
h => ncc.CollectionChanged -= h,
() => settings.CustomSize = new CustomSize());
// Reset is used instead of Replace to avoid ArgumentOutOfRangeException
// when notifying changes for virtual items (CustomSize/AiSize) that exist
// outside the bounds of the underlying _sizes collection.
Assert.AreEqual(NotifyCollectionChangedAction.Reset, result.Arguments.Action);
Assert.AreEqual(NotifyCollectionChangedAction.Replace, result.Arguments.Action);
Assert.AreEqual(1, result.Arguments.NewItems.Count);
Assert.AreEqual(settings.CustomSize, result.Arguments.NewItems[0]);
Assert.AreEqual(0, result.Arguments.NewStartingIndex);
Assert.AreEqual(1, result.Arguments.OldItems.Count);
Assert.AreEqual(originalCustomSize, result.Arguments.OldItems[0]);
Assert.AreEqual(0, result.Arguments.OldStartingIndex);
}
[TestMethod]

View File

@@ -20,7 +20,7 @@ namespace ImageResizer
{
public partial class App : Application, IDisposable
{
private const string LogSubFolder = "\\Image Resizer\\Logs";
private const string LogSubFolder = "\\ImageResizer\\Logs";
/// <summary>
/// Gets cached AI availability state, checked at app startup.
@@ -67,27 +67,12 @@ namespace ImageResizer
// Fix for .net 3.1.19 making Image Resizer not adapt to DPI changes.
NativeMethods.SetProcessDPIAware();
// TODO: Re-enable AI Super Resolution in next release by removing this #if block
// Temporarily disable AI Super Resolution feature (hide from UI but keep code)
#if true // Set to false to re-enable AI Super Resolution
AiAvailabilityState = AiAvailabilityState.NotSupported;
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
// Skip AI detection mode as well
if (e?.Args?.Length > 0 && e.Args[0] == "--detect-ai")
{
Services.AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
Environment.Exit(0);
return;
}
#else
// Check for AI detection mode (called by Runner in background)
if (e?.Args?.Length > 0 && e.Args[0] == "--detect-ai")
{
RunAiDetectionMode();
return;
}
#endif
if (PowerToys.GPOWrapperProjection.GPOWrapper.GetConfiguredImageResizerEnabledValue() == PowerToys.GPOWrapperProjection.GpoRuleConfigured.Disabled)
{
@@ -188,8 +173,31 @@ namespace ImageResizer
/// </summary>
private static AiAvailabilityState CheckAiAvailability()
{
// AI feature disabled - always return NotSupported
return AiAvailabilityState.NotSupported;
try
{
// Check Windows AI service model ready state
// it's so slow, why?
var readyState = Services.WinAiSuperResolutionService.GetModelReadyState();
// Map AI service state to our availability state
switch (readyState)
{
case Microsoft.Windows.AI.AIFeatureReadyState.Ready:
return AiAvailabilityState.Ready;
case Microsoft.Windows.AI.AIFeatureReadyState.NotReady:
return AiAvailabilityState.ModelNotReady;
case Microsoft.Windows.AI.AIFeatureReadyState.DisabledByUser:
case Microsoft.Windows.AI.AIFeatureReadyState.NotSupportedOnCurrentSystem:
default:
return AiAvailabilityState.NotSupported;
}
}
catch (Exception)
{
return AiAvailabilityState.NotSupported;
}
}
/// <summary>

View File

@@ -216,15 +216,27 @@ namespace ImageResizer.Properties
{
if (e.PropertyName == nameof(Models.CustomSize))
{
var oldCustomSize = _customSize;
_customSize = settings.CustomSize;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Replace,
_customSize,
oldCustomSize,
_sizes.Count));
}
else if (e.PropertyName == nameof(Models.AiSize))
{
var oldAiSize = _aiSize;
_aiSize = settings.AiSize;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Replace,
_aiSize,
oldAiSize,
_sizes.Count + 1));
}
else if (e.PropertyName == nameof(Sizes))
{

View File

@@ -39,8 +39,29 @@ namespace ImageResizer.Services
/// </summary>
public static AiAvailabilityState? LoadCache()
{
// Cache disabled - always return null to use default value
return null;
try
{
if (!File.Exists(CachePath))
{
return null;
}
var json = File.ReadAllText(CachePath);
var cache = JsonSerializer.Deserialize<AiCapabilityCache>(json);
if (!IsCacheValid(cache))
{
return null;
}
return (AiAvailabilityState)cache.State;
}
catch (Exception ex)
{
// Read failure (file locked, corrupted JSON, etc.) - return null and use fallback
Logger.LogError($"Failed to load AI cache: {ex.Message}");
return null;
}
}
/// <summary>
@@ -49,8 +70,32 @@ namespace ImageResizer.Services
/// </summary>
public static void SaveCache(AiAvailabilityState state)
{
// Cache disabled - do not save anything
return;
try
{
var cache = new AiCapabilityCache
{
Version = CacheVersion,
State = (int)state,
WindowsBuild = Environment.OSVersion.Version.ToString(),
Architecture = RuntimeInformation.ProcessArchitecture.ToString(),
Timestamp = DateTime.UtcNow.ToString("o"),
};
var dir = Path.GetDirectoryName(CachePath);
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
var json = JsonSerializer.Serialize(cache, SerializerOptions);
File.WriteAllText(CachePath, json);
Logger.LogInfo($"AI cache saved: {state}");
}
catch (Exception ex)
{
Logger.LogError($"Failed to save AI cache: {ex.Message}");
}
}
/// <summary>

View File

@@ -388,13 +388,6 @@ namespace Peek.UI
IsErrorVisible = true;
}
public void ShowError(string message)
{
IsErrorVisible = false;
ErrorMessage = message;
IsErrorVisible = true;
}
private void NavigationThrottleTimer_Tick(object? sender, object e)
{
if (sender == null)

View File

@@ -50,8 +50,7 @@
Item="{x:Bind ViewModel.CurrentItem, Mode=OneWay}"
NumberOfFiles="{x:Bind ViewModel.DisplayItemCount, Mode=OneWay}"
PreviewSizeChanged="FilePreviewer_PreviewSizeChanged"
ScalingFactor="{x:Bind ViewModel.ScalingFactor, Mode=OneWay}"
Visibility="{x:Bind ContentVisibility(ViewModel.IsErrorVisible), Mode=OneWay}" />
ScalingFactor="{x:Bind ViewModel.ScalingFactor, Mode=OneWay}" />
<InfoBar
x:Name="ErrorInfoBar"
@@ -60,7 +59,6 @@
Grid.RowSpan="2"
Margin="4,0,4,6"
VerticalAlignment="Bottom"
Closed="ErrorInfoBar_Closed"
IsOpen="{x:Bind ViewModel.IsErrorVisible, Mode=TwoWay}"
Message="{x:Bind ViewModel.ErrorMessage, Mode=OneWay}"
Severity="Error" />

View File

@@ -14,7 +14,6 @@ using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Peek.Common.Constants;
using Peek.Common.Extensions;
using Peek.Common.Helpers;
using Peek.FilePreviewer.Models;
using Peek.FilePreviewer.Previewers;
using Peek.UI.Extensions;
@@ -196,14 +195,6 @@ namespace Peek.UI
bootTime.Start();
ViewModel.Initialize(selectedItem);
// If no files were found (e.g., user is typing in rename/search box, or in virtual folders),
// don't show anything - just return silently to avoid stealing focus
if (ViewModel.CurrentItem == null)
{
return;
}
ViewModel.ScalingFactor = this.GetMonitorScale();
this.Content.KeyUp += Content_KeyUp;
@@ -311,24 +302,5 @@ namespace Peek.UI
{
themeListener?.Dispose();
}
/// <summary>
/// Returns Visibility.Collapsed when error is showing, Visibility.Visible when not.
/// </summary>
public Visibility ContentVisibility(bool isErrorVisible)
{
return isErrorVisible ? Visibility.Collapsed : Visibility.Visible;
}
/// <summary>
/// Handle InfoBar closed - if there's no current item, close the window.
/// </summary>
private void ErrorInfoBar_Closed(InfoBar sender, InfoBarClosedEventArgs args)
{
if (ViewModel.CurrentItem == null)
{
Uninitialize();
}
}
}
}

View File

@@ -341,10 +341,6 @@
<value>No more files to preview.</value>
<comment>The message to show when there are no files remaining to preview.</comment>
</data>
<data name="NoFilesSelected" xml:space="preserve">
<value>No files selected or this folder is not supported for preview.</value>
<comment>Displayed when Peek is activated in a virtual folder (like Home or Recent) where file selection cannot be retrieved.</comment>
</data>
<data name="DeleteFileError_NotFound" xml:space="preserve">
<value>The file cannot be found. Please check if the file has been moved, renamed, or deleted.</value>
<comment>Displayed if the file or path was not found</comment>

View File

@@ -33,27 +33,23 @@ static std::wstring SanitizeAndNormalize(const std::wstring& input)
// Normalize to NFC (Precomposed).
// Get the size needed for the normalized string, including null terminator.
int sizeEstimate = NormalizeString(NormalizationC, sanitized.c_str(), -1, nullptr, 0);
if (sizeEstimate <= 0)
int size = NormalizeString(NormalizationC, sanitized.c_str(), -1, nullptr, 0);
if (size <= 0)
{
return sanitized; // Return unaltered if normalization fails.
}
// Perform the normalization.
std::wstring normalized;
normalized.resize(sizeEstimate);
int actualSize = NormalizeString(NormalizationC, sanitized.c_str(), -1, &normalized[0], sizeEstimate);
normalized.resize(size);
NormalizeString(NormalizationC, sanitized.c_str(), -1, &normalized[0], size);
if (actualSize <= 0)
// Remove the explicit null terminator added by NormalizeString.
if (!normalized.empty() && normalized.back() == L'\0')
{
// Normalization failed, return sanitized string.
return sanitized;
normalized.pop_back();
}
// Resize to actual size minus the null terminator.
// actualSize includes the null terminator when input length is -1.
normalized.resize(static_cast<size_t>(actualSize) - 1);
return normalized;
}

View File

@@ -695,38 +695,6 @@ TEST_METHOD(VerifyUnicodeAndWhitespaceNormalizationRegex)
VerifyNormalizationHelper(UseRegularExpressions);
}
TEST_METHOD(VerifyRegexMetacharacterDollarSign)
{
CComPtr<IPowerRenameRegEx> renameRegEx;
Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK);
DWORD flags = UseRegularExpressions;
Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK);
PWSTR result = nullptr;
Assert::IsTrue(renameRegEx->PutSearchTerm(L"$") == S_OK);
Assert::IsTrue(renameRegEx->PutReplaceTerm(L"_end") == S_OK);
unsigned long index = {};
Assert::IsTrue(renameRegEx->Replace(L"test.txt", &result, index) == S_OK);
Assert::AreEqual(L"test.txt_end", result);
CoTaskMemFree(result);
}
TEST_METHOD(VerifyRegexMetacharacterCaret)
{
CComPtr<IPowerRenameRegEx> renameRegEx;
Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK);
DWORD flags = UseRegularExpressions;
Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK);
PWSTR result = nullptr;
Assert::IsTrue(renameRegEx->PutSearchTerm(L"^") == S_OK);
Assert::IsTrue(renameRegEx->PutReplaceTerm(L"start_") == S_OK);
unsigned long index = {};
Assert::IsTrue(renameRegEx->Replace(L"test.txt", &result, index) == S_OK);
Assert::AreEqual(L"start_test.txt", result);
CoTaskMemFree(result);
}
#ifndef TESTS_PARTIAL
};
}

View File

@@ -173,10 +173,6 @@
<value>Settings\tDouble-click</value>
<comment>Don't localize "\t" as that is what separates the click portion to be right aligned in the menu.</comment>
</data>
<data name="SETTINGS_MENU_TEXT_LEFTCLICK" xml:space="preserve">
<value>Settings\tLeft-click</value>
<comment>Don't localize "\t" as that is what separates the click portion to be right aligned in the menu. This is shown when Quick Access is disabled.</comment>
</data>
<data name="DOCUMENTATION_MENU_TEXT" xml:space="preserve">
<value>Documentation</value>
</data>

View File

@@ -193,102 +193,6 @@ 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;
@@ -463,21 +367,11 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save)
if (json::has(general_configs, L"show_theme_adaptive_tray_icon", json::JsonValueType::Boolean))
{
bool new_theme_adaptive = general_configs.GetNamedBoolean(L"show_theme_adaptive_tray_icon");
Logger::info(L"apply_general_settings: show_theme_adaptive_tray_icon current={}, new={}",
show_theme_adaptive_tray_icon, new_theme_adaptive);
if (show_theme_adaptive_tray_icon != new_theme_adaptive)
{
show_theme_adaptive_tray_icon = new_theme_adaptive;
set_tray_icon_theme_adaptive(show_theme_adaptive_tray_icon);
}
else
{
Logger::info(L"apply_general_settings: show_theme_adaptive_tray_icon unchanged, skipping update");
}
}
else
{
Logger::warn(L"apply_general_settings: show_theme_adaptive_tray_icon not found in config");
}
if (json::has(general_configs, L"ignored_conflict_properties", json::JsonValueType::Object))

View File

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

View File

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

View File

@@ -81,36 +81,3 @@ void Trace::UpdateDownloadCompleted(bool success, const std::wstring& version)
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}
void Trace::TrayIconLeftClick(bool quickAccessEnabled)
{
TraceLoggingWriteWrapper(
g_hProvider,
"TrayIcon_LeftClick",
TraceLoggingBoolean(quickAccessEnabled, "QuickAccessEnabled"),
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}
void Trace::TrayIconDoubleClick(bool quickAccessEnabled)
{
TraceLoggingWriteWrapper(
g_hProvider,
"TrayIcon_DoubleClick",
TraceLoggingBoolean(quickAccessEnabled, "QuickAccessEnabled"),
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}
void Trace::TrayIconRightClick(bool quickAccessEnabled)
{
TraceLoggingWriteWrapper(
g_hProvider,
"TrayIcon_RightClick",
TraceLoggingBoolean(quickAccessEnabled, "QuickAccessEnabled"),
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}

View File

@@ -13,9 +13,4 @@ public:
// Auto-update telemetry
static void UpdateCheckCompleted(bool success, bool updateAvailable, const std::wstring& fromVersion, const std::wstring& toVersion);
static void UpdateDownloadCompleted(bool success, const std::wstring& version);
// Tray icon interaction telemetry
static void TrayIconLeftClick(bool quickAccessEnabled);
static void TrayIconDoubleClick(bool quickAccessEnabled);
static void TrayIconRightClick(bool quickAccessEnabled);
};

View File

@@ -7,7 +7,6 @@
#include "centralized_kb_hook.h"
#include "quick_access_host.h"
#include "hotkey_conflict_detector.h"
#include "trace.h"
#include <Windows.h>
#include <common/utils/resources.h>
@@ -15,7 +14,6 @@
#include <common/logger/logger.h>
#include <common/utils/elevation.h>
#include <common/Themes/theme_listener.h>
#include <common/Themes/theme_helpers.h>
#include "bug_report.h"
namespace
@@ -41,7 +39,6 @@ namespace
bool double_click_timer_running = false;
bool double_clicked = false;
POINT tray_icon_click_point;
std::optional<bool> last_quick_access_state; // Track the last known Quick Access state
static ThemeListener theme_listener;
static bool theme_adaptive_enabled = false;
@@ -132,9 +129,6 @@ void click_timer_elapsed()
double_click_timer_running = false;
if (!double_clicked)
{
// Log telemetry for single click (confirmed it's not a double click)
Trace::TrayIconLeftClick(get_general_settings().enableQuickAccess);
if (get_general_settings().enableQuickAccess)
{
open_quick_access_flyout_window();
@@ -200,21 +194,6 @@ LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam
case WM_RBUTTONUP:
case WM_CONTEXTMENU:
{
bool quick_access_enabled = get_general_settings().enableQuickAccess;
// Log telemetry
Trace::TrayIconRightClick(quick_access_enabled);
// Reload menu if Quick Access state has changed or is first time
if (h_menu && (!last_quick_access_state.has_value() || quick_access_enabled != last_quick_access_state.value()))
{
DestroyMenu(h_menu);
h_menu = nullptr;
h_sub_menu = nullptr;
}
last_quick_access_state = quick_access_enabled;
if (!h_menu)
{
h_menu = LoadMenu(reinterpret_cast<HINSTANCE>(&__ImageBase), MAKEINTRESOURCE(ID_TRAY_MENU));
@@ -222,39 +201,17 @@ LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam
if (h_menu)
{
static std::wstring settings_menuitem_label = GET_RESOURCE_STRING(IDS_SETTINGS_MENU_TEXT);
static std::wstring settings_menuitem_label_leftclick = GET_RESOURCE_STRING(IDS_SETTINGS_MENU_TEXT_LEFTCLICK);
static std::wstring close_menuitem_label = GET_RESOURCE_STRING(IDS_CLOSE_MENU_TEXT);
static std::wstring submit_bug_menuitem_label = GET_RESOURCE_STRING(IDS_SUBMIT_BUG_TEXT);
static std::wstring documentation_menuitem_label = GET_RESOURCE_STRING(IDS_DOCUMENTATION_MENU_TEXT);
static std::wstring quick_access_menuitem_label = GET_RESOURCE_STRING(IDS_QUICK_ACCESS_MENU_TEXT);
// Update Settings menu text based on Quick Access state
if (quick_access_enabled)
{
change_menu_item_text(ID_SETTINGS_MENU_COMMAND, settings_menuitem_label.data());
}
else
{
change_menu_item_text(ID_SETTINGS_MENU_COMMAND, settings_menuitem_label_leftclick.data());
}
change_menu_item_text(ID_SETTINGS_MENU_COMMAND, settings_menuitem_label.data());
change_menu_item_text(ID_CLOSE_MENU_COMMAND, close_menuitem_label.data());
change_menu_item_text(ID_REPORT_BUG_COMMAND, submit_bug_menuitem_label.data());
bool bug_report_disabled = is_bug_report_running();
EnableMenuItem(h_sub_menu, ID_REPORT_BUG_COMMAND, MF_BYCOMMAND | (bug_report_disabled ? MF_GRAYED : MF_ENABLED));
change_menu_item_text(ID_DOCUMENTATION_MENU_COMMAND, documentation_menuitem_label.data());
change_menu_item_text(ID_QUICK_ACCESS_MENU_COMMAND, quick_access_menuitem_label.data());
// Hide or show Quick Access menu item based on setting
if (!h_sub_menu)
{
h_sub_menu = GetSubMenu(h_menu, 0);
}
if (!quick_access_enabled)
{
// Remove Quick Access menu item when disabled
DeleteMenu(h_sub_menu, ID_QUICK_ACCESS_MENU_COMMAND, MF_BYCOMMAND);
}
}
if (!h_sub_menu)
{
@@ -285,9 +242,6 @@ LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam
}
case WM_LBUTTONDBLCLK:
{
// Log telemetry
Trace::TrayIconDoubleClick(get_general_settings().enableQuickAccess);
double_clicked = true;
open_settings_window(std::nullopt);
break;
@@ -319,19 +273,12 @@ static HICON get_icon(Theme theme)
{
std::wstring icon_path = get_module_folderpath();
icon_path += theme == Theme::Dark ? L"\\svgs\\PowerToysWhite.ico" : L"\\svgs\\PowerToysDark.ico";
Logger::trace(L"get_icon: Loading icon from path: {}", icon_path);
HICON icon = static_cast<HICON>(LoadImage(NULL,
return static_cast<HICON>(LoadImage(NULL,
icon_path.c_str(),
IMAGE_ICON,
0,
0,
LR_LOADFROMFILE | LR_DEFAULTSIZE | LR_SHARED));
if (!icon)
{
Logger::warn(L"get_icon: Failed to load icon from {}, error: {}", icon_path, GetLastError());
}
return icon;
}
@@ -339,7 +286,7 @@ static void handle_theme_change()
{
if (theme_adaptive_enabled)
{
tray_icon_data.hIcon = get_icon(ThemeHelpers::GetSystemTheme());
tray_icon_data.hIcon = get_icon(theme_listener.AppTheme);
Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data);
}
}
@@ -356,7 +303,7 @@ void start_tray_icon(bool isProcessElevated, bool theme_adaptive)
{
theme_adaptive_enabled = theme_adaptive;
auto h_instance = reinterpret_cast<HINSTANCE>(&__ImageBase);
HICON const icon = theme_adaptive ? get_icon(ThemeHelpers::GetSystemTheme()) : LoadIcon(h_instance, MAKEINTRESOURCE(APPICON));
HICON const icon = theme_adaptive ? get_icon(theme_listener.AppTheme) : LoadIcon(h_instance, MAKEINTRESOURCE(APPICON));
if (icon)
{
UINT id_tray_icon = 1;
@@ -403,7 +350,7 @@ void start_tray_icon(bool isProcessElevated, bool theme_adaptive)
ChangeWindowMessageFilterEx(hwnd, WM_COMMAND, MSGFLT_ALLOW, nullptr);
tray_icon_created = Shell_NotifyIcon(NIM_ADD, &tray_icon_data) == TRUE;
theme_listener.AddSystemThemeChangedHandler(&handle_theme_change);
theme_listener.AddChangedHandler(&handle_theme_change);
// Register callback to update bug report menu item status
BugReportManager::instance().register_callback([](bool isRunning) {
@@ -427,45 +374,13 @@ void set_tray_icon_visible(bool shouldIconBeVisible)
void set_tray_icon_theme_adaptive(bool theme_adaptive)
{
Logger::info(L"set_tray_icon_theme_adaptive: Called with theme_adaptive={}, current theme_adaptive_enabled={}",
theme_adaptive, theme_adaptive_enabled);
auto h_instance = reinterpret_cast<HINSTANCE>(&__ImageBase);
HICON icon = nullptr;
if (theme_adaptive)
{
icon = get_icon(ThemeHelpers::GetSystemTheme());
if (!icon)
{
Logger::warn(L"set_tray_icon_theme_adaptive: Failed to load theme adaptive icon, falling back to default");
}
}
// If not requesting adaptive icon, or if adaptive icon failed to load, use default icon
if (!icon)
{
icon = LoadIcon(h_instance, MAKEINTRESOURCE(APPICON));
if (theme_adaptive && icon)
{
// We requested adaptive but had to fall back, so update the flag
theme_adaptive = false;
Logger::info(L"set_tray_icon_theme_adaptive: Using default icon as fallback");
}
}
theme_adaptive_enabled = theme_adaptive;
auto h_instance = reinterpret_cast<HINSTANCE>(&__ImageBase);
HICON const icon = theme_adaptive ? get_icon(theme_listener.AppTheme) : LoadIcon(h_instance, MAKEINTRESOURCE(APPICON));
if (icon)
{
tray_icon_data.hIcon = icon;
BOOL result = Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data);
Logger::info(L"set_tray_icon_theme_adaptive: Icon updated, theme_adaptive_enabled={}, Shell_NotifyIcon result={}",
theme_adaptive_enabled, result);
}
else
{
Logger::error(L"set_tray_icon_theme_adaptive: Failed to load any icon");
Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data);
}
}

View File

@@ -7,7 +7,6 @@
<TargetFramework>net9.0-windows10.0.26100.0</TargetFramework>
<RootNamespace>Microsoft.PowerToys.QuickAccess</RootNamespace>
<AssemblyName>PowerToys.QuickAccess</AssemblyName>
<ApplicationIcon>..\..\runner\svgs\icon.ico</ApplicationIcon>
<UseWinUI>true</UseWinUI>
<WindowsPackageType>None</WindowsPackageType>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>

View File

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

View File

@@ -3,9 +3,7 @@
// 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;
@@ -21,9 +19,9 @@ public interface IQuickAccessCoordinator
Task<bool> ShowDocumentationAsync();
bool UpdateModuleEnabled(ModuleType moduleType, bool isEnabled);
void NotifyUserSettingsInteraction();
void SendSortOrderUpdate(GeneralSettings generalSettings);
bool UpdateModuleEnabled(ModuleType moduleType, bool isEnabled);
void ReportBug();

View File

@@ -55,8 +55,37 @@ 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)
=> TrySendIpcMessage($"{{\"module_status\": {{\"{ModuleHelper.GetModuleKey(moduleType)}\": {isEnabled.ToString().ToLowerInvariant()}}}}}", "module status update");
{
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;
}
public void ReportBug()
{
@@ -102,10 +131,20 @@ internal sealed class QuickAccessCoordinator : IQuickAccessCoordinator, IDisposa
Logger.LogDebug($"QuickAccessCoordinator received IPC payload: {message}");
}
public void SendSortOrderUpdate(GeneralSettings generalSettings)
private void SendGeneralSettingsUpdate(GeneralSettings updatedSettings)
{
var outgoing = new OutGoingGeneralSettings(generalSettings);
TrySendIpcMessage(outgoing.ToString(), "sort order update");
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");
}
private bool TrySendIpcMessage(string payload, string operationDescription)

View File

@@ -22,7 +22,6 @@ 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;
@@ -31,9 +30,6 @@ 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
@@ -44,9 +40,9 @@ public sealed class AllAppsViewModel : Observable
if (_generalSettings.DashboardSortOrder != value)
{
_generalSettings.DashboardSortOrder = value;
_coordinator.SendSortOrderUpdate(_generalSettings);
_settingsUtils.SaveSettings(_generalSettings.ToJsonString(), _generalSettings.GetModuleName());
OnPropertyChanged();
SortFlyoutMenuItems();
RefreshFlyoutMenuItems();
}
}
}
@@ -58,6 +54,7 @@ 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;
@@ -90,6 +87,7 @@ public sealed class AllAppsViewModel : Observable
_dispatcherQueue.TryEnqueue(() =>
{
_generalSettings = newSettings;
_generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
OnPropertyChanged(nameof(DashboardSortOrder));
RefreshFlyoutMenuItems();
});
@@ -122,55 +120,30 @@ public sealed class AllAppsViewModel : Observable
}
}
SortFlyoutMenuItems();
}
private void SortFlyoutMenuItems()
{
if (_isSorting)
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;
}
lock (_sortLock)
for (int i = 0; i < sortedItems.Count; i++)
{
_isSorting = true;
try
var item = sortedItems[i];
var oldIndex = FlyoutMenuItems.IndexOf(item);
if (oldIndex != -1 && 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;
});
FlyoutMenuItems.Move(oldIndex, i);
}
}
}
@@ -178,17 +151,14 @@ public sealed class AllAppsViewModel : Observable
private void EnabledChangedOnUI(ModuleListItem item)
{
var flyoutItem = (FlyoutMenuItem)item;
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)
if (_coordinator.UpdateModuleEnabled(flyoutItem.Tag, flyoutItem.IsEnabled))
{
flyoutItem.UpdateStatus(!isEnabled);
return;
_coordinator.NotifyUserSettingsInteraction();
}
}
_coordinator.UpdateModuleEnabled(flyoutItem.Tag, flyoutItem.IsEnabled);
SortFlyoutMenuItems();
private void ModuleEnabledChangedOnSettingsPage()
{
RefreshFlyoutMenuItems();
}
}

View File

@@ -34,21 +34,21 @@ public sealed class AdvancedPastePasteAsFileAction : Observable, IAdvancedPasteA
public AdvancedPasteAdditionalAction PasteAsTxtFile
{
get => _pasteAsTxtFile;
init => Set(ref _pasteAsTxtFile, value ?? new());
init => Set(ref _pasteAsTxtFile, value);
}
[JsonPropertyName(PropertyNames.PasteAsPngFile)]
public AdvancedPasteAdditionalAction PasteAsPngFile
{
get => _pasteAsPngFile;
init => Set(ref _pasteAsPngFile, value ?? new());
init => Set(ref _pasteAsPngFile, value);
}
[JsonPropertyName(PropertyNames.PasteAsHtmlFile)]
public AdvancedPasteAdditionalAction PasteAsHtmlFile
{
get => _pasteAsHtmlFile;
init => Set(ref _pasteAsHtmlFile, value ?? new());
init => Set(ref _pasteAsHtmlFile, value);
}
[JsonIgnore]

View File

@@ -93,11 +93,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("custom-actions")]
[CmdConfigureIgnoreAttribute]
public AdvancedPasteCustomActions CustomActions { get; set; }
public AdvancedPasteCustomActions CustomActions { get; init; }
[JsonPropertyName("additional-actions")]
[CmdConfigureIgnoreAttribute]
public AdvancedPasteAdditionalActions AdditionalActions { get; set; }
public AdvancedPasteAdditionalActions AdditionalActions { get; init; }
[JsonPropertyName("paste-ai-configuration")]
[CmdConfigureIgnoreAttribute]

View File

@@ -32,14 +32,14 @@ public sealed class AdvancedPasteTranscodeAction : Observable, IAdvancedPasteAct
public AdvancedPasteAdditionalAction TranscodeToMp3
{
get => _transcodeToMp3;
init => Set(ref _transcodeToMp3, value ?? new());
init => Set(ref _transcodeToMp3, value);
}
[JsonPropertyName(PropertyNames.TranscodeToMp4)]
public AdvancedPasteAdditionalAction TranscodeToMp4
{
get => _transcodeToMp4;
init => Set(ref _transcodeToMp4, value ?? new());
init => Set(ref _transcodeToMp4, value);
}
[JsonIgnore]

View File

@@ -22,15 +22,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("disable_wrap_during_drag")]
public BoolProperty DisableWrapDuringDrag { get; set; }
[JsonPropertyName("wrap_mode")]
public IntProperty WrapMode { get; set; }
public CursorWrapProperties()
{
ActivationShortcut = DefaultActivationShortcut;
AutoActivate = new BoolProperty(false);
DisableWrapDuringDrag = new BoolProperty(true);
WrapMode = new IntProperty(0); // 0=Both (default), 1=VerticalOnly, 2=HorizontalOnly
}
}
}

View File

@@ -47,16 +47,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
// This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration()
{
bool settingsUpgraded = false;
// Add WrapMode property if it doesn't exist (for users upgrading from older versions)
if (Properties.WrapMode == null)
{
Properties.WrapMode = new IntProperty(0); // Default to Both
settingsUpgraded = true;
}
return settingsUpgraded;
return false;
}
}
}

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ 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;
@@ -226,6 +227,7 @@ 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
@@ -255,10 +257,11 @@ namespace Microsoft.PowerToys.Settings.UI
else if (ShowScoobe)
{
PowerToysTelemetry.Log.WriteEvent(new ScoobeStartedEvent());
ScoobeWindow newScoobeWindow = new ScoobeWindow();
newScoobeWindow.Activate();
OobeWindow scoobeWindow = new OobeWindow(OOBE.Enums.PowerToysModules.WhatsNew);
scoobeWindow.Activate();
scoobeWindow.ExtendsContentIntoTitleBar = true;
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(WindowNative.GetWindowHandle(settingsWindow));
SetScoobeWindow(newScoobeWindow);
SetOobeWindow(scoobeWindow);
}
}
}
@@ -336,7 +339,6 @@ namespace Microsoft.PowerToys.Settings.UI
private static MainWindow settingsWindow;
private static OobeWindow oobeWindow;
private static ScoobeWindow scoobeWindow;
public static void ClearSettingsWindow()
{
@@ -363,21 +365,6 @@ namespace Microsoft.PowerToys.Settings.UI
oobeWindow = null;
}
public static ScoobeWindow GetScoobeWindow()
{
return scoobeWindow;
}
public static void SetScoobeWindow(ScoobeWindow window)
{
scoobeWindow = window;
}
public static void ClearScoobeWindow()
{
scoobeWindow = null;
}
public static Type GetPage(string settingWindow)
{
switch (settingWindow)

View File

@@ -20,6 +20,9 @@ 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)
@@ -32,12 +35,10 @@ 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 = WindowNative.GetWindowHandle(this);
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
var placement = WindowHelper.DeserializePlacementOrDefault(hWnd);
if (createHidden)
{
@@ -120,12 +121,16 @@ namespace Microsoft.PowerToys.Settings.UI
// open whats new window
ShellPage.SetOpenWhatIsNewCallback(() =>
{
if (App.GetScoobeWindow() == null)
if (App.GetOobeWindow() == null)
{
App.SetScoobeWindow(new ScoobeWindow());
App.SetOobeWindow(new OobeWindow(OOBE.Enums.PowerToysModules.WhatsNew));
}
else
{
App.GetOobeWindow().SetAppWindow(OOBE.Enums.PowerToysModules.WhatsNew);
}
App.GetScoobeWindow().Activate();
App.GetOobeWindow().Activate();
});
this.InitializeComponent();
@@ -182,7 +187,7 @@ namespace Microsoft.PowerToys.Settings.UI
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
WindowHelper.SerializePlacement(hWnd);
if (App.GetOobeWindow() == null && App.GetScoobeWindow() == null)
if (App.GetOobeWindow() == null)
{
App.ClearSettingsWindow();
}

View File

@@ -1,44 +1,60 @@
<Page
<UserControl
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="48" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TitleBar
<Button
x:Name="PaneToggleBtn"
Width="48"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Click="PaneToggleBtn_Click"
Style="{StaticResource PaneToggleButtonStyle}" />
<Grid
x:Name="AppTitleBar"
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="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"
Height="16"
Margin="16,0,0,0"
Source="/Assets/Settings/icon.ico" />
</TitleBar.LeftHeader>
</TitleBar>
<TextBlock
x:Name="AppTitleBarText"
x:Uid="OobeWindow_TitleTxt"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
TextWrapping="NoWrap" />
</StackPanel>
</Grid>
<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
@@ -158,16 +174,34 @@
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/ZoomIt.png}"
Tag="ZoomIt" />
</NavigationView.MenuItems>
<NavigationView.PaneFooter>
<NavigationView.FooterMenuItems>
<NavigationViewItem
x:Uid="Shell_WhatsNew"
AutomationProperties.AutomationId="WhatIsNewNavItem"
Icon="{ui:FontIcon Glyph=&#xE789;}"
Tapped="WhatIsNewItem_Tapped" />
</NavigationView.PaneFooter>
Tag="WhatsNew" />
</NavigationView.FooterMenuItems>
<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>
</Page>
</UserControl>

View File

@@ -5,17 +5,18 @@
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 Microsoft.UI.Xaml.Input;
using WinRT.Interop;
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
public sealed partial class OobeShellPage : Page
public sealed partial class OobeShellPage : UserControl
{
public static Func<string> RunSharedEventCallback { get; set; }
@@ -62,6 +63,7 @@ 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>();
@@ -200,6 +202,12 @@ 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",
@@ -221,7 +229,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
public void OnClosing()
{
NavigationViewItem selectedItem = this.navigationView.SelectedItem as NavigationViewItem;
Microsoft.UI.Xaml.Controls.NavigationViewItem selectedItem = this.navigationView.SelectedItem as Microsoft.UI.Xaml.Controls.NavigationViewItem;
if (selectedItem != null)
{
Modules[(int)(PowerToysModules)Enum.Parse(typeof(PowerToysModules), (string)selectedItem.Tag, true)].LogClosingModuleEvent();
@@ -230,22 +238,19 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
public void NavigateToModule(PowerToysModules selectedModule)
{
navigationView.SelectedItem = navigationView.MenuItems[(int)selectedModule];
}
private static void OpenScoobeWindow()
{
if (App.GetScoobeWindow() == null)
if (selectedModule == PowerToysModules.WhatsNew)
{
App.SetScoobeWindow(new ScoobeWindow());
navigationView.SelectedItem = navigationView.FooterMenuItems[0];
}
else
{
navigationView.SelectedItem = navigationView.MenuItems[(int)selectedModule];
}
App.GetScoobeWindow().Activate();
}
private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
private void NavigationView_SelectionChanged(Microsoft.UI.Xaml.Controls.NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewSelectionChangedEventArgs args)
{
NavigationViewItem selectedItem = args.SelectedItem as NavigationViewItem;
Microsoft.UI.Xaml.Controls.NavigationViewItem selectedItem = args.SelectedItem as Microsoft.UI.Xaml.Controls.NavigationViewItem;
if (selectedItem != null)
{
@@ -273,7 +278,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;
@@ -306,37 +311,43 @@ 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)
{
// Select the first module by default
if (navigationView.MenuItems.Count > 0)
{
navigationView.SelectedItem = navigationView.MenuItems[0];
}
SetTitleBar();
}
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;
PaneToggleBtn.Visibility = Visibility.Visible;
AppTitleBar.Margin = new Thickness(48, 0, 0, 0);
AppTitleBarText.Margin = new Thickness(12, 0, 0, 0);
}
else
{
TitleBarIcon.Margin = new Thickness(16, 0, 0, 0); // Workaround, see XAML comment
AppTitleBar.IsPaneToggleButtonVisible = false;
PaneToggleBtn.Visibility = Visibility.Collapsed;
AppTitleBar.Margin = new Thickness(16, 0, 0, 0);
AppTitleBarText.Margin = new Thickness(16, 0, 0, 0);
}
}
private void TitleBar_PaneButtonClick(TitleBar sender, object args)
private void PaneToggleBtn_Click(object sender, RoutedEventArgs e)
{
navigationView.IsPaneOpen = !navigationView.IsPaneOpen;
}
private void WhatIsNewItem_Tapped(object sender, TappedRoutedEventArgs e)
{
OpenScoobeWindow();
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -44,7 +44,6 @@ 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;
@@ -61,7 +60,7 @@ namespace Microsoft.PowerToys.Settings.UI
this.SizeChanged += OobeWindow_SizeChanged;
var loader = ResourceLoaderInstance.ResourceLoader;
var loader = Helpers.ResourceLoaderInstance.ResourceLoader;
Title = loader.GetString("OobeWindow_Title");
if (shellPage != null)

View File

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

View File

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

View File

@@ -8,6 +8,8 @@ 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;
@@ -48,30 +50,26 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void WhatsNewButton_Click(object sender, RoutedEventArgs e)
{
if (App.GetScoobeWindow() == null)
if (App.GetOobeWindow() == null)
{
App.SetScoobeWindow(new ScoobeWindow());
App.SetOobeWindow(new OobeWindow(PowerToysModules.WhatsNew));
}
else
{
App.GetOobeWindow().SetAppWindow(PowerToysModules.WhatsNew);
}
App.GetScoobeWindow().Activate();
App.GetOobeWindow().Activate();
}
private void SortAlphabetical_Click(object sender, RoutedEventArgs e)
{
ViewModel.DashboardSortOrder = DashboardSortOrder.Alphabetical;
if (sender is ToggleMenuFlyoutItem item)
{
item.IsChecked = true;
}
}
private void SortByStatus_Click(object sender, RoutedEventArgs e)
{
ViewModel.DashboardSortOrder = DashboardSortOrder.ByStatus;
if (sender is ToggleMenuFlyoutItem item)
{
item.IsChecked = true;
}
}
}
}

View File

@@ -47,13 +47,6 @@
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind ViewModel.IsCursorWrapEnabled, Mode=OneWay}">
<CheckBox x:Uid="MouseUtils_CursorWrap_DisableWrapDuringDrag" IsChecked="{x:Bind ViewModel.CursorWrapDisableWrapDuringDrag, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="MouseUtilsCursorWrapWrapMode" x:Uid="MouseUtils_CursorWrap_WrapMode">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind Path=ViewModel.CursorWrapWrapMode, Mode=TwoWay}">
<ComboBoxItem x:Uid="MouseUtils_CursorWrap_WrapMode_Both" />
<ComboBoxItem x:Uid="MouseUtils_CursorWrap_WrapMode_VerticalOnly" />
<ComboBoxItem x:Uid="MouseUtils_CursorWrap_WrapMode_HorizontalOnly" />
</ComboBox>
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</controls:SettingsGroup>

View File

@@ -2419,15 +2419,9 @@ 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.Title" xml:space="preserve">
<data name="OobeWindow_TitleTxt.Text" 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>
@@ -2728,18 +2722,6 @@ From there, simply click on one of the supported files in the File Explorer and
<data name="MouseUtils_CursorWrap_AutoActivate.Content" xml:space="preserve">
<value>Automatically activate on utility startup</value>
</data>
<data name="MouseUtils_CursorWrap_WrapMode.Header" xml:space="preserve">
<value>Wrap mode</value>
</data>
<data name="MouseUtils_CursorWrap_WrapMode_VerticalOnly.Content" xml:space="preserve">
<value>Vertical only</value>
</data>
<data name="MouseUtils_CursorWrap_WrapMode_HorizontalOnly.Content" xml:space="preserve">
<value>Horizontal only</value>
</data>
<data name="MouseUtils_CursorWrap_WrapMode_Both.Content" xml:space="preserve">
<value>Vertical and horizontal</value>
</data>
<data name="Oobe_MouseUtils_MousePointerCrosshairs.Text" xml:space="preserve">
<value>Mouse Pointer Crosshairs</value>
<comment>Mouse as in the hardware peripheral.</comment>

View File

@@ -76,24 +76,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
GeneralSettingsConfig = settingsRepository.SettingsConfig;
// To obtain the settings configurations of Advanced Paste.
ArgumentNullException.ThrowIfNull(advancedPasteSettingsRepository);
// To obtain the settings configurations of Fancy zones.
ArgumentNullException.ThrowIfNull(settingsRepository);
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
_advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig ?? throw new ArgumentException("SettingsConfig cannot be null", nameof(advancedPasteSettingsRepository));
ArgumentNullException.ThrowIfNull(advancedPasteSettingsRepository);
if (_advancedPasteSettings.Properties is null)
{
throw new ArgumentException("AdvancedPasteSettings.Properties cannot be null", nameof(advancedPasteSettingsRepository));
}
// Ensure AdditionalActions and CustomActions are initialized to prevent null reference exceptions
// This handles legacy settings files that may be missing these properties
_advancedPasteSettings.Properties.AdditionalActions ??= new AdvancedPasteAdditionalActions();
_advancedPasteSettings.Properties.CustomActions ??= new AdvancedPasteCustomActions();
_advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig;
AttachConfigurationHandlers();
@@ -101,7 +93,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
SendConfigMSG = ipcMSGCallBackFunc;
_additionalActions = _advancedPasteSettings.Properties.AdditionalActions;
_customActions = _advancedPasteSettings.Properties.CustomActions.Value ?? new ObservableCollection<AdvancedPasteCustomAction>();
_customActions = _advancedPasteSettings.Properties.CustomActions.Value;
SetupSettingsFileWatcher();
@@ -477,13 +469,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public PasteAIConfiguration PasteAIConfiguration
{
get
{
// Ensure PasteAIConfiguration is never null for XAML binding
_advancedPasteSettings.Properties.PasteAIConfiguration ??= new PasteAIConfiguration();
return _advancedPasteSettings.Properties.PasteAIConfiguration;
}
get => _advancedPasteSettings.Properties.PasteAIConfiguration;
set
{
if (!ReferenceEquals(value, _advancedPasteSettings.Properties.PasteAIConfiguration))

View File

@@ -29,8 +29,6 @@ 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;
@@ -53,9 +51,6 @@ 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
@@ -85,17 +80,15 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
get => generalSettingsConfig.DashboardSortOrder;
set
{
if (_dashboardSortOrder != value)
if (Set(ref _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();
}
}
@@ -110,7 +103,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
@@ -135,14 +128,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
dispatcher.BeginInvoke(() =>
{
generalSettingsConfig = newSettings;
// Update local field and notify UI if sort order changed
if (_dashboardSortOrder != generalSettingsConfig.DashboardSortOrder)
{
_dashboardSortOrder = generalSettingsConfig.DashboardSortOrder;
OnPropertyChanged(nameof(DashboardSortOrder));
}
generalSettingsConfig.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
ModuleEnabledChangedOnSettingsPage();
});
}
@@ -212,58 +198,40 @@ 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()
{
if (_isSorting)
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;
}
lock (_sortLock)
// Otherwise, update the collection in place using Move to avoid UI glitches.
for (int i = 0; i < sortedItems.Count; i++)
{
_isSorting = true;
try
var currentItem = sortedItems[i];
var currentIndex = AllModules.IndexOf(currentItem);
if (currentIndex != -1 && 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;
});
AllModules.Move(currentIndex, i);
}
}
// Notify that DashboardSortOrder changed so the menu updates its checked state.
OnPropertyChanged(nameof(DashboardSortOrder));
}
/// <summary>
@@ -311,25 +279,10 @@ 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
{
// 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);
Views.ShellPage.UpdateGeneralSettingsCallback(dashboardListItem.Tag, isEnabled);
if (dashboardListItem.Tag == ModuleType.NewPlus && isEnabled == true)
{

View File

@@ -113,9 +113,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// Null-safe access in case property wasn't upgraded yet - default to TRUE
_cursorWrapDisableWrapDuringDrag = CursorWrapSettingsConfig.Properties.DisableWrapDuringDrag?.Value ?? true;
// Null-safe access in case property wasn't upgraded yet - default to 0 (Both)
_cursorWrapWrapMode = CursorWrapSettingsConfig.Properties.WrapMode?.Value ?? 0;
int isEnabled = 0;
Utilities.NativeMethods.SystemParametersInfo(Utilities.NativeMethods.SPI_GETCLIENTAREAANIMATION, 0, ref isEnabled, 0);
@@ -1086,34 +1083,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public int CursorWrapWrapMode
{
get
{
return _cursorWrapWrapMode;
}
set
{
if (value != _cursorWrapWrapMode)
{
_cursorWrapWrapMode = value;
// Ensure the property exists before setting value
if (CursorWrapSettingsConfig.Properties.WrapMode == null)
{
CursorWrapSettingsConfig.Properties.WrapMode = new IntProperty(value);
}
else
{
CursorWrapSettingsConfig.Properties.WrapMode.Value = value;
}
NotifyCursorWrapPropertyChanged();
}
}
}
public void NotifyCursorWrapPropertyChanged([CallerMemberName] string propertyName = null)
{
OnPropertyChanged(propertyName);
@@ -1185,6 +1154,5 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private bool _isCursorWrapEnabled;
private bool _cursorWrapAutoActivate;
private bool _cursorWrapDisableWrapDuringDrag; // Will be initialized in constructor from settings
private int _cursorWrapWrapMode; // 0=Both, 1=VerticalOnly, 2=HorizontalOnly
}
}