mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-28 06:57:27 +01:00
Compare commits
20 Commits
leilzh/fix
...
0.97.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5466ab6cf8 | ||
|
|
bf19bdc1ee | ||
|
|
2441621b80 | ||
|
|
1ca9d10ff5 | ||
|
|
b438f15f6e | ||
|
|
48de981f50 | ||
|
|
9ab6559fac | ||
|
|
8cc32d3098 | ||
|
|
3b7eedfb67 | ||
|
|
d7e1b18ba4 | ||
|
|
0206fdbec1 | ||
|
|
bdaf644f02 | ||
|
|
329c8c2616 | ||
|
|
b081e413b1 | ||
|
|
290fa01adf | ||
|
|
4cde968c9b | ||
|
|
e148a89288 | ||
|
|
1dddf9fa2c | ||
|
|
0d59b9f790 | ||
|
|
e314485e85 |
61
.github/actions/spell-check/expect.txt
vendored
61
.github/actions/spell-check/expect.txt
vendored
@@ -22,7 +22,6 @@ ADate
|
||||
ADDSTRING
|
||||
ADDUNDORECORD
|
||||
ADifferent
|
||||
adjacents
|
||||
ADMINS
|
||||
adml
|
||||
admx
|
||||
@@ -99,7 +98,6 @@ ASYNCWINDOWPLACEMENT
|
||||
ASYNCWINDOWPOS
|
||||
atl
|
||||
ATRIOX
|
||||
ATX
|
||||
aumid
|
||||
authenticode
|
||||
AUTOBUDDY
|
||||
@@ -216,8 +214,10 @@ cim
|
||||
CImage
|
||||
cla
|
||||
CLASSDC
|
||||
classguid
|
||||
classmethod
|
||||
CLASSNOTAVAILABLE
|
||||
claude
|
||||
CLEARTYPE
|
||||
clickable
|
||||
clickonce
|
||||
@@ -256,7 +256,6 @@ colorhistory
|
||||
colorhistorylimit
|
||||
COLORKEY
|
||||
colorref
|
||||
Convs
|
||||
comctl
|
||||
comdlg
|
||||
comexp
|
||||
@@ -277,6 +276,7 @@ CONTEXTHELP
|
||||
CONTEXTMENUHANDLER
|
||||
contractversion
|
||||
CONTROLPARENT
|
||||
Convs
|
||||
copiedcolorrepresentation
|
||||
coppied
|
||||
copyable
|
||||
@@ -292,7 +292,6 @@ cpcontrols
|
||||
cph
|
||||
cplusplus
|
||||
CPower
|
||||
cppcoreguidelines
|
||||
cpptools
|
||||
cppvsdbg
|
||||
cppwinrt
|
||||
@@ -321,7 +320,7 @@ CURRENTDIR
|
||||
CURSORINFO
|
||||
cursorpos
|
||||
CURSORSHOWING
|
||||
CURSORWRAP
|
||||
cursorwrap
|
||||
customaction
|
||||
CUSTOMACTIONTEST
|
||||
CUSTOMFORMATPLACEHOLDER
|
||||
@@ -344,12 +343,14 @@ datareader
|
||||
datatracker
|
||||
dataversion
|
||||
Dayof
|
||||
dbcc
|
||||
DBID
|
||||
DBLCLKS
|
||||
DBLEPSILON
|
||||
DBPROP
|
||||
DBPROPIDSET
|
||||
DBPROPSET
|
||||
DBT
|
||||
DCBA
|
||||
DCOM
|
||||
DComposition
|
||||
@@ -367,8 +368,7 @@ DEFAULTICON
|
||||
defaultlib
|
||||
DEFAULTONLY
|
||||
DEFAULTSIZE
|
||||
DEFAULTTONEAREST
|
||||
Defaulttonearest
|
||||
defaulttonearest
|
||||
DEFAULTTONULL
|
||||
DEFAULTTOPRIMARY
|
||||
DEFERERASE
|
||||
@@ -390,14 +390,19 @@ DESKTOPVERTRES
|
||||
devblogs
|
||||
devdocs
|
||||
devenv
|
||||
DEVICEINTERFACE
|
||||
devicetype
|
||||
DEVINTERFACE
|
||||
devmgmt
|
||||
DEVMODE
|
||||
DEVMODEW
|
||||
DEVNODES
|
||||
devpal
|
||||
DEVTYP
|
||||
dfx
|
||||
DIALOGEX
|
||||
digicert
|
||||
diffs
|
||||
digicert
|
||||
DINORMAL
|
||||
DISABLEASACTIONKEY
|
||||
DISABLENOSCROLL
|
||||
@@ -540,7 +545,6 @@ fdx
|
||||
FErase
|
||||
fesf
|
||||
FFFF
|
||||
FInc
|
||||
Figma
|
||||
FILEEXPLORER
|
||||
fileexploreraddons
|
||||
@@ -561,6 +565,7 @@ FILESYSPATH
|
||||
Filetime
|
||||
FILEVERSION
|
||||
FILTERMODE
|
||||
FInc
|
||||
findfast
|
||||
findmymouse
|
||||
FIXEDFILEINFO
|
||||
@@ -662,13 +667,14 @@ HCRYPTPROV
|
||||
hcursor
|
||||
hcwhite
|
||||
hdc
|
||||
HDEVNOTIFY
|
||||
hdr
|
||||
hdrop
|
||||
hdwwiz
|
||||
Helpline
|
||||
helptext
|
||||
HGFE
|
||||
hgdiobj
|
||||
HGFE
|
||||
hglobal
|
||||
hhk
|
||||
HHmmssfff
|
||||
@@ -744,9 +750,9 @@ HWNDPARENT
|
||||
HWNDPREV
|
||||
hyjiacan
|
||||
IAI
|
||||
icf
|
||||
ICONERROR
|
||||
ICONLOCATION
|
||||
icf
|
||||
IDCANCEL
|
||||
IDD
|
||||
idk
|
||||
@@ -837,8 +843,8 @@ jeli
|
||||
jfif
|
||||
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
||||
jjw
|
||||
JOBOBJECT
|
||||
jobject
|
||||
JOBOBJECT
|
||||
jpe
|
||||
jpnime
|
||||
Jsons
|
||||
@@ -882,6 +888,7 @@ Ldr
|
||||
LEFTALIGN
|
||||
LEFTSCROLLBAR
|
||||
LEFTTEXT
|
||||
leftclick
|
||||
LError
|
||||
LEVELID
|
||||
LExit
|
||||
@@ -924,9 +931,9 @@ LOWORD
|
||||
lparam
|
||||
LPBITMAPINFOHEADER
|
||||
LPCFHOOKPROC
|
||||
lpch
|
||||
LPCITEMIDLIST
|
||||
LPCLSID
|
||||
lpch
|
||||
lpcmi
|
||||
LPCMINVOKECOMMANDINFO
|
||||
LPCREATESTRUCT
|
||||
@@ -942,6 +949,7 @@ LPMONITORINFO
|
||||
LPOSVERSIONINFOEXW
|
||||
LPQUERY
|
||||
lprc
|
||||
LPrivate
|
||||
LPSAFEARRAY
|
||||
lpstr
|
||||
lpsz
|
||||
@@ -951,7 +959,6 @@ lptpm
|
||||
LPTR
|
||||
LPTSTR
|
||||
lpv
|
||||
LPrivate
|
||||
LPW
|
||||
lpwcx
|
||||
lpwndpl
|
||||
@@ -995,18 +1002,19 @@ mber
|
||||
MBM
|
||||
MBR
|
||||
Mbuttondown
|
||||
mcp
|
||||
MDICHILD
|
||||
MDL
|
||||
mdtext
|
||||
mdtxt
|
||||
mdwn
|
||||
meme
|
||||
mcp
|
||||
memicmp
|
||||
MENUITEMINFO
|
||||
MENUITEMINFOW
|
||||
MERGECOPY
|
||||
MERGEPAINT
|
||||
Metacharacter
|
||||
metadatamatters
|
||||
Metadatas
|
||||
metafile
|
||||
@@ -1037,6 +1045,7 @@ mmsys
|
||||
mobileredirect
|
||||
mockapi
|
||||
MODALFRAME
|
||||
modelcontextprotocol
|
||||
MODESPRUNED
|
||||
MONITORENUMPROC
|
||||
MONITORINFO
|
||||
@@ -1080,9 +1089,9 @@ MSLLHOOKSTRUCT
|
||||
Mso
|
||||
msrc
|
||||
msstore
|
||||
mstsc
|
||||
msvcp
|
||||
MT
|
||||
mstsc
|
||||
MTND
|
||||
MULTIPLEUSE
|
||||
multizone
|
||||
@@ -1092,11 +1101,11 @@ muxxc
|
||||
muxxh
|
||||
MVPs
|
||||
mvvm
|
||||
myorg
|
||||
myrepo
|
||||
MVVMTK
|
||||
MWBEx
|
||||
MYICON
|
||||
myorg
|
||||
myrepo
|
||||
NAMECHANGE
|
||||
namespaceanddescendants
|
||||
nao
|
||||
@@ -1237,10 +1246,8 @@ opencode
|
||||
OPENFILENAME
|
||||
openrdp
|
||||
opensource
|
||||
openxmlformats
|
||||
ollama
|
||||
onnx
|
||||
openurl
|
||||
openxmlformats
|
||||
OPTIMIZEFORINVOKE
|
||||
ORPHANEDDIALOGTITLE
|
||||
ORSCANS
|
||||
@@ -1456,7 +1463,6 @@ rbhid
|
||||
Rbuttondown
|
||||
rclsid
|
||||
RCZOOMIT
|
||||
remotedesktop
|
||||
rdp
|
||||
RDW
|
||||
READMODE
|
||||
@@ -1485,6 +1491,7 @@ remappings
|
||||
REMAPSUCCESSFUL
|
||||
REMAPUNSUCCESSFUL
|
||||
Remotable
|
||||
remotedesktop
|
||||
remoteip
|
||||
Removelnk
|
||||
renamable
|
||||
@@ -1518,8 +1525,8 @@ RIGHTSCROLLBAR
|
||||
riid
|
||||
RKey
|
||||
RNumber
|
||||
rop
|
||||
rollups
|
||||
rop
|
||||
ROUNDSMALL
|
||||
ROWSETEXT
|
||||
rpcrt
|
||||
@@ -1757,8 +1764,7 @@ SVGIO
|
||||
svgz
|
||||
SVSI
|
||||
SWFO
|
||||
SWP
|
||||
Swp
|
||||
swp
|
||||
SWPNOSIZE
|
||||
SWPNOZORDER
|
||||
SWRESTORE
|
||||
@@ -1777,8 +1783,7 @@ SYSKEY
|
||||
syskeydown
|
||||
SYSKEYUP
|
||||
SYSLIB
|
||||
SYSMENU
|
||||
Sysmenu
|
||||
sysmenu
|
||||
systemai
|
||||
SYSTEMAPPS
|
||||
SYSTEMMODAL
|
||||
@@ -1882,9 +1887,9 @@ uitests
|
||||
UITo
|
||||
ULONGLONG
|
||||
Ultrawide
|
||||
ums
|
||||
UMax
|
||||
UMin
|
||||
ums
|
||||
uncompilable
|
||||
UNCPRIORITY
|
||||
UNDNAME
|
||||
|
||||
@@ -504,6 +504,14 @@ 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
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.251002-build.2316" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260116-build.2514" />
|
||||
<PackageVersion Include="ControlzEx" Version="6.0.0" />
|
||||
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
||||
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />
|
||||
|
||||
@@ -48,7 +48,7 @@ But to get started quickly, choose one of the installation methods below:
|
||||
<details open>
|
||||
<summary><strong>Download .exe from GitHub</strong></summary>
|
||||
<br/>
|
||||
Go to the [PowerToys GitHub releases][github-release-link], click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
||||
Go to the <a href="https://aka.ms/installPowerToys">PowerToys GitHub releases</a>, click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
||||
|
||||
<!-- items that need to be updated release to release -->
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.97%22
|
||||
@@ -83,7 +83,7 @@ You can easily install PowerToys from the Microsoft Store:
|
||||
<details>
|
||||
<summary><strong>WinGet</strong></summary>
|
||||
<br/>
|
||||
Download PowerToys from [WinGet][winget-link]. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
|
||||
Download PowerToys from <a href="https://github.com/microsoft/winget-cli#installing-the-client">WinGet</a>. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
|
||||
|
||||
*User scope installer [default]*
|
||||
```powershell
|
||||
@@ -99,7 +99,7 @@ winget install --scope machine Microsoft.PowerToys -s winget
|
||||
<details>
|
||||
<summary><strong>Other methods</strong></summary>
|
||||
<br/>
|
||||
There are [community driven install methods](./doc/unofficialInstallMethods.md) such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
|
||||
There are <a href="https://learn.microsoft.com/windows/powertoys/install#community-driven-install-tools">community driven install methods</a> such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
|
||||
</details>
|
||||
|
||||
## ✨ What's new
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<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?>
|
||||
|
||||
@@ -16,13 +16,50 @@ 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);
|
||||
handles.erase(it);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void ThemeListener::CheckTheme()
|
||||
@@ -48,13 +85,51 @@ void ThemeListener::CheckTheme()
|
||||
|
||||
WaitForSingleObject(hEvent, INFINITE);
|
||||
|
||||
auto _theme = ThemeHelpers::GetAppTheme();
|
||||
if (AppTheme != _theme)
|
||||
auto _appTheme = ThemeHelpers::GetAppTheme();
|
||||
auto _systemTheme = ThemeHelpers::GetSystemTheme();
|
||||
|
||||
bool appThemeChanged = (AppTheme != _appTheme);
|
||||
bool systemThemeChanged = (SystemTheme != _systemTheme);
|
||||
|
||||
if (appThemeChanged || systemThemeChanged)
|
||||
{
|
||||
AppTheme = _theme;
|
||||
for (int i = 0; i < handles.size(); i++)
|
||||
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;
|
||||
|
||||
{
|
||||
handles[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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <windows.h>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
|
||||
typedef void (*THEME_HANDLE)();
|
||||
DWORD WINAPI _checkTheme(LPVOID lpParam);
|
||||
@@ -14,6 +15,7 @@ public:
|
||||
ThemeListener()
|
||||
{
|
||||
AppTheme = ThemeHelpers::GetAppTheme();
|
||||
SystemTheme = ThemeHelpers::GetSystemTheme();
|
||||
dwThreadHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)_checkTheme, this, 0, &dwThreadId);
|
||||
}
|
||||
~ThemeListener()
|
||||
@@ -23,12 +25,20 @@ 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;
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <appxpackaging.h>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
@@ -337,6 +338,30 @@ 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)
|
||||
{
|
||||
|
||||
@@ -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.18" 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.19" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||
<policyNamespaces>
|
||||
<target prefix="powertoys" namespace="Microsoft.Policies.PowerToys" />
|
||||
</policyNamespaces>
|
||||
<resources minRequiredRevision="1.18"/><!-- Last changed with PowerToys v0.96.0 -->
|
||||
<resources minRequiredRevision="1.19"/><!-- Last changed with PowerToys v0.97.0 -->
|
||||
<supportedOn>
|
||||
<definitions>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_64_0" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0)"/>
|
||||
@@ -27,6 +27,7 @@
|
||||
<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>
|
||||
@@ -338,6 +339,16 @@
|
||||
<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" />
|
||||
|
||||
@@ -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.18" 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.19" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||
<displayName>PowerToys</displayName>
|
||||
<description>PowerToys</description>
|
||||
<resources>
|
||||
@@ -34,6 +34,7 @@
|
||||
<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.
|
||||
@@ -266,6 +267,7 @@ 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>
|
||||
|
||||
@@ -84,14 +84,17 @@
|
||||
</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>
|
||||
|
||||
268
src/modules/MouseUtils/CursorWrap/CursorWrapCore.cpp
Normal file
268
src/modules/MouseUtils/CursorWrap/CursorWrapCore.cpp
Normal file
@@ -0,0 +1,268 @@
|
||||
// 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;
|
||||
}
|
||||
33
src/modules/MouseUtils/CursorWrap/CursorWrapCore.h
Normal file
33
src/modules/MouseUtils/CursorWrap/CursorWrapCore.h
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#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;
|
||||
};
|
||||
546
src/modules/MouseUtils/CursorWrap/MonitorTopology.cpp
Normal file
546
src/modules/MouseUtils/CursorWrap/MonitorTopology.cpp
Normal file
@@ -0,0 +1,546 @@
|
||||
// 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
|
||||
}
|
||||
|
||||
106
src/modules/MouseUtils/CursorWrap/MonitorTopology.h
Normal file
106
src/modules/MouseUtils/CursorWrap/MonitorTopology.h
Normal file
@@ -0,0 +1,106 @@
|
||||
// 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
@@ -17,12 +17,25 @@ 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 => _details.Value; set => base.Details = value; }
|
||||
public override IDetails? Details
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_isLoadingDetails.Set())
|
||||
{
|
||||
_ = LoadDetailsAsync();
|
||||
}
|
||||
|
||||
return base.Details;
|
||||
}
|
||||
set => base.Details = value;
|
||||
}
|
||||
|
||||
public override IIconInfo? Icon
|
||||
{
|
||||
@@ -52,16 +65,22 @@ public sealed partial class AppListItem : ListItem
|
||||
|
||||
MoreCommands = AddPinCommands(_app.Commands!, isPinned);
|
||||
|
||||
_details = new Lazy<Details>(() =>
|
||||
{
|
||||
var t = BuildDetails();
|
||||
t.Wait();
|
||||
return t.Result;
|
||||
});
|
||||
|
||||
_detailsLoadTask = new Lazy<Task<Details>>(BuildDetails);
|
||||
_iconLoadTask = new Lazy<Task<IconInfo?>>(async () => await FetchIcon(useThumbnails));
|
||||
}
|
||||
|
||||
private async Task LoadDetailsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Details = await _detailsLoadTask.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to load details for {AppIdentifier}\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadIconAsync()
|
||||
{
|
||||
try
|
||||
@@ -98,18 +117,16 @@ public sealed partial class AppListItem : ListItem
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
// 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))
|
||||
{
|
||||
var stream = await ThumbnailHelper.GetThumbnail(_app.ExePath, true);
|
||||
if (stream is not null)
|
||||
{
|
||||
heroImage = IconInfo.FromStream(stream);
|
||||
}
|
||||
heroImage = await TryLoadThumbnail(_app.IcoPath, jumbo: true, logOnFailure: false);
|
||||
}
|
||||
catch (Exception)
|
||||
|
||||
if (heroImage == null && !string.IsNullOrEmpty(_app.ExePath))
|
||||
{
|
||||
// do nothing if we fail to load an icon.
|
||||
// Logging it would be too NOISY, there's really no need.
|
||||
heroImage = await TryLoadThumbnail(_app.ExePath, jumbo: true, logOnFailure: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,26 +149,19 @@ public sealed partial class AppListItem : ListItem
|
||||
|
||||
if (useThumbnails)
|
||||
{
|
||||
try
|
||||
if (!string.IsNullOrEmpty(_app.IcoPath))
|
||||
{
|
||||
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}");
|
||||
icon = await TryLoadThumbnail(_app.IcoPath, jumbo: false, logOnFailure: true);
|
||||
}
|
||||
|
||||
icon = icon ?? new IconInfo(_app.IcoPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
icon = new IconInfo(_app.IcoPath);
|
||||
if (icon == null && !string.IsNullOrEmpty(_app.ExePath))
|
||||
{
|
||||
icon = await TryLoadThumbnail(_app.ExePath, jumbo: false, logOnFailure: true);
|
||||
}
|
||||
}
|
||||
|
||||
icon ??= new IconInfo(_app.IcoPath);
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
@@ -183,4 +193,25 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1052,9 +1052,6 @@ public class Win32Program : IProgram
|
||||
app.FullPath) :
|
||||
app.IcoPath;
|
||||
|
||||
icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ?
|
||||
app.FullPath :
|
||||
icoPath;
|
||||
return new AppItem()
|
||||
{
|
||||
Name = app.Name,
|
||||
|
||||
@@ -41,16 +41,13 @@ internal static class FancyZonesDataService
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(FZPaths.EditorParameters))
|
||||
{
|
||||
error = Resources.FancyZones_MonitorDataNotFound;
|
||||
Logger.LogWarning($"TryGetMonitors: File not found. Path={FZPaths.EditorParameters}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"TryGetMonitors: File exists, reading...");
|
||||
var editorParams = FancyZonesDataIO.ReadEditorParameters();
|
||||
Logger.LogInfo($"TryGetMonitors: ReadEditorParameters returned. Monitors={editorParams.Monitors?.Count ?? -1}");
|
||||
// Request FancyZones to save current monitor configuration.
|
||||
// The editor-parameters.json file is only written when:
|
||||
// 1. Opening the FancyZones Editor
|
||||
// 2. Receiving the WM_PRIV_SAVE_EDITOR_PARAMETERS message
|
||||
// Without this, monitor changes (plug/unplug) won't be reflected in the file.
|
||||
var editorParams = ReadEditorParametersWithRefresh();
|
||||
Logger.LogInfo($"TryGetMonitors: ReadEditorParametersWithRefreshWithRefresh returned. Monitors={editorParams.Monitors?.Count ?? -1}");
|
||||
|
||||
var editorMonitors = editorParams.Monitors;
|
||||
if (editorMonitors is null || editorMonitors.Count == 0)
|
||||
@@ -74,6 +71,23 @@ internal static class FancyZonesDataService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests FancyZones to save the current monitor configuration and reads the file.
|
||||
/// This is a best-effort approach for performance: we send the save request and immediately
|
||||
/// read the file without waiting. If the file hasn't been updated yet, the next call will
|
||||
/// see the updated data since FancyZones processes the message asynchronously.
|
||||
/// </summary>
|
||||
private static EditorParameters.ParamsWrapper ReadEditorParametersWithRefresh()
|
||||
{
|
||||
// Request FancyZones to save the current monitor configuration.
|
||||
// This is fire-and-forget for performance - we don't wait for the save to complete.
|
||||
// If this is the first call after a monitor change, we may read stale data, but the
|
||||
// next call will see the updated file since FancyZones will have processed the message.
|
||||
FancyZonesNotifier.NotifySaveEditorParameters();
|
||||
|
||||
return FancyZonesDataIO.ReadEditorParameters();
|
||||
}
|
||||
|
||||
public static IReadOnlyList<FancyZonesLayoutDescriptor> GetLayouts()
|
||||
{
|
||||
Logger.LogInfo($"GetLayouts: Starting. LayoutTemplatesPath={FZPaths.LayoutTemplates} CustomLayoutsPath={FZPaths.CustomLayouts}");
|
||||
|
||||
@@ -10,13 +10,25 @@ namespace PowerToysExtension.Helpers;
|
||||
internal static class FancyZonesNotifier
|
||||
{
|
||||
private const string AppliedLayoutsFileUpdateMessage = "{2ef2c8a7-e0d5-4f31-9ede-52aade2d284d}";
|
||||
private const string SaveEditorParametersMessage = "{d8f9c0e3-5d77-4e83-8a4f-7c704c2bfb4a}";
|
||||
|
||||
private static readonly uint WmPrivAppliedLayoutsFileUpdate = RegisterWindowMessageW(AppliedLayoutsFileUpdateMessage);
|
||||
private static readonly uint WmPrivSaveEditorParameters = RegisterWindowMessageW(SaveEditorParametersMessage);
|
||||
|
||||
public static void NotifyAppliedLayoutsChanged()
|
||||
{
|
||||
_ = PostMessageW(new IntPtr(0xFFFF), WmPrivAppliedLayoutsFileUpdate, UIntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies FancyZones to save the current monitor configuration to editor-parameters.json.
|
||||
/// This is needed because FancyZones only writes this file when opening the editor or when explicitly requested.
|
||||
/// </summary>
|
||||
public static void NotifySaveEditorParameters()
|
||||
{
|
||||
_ = PostMessageW(new IntPtr(0xFFFF), WmPrivSaveEditorParameters, UIntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern uint RegisterWindowMessageW(string lpString);
|
||||
|
||||
|
||||
@@ -69,14 +69,14 @@ internal sealed partial class SampleMarkdownImagesPage : ContentPage
|
||||
|
||||
### Web URL
|
||||
```xml
|
||||

|
||||

|
||||
```
|
||||

|
||||

|
||||
|
||||
```xml
|
||||

|
||||

|
||||
```
|
||||

|
||||

|
||||
|
||||
### File URL (PNG):
|
||||
```xml
|
||||
|
||||
@@ -81,7 +81,7 @@ Result:
|
||||
|
||||
Result:
|
||||
|
||||

|
||||

|
||||
|
||||
### Links
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace ImageResizer
|
||||
{
|
||||
public partial class App : Application, IDisposable
|
||||
{
|
||||
private const string LogSubFolder = "\\ImageResizer\\Logs";
|
||||
private const string LogSubFolder = "\\Image Resizer\\Logs";
|
||||
|
||||
/// <summary>
|
||||
/// Gets cached AI availability state, checked at app startup.
|
||||
@@ -67,12 +67,27 @@ 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)
|
||||
{
|
||||
@@ -173,31 +188,8 @@ namespace ImageResizer
|
||||
/// </summary>
|
||||
private static AiAvailabilityState CheckAiAvailability()
|
||||
{
|
||||
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;
|
||||
}
|
||||
// AI feature disabled - always return NotSupported
|
||||
return AiAvailabilityState.NotSupported;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -39,29 +39,8 @@ namespace ImageResizer.Services
|
||||
/// </summary>
|
||||
public static AiAvailabilityState? LoadCache()
|
||||
{
|
||||
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;
|
||||
}
|
||||
// Cache disabled - always return null to use default value
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -70,32 +49,8 @@ namespace ImageResizer.Services
|
||||
/// </summary>
|
||||
public static void SaveCache(AiAvailabilityState state)
|
||||
{
|
||||
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}");
|
||||
}
|
||||
// Cache disabled - do not save anything
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -197,16 +197,10 @@ namespace Peek.UI
|
||||
|
||||
ViewModel.Initialize(selectedItem);
|
||||
|
||||
// If no files were found (e.g., in virtual folders like Home/Recent), show an error
|
||||
// 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)
|
||||
{
|
||||
Logger.LogInfo("Peek: No files found to preview, showing error.");
|
||||
var errorMessage = ResourceLoaderInstance.ResourceLoader.GetString("NoFilesSelected");
|
||||
ViewModel.ShowError(errorMessage);
|
||||
|
||||
// Still show the window so user can see the warning
|
||||
this.Show();
|
||||
WindowHelpers.BringToForeground(this.GetWindowHandle());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,23 +33,27 @@ static std::wstring SanitizeAndNormalize(const std::wstring& input)
|
||||
|
||||
// Normalize to NFC (Precomposed).
|
||||
// Get the size needed for the normalized string, including null terminator.
|
||||
int size = NormalizeString(NormalizationC, sanitized.c_str(), -1, nullptr, 0);
|
||||
if (size <= 0)
|
||||
int sizeEstimate = NormalizeString(NormalizationC, sanitized.c_str(), -1, nullptr, 0);
|
||||
if (sizeEstimate <= 0)
|
||||
{
|
||||
return sanitized; // Return unaltered if normalization fails.
|
||||
}
|
||||
|
||||
// Perform the normalization.
|
||||
std::wstring normalized;
|
||||
normalized.resize(size);
|
||||
NormalizeString(NormalizationC, sanitized.c_str(), -1, &normalized[0], size);
|
||||
normalized.resize(sizeEstimate);
|
||||
int actualSize = NormalizeString(NormalizationC, sanitized.c_str(), -1, &normalized[0], sizeEstimate);
|
||||
|
||||
// Remove the explicit null terminator added by NormalizeString.
|
||||
if (!normalized.empty() && normalized.back() == L'\0')
|
||||
if (actualSize <= 0)
|
||||
{
|
||||
normalized.pop_back();
|
||||
// Normalization failed, return sanitized string.
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -695,6 +695,38 @@ 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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -173,6 +173,10 @@
|
||||
<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>
|
||||
|
||||
@@ -193,6 +193,102 @@ GeneralSettings get_general_settings()
|
||||
return settings;
|
||||
}
|
||||
|
||||
void apply_module_status_update(const json::JsonObject& module_config, bool save)
|
||||
{
|
||||
Logger::info(L"apply_module_status_update: {}", std::wstring{ module_config.ToString() });
|
||||
|
||||
// Expected format: {"ModuleName": true/false} - only one module per update
|
||||
auto iter = module_config.First();
|
||||
if (!iter.HasCurrent())
|
||||
{
|
||||
Logger::warn(L"apply_module_status_update: Empty module config");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& element = iter.Current();
|
||||
const auto value = element.Value();
|
||||
if (value.ValueType() != json::JsonValueType::Boolean)
|
||||
{
|
||||
Logger::warn(L"apply_module_status_update: Invalid value type for module status");
|
||||
return;
|
||||
}
|
||||
|
||||
const std::wstring name{ element.Key().c_str() };
|
||||
if (modules().find(name) == modules().end())
|
||||
{
|
||||
Logger::warn(L"apply_module_status_update: Module {} not found", name);
|
||||
return;
|
||||
}
|
||||
|
||||
PowertoyModule& powertoy = modules().at(name);
|
||||
const bool module_inst_enabled = powertoy->is_enabled();
|
||||
bool target_enabled = value.GetBoolean();
|
||||
|
||||
auto gpo_rule = powertoy->gpo_policy_enabled_configuration();
|
||||
if (gpo_rule == powertoys_gpo::gpo_rule_configured_enabled || gpo_rule == powertoys_gpo::gpo_rule_configured_disabled)
|
||||
{
|
||||
// Apply the GPO Rule.
|
||||
target_enabled = gpo_rule == powertoys_gpo::gpo_rule_configured_enabled;
|
||||
}
|
||||
|
||||
if (module_inst_enabled == target_enabled)
|
||||
{
|
||||
Logger::info(L"apply_module_status_update: Module {} already in target state {}", name, target_enabled);
|
||||
return;
|
||||
}
|
||||
|
||||
if (target_enabled)
|
||||
{
|
||||
Logger::info(L"apply_module_status_update: Enabling powertoy {}", name);
|
||||
powertoy->enable();
|
||||
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
|
||||
hkmng.EnableHotkeyByModule(name);
|
||||
|
||||
// Trigger AI capability detection when ImageResizer is enabled
|
||||
if (name == L"Image Resizer")
|
||||
{
|
||||
Logger::info(L"ImageResizer enabled, triggering AI capability detection");
|
||||
DetectAiCapabilitiesAsync(true); // Skip settings check since we know it's being enabled
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info(L"apply_module_status_update: Disabling powertoy {}", name);
|
||||
powertoy->disable();
|
||||
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
|
||||
hkmng.DisableHotkeyByModule(name);
|
||||
}
|
||||
// Sync the hotkey state with the module state, so it can be removed for disabled modules.
|
||||
powertoy.UpdateHotkeyEx();
|
||||
|
||||
if (save)
|
||||
{
|
||||
// Load existing settings and only update the specific module's enabled state
|
||||
json::JsonObject current_settings = PTSettingsHelper::load_general_settings();
|
||||
|
||||
json::JsonObject enabled;
|
||||
if (current_settings.HasKey(L"enabled"))
|
||||
{
|
||||
enabled = current_settings.GetNamedObject(L"enabled");
|
||||
}
|
||||
|
||||
// Check if the saved state is different from the requested state
|
||||
bool current_saved = enabled.HasKey(name) ? enabled.GetNamedBoolean(name, true) : true;
|
||||
|
||||
if (current_saved != target_enabled)
|
||||
{
|
||||
// Update only this module's enabled state
|
||||
enabled.SetNamedValue(name, json::value(target_enabled));
|
||||
current_settings.SetNamedValue(L"enabled", enabled);
|
||||
|
||||
PTSettingsHelper::save_general_settings(current_settings);
|
||||
|
||||
GeneralSettings settings_for_trace = get_general_settings();
|
||||
Trace::SettingsChanged(settings_for_trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void apply_general_settings(const json::JsonObject& general_configs, bool save)
|
||||
{
|
||||
std::wstring old_settings_json_string;
|
||||
|
||||
@@ -38,4 +38,5 @@ struct GeneralSettings
|
||||
json::JsonObject load_general_settings();
|
||||
GeneralSettings get_general_settings();
|
||||
void apply_general_settings(const json::JsonObject& general_configs, bool save = true);
|
||||
void apply_module_status_update(const json::JsonObject& module_config, bool save = true);
|
||||
void start_enabled_powertoys();
|
||||
@@ -215,6 +215,12 @@ void dispatch_received_json(const std::wstring& json_to_parse)
|
||||
// current_settings_ipc->send(settings_string);
|
||||
// }
|
||||
}
|
||||
else if (name == L"module_status")
|
||||
{
|
||||
// Handle single module enable/disable update
|
||||
// Expected format: {"module_status": {"ModuleName": true/false}}
|
||||
apply_module_status_update(value.GetObjectW());
|
||||
}
|
||||
else if (name == L"powertoys")
|
||||
{
|
||||
dispatch_json_config_to_modules(value.GetObjectW());
|
||||
|
||||
@@ -81,3 +81,36 @@ 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));
|
||||
}
|
||||
|
||||
@@ -13,4 +13,9 @@ 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);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#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>
|
||||
@@ -14,6 +15,7 @@
|
||||
#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
|
||||
@@ -39,6 +41,7 @@ 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;
|
||||
@@ -129,6 +132,9 @@ 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();
|
||||
@@ -194,6 +200,21 @@ 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));
|
||||
@@ -201,17 +222,39 @@ 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);
|
||||
change_menu_item_text(ID_SETTINGS_MENU_COMMAND, settings_menuitem_label.data());
|
||||
|
||||
// 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_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)
|
||||
{
|
||||
@@ -242,6 +285,9 @@ 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;
|
||||
@@ -293,7 +339,7 @@ static void handle_theme_change()
|
||||
{
|
||||
if (theme_adaptive_enabled)
|
||||
{
|
||||
tray_icon_data.hIcon = get_icon(theme_listener.AppTheme);
|
||||
tray_icon_data.hIcon = get_icon(ThemeHelpers::GetSystemTheme());
|
||||
Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data);
|
||||
}
|
||||
}
|
||||
@@ -310,7 +356,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(theme_listener.AppTheme) : LoadIcon(h_instance, MAKEINTRESOURCE(APPICON));
|
||||
HICON const icon = theme_adaptive ? get_icon(ThemeHelpers::GetSystemTheme()) : LoadIcon(h_instance, MAKEINTRESOURCE(APPICON));
|
||||
if (icon)
|
||||
{
|
||||
UINT id_tray_icon = 1;
|
||||
@@ -357,7 +403,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.AddChangedHandler(&handle_theme_change);
|
||||
theme_listener.AddSystemThemeChangedHandler(&handle_theme_change);
|
||||
|
||||
// Register callback to update bug report menu item status
|
||||
BugReportManager::instance().register_callback([](bool isRunning) {
|
||||
@@ -389,7 +435,7 @@ void set_tray_icon_theme_adaptive(bool theme_adaptive)
|
||||
|
||||
if (theme_adaptive)
|
||||
{
|
||||
icon = get_icon(theme_listener.AppTheme);
|
||||
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");
|
||||
|
||||
@@ -51,6 +51,7 @@ public sealed partial class AppsListPage : Page
|
||||
if (ViewModel != null)
|
||||
{
|
||||
ViewModel.DashboardSortOrder = DashboardSortOrder.Alphabetical;
|
||||
((ToggleMenuFlyoutItem)sender).IsChecked = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +60,7 @@ public sealed partial class AppsListPage : Page
|
||||
if (ViewModel != null)
|
||||
{
|
||||
ViewModel.DashboardSortOrder = DashboardSortOrder.ByStatus;
|
||||
((ToggleMenuFlyoutItem)sender).IsChecked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace Microsoft.PowerToys.QuickAccess.Services;
|
||||
|
||||
@@ -19,10 +21,10 @@ public interface IQuickAccessCoordinator
|
||||
|
||||
Task<bool> ShowDocumentationAsync();
|
||||
|
||||
void NotifyUserSettingsInteraction();
|
||||
|
||||
bool UpdateModuleEnabled(ModuleType moduleType, bool isEnabled);
|
||||
|
||||
void SendSortOrderUpdate(GeneralSettings generalSettings);
|
||||
|
||||
void ReportBug();
|
||||
|
||||
void OnModuleLaunched(ModuleType moduleType);
|
||||
|
||||
@@ -55,37 +55,8 @@ internal sealed class QuickAccessCoordinator : IQuickAccessCoordinator, IDisposa
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
public void NotifyUserSettingsInteraction()
|
||||
{
|
||||
Logger.LogDebug("QuickAccessCoordinator.NotifyUserSettingsInteraction invoked.");
|
||||
}
|
||||
|
||||
public bool UpdateModuleEnabled(ModuleType moduleType, bool isEnabled)
|
||||
{
|
||||
GeneralSettings? updatedSettings = null;
|
||||
lock (_generalSettingsLock)
|
||||
{
|
||||
var repository = SettingsRepository<GeneralSettings>.GetInstance(_settingsUtils);
|
||||
var generalSettings = repository.SettingsConfig;
|
||||
var current = ModuleHelper.GetIsModuleEnabled(generalSettings, moduleType);
|
||||
if (current == isEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ModuleHelper.SetIsModuleEnabled(generalSettings, moduleType, isEnabled);
|
||||
_settingsUtils.SaveSettings(generalSettings.ToJsonString());
|
||||
Logger.LogInfo($"QuickAccess updated module '{moduleType}' enabled state to {isEnabled}.");
|
||||
updatedSettings = generalSettings;
|
||||
}
|
||||
|
||||
if (updatedSettings != null)
|
||||
{
|
||||
SendGeneralSettingsUpdate(updatedSettings);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
=> TrySendIpcMessage($"{{\"module_status\": {{\"{ModuleHelper.GetModuleKey(moduleType)}\": {isEnabled.ToString().ToLowerInvariant()}}}}}", "module status update");
|
||||
|
||||
public void ReportBug()
|
||||
{
|
||||
@@ -131,20 +102,10 @@ internal sealed class QuickAccessCoordinator : IQuickAccessCoordinator, IDisposa
|
||||
Logger.LogDebug($"QuickAccessCoordinator received IPC payload: {message}");
|
||||
}
|
||||
|
||||
private void SendGeneralSettingsUpdate(GeneralSettings updatedSettings)
|
||||
public void SendSortOrderUpdate(GeneralSettings generalSettings)
|
||||
{
|
||||
string payload;
|
||||
try
|
||||
{
|
||||
payload = new OutGoingGeneralSettings(updatedSettings).ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("QuickAccessCoordinator: failed to serialize general settings payload.", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
TrySendIpcMessage(payload, "general settings update");
|
||||
var outgoing = new OutGoingGeneralSettings(generalSettings);
|
||||
TrySendIpcMessage(outgoing.ToString(), "sort order update");
|
||||
}
|
||||
|
||||
private bool TrySendIpcMessage(string payload, string operationDescription)
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace Microsoft.PowerToys.QuickAccess.ViewModels;
|
||||
|
||||
public sealed class AllAppsViewModel : Observable
|
||||
{
|
||||
private readonly object _sortLock = new object();
|
||||
private readonly IQuickAccessCoordinator _coordinator;
|
||||
private readonly ISettingsRepository<GeneralSettings> _settingsRepository;
|
||||
private readonly SettingsUtils _settingsUtils;
|
||||
@@ -30,6 +31,9 @@ public sealed class AllAppsViewModel : Observable
|
||||
private readonly List<FlyoutMenuItem> _allFlyoutMenuItems = new();
|
||||
private GeneralSettings _generalSettings;
|
||||
|
||||
// Flag to prevent toggle operations during sorting to avoid race conditions.
|
||||
private bool _isSorting;
|
||||
|
||||
public ObservableCollection<FlyoutMenuItem> FlyoutMenuItems { get; }
|
||||
|
||||
public DashboardSortOrder DashboardSortOrder
|
||||
@@ -40,9 +44,9 @@ public sealed class AllAppsViewModel : Observable
|
||||
if (_generalSettings.DashboardSortOrder != value)
|
||||
{
|
||||
_generalSettings.DashboardSortOrder = value;
|
||||
_settingsUtils.SaveSettings(_generalSettings.ToJsonString(), _generalSettings.GetModuleName());
|
||||
_coordinator.SendSortOrderUpdate(_generalSettings);
|
||||
OnPropertyChanged();
|
||||
RefreshFlyoutMenuItems();
|
||||
SortFlyoutMenuItems();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,7 +58,6 @@ public sealed class AllAppsViewModel : Observable
|
||||
_settingsUtils = SettingsUtils.Default;
|
||||
_settingsRepository = SettingsRepository<GeneralSettings>.GetInstance(_settingsUtils);
|
||||
_generalSettings = _settingsRepository.SettingsConfig;
|
||||
_generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
|
||||
_settingsRepository.SettingsChanged += OnSettingsChanged;
|
||||
|
||||
_resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
|
||||
@@ -87,7 +90,6 @@ public sealed class AllAppsViewModel : Observable
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
_generalSettings = newSettings;
|
||||
_generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
|
||||
OnPropertyChanged(nameof(DashboardSortOrder));
|
||||
RefreshFlyoutMenuItems();
|
||||
});
|
||||
@@ -120,30 +122,55 @@ public sealed class AllAppsViewModel : Observable
|
||||
}
|
||||
}
|
||||
|
||||
var sortedItems = DashboardSortOrder switch
|
||||
{
|
||||
DashboardSortOrder.ByStatus => _allFlyoutMenuItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label).ToList(),
|
||||
_ => _allFlyoutMenuItems.OrderBy(x => x.Label).ToList(),
|
||||
};
|
||||
SortFlyoutMenuItems();
|
||||
}
|
||||
|
||||
if (FlyoutMenuItems.Count == 0)
|
||||
private void SortFlyoutMenuItems()
|
||||
{
|
||||
if (_isSorting)
|
||||
{
|
||||
foreach (var item in sortedItems)
|
||||
{
|
||||
FlyoutMenuItems.Add(item);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < sortedItems.Count; i++)
|
||||
lock (_sortLock)
|
||||
{
|
||||
var item = sortedItems[i];
|
||||
var oldIndex = FlyoutMenuItems.IndexOf(item);
|
||||
|
||||
if (oldIndex != -1 && oldIndex != i)
|
||||
_isSorting = true;
|
||||
try
|
||||
{
|
||||
FlyoutMenuItems.Move(oldIndex, i);
|
||||
var sortedItems = DashboardSortOrder switch
|
||||
{
|
||||
DashboardSortOrder.ByStatus => _allFlyoutMenuItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label).ToList(),
|
||||
_ => _allFlyoutMenuItems.OrderBy(x => x.Label).ToList(),
|
||||
};
|
||||
|
||||
if (FlyoutMenuItems.Count == 0)
|
||||
{
|
||||
foreach (var item in sortedItems)
|
||||
{
|
||||
FlyoutMenuItems.Add(item);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < sortedItems.Count; i++)
|
||||
{
|
||||
var item = sortedItems[i];
|
||||
var oldIndex = FlyoutMenuItems.IndexOf(item);
|
||||
|
||||
if (oldIndex != -1 && oldIndex != i)
|
||||
{
|
||||
FlyoutMenuItems.Move(oldIndex, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Use dispatcher to reset flag after UI updates complete
|
||||
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
_isSorting = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,17 +178,17 @@ public sealed class AllAppsViewModel : Observable
|
||||
private void EnabledChangedOnUI(ModuleListItem item)
|
||||
{
|
||||
var flyoutItem = (FlyoutMenuItem)item;
|
||||
if (_coordinator.UpdateModuleEnabled(flyoutItem.Tag, flyoutItem.IsEnabled))
|
||||
var isEnabled = flyoutItem.IsEnabled;
|
||||
|
||||
// Ignore toggle operations during sorting to prevent race conditions.
|
||||
// Revert the toggle state since UI already changed due to TwoWay binding.
|
||||
if (_isSorting)
|
||||
{
|
||||
_coordinator.NotifyUserSettingsInteraction();
|
||||
|
||||
// Trigger re-sort immediately when status changes on UI
|
||||
RefreshFlyoutMenuItems();
|
||||
flyoutItem.UpdateStatus(!isEnabled);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void ModuleEnabledChangedOnSettingsPage()
|
||||
{
|
||||
RefreshFlyoutMenuItems();
|
||||
_coordinator.UpdateModuleEnabled(flyoutItem.Tag, flyoutItem.IsEnabled);
|
||||
SortFlyoutMenuItems();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,21 +34,21 @@ public sealed class AdvancedPastePasteAsFileAction : Observable, IAdvancedPasteA
|
||||
public AdvancedPasteAdditionalAction PasteAsTxtFile
|
||||
{
|
||||
get => _pasteAsTxtFile;
|
||||
init => Set(ref _pasteAsTxtFile, value);
|
||||
init => Set(ref _pasteAsTxtFile, value ?? new());
|
||||
}
|
||||
|
||||
[JsonPropertyName(PropertyNames.PasteAsPngFile)]
|
||||
public AdvancedPasteAdditionalAction PasteAsPngFile
|
||||
{
|
||||
get => _pasteAsPngFile;
|
||||
init => Set(ref _pasteAsPngFile, value);
|
||||
init => Set(ref _pasteAsPngFile, value ?? new());
|
||||
}
|
||||
|
||||
[JsonPropertyName(PropertyNames.PasteAsHtmlFile)]
|
||||
public AdvancedPasteAdditionalAction PasteAsHtmlFile
|
||||
{
|
||||
get => _pasteAsHtmlFile;
|
||||
init => Set(ref _pasteAsHtmlFile, value);
|
||||
init => Set(ref _pasteAsHtmlFile, value ?? new());
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
|
||||
@@ -93,11 +93,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
[JsonPropertyName("custom-actions")]
|
||||
[CmdConfigureIgnoreAttribute]
|
||||
public AdvancedPasteCustomActions CustomActions { get; init; }
|
||||
public AdvancedPasteCustomActions CustomActions { get; set; }
|
||||
|
||||
[JsonPropertyName("additional-actions")]
|
||||
[CmdConfigureIgnoreAttribute]
|
||||
public AdvancedPasteAdditionalActions AdditionalActions { get; init; }
|
||||
public AdvancedPasteAdditionalActions AdditionalActions { get; set; }
|
||||
|
||||
[JsonPropertyName("paste-ai-configuration")]
|
||||
[CmdConfigureIgnoreAttribute]
|
||||
|
||||
@@ -32,14 +32,14 @@ public sealed class AdvancedPasteTranscodeAction : Observable, IAdvancedPasteAct
|
||||
public AdvancedPasteAdditionalAction TranscodeToMp3
|
||||
{
|
||||
get => _transcodeToMp3;
|
||||
init => Set(ref _transcodeToMp3, value);
|
||||
init => Set(ref _transcodeToMp3, value ?? new());
|
||||
}
|
||||
|
||||
[JsonPropertyName(PropertyNames.TranscodeToMp4)]
|
||||
public AdvancedPasteAdditionalAction TranscodeToMp4
|
||||
{
|
||||
get => _transcodeToMp4;
|
||||
init => Set(ref _transcodeToMp4, value);
|
||||
init => Set(ref _transcodeToMp4, value ?? new());
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
|
||||
@@ -22,11 +22,15 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,16 @@ 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()
|
||||
{
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,5 +117,47 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
|
||||
case ModuleType.GeneralSettings: generalSettingsConfig.EnableQuickAccess = isEnabled; break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the module key name used in IPC messages and settings JSON.
|
||||
/// These names match the JsonPropertyName attributes in EnabledModules class.
|
||||
/// </summary>
|
||||
public static string GetModuleKey(ModuleType moduleType)
|
||||
{
|
||||
return moduleType switch
|
||||
{
|
||||
ModuleType.AdvancedPaste => AdvancedPasteSettings.ModuleName,
|
||||
ModuleType.AlwaysOnTop => AlwaysOnTopSettings.ModuleName,
|
||||
ModuleType.Awake => AwakeSettings.ModuleName,
|
||||
ModuleType.CmdPal => "CmdPal", // No dedicated settings class
|
||||
ModuleType.ColorPicker => ColorPickerSettings.ModuleName,
|
||||
ModuleType.CropAndLock => CropAndLockSettings.ModuleName,
|
||||
ModuleType.CursorWrap => CursorWrapSettings.ModuleName,
|
||||
ModuleType.EnvironmentVariables => EnvironmentVariablesSettings.ModuleName,
|
||||
ModuleType.FancyZones => FancyZonesSettings.ModuleName,
|
||||
ModuleType.FileLocksmith => FileLocksmithSettings.ModuleName,
|
||||
ModuleType.FindMyMouse => FindMyMouseSettings.ModuleName,
|
||||
ModuleType.Hosts => HostsSettings.ModuleName,
|
||||
ModuleType.ImageResizer => ImageResizerSettings.ModuleName,
|
||||
ModuleType.KeyboardManager => KeyboardManagerSettings.ModuleName,
|
||||
ModuleType.LightSwitch => LightSwitchSettings.ModuleName,
|
||||
ModuleType.MouseHighlighter => MouseHighlighterSettings.ModuleName,
|
||||
ModuleType.MouseJump => MouseJumpSettings.ModuleName,
|
||||
ModuleType.MousePointerCrosshairs => MousePointerCrosshairsSettings.ModuleName,
|
||||
ModuleType.MouseWithoutBorders => MouseWithoutBordersSettings.ModuleName,
|
||||
ModuleType.NewPlus => NewPlusSettings.ModuleName,
|
||||
ModuleType.Peek => PeekSettings.ModuleName,
|
||||
ModuleType.PowerRename => PowerRenameSettings.ModuleName,
|
||||
ModuleType.PowerLauncher => PowerLauncherSettings.ModuleName,
|
||||
ModuleType.PowerAccent => PowerAccentSettings.ModuleName,
|
||||
ModuleType.RegistryPreview => RegistryPreviewSettings.ModuleName,
|
||||
ModuleType.MeasureTool => MeasureToolSettings.ModuleName,
|
||||
ModuleType.ShortcutGuide => ShortcutGuideSettings.ModuleName,
|
||||
ModuleType.PowerOCR => PowerOcrSettings.ModuleName,
|
||||
ModuleType.Workspaces => WorkspacesSettings.ModuleName,
|
||||
ModuleType.ZoomIt => ZoomItSettings.ModuleName,
|
||||
_ => moduleType.ToString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
|
||||
MeasureTool,
|
||||
Hosts,
|
||||
Workspaces,
|
||||
WhatsNew,
|
||||
RegistryPreview,
|
||||
NewPlus,
|
||||
ZoomIt,
|
||||
|
||||
@@ -9,7 +9,6 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
@@ -227,7 +226,6 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
{
|
||||
settingsWindow = new MainWindow();
|
||||
settingsWindow.Activate();
|
||||
settingsWindow.ExtendsContentIntoTitleBar = true;
|
||||
settingsWindow.NavigateToSection(StartupPage);
|
||||
|
||||
// https://github.com/microsoft/microsoft-ui-xaml/issues/7595 - Activate doesn't bring window to the foreground
|
||||
@@ -257,11 +255,10 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
else if (ShowScoobe)
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new ScoobeStartedEvent());
|
||||
OobeWindow scoobeWindow = new OobeWindow(OOBE.Enums.PowerToysModules.WhatsNew);
|
||||
scoobeWindow.Activate();
|
||||
scoobeWindow.ExtendsContentIntoTitleBar = true;
|
||||
ScoobeWindow newScoobeWindow = new ScoobeWindow();
|
||||
newScoobeWindow.Activate();
|
||||
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(WindowNative.GetWindowHandle(settingsWindow));
|
||||
SetOobeWindow(scoobeWindow);
|
||||
SetScoobeWindow(newScoobeWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,6 +336,7 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
|
||||
private static MainWindow settingsWindow;
|
||||
private static OobeWindow oobeWindow;
|
||||
private static ScoobeWindow scoobeWindow;
|
||||
|
||||
public static void ClearSettingsWindow()
|
||||
{
|
||||
@@ -365,6 +363,21 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
oobeWindow = null;
|
||||
}
|
||||
|
||||
public static ScoobeWindow GetScoobeWindow()
|
||||
{
|
||||
return scoobeWindow;
|
||||
}
|
||||
|
||||
public static void SetScoobeWindow(ScoobeWindow window)
|
||||
{
|
||||
scoobeWindow = window;
|
||||
}
|
||||
|
||||
public static void ClearScoobeWindow()
|
||||
{
|
||||
scoobeWindow = null;
|
||||
}
|
||||
|
||||
public static Type GetPage(string settingWindow)
|
||||
{
|
||||
switch (settingWindow)
|
||||
|
||||
@@ -20,9 +20,6 @@ using WinUIEx;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// An empty window that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
public sealed partial class MainWindow : WindowEx
|
||||
{
|
||||
public MainWindow(bool createHidden = false)
|
||||
@@ -35,10 +32,12 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
App.ThemeService.ThemeChanged += OnThemeChanged;
|
||||
App.ThemeService.ApplyTheme();
|
||||
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
|
||||
ShellPage.SetElevationStatus(App.IsElevated);
|
||||
ShellPage.SetIsUserAnAdmin(App.IsUserAnAdmin);
|
||||
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
var hWnd = WindowNative.GetWindowHandle(this);
|
||||
var placement = WindowHelper.DeserializePlacementOrDefault(hWnd);
|
||||
if (createHidden)
|
||||
{
|
||||
@@ -121,16 +120,12 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
// open whats new window
|
||||
ShellPage.SetOpenWhatIsNewCallback(() =>
|
||||
{
|
||||
if (App.GetOobeWindow() == null)
|
||||
if (App.GetScoobeWindow() == null)
|
||||
{
|
||||
App.SetOobeWindow(new OobeWindow(OOBE.Enums.PowerToysModules.WhatsNew));
|
||||
}
|
||||
else
|
||||
{
|
||||
App.GetOobeWindow().SetAppWindow(OOBE.Enums.PowerToysModules.WhatsNew);
|
||||
App.SetScoobeWindow(new ScoobeWindow());
|
||||
}
|
||||
|
||||
App.GetOobeWindow().Activate();
|
||||
App.GetScoobeWindow().Activate();
|
||||
});
|
||||
|
||||
this.InitializeComponent();
|
||||
@@ -187,7 +182,7 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
WindowHelper.SerializePlacement(hWnd);
|
||||
|
||||
if (App.GetOobeWindow() == null)
|
||||
if (App.GetOobeWindow() == null && App.GetScoobeWindow() == null)
|
||||
{
|
||||
App.ClearSettingsWindow();
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button
|
||||
x:Name="LaunchButton"
|
||||
x:Uid="Launch_ColorPicker"
|
||||
Click="Start_ColorPicker_Click"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
using System.Threading;
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
|
||||
using Microsoft.PowerToys.Settings.UI.Views;
|
||||
@@ -54,10 +53,6 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
ColorPickerSettings settings = SettingsUtils.Default.GetSettingsOrDefault<ColorPickerSettings, ColorPickerSettingsVersion1>(ColorPickerSettings.ModuleName, settingsUpgrader: ColorPickerSettings.UpgradeSettings);
|
||||
|
||||
HotkeyControl.Keys = settings.Properties.ActivationShortcut.GetKeysList();
|
||||
|
||||
// Disable the Launch button if the module is disabled
|
||||
var generalSettings = SettingsRepository<GeneralSettings>.GetInstance(SettingsUtils.Default).SettingsConfig;
|
||||
LaunchButton.IsEnabled = ModuleHelper.GetIsModuleEnabled(generalSettings, ManagedCommon.ModuleType.ColorPicker);
|
||||
}
|
||||
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<StackPanel Orientation="Vertical" Spacing="12">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button
|
||||
x:Name="LaunchButton"
|
||||
x:Uid="Launch_EnvironmentVariables"
|
||||
Click="Launch_EnvironmentVariables_Click"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
using System.Threading;
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
|
||||
using Microsoft.PowerToys.Settings.UI.Views;
|
||||
@@ -29,10 +28,6 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
ViewModel.LogOpeningModuleEvent();
|
||||
|
||||
// Disable the Launch button if the module is disabled
|
||||
var generalSettings = SettingsRepository<GeneralSettings>.GetInstance(SettingsUtils.Default).SettingsConfig;
|
||||
LaunchButton.IsEnabled = ModuleHelper.GetIsModuleEnabled(generalSettings, ManagedCommon.ModuleType.EnvironmentVariables);
|
||||
}
|
||||
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<StackPanel Orientation="Vertical" Spacing="12">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button
|
||||
x:Name="LaunchButton"
|
||||
x:Uid="Launch_Hosts"
|
||||
Click="Launch_Hosts_Click"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
using System.Threading;
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
|
||||
using Microsoft.PowerToys.Settings.UI.Views;
|
||||
@@ -29,10 +28,6 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
ViewModel.LogOpeningModuleEvent();
|
||||
|
||||
// Disable the Launch button if the module is disabled
|
||||
var generalSettings = SettingsRepository<GeneralSettings>.GetInstance(SettingsUtils.Default).SettingsConfig;
|
||||
LaunchButton.IsEnabled = ModuleHelper.GetIsModuleEnabled(generalSettings, ManagedCommon.ModuleType.Hosts);
|
||||
}
|
||||
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button
|
||||
x:Name="LaunchButton"
|
||||
x:Uid="Launch_RegistryPreview"
|
||||
Click="Launch_RegistryPreview_Click"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
|
||||
using Microsoft.PowerToys.Settings.UI.Views;
|
||||
@@ -44,10 +43,6 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
ViewModel.LogOpeningModuleEvent();
|
||||
|
||||
// Disable the Launch button if the module is disabled
|
||||
var generalSettings = SettingsRepository<GeneralSettings>.GetInstance(SettingsUtils.Default).SettingsConfig;
|
||||
LaunchButton.IsEnabled = ModuleHelper.GetIsModuleEnabled(generalSettings, ManagedCommon.ModuleType.RegistryPreview);
|
||||
}
|
||||
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button
|
||||
x:Name="LaunchButton"
|
||||
x:Uid="Launch_Run"
|
||||
Click="Start_Run_Click"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
using System.Threading;
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
|
||||
using Microsoft.PowerToys.Settings.UI.Views;
|
||||
@@ -56,10 +55,6 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
ViewModel.LogOpeningModuleEvent();
|
||||
|
||||
HotkeyControl.Keys = SettingsRepository<PowerLauncherSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.OpenPowerLauncher.GetKeysList();
|
||||
|
||||
// Disable the Launch button if the module is disabled
|
||||
var generalSettings = SettingsRepository<GeneralSettings>.GetInstance(SettingsUtils.Default).SettingsConfig;
|
||||
LaunchButton.IsEnabled = ModuleHelper.GetIsModuleEnabled(generalSettings, ManagedCommon.ModuleType.PowerLauncher);
|
||||
}
|
||||
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
|
||||
@@ -1,60 +1,44 @@
|
||||
<UserControl
|
||||
<Page
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.OobeShellPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
HighContrastAdjustment="None"
|
||||
Loaded="ShellPage_Loaded"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="48" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Button
|
||||
x:Name="PaneToggleBtn"
|
||||
Width="48"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Click="PaneToggleBtn_Click"
|
||||
Style="{StaticResource PaneToggleButtonStyle}" />
|
||||
<Grid
|
||||
<TitleBar
|
||||
x:Name="AppTitleBar"
|
||||
Height="48"
|
||||
Margin="48,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
IsHitTestVisible="True">
|
||||
<animations:Implicit.Animations>
|
||||
<animations:OffsetAnimation Duration="0:0:0.3" />
|
||||
</animations:Implicit.Animations>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image
|
||||
Width="16"
|
||||
x:Uid="OobeWindow_TitleTxt"
|
||||
IsBackButtonVisible="False"
|
||||
IsPaneToggleButtonVisible="False"
|
||||
PaneToggleRequested="TitleBar_PaneButtonClick">
|
||||
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
|
||||
<TitleBar.LeftHeader>
|
||||
<ImageIcon
|
||||
x:Name="TitleBarIcon"
|
||||
Height="16"
|
||||
Margin="16,0,0,0"
|
||||
Source="/Assets/Settings/icon.ico" />
|
||||
<TextBlock
|
||||
x:Name="AppTitleBarText"
|
||||
x:Uid="OobeWindow_TitleTxt"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
TextWrapping="NoWrap" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
</TitleBar.LeftHeader>
|
||||
</TitleBar>
|
||||
<NavigationView
|
||||
x:Name="navigationView"
|
||||
Grid.Row="1"
|
||||
CompactModeThresholdWidth="1007"
|
||||
DisplayModeChanged="NavigationView_DisplayModeChanged"
|
||||
ExpandedModeThresholdWidth="1007"
|
||||
IsBackButtonVisible="Collapsed"
|
||||
IsPaneOpen="True"
|
||||
IsPaneToggleButtonVisible="False"
|
||||
IsSettingsVisible="False"
|
||||
OpenPaneLength="296"
|
||||
PaneDisplayMode="Left"
|
||||
SelectionChanged="NavigationView_SelectionChanged">
|
||||
<NavigationView.MenuItems>
|
||||
<NavigationViewItem
|
||||
@@ -174,34 +158,16 @@
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/ZoomIt.png}"
|
||||
Tag="ZoomIt" />
|
||||
</NavigationView.MenuItems>
|
||||
<NavigationView.FooterMenuItems>
|
||||
<NavigationView.PaneFooter>
|
||||
<NavigationViewItem
|
||||
x:Uid="Shell_WhatsNew"
|
||||
AutomationProperties.AutomationId="WhatIsNewNavItem"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
Tag="WhatsNew" />
|
||||
</NavigationView.FooterMenuItems>
|
||||
Tapped="WhatIsNewItem_Tapped" />
|
||||
</NavigationView.PaneFooter>
|
||||
<NavigationView.Content>
|
||||
<Frame x:Name="NavigationFrame" />
|
||||
</NavigationView.Content>
|
||||
</NavigationView>
|
||||
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="LayoutVisualStates">
|
||||
<VisualState x:Name="WideLayout">
|
||||
<VisualState.StateTriggers>
|
||||
<AdaptiveTrigger MinWindowWidth="720" />
|
||||
</VisualState.StateTriggers>
|
||||
</VisualState>
|
||||
<VisualState x:Name="SmallLayout">
|
||||
<VisualState.StateTriggers>
|
||||
<AdaptiveTrigger MinWindowWidth="600" />
|
||||
<AdaptiveTrigger MinWindowWidth="0" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="navigationView.PaneDisplayMode" Value="LeftMinimal" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</Page>
|
||||
|
||||
@@ -5,18 +5,17 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using WinRT.Interop;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
{
|
||||
public sealed partial class OobeShellPage : UserControl
|
||||
public sealed partial class OobeShellPage : Page
|
||||
{
|
||||
public static Func<string> RunSharedEventCallback { get; set; }
|
||||
|
||||
@@ -63,7 +62,6 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
|
||||
// NOTE: Experimentation for OOBE is currently turned off on server side. Keeping this code in a comment to allow future experiments.
|
||||
// ExperimentationToggleSwitchEnabled = SettingsRepository<GeneralSettings>.GetInstance(settingsUtils).SettingsConfig.EnableExperimentation;
|
||||
SetTitleBar();
|
||||
DataContext = ViewModel;
|
||||
OobeShellHandler = this;
|
||||
Modules = new ObservableCollection<OobePowerToysModule>();
|
||||
@@ -202,12 +200,6 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
IsNew = true,
|
||||
});
|
||||
|
||||
Modules.Insert((int)PowerToysModules.WhatsNew, new OobePowerToysModule()
|
||||
{
|
||||
ModuleName = "WhatsNew",
|
||||
IsNew = false,
|
||||
});
|
||||
|
||||
Modules.Insert((int)PowerToysModules.RegistryPreview, new OobePowerToysModule()
|
||||
{
|
||||
ModuleName = "RegistryPreview",
|
||||
@@ -229,7 +221,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
|
||||
public void OnClosing()
|
||||
{
|
||||
Microsoft.UI.Xaml.Controls.NavigationViewItem selectedItem = this.navigationView.SelectedItem as Microsoft.UI.Xaml.Controls.NavigationViewItem;
|
||||
NavigationViewItem selectedItem = this.navigationView.SelectedItem as NavigationViewItem;
|
||||
if (selectedItem != null)
|
||||
{
|
||||
Modules[(int)(PowerToysModules)Enum.Parse(typeof(PowerToysModules), (string)selectedItem.Tag, true)].LogClosingModuleEvent();
|
||||
@@ -238,19 +230,22 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
|
||||
public void NavigateToModule(PowerToysModules selectedModule)
|
||||
{
|
||||
if (selectedModule == PowerToysModules.WhatsNew)
|
||||
{
|
||||
navigationView.SelectedItem = navigationView.FooterMenuItems[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
navigationView.SelectedItem = navigationView.MenuItems[(int)selectedModule];
|
||||
}
|
||||
navigationView.SelectedItem = navigationView.MenuItems[(int)selectedModule];
|
||||
}
|
||||
|
||||
private void NavigationView_SelectionChanged(Microsoft.UI.Xaml.Controls.NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewSelectionChangedEventArgs args)
|
||||
private static void OpenScoobeWindow()
|
||||
{
|
||||
Microsoft.UI.Xaml.Controls.NavigationViewItem selectedItem = args.SelectedItem as Microsoft.UI.Xaml.Controls.NavigationViewItem;
|
||||
if (App.GetScoobeWindow() == null)
|
||||
{
|
||||
App.SetScoobeWindow(new ScoobeWindow());
|
||||
}
|
||||
|
||||
App.GetScoobeWindow().Activate();
|
||||
}
|
||||
|
||||
private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
|
||||
{
|
||||
NavigationViewItem selectedItem = args.SelectedItem as NavigationViewItem;
|
||||
|
||||
if (selectedItem != null)
|
||||
{
|
||||
@@ -278,7 +273,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
break;
|
||||
}
|
||||
*/
|
||||
case "WhatsNew": NavigationFrame.Navigate(typeof(OobeWhatsNew)); break;
|
||||
|
||||
case "AdvancedPaste": NavigationFrame.Navigate(typeof(OobeAdvancedPaste)); break;
|
||||
case "AlwaysOnTop": NavigationFrame.Navigate(typeof(OobeAlwaysOnTop)); break;
|
||||
case "Awake": NavigationFrame.Navigate(typeof(OobeAwake)); break;
|
||||
@@ -311,43 +306,37 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
}
|
||||
}
|
||||
|
||||
private void SetTitleBar()
|
||||
{
|
||||
var u = App.GetOobeWindow();
|
||||
if (u != null)
|
||||
{
|
||||
// A custom title bar is required for full window theme and Mica support.
|
||||
// https://docs.microsoft.com/windows/apps/develop/title-bar?tabs=winui3#full-customization
|
||||
u.ExtendsContentIntoTitleBar = true;
|
||||
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(WindowNative.GetWindowHandle(u));
|
||||
u.SetTitleBar(AppTitleBar);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShellPage_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SetTitleBar();
|
||||
// Select the first module by default
|
||||
if (navigationView.MenuItems.Count > 0)
|
||||
{
|
||||
navigationView.SelectedItem = navigationView.MenuItems[0];
|
||||
}
|
||||
}
|
||||
|
||||
private void NavigationView_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
|
||||
{
|
||||
if (args.DisplayMode == NavigationViewDisplayMode.Compact || args.DisplayMode == NavigationViewDisplayMode.Minimal)
|
||||
{
|
||||
PaneToggleBtn.Visibility = Visibility.Visible;
|
||||
AppTitleBar.Margin = new Thickness(48, 0, 0, 0);
|
||||
AppTitleBarText.Margin = new Thickness(12, 0, 0, 0);
|
||||
TitleBarIcon.Margin = new Thickness(0, 0, 8, 0); // Workaround, see XAML comment
|
||||
AppTitleBar.IsPaneToggleButtonVisible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
PaneToggleBtn.Visibility = Visibility.Collapsed;
|
||||
AppTitleBar.Margin = new Thickness(16, 0, 0, 0);
|
||||
AppTitleBarText.Margin = new Thickness(16, 0, 0, 0);
|
||||
TitleBarIcon.Margin = new Thickness(16, 0, 0, 0); // Workaround, see XAML comment
|
||||
AppTitleBar.IsPaneToggleButtonVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void PaneToggleBtn_Click(object sender, RoutedEventArgs e)
|
||||
private void TitleBar_PaneButtonClick(TitleBar sender, object args)
|
||||
{
|
||||
navigationView.IsPaneOpen = !navigationView.IsPaneOpen;
|
||||
}
|
||||
|
||||
private void WhatIsNewItem_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
OpenScoobeWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button
|
||||
x:Name="LaunchButton"
|
||||
x:Uid="Launch_ShortcutGuide"
|
||||
Click="Start_ShortcutGuide_Click"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
|
||||
@@ -8,7 +8,6 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
|
||||
using Microsoft.PowerToys.Settings.UI.Views;
|
||||
@@ -67,10 +66,6 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
{
|
||||
HotkeyControl.Keys = settingsProperties.OpenShortcutGuide.GetKeysList();
|
||||
}
|
||||
|
||||
// Disable the Launch button if the module is disabled
|
||||
var generalSettings = SettingsRepository<GeneralSettings>.GetInstance(SettingsUtils.Default).SettingsConfig;
|
||||
LaunchButton.IsEnabled = ModuleHelper.GetIsModuleEnabled(generalSettings, ManagedCommon.ModuleType.ShortcutGuide);
|
||||
}
|
||||
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
<Page
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.OobeWhatsNew"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
Loaded="Page_Loaded"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<tkcontrols:MarkdownThemes
|
||||
x:Key="ReleaseNotesMarkdownThemeConfig"
|
||||
H1FontSize="22"
|
||||
H1FontWeight="SemiBold"
|
||||
H1Margin="0, 36, 0, 8"
|
||||
H2FontSize="16"
|
||||
H2FontWeight="SemiBold"
|
||||
H2Margin="0, 16, 0, 4"
|
||||
H3FontSize="16"
|
||||
H3FontWeight="SemiBold"
|
||||
H3Margin="0, 16, 0, 4" />
|
||||
<tkcontrols:MarkdownConfig x:Key="ReleaseNotesMarkdownConfig" Themes="{StaticResource ReleaseNotesMarkdownThemeConfig}" />
|
||||
</Page.Resources>
|
||||
|
||||
<!-- Main layout container -->
|
||||
<Grid>
|
||||
<!-- Main content grid -->
|
||||
<Grid Margin="0,24,0,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Compact Header overlay that covers both InfoBar and Title sections -->
|
||||
<Border
|
||||
x:Name="HeaderOverlay"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Margin="0,-24,0,0"
|
||||
VerticalAlignment="Top"
|
||||
BorderThickness="0"
|
||||
Canvas.ZIndex="1">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<tkcontrols:SettingsCard
|
||||
x:Name="WhatsNewDataDiagnosticsInfoBar"
|
||||
x:Uid="Oobe_WhatsNew_DataDiagnostics_InfoBar"
|
||||
Grid.Row="0"
|
||||
Padding="12,8,12,8"
|
||||
Background="{ThemeResource InfoBarInformationalSeverityBackgroundBrush}"
|
||||
IsTabStop="{x:Bind ShowDataDiagnosticsInfoBar, Mode=OneWay}"
|
||||
Visibility="{x:Bind ShowDataDiagnosticsInfoBar, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<tkcontrols:SettingsCard.HeaderIcon>
|
||||
<FontIcon Foreground="{ThemeResource InfoBarInformationalSeverityIconBackground}" Glyph="" />
|
||||
</tkcontrols:SettingsCard.HeaderIcon>
|
||||
<tkcontrols:SettingsCard.Description>
|
||||
<StackPanel>
|
||||
<TextBlock x:Name="WhatsNewDataDiagnosticsInfoBarDescText">
|
||||
<Hyperlink NavigateUri="https://aka.ms/powertoys-data-and-privacy-documentation">
|
||||
<Run x:Uid="Oobe_WhatsNew_DataDiagnostics_InfoBar_Desc" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
<TextBlock x:Name="WhatsNewDataDiagnosticsInfoBarDescTextYesClicked" Visibility="Collapsed">
|
||||
<Run x:Uid="Oobe_WhatsNew_DataDiagnostics_Yes_Click_InfoBar_Desc" />
|
||||
<Hyperlink Click="DataDiagnostics_OpenSettings_Click">
|
||||
<Run x:Uid="Oobe_WhatsNew_DataDiagnostics_Yes_Click_OpenSettings_Text" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard.Description>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button
|
||||
x:Name="DataDiagnosticsButtonYes"
|
||||
x:Uid="Oobe_WhatsNew_DataDiagnostics_Button_Yes"
|
||||
Click="DataDiagnostics_InfoBar_YesNo_Click"
|
||||
CommandParameter="Yes" />
|
||||
<HyperlinkButton
|
||||
x:Name="DataDiagnosticsButtonNo"
|
||||
x:Uid="Oobe_WhatsNew_DataDiagnostics_Button_No"
|
||||
Click="DataDiagnostics_InfoBar_YesNo_Click"
|
||||
CommandParameter="No" />
|
||||
<Button
|
||||
Margin="16,0,0,0"
|
||||
Click="DataDiagnostics_InfoBar_Close_Click"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
Style="{StaticResource SubtleButtonStyle}" />
|
||||
</StackPanel>
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<Grid Grid.Row="1" Margin="16,12,0,12">
|
||||
<StackPanel
|
||||
VerticalAlignment="Top"
|
||||
Orientation="Vertical"
|
||||
Spacing="4">
|
||||
<TextBlock
|
||||
x:Uid="Oobe_WhatsNew"
|
||||
AutomationProperties.HeadingLevel="Level1"
|
||||
Style="{StaticResource TitleTextBlockStyle}" />
|
||||
<HyperlinkButton NavigateUri="https://github.com/microsoft/PowerToys/releases" Style="{StaticResource TextButtonStyle}">
|
||||
<TextBlock x:Uid="Oobe_WhatsNew_DetailedReleaseNotesLink" TextWrapping="Wrap" />
|
||||
</HyperlinkButton>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ShortcutConflictControl positioned at the right side -->
|
||||
<controls:ShortcutConflictControl
|
||||
Grid.RowSpan="2"
|
||||
Margin="0,0,16,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
AllHotkeyConflictsData="{x:Bind AllHotkeyConflictsData, Mode=OneWay}"
|
||||
Visibility="{x:Bind HasConflicts, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Reduced spacer for the compact header overlay -->
|
||||
<Grid Grid.Row="0" Height="0" />
|
||||
<Grid Grid.Row="1" Height="80" />
|
||||
|
||||
<InfoBar
|
||||
x:Name="ErrorInfoBar"
|
||||
x:Uid="Oobe_WhatsNew_LoadingError"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Top"
|
||||
IsClosable="False"
|
||||
IsTabStop="False"
|
||||
Severity="Error">
|
||||
<InfoBar.ActionButton>
|
||||
<Button
|
||||
x:Uid="RetryBtn"
|
||||
HorizontalAlignment="Right"
|
||||
Click="LoadReleaseNotes_Click">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="RetryLabel" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</InfoBar.ActionButton>
|
||||
</InfoBar>
|
||||
<InfoBar
|
||||
x:Name="ProxyWarningInfoBar"
|
||||
x:Uid="Oobe_WhatsNew_ProxyAuthenticationWarning"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Top"
|
||||
IsClosable="False"
|
||||
IsTabStop="False"
|
||||
Severity="Warning">
|
||||
<InfoBar.ActionButton>
|
||||
<Button
|
||||
x:Uid="RetryBtn"
|
||||
HorizontalAlignment="Right"
|
||||
Click="LoadReleaseNotes_Click">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="RetryLabel" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</InfoBar.ActionButton>
|
||||
</InfoBar>
|
||||
|
||||
<ScrollViewer Grid.Row="3" VerticalScrollBarVisibility="Auto">
|
||||
<Grid Margin="32,16,32,24">
|
||||
<ProgressRing
|
||||
x:Name="LoadingProgressRing"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsIndeterminate="True"
|
||||
Visibility="Visible" />
|
||||
<tkcontrols:MarkdownTextBlock
|
||||
x:Name="ReleaseNotesMarkdown"
|
||||
Config="{StaticResource ReleaseNotesMarkdownConfig}"
|
||||
UseAutoLinks="True"
|
||||
UseEmphasisExtras="True"
|
||||
UseListExtras="True"
|
||||
UsePipeTables="True"
|
||||
UseTaskLists="True" />
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -1,359 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using global::PowerToys.GPOWrapper;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
|
||||
using Microsoft.PowerToys.Settings.UI.SerializationContext;
|
||||
using Microsoft.PowerToys.Settings.UI.Services;
|
||||
using Microsoft.PowerToys.Settings.UI.Views;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Text;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
{
|
||||
public sealed partial class OobeWhatsNew : Page, INotifyPropertyChanged
|
||||
{
|
||||
public OobePowerToysModule ViewModel { get; set; }
|
||||
|
||||
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
|
||||
|
||||
public bool ShowDataDiagnosticsInfoBar => GetShowDataDiagnosticsInfoBar();
|
||||
|
||||
private int _conflictCount;
|
||||
|
||||
public AllHotkeyConflictsData AllHotkeyConflictsData
|
||||
{
|
||||
get => _allHotkeyConflictsData;
|
||||
set
|
||||
{
|
||||
if (_allHotkeyConflictsData != value)
|
||||
{
|
||||
_allHotkeyConflictsData = value;
|
||||
|
||||
UpdateConflictCount();
|
||||
|
||||
OnPropertyChanged(nameof(AllHotkeyConflictsData));
|
||||
OnPropertyChanged(nameof(HasConflicts));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasConflicts => _conflictCount > 0;
|
||||
|
||||
private void UpdateConflictCount()
|
||||
{
|
||||
int count = 0;
|
||||
if (AllHotkeyConflictsData == null)
|
||||
{
|
||||
_conflictCount = count;
|
||||
}
|
||||
|
||||
if (AllHotkeyConflictsData.InAppConflicts != null)
|
||||
{
|
||||
foreach (var inAppConflict in AllHotkeyConflictsData.InAppConflicts)
|
||||
{
|
||||
if (!inAppConflict.ConflictIgnored)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (AllHotkeyConflictsData.SystemConflicts != null)
|
||||
{
|
||||
foreach (var systemConflict in AllHotkeyConflictsData.SystemConflicts)
|
||||
{
|
||||
if (!systemConflict.ConflictIgnored)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_conflictCount = count;
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OobeWhatsNew"/> class.
|
||||
/// </summary>
|
||||
public OobeWhatsNew()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.WhatsNew]);
|
||||
DataContext = this;
|
||||
|
||||
// Subscribe to hotkey conflict updates
|
||||
if (GlobalHotkeyConflictManager.Instance != null)
|
||||
{
|
||||
GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated;
|
||||
GlobalHotkeyConflictManager.Instance.RequestAllConflicts();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
|
||||
{
|
||||
this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
|
||||
{
|
||||
var allConflictData = e.Conflicts;
|
||||
foreach (var inAppConflict in allConflictData.InAppConflicts)
|
||||
{
|
||||
var hotkey = inAppConflict.Hotkey;
|
||||
var hotkeySetting = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key);
|
||||
inAppConflict.ConflictIgnored = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySetting);
|
||||
}
|
||||
|
||||
foreach (var systemConflict in allConflictData.SystemConflicts)
|
||||
{
|
||||
var hotkey = systemConflict.Hotkey;
|
||||
var hotkeySetting = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key);
|
||||
systemConflict.ConflictIgnored = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySetting);
|
||||
}
|
||||
|
||||
AllHotkeyConflictsData = e.Conflicts ?? new AllHotkeyConflictsData();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnPropertyChanged(string propertyName)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
private bool GetShowDataDiagnosticsInfoBar()
|
||||
{
|
||||
var isDataDiagnosticsGpoDisallowed = GPOWrapper.GetAllowDataDiagnosticsValue() == GpoRuleConfigured.Disabled;
|
||||
|
||||
if (isDataDiagnosticsGpoDisallowed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool userActed = DataDiagnosticsSettings.GetUserActionValue();
|
||||
|
||||
if (userActed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool registryValue = DataDiagnosticsSettings.GetEnabledValue();
|
||||
|
||||
bool isFirstRunAfterUpdate = (App.Current as Microsoft.PowerToys.Settings.UI.App).ShowScoobe;
|
||||
if (isFirstRunAfterUpdate && registryValue == false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regex to remove installer hash sections from the release notes.
|
||||
/// </summary>
|
||||
private const string RemoveInstallerHashesRegex = @"(\r\n)+## Installer Hashes(\r\n.*)+## Highlights";
|
||||
private const string RemoveHotFixInstallerHashesRegex = @"(\r\n)+## Installer Hashes(\r\n.*)+$";
|
||||
private const RegexOptions RemoveInstallerHashesRegexOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
|
||||
private bool _loadingReleaseNotes;
|
||||
|
||||
private static async Task<string> GetReleaseNotesMarkdown()
|
||||
{
|
||||
string releaseNotesJSON = string.Empty;
|
||||
|
||||
// Let's use system proxy
|
||||
using var proxyClientHandler = new HttpClientHandler
|
||||
{
|
||||
DefaultProxyCredentials = CredentialCache.DefaultCredentials,
|
||||
Proxy = WebRequest.GetSystemWebProxy(),
|
||||
PreAuthenticate = true,
|
||||
};
|
||||
|
||||
using var getReleaseInfoClient = new HttpClient(proxyClientHandler);
|
||||
|
||||
// GitHub APIs require sending an user agent
|
||||
// https://docs.github.com/rest/overview/resources-in-the-rest-api#user-agent-required
|
||||
getReleaseInfoClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "PowerToys");
|
||||
releaseNotesJSON = await getReleaseInfoClient.GetStringAsync("https://api.github.com/repos/microsoft/PowerToys/releases");
|
||||
IList<PowerToysReleaseInfo> releases = JsonSerializer.Deserialize<IList<PowerToysReleaseInfo>>(releaseNotesJSON, SourceGenerationContextContext.Default.IListPowerToysReleaseInfo);
|
||||
|
||||
// Get the latest releases
|
||||
var latestReleases = releases.OrderByDescending(release => release.PublishedDate).Take(5);
|
||||
|
||||
StringBuilder releaseNotesHtmlBuilder = new StringBuilder(string.Empty);
|
||||
|
||||
// Regex to remove installer hash sections from the release notes.
|
||||
Regex removeHashRegex = new Regex(RemoveInstallerHashesRegex, RemoveInstallerHashesRegexOptions);
|
||||
|
||||
// Regex to remove installer hash sections from the release notes, since there'll be no Highlights section for hotfix releases.
|
||||
Regex removeHotfixHashRegex = new Regex(RemoveHotFixInstallerHashesRegex, RemoveInstallerHashesRegexOptions);
|
||||
int counter = 0;
|
||||
foreach (var release in latestReleases)
|
||||
{
|
||||
releaseNotesHtmlBuilder.AppendLine("# " + release.Name);
|
||||
var notes = removeHashRegex.Replace(release.ReleaseNotes, "\r\n### Highlights");
|
||||
|
||||
// Add a unique counter to [github-current-release-work] to distinguish each release,
|
||||
// since this variable is used for all latest releases when they are merged.
|
||||
notes = notes.Replace("[github-current-release-work]", $"[github-current-release-work{++counter}]");
|
||||
notes = removeHotfixHashRegex.Replace(notes, string.Empty);
|
||||
releaseNotesHtmlBuilder.AppendLine(notes);
|
||||
releaseNotesHtmlBuilder.AppendLine(" ");
|
||||
}
|
||||
|
||||
return releaseNotesHtmlBuilder.ToString();
|
||||
}
|
||||
|
||||
private async Task Reload()
|
||||
{
|
||||
if (_loadingReleaseNotes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_loadingReleaseNotes = true;
|
||||
ReleaseNotesMarkdown.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
LoadingProgressRing.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
|
||||
string releaseNotesMarkdown = await GetReleaseNotesMarkdown();
|
||||
ProxyWarningInfoBar.IsOpen = false;
|
||||
ErrorInfoBar.IsOpen = false;
|
||||
|
||||
ReleaseNotesMarkdown.Text = releaseNotesMarkdown;
|
||||
ReleaseNotesMarkdown.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
|
||||
LoadingProgressRing.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
}
|
||||
catch (HttpRequestException httpEx)
|
||||
{
|
||||
Logger.LogError("Exception when loading the release notes", httpEx);
|
||||
if (httpEx.Message.Contains("407", StringComparison.CurrentCulture))
|
||||
{
|
||||
ProxyWarningInfoBar.IsOpen = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorInfoBar.IsOpen = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Exception when loading the release notes", ex);
|
||||
ErrorInfoBar.IsOpen = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
LoadingProgressRing.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
_loadingReleaseNotes = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async void Page_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
await Reload();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
ViewModel.LogOpeningModuleEvent();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
{
|
||||
ViewModel.LogClosingModuleEvent();
|
||||
|
||||
// Unsubscribe from conflict updates when leaving the page
|
||||
if (GlobalHotkeyConflictManager.Instance != null)
|
||||
{
|
||||
GlobalHotkeyConflictManager.Instance.ConflictsUpdated -= OnConflictsUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
private void DataDiagnostics_InfoBar_YesNo_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
string commandArg = string.Empty;
|
||||
if (sender is Button senderBtn)
|
||||
{
|
||||
commandArg = senderBtn.CommandParameter.ToString();
|
||||
}
|
||||
else if (sender is HyperlinkButton senderLink)
|
||||
{
|
||||
commandArg = senderLink.CommandParameter.ToString();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(commandArg))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Update UI
|
||||
if (commandArg == "Yes")
|
||||
{
|
||||
WhatsNewDataDiagnosticsInfoBar.Header = ResourceLoaderInstance.ResourceLoader.GetString("Oobe_WhatsNew_DataDiagnostics_Yes_Click_InfoBar_Title");
|
||||
}
|
||||
else
|
||||
{
|
||||
WhatsNewDataDiagnosticsInfoBar.Header = ResourceLoaderInstance.ResourceLoader.GetString("Oobe_WhatsNew_DataDiagnostics_No_Click_InfoBar_Title");
|
||||
}
|
||||
|
||||
WhatsNewDataDiagnosticsInfoBarDescText.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
WhatsNewDataDiagnosticsInfoBarDescTextYesClicked.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
|
||||
DataDiagnosticsButtonYes.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
DataDiagnosticsButtonNo.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
|
||||
// Set Data Diagnostics registry values
|
||||
if (commandArg == "Yes")
|
||||
{
|
||||
DataDiagnosticsSettings.SetEnabledValue(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
DataDiagnosticsSettings.SetEnabledValue(false);
|
||||
}
|
||||
|
||||
DataDiagnosticsSettings.SetUserActionValue(true);
|
||||
|
||||
this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
|
||||
{
|
||||
ShellPage.ShellHandler?.SignalGeneralDataUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
private void DataDiagnostics_InfoBar_Close_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
WhatsNewDataDiagnosticsInfoBar.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void DataDiagnostics_OpenSettings_Click(Microsoft.UI.Xaml.Documents.Hyperlink sender, Microsoft.UI.Xaml.Documents.HyperlinkClickEventArgs args)
|
||||
{
|
||||
Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Overview);
|
||||
}
|
||||
|
||||
private async void LoadReleaseNotes_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
await Reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// View model for a group of releases (grouped by major.minor version).
|
||||
/// </summary>
|
||||
public class ScoobeReleaseGroupViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the list of releases in this group.
|
||||
/// </summary>
|
||||
public IList<PowerToysReleaseInfo> Releases { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version text to display (e.g., "0.96.0").
|
||||
/// </summary>
|
||||
public string VersionText { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the date text to display (e.g., "December 2025").
|
||||
/// </summary>
|
||||
public string DateText { get; }
|
||||
|
||||
public ScoobeReleaseGroupViewModel(IList<PowerToysReleaseInfo> releases)
|
||||
{
|
||||
Releases = releases ?? throw new ArgumentNullException(nameof(releases));
|
||||
|
||||
if (releases.Count > 0)
|
||||
{
|
||||
var latestRelease = releases[0];
|
||||
VersionText = GetVersionFromRelease(latestRelease);
|
||||
DateText = latestRelease.PublishedDate.ToString("MMMM yyyy", CultureInfo.CurrentCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
VersionText = "Unknown";
|
||||
DateText = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetVersionFromRelease(PowerToysReleaseInfo release)
|
||||
{
|
||||
// TagName is typically like "v0.96.0", Name might be "Release v0.96.0"
|
||||
string version = release.TagName ?? release.Name ?? "Unknown";
|
||||
if (version.StartsWith("v", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
version = version.Substring(1);
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<Page
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.ScoobeReleaseNotesPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
|
||||
Loaded="Page_Loaded"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<tkcontrols:MarkdownThemes
|
||||
x:Key="ReleaseNotesMarkdownThemeConfig"
|
||||
BoldFontWeight="SemiBold"
|
||||
H1FontSize="28"
|
||||
H1FontWeight="SemiBold"
|
||||
H1Margin="0, 36, 0, 8"
|
||||
H2FontSize="20"
|
||||
H2FontWeight="SemiBold"
|
||||
H2Margin="0, 16, 0, 4"
|
||||
H3FontSize="16"
|
||||
H3FontWeight="SemiBold"
|
||||
H3Margin="0, 16, 0, 4"
|
||||
HorizontalRuleBrush="{StaticResource DividerStrokeColorDefaultBrush}"
|
||||
HorizontalRuleThickness="1"
|
||||
ImageStretch="Uniform"
|
||||
ListBulletSpacing="1"
|
||||
ListGutterWidth="10" />
|
||||
<tkcontrols:MarkdownConfig x:Key="ReleaseNotesMarkdownConfig" Themes="{StaticResource ReleaseNotesMarkdownThemeConfig}" />
|
||||
</Page.Resources>
|
||||
|
||||
<!-- Main layout container -->
|
||||
<Grid MaxWidth="1000">
|
||||
<ScrollViewer Padding="0,0,0,0" VerticalScrollBarVisibility="Auto">
|
||||
<Grid Margin="0,0,0,24">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Image
|
||||
x:Name="HeroImageHolder"
|
||||
Height="186"
|
||||
HorizontalAlignment="Left"
|
||||
Stretch="UniformToFill" />
|
||||
<Grid Grid.Row="1" Margin="24,16,24,24">
|
||||
<ProgressRing
|
||||
x:Name="LoadingProgressRing"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsIndeterminate="True"
|
||||
Visibility="Visible" />
|
||||
<tkcontrols:MarkdownTextBlock
|
||||
x:Name="ReleaseNotesMarkdown"
|
||||
Config="{StaticResource ReleaseNotesMarkdownConfig}"
|
||||
UseAutoLinks="True"
|
||||
UseEmphasisExtras="True"
|
||||
UseListExtras="True"
|
||||
UsePipeTables="True"
|
||||
UseTaskLists="True" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
{
|
||||
public sealed partial class ScoobeReleaseNotesPage : Page
|
||||
{
|
||||
private IList<PowerToysReleaseInfo> _currentReleases;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScoobeReleaseNotesPage"/> class.
|
||||
/// </summary>
|
||||
public ScoobeReleaseNotesPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regex to remove installer hash sections from the release notes.
|
||||
/// </summary>
|
||||
private const string RemoveInstallerHashesRegex = @"(\r\n)+## Installer Hashes(\r\n.*)+## Highlights";
|
||||
private const string RemoveHotFixInstallerHashesRegex = @"(\r\n)+## Installer Hashes(\r\n.*)+$";
|
||||
private const RegexOptions RemoveInstallerHashesRegexOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
|
||||
|
||||
/// <summary>
|
||||
/// Regex to match markdown images with 'Hero' in the alt text.
|
||||
/// Matches: 
|
||||
/// </summary>
|
||||
private static readonly Regex HeroImageRegex = new Regex(
|
||||
@"!\[([^\]]*Hero[^\]]*)\]\(([^)]+)\)",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Regex to match GitHub PR/Issue references (e.g., #41029).
|
||||
/// Only matches # followed by digits that are not already part of a markdown link.
|
||||
/// </summary>
|
||||
private static readonly Regex GitHubPrReferenceRegex = new Regex(
|
||||
@"(?<!\[)#(\d+)(?!\])",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly CompositeFormat GitHubPrLinkTemplate = CompositeFormat.Parse("[#{0}](https://github.com/microsoft/PowerToys/pull/{0})");
|
||||
private static readonly CompositeFormat GitHubReleaseLinkTemplate = CompositeFormat.Parse("https://github.com/microsoft/PowerToys/releases/tag/{0}");
|
||||
|
||||
private static (string Markdown, string HeroImageUrl) ProcessReleaseNotesMarkdown(IList<PowerToysReleaseInfo> releases)
|
||||
{
|
||||
if (releases == null || releases.Count == 0)
|
||||
{
|
||||
return (string.Empty, null);
|
||||
}
|
||||
|
||||
StringBuilder releaseNotesHtmlBuilder = new StringBuilder(string.Empty);
|
||||
|
||||
// Regex to remove installer hash sections from the release notes.
|
||||
Regex removeHashRegex = new Regex(RemoveInstallerHashesRegex, RemoveInstallerHashesRegexOptions);
|
||||
|
||||
// Regex to remove installer hash sections from the release notes, since there'll be no Highlights section for hotfix releases.
|
||||
Regex removeHotfixHashRegex = new Regex(RemoveHotFixInstallerHashesRegex, RemoveInstallerHashesRegexOptions);
|
||||
|
||||
string lastHeroImageUrl = null;
|
||||
|
||||
int counter = 0;
|
||||
bool isFirst = true;
|
||||
foreach (var release in releases)
|
||||
{
|
||||
// Add separator between releases
|
||||
if (!isFirst)
|
||||
{
|
||||
releaseNotesHtmlBuilder.AppendLine("---");
|
||||
releaseNotesHtmlBuilder.AppendLine();
|
||||
}
|
||||
|
||||
isFirst = false;
|
||||
|
||||
var releaseUrl = string.Format(CultureInfo.InvariantCulture, GitHubReleaseLinkTemplate, release.TagName);
|
||||
releaseNotesHtmlBuilder.AppendLine(CultureInfo.InvariantCulture, $"# {release.Name}");
|
||||
releaseNotesHtmlBuilder.AppendLine(CultureInfo.InvariantCulture, $"{release.PublishedDate.ToString("MMMM d, yyyy", CultureInfo.CurrentCulture)} <20> [View on GitHub]({releaseUrl})");
|
||||
releaseNotesHtmlBuilder.AppendLine();
|
||||
releaseNotesHtmlBuilder.AppendLine(" ");
|
||||
releaseNotesHtmlBuilder.AppendLine();
|
||||
var notes = removeHashRegex.Replace(release.ReleaseNotes, "\r\n## Highlights");
|
||||
notes = notes.Replace("[github-current-release-work]", $"[github-current-release-work{++counter}]");
|
||||
notes = removeHotfixHashRegex.Replace(notes, string.Empty);
|
||||
|
||||
// Find all Hero images and keep track of the last one
|
||||
var heroMatches = HeroImageRegex.Matches(notes);
|
||||
foreach (Match match in heroMatches)
|
||||
{
|
||||
lastHeroImageUrl = match.Groups[2].Value;
|
||||
}
|
||||
|
||||
// Remove Hero images from the markdown
|
||||
notes = HeroImageRegex.Replace(notes, string.Empty);
|
||||
|
||||
// Convert GitHub PR/Issue references to hyperlinks
|
||||
notes = GitHubPrReferenceRegex.Replace(notes, match =>
|
||||
string.Format(CultureInfo.InvariantCulture, GitHubPrLinkTemplate, match.Groups[1].Value));
|
||||
|
||||
releaseNotesHtmlBuilder.AppendLine(notes);
|
||||
releaseNotesHtmlBuilder.AppendLine(" ");
|
||||
}
|
||||
|
||||
return (releaseNotesHtmlBuilder.ToString(), lastHeroImageUrl);
|
||||
}
|
||||
|
||||
private void DisplayReleaseNotes()
|
||||
{
|
||||
if (_currentReleases == null || _currentReleases.Count == 0)
|
||||
{
|
||||
ReleaseNotesMarkdown.Visibility = Visibility.Collapsed;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LoadingProgressRing.Visibility = Visibility.Collapsed;
|
||||
|
||||
var (releaseNotesMarkdown, heroImageUrl) = ProcessReleaseNotesMarkdown(_currentReleases);
|
||||
|
||||
// Set the Hero image if found
|
||||
if (!string.IsNullOrEmpty(heroImageUrl))
|
||||
{
|
||||
HeroImageHolder.Source = new BitmapImage(new Uri(heroImageUrl));
|
||||
HeroImageHolder.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
HeroImageHolder.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
ReleaseNotesMarkdown.Text = releaseNotesMarkdown;
|
||||
ReleaseNotesMarkdown.Visibility = Visibility.Visible;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Exception when displaying the release notes", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void Page_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DisplayReleaseNotes();
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
if (e.Parameter is IList<PowerToysReleaseInfo> releases)
|
||||
{
|
||||
_currentReleases = releases;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<Page
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.ScoobeShellPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.PowerToys.Settings.UI.OOBE.Views"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
HighContrastAdjustment="None"
|
||||
Loaded="ShellPage_Loaded"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<!-- Template for NavigationViewItem content with version and date -->
|
||||
<DataTemplate x:Key="ReleaseNavItemTemplate" x:DataType="local:ScoobeReleaseGroupViewModel">
|
||||
<StackPanel
|
||||
Margin="0,8,0,8"
|
||||
Orientation="Vertical"
|
||||
Spacing="4">
|
||||
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{x:Bind DateText}" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind VersionText}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</Page.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="48" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TitleBar
|
||||
x:Name="AppTitleBar"
|
||||
x:Uid="ScoobeWindow_TitleTxt"
|
||||
IsBackButtonVisible="False"
|
||||
IsPaneToggleButtonVisible="False"
|
||||
PaneToggleRequested="TitleBar_PaneButtonClick">
|
||||
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
|
||||
<TitleBar.LeftHeader>
|
||||
<ImageIcon
|
||||
x:Name="TitleBarIcon"
|
||||
Height="16"
|
||||
Margin="16,0,0,0"
|
||||
Source="/Assets/Settings/icon.ico" />
|
||||
</TitleBar.LeftHeader>
|
||||
</TitleBar>
|
||||
<NavigationView
|
||||
x:Name="navigationView"
|
||||
Grid.Row="1"
|
||||
CompactModeThresholdWidth="1007"
|
||||
DisplayModeChanged="NavigationView_DisplayModeChanged"
|
||||
ExpandedModeThresholdWidth="1007"
|
||||
IsBackButtonVisible="Collapsed"
|
||||
IsPaneOpen="True"
|
||||
IsPaneToggleButtonVisible="False"
|
||||
IsSettingsVisible="False"
|
||||
MenuItemTemplate="{StaticResource ReleaseNavItemTemplate}"
|
||||
OpenPaneLength="186"
|
||||
SelectionChanged="NavigationView_SelectionChanged">
|
||||
<NavigationView.MenuItems>
|
||||
<!-- Items are added dynamically -->
|
||||
</NavigationView.MenuItems>
|
||||
<NavigationView.Content>
|
||||
<Grid>
|
||||
<ProgressRing
|
||||
x:Name="LoadingProgressRing"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsIndeterminate="True"
|
||||
Visibility="Collapsed" />
|
||||
<InfoBar
|
||||
x:Name="ErrorInfoBar"
|
||||
x:Uid="Oobe_WhatsNew_LoadingError"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsClosable="False"
|
||||
IsOpen="False"
|
||||
Severity="Error">
|
||||
<InfoBar.ActionButton>
|
||||
<Button
|
||||
x:Uid="RetryBtn"
|
||||
HorizontalAlignment="Right"
|
||||
Click="RetryButton_Click">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="RetryLabel" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</InfoBar.ActionButton>
|
||||
</InfoBar>
|
||||
<Frame x:Name="NavigationFrame" />
|
||||
</Grid>
|
||||
</NavigationView.Content>
|
||||
</NavigationView>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -0,0 +1,194 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.SerializationContext;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
{
|
||||
public sealed partial class ScoobeShellPage : Page
|
||||
{
|
||||
public static Action<Type> OpenMainWindowCallback { get; set; }
|
||||
|
||||
public static void SetOpenMainWindowCallback(Action<Type> implementation)
|
||||
{
|
||||
OpenMainWindowCallback = implementation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a shell handler to be used to update contents of the shell dynamically from page within the frame.
|
||||
/// </summary>
|
||||
public static ScoobeShellPage ScoobeShellHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of release groups loaded from GitHub (grouped by major.minor version).
|
||||
/// </summary>
|
||||
public IList<IList<PowerToysReleaseInfo>> ReleaseGroups { get; private set; }
|
||||
|
||||
private bool _isLoading;
|
||||
|
||||
public ScoobeShellPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
ScoobeShellHandler = this;
|
||||
}
|
||||
|
||||
private async void ShellPage_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SetTitleBar();
|
||||
await LoadReleasesAsync();
|
||||
}
|
||||
|
||||
private async Task LoadReleasesAsync()
|
||||
{
|
||||
if (_isLoading)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
LoadingProgressRing.Visibility = Visibility.Visible;
|
||||
ErrorInfoBar.IsOpen = false;
|
||||
navigationView.MenuItems.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var releases = await FetchReleasesFromGitHubAsync();
|
||||
ReleaseGroups = GroupReleasesByMajorMinor(releases);
|
||||
PopulateNavigationItems();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to load releases", ex);
|
||||
ErrorInfoBar.IsOpen = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
LoadingProgressRing.Visibility = Visibility.Collapsed;
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IList<PowerToysReleaseInfo>> FetchReleasesFromGitHubAsync()
|
||||
{
|
||||
using var proxyClientHandler = new HttpClientHandler
|
||||
{
|
||||
DefaultProxyCredentials = CredentialCache.DefaultCredentials,
|
||||
Proxy = WebRequest.GetSystemWebProxy(),
|
||||
PreAuthenticate = true,
|
||||
};
|
||||
|
||||
using var httpClient = new HttpClient(proxyClientHandler);
|
||||
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "PowerToys");
|
||||
|
||||
string json = await httpClient.GetStringAsync("https://api.github.com/repos/microsoft/PowerToys/releases?per_page=20");
|
||||
var allReleases = JsonSerializer.Deserialize<IList<PowerToysReleaseInfo>>(json, SourceGenerationContextContext.Default.IListPowerToysReleaseInfo);
|
||||
|
||||
return allReleases
|
||||
.OrderByDescending(r => r.PublishedDate)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IList<IList<PowerToysReleaseInfo>> GroupReleasesByMajorMinor(IList<PowerToysReleaseInfo> releases)
|
||||
{
|
||||
return releases
|
||||
.GroupBy(r => GetMajorMinorVersion(r))
|
||||
.Select(g => g.OrderByDescending(r => r.PublishedDate).ToList() as IList<PowerToysReleaseInfo>)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string GetMajorMinorVersion(PowerToysReleaseInfo release)
|
||||
{
|
||||
string version = GetVersionFromRelease(release);
|
||||
var parts = version.Split('.');
|
||||
if (parts.Length >= 2)
|
||||
{
|
||||
return $"{parts[0]}.{parts[1]}";
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
private static string GetVersionFromRelease(PowerToysReleaseInfo release)
|
||||
{
|
||||
// TagName is typically like "v0.96.0", Name might be "Release v0.96.0"
|
||||
string version = release.TagName ?? release.Name ?? "Unknown";
|
||||
if (version.StartsWith("v", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
version = version.Substring(1);
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
private void PopulateNavigationItems()
|
||||
{
|
||||
if (ReleaseGroups == null || ReleaseGroups.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var releaseGroup in ReleaseGroups)
|
||||
{
|
||||
var viewModel = new ScoobeReleaseGroupViewModel(releaseGroup);
|
||||
navigationView.MenuItems.Add(viewModel);
|
||||
}
|
||||
|
||||
// Select the first item to trigger navigation
|
||||
navigationView.SelectedItem = navigationView.MenuItems[0];
|
||||
}
|
||||
|
||||
private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
|
||||
{
|
||||
if (args.SelectedItem is ScoobeReleaseGroupViewModel viewModel)
|
||||
{
|
||||
NavigationFrame.Navigate(typeof(ScoobeReleaseNotesPage), viewModel.Releases);
|
||||
}
|
||||
}
|
||||
|
||||
private async void RetryButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await LoadReleasesAsync();
|
||||
}
|
||||
|
||||
private void SetTitleBar()
|
||||
{
|
||||
var window = App.GetScoobeWindow();
|
||||
if (window != null)
|
||||
{
|
||||
window.ExtendsContentIntoTitleBar = true;
|
||||
window.SetTitleBar(AppTitleBar);
|
||||
}
|
||||
}
|
||||
|
||||
private void NavigationView_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
|
||||
{
|
||||
if (args.DisplayMode == NavigationViewDisplayMode.Compact || args.DisplayMode == NavigationViewDisplayMode.Minimal)
|
||||
{
|
||||
TitleBarIcon.Margin = new Thickness(0, 0, 8, 0); // Workaround, see XAML comment
|
||||
AppTitleBar.IsPaneToggleButtonVisible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
TitleBarIcon.Margin = new Thickness(16, 0, 0, 0); // Workaround, see XAML comment
|
||||
AppTitleBar.IsPaneToggleButtonVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void TitleBar_PaneButtonClick(TitleBar sender, object args)
|
||||
{
|
||||
navigationView.IsPaneOpen = !navigationView.IsPaneOpen;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
_windowId = Win32Interop.GetWindowIdFromWindow(_hWnd);
|
||||
_appWindow = AppWindow.GetFromWindowId(_windowId);
|
||||
this.Activated += Window_Activated_SetIcon;
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
|
||||
var dpi = NativeMethods.GetDpiForWindow(_hWnd);
|
||||
_currentDPI = dpi;
|
||||
@@ -60,7 +61,7 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
|
||||
this.SizeChanged += OobeWindow_SizeChanged;
|
||||
|
||||
var loader = Helpers.ResourceLoaderInstance.ResourceLoader;
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
Title = loader.GetString("OobeWindow_Title");
|
||||
|
||||
if (shellPage != null)
|
||||
|
||||
17
src/settings-ui/Settings.UI/SettingsXAML/ScoobeWindow.xaml
Normal file
17
src/settings-ui/Settings.UI/SettingsXAML/ScoobeWindow.xaml
Normal file
@@ -0,0 +1,17 @@
|
||||
<winuiex:WindowEx
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.ScoobeWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.PowerToys.Settings.UI.OOBE.Views"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
MinWidth="480"
|
||||
MinHeight="480"
|
||||
Closed="Window_Closed"
|
||||
mc:Ignorable="d">
|
||||
<Window.SystemBackdrop>
|
||||
<MicaBackdrop />
|
||||
</Window.SystemBackdrop>
|
||||
<local:ScoobeShellPage x:Name="shellPage" />
|
||||
</winuiex:WindowEx>
|
||||
121
src/settings-ui/Settings.UI/SettingsXAML/ScoobeWindow.xaml.cs
Normal file
121
src/settings-ui/Settings.UI/SettingsXAML/ScoobeWindow.xaml.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Views;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using PowerToys.Interop;
|
||||
using Windows.Graphics;
|
||||
using WinUIEx;
|
||||
using WinUIEx.Messaging;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI
|
||||
{
|
||||
public sealed partial class ScoobeWindow : WindowEx, IDisposable
|
||||
{
|
||||
private const int ExpectedWidth = 1100;
|
||||
private const int ExpectedHeight = 700;
|
||||
private const int DefaultDPI = 96;
|
||||
private int _currentDPI;
|
||||
private WindowId _windowId;
|
||||
private IntPtr _hWnd;
|
||||
private AppWindow _appWindow;
|
||||
private bool disposedValue;
|
||||
|
||||
public ScoobeWindow()
|
||||
{
|
||||
App.ThemeService.ThemeChanged += OnThemeChanged;
|
||||
App.ThemeService.ApplyTheme();
|
||||
|
||||
this.InitializeComponent();
|
||||
|
||||
_hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
_windowId = Win32Interop.GetWindowIdFromWindow(_hWnd);
|
||||
_appWindow = AppWindow.GetFromWindowId(_windowId);
|
||||
this.Activated += Window_Activated_SetIcon;
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
|
||||
var dpi = NativeMethods.GetDpiForWindow(_hWnd);
|
||||
_currentDPI = dpi;
|
||||
float scalingFactor = (float)dpi / DefaultDPI;
|
||||
int width = (int)(ExpectedWidth * scalingFactor);
|
||||
int height = (int)(ExpectedHeight * scalingFactor);
|
||||
|
||||
SizeInt32 size;
|
||||
size.Width = width;
|
||||
size.Height = height;
|
||||
_appWindow.Resize(size);
|
||||
|
||||
this.SizeChanged += ScoobeWindow_SizeChanged;
|
||||
|
||||
var loader = Helpers.ResourceLoaderInstance.ResourceLoader;
|
||||
Title = loader.GetString("ScoobeWindow_Title");
|
||||
|
||||
ScoobeShellPage.SetOpenMainWindowCallback((Type type) =>
|
||||
{
|
||||
App.OpenSettingsWindow(type);
|
||||
});
|
||||
}
|
||||
|
||||
private void Window_Activated_SetIcon(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
// Set window icon
|
||||
_appWindow.SetIcon("Assets\\Settings\\icon.ico");
|
||||
}
|
||||
|
||||
private void ScoobeWindow_SizeChanged(object sender, WindowSizeChangedEventArgs args)
|
||||
{
|
||||
var dpi = NativeMethods.GetDpiForWindow(_hWnd);
|
||||
if (_currentDPI != dpi)
|
||||
{
|
||||
// Reacting to a DPI change. Should not cause a resize -> sizeChanged loop.
|
||||
_currentDPI = dpi;
|
||||
float scalingFactor = (float)dpi / DefaultDPI;
|
||||
int width = (int)(ExpectedWidth * scalingFactor);
|
||||
int height = (int)(ExpectedHeight * scalingFactor);
|
||||
SizeInt32 size;
|
||||
size.Width = width;
|
||||
size.Height = height;
|
||||
_appWindow.Resize(size);
|
||||
}
|
||||
}
|
||||
|
||||
private void Window_Closed(object sender, WindowEventArgs args)
|
||||
{
|
||||
App.ClearScoobeWindow();
|
||||
|
||||
var mainWindow = App.GetSettingsWindow();
|
||||
if (mainWindow != null)
|
||||
{
|
||||
mainWindow.CloseHiddenWindow();
|
||||
}
|
||||
|
||||
App.ThemeService.ThemeChanged -= OnThemeChanged;
|
||||
}
|
||||
|
||||
private void OnThemeChanged(object sender, ElementTheme theme)
|
||||
{
|
||||
WindowHelper.SetTheme(this, theme);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,6 @@ using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Views;
|
||||
using Microsoft.PowerToys.Settings.UI.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
@@ -50,26 +48,30 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
private void WhatsNewButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (App.GetOobeWindow() == null)
|
||||
if (App.GetScoobeWindow() == null)
|
||||
{
|
||||
App.SetOobeWindow(new OobeWindow(PowerToysModules.WhatsNew));
|
||||
}
|
||||
else
|
||||
{
|
||||
App.GetOobeWindow().SetAppWindow(PowerToysModules.WhatsNew);
|
||||
App.SetScoobeWindow(new ScoobeWindow());
|
||||
}
|
||||
|
||||
App.GetOobeWindow().Activate();
|
||||
App.GetScoobeWindow().Activate();
|
||||
}
|
||||
|
||||
private void SortAlphabetical_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.DashboardSortOrder = DashboardSortOrder.Alphabetical;
|
||||
if (sender is ToggleMenuFlyoutItem item)
|
||||
{
|
||||
item.IsChecked = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void SortByStatus_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.DashboardSortOrder = DashboardSortOrder.ByStatus;
|
||||
if (sender is ToggleMenuFlyoutItem item)
|
||||
{
|
||||
item.IsChecked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,13 @@
|
||||
<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>
|
||||
|
||||
@@ -2419,9 +2419,15 @@ From there, simply click on one of the supported files in the File Explorer and
|
||||
<data name="OobeWindow_Title" xml:space="preserve">
|
||||
<value>Welcome to PowerToys</value>
|
||||
</data>
|
||||
<data name="OobeWindow_TitleTxt.Text" xml:space="preserve">
|
||||
<data name="OobeWindow_TitleTxt.Title" xml:space="preserve">
|
||||
<value>Welcome to PowerToys</value>
|
||||
</data>
|
||||
<data name="ScoobeWindow_Title" xml:space="preserve">
|
||||
<value>What's new in PowerToys</value>
|
||||
</data>
|
||||
<data name="ScoobeWindow_TitleTxt.Title" xml:space="preserve">
|
||||
<value>What's new in PowerToys</value>
|
||||
</data>
|
||||
<data name="SettingsWindow_Title" xml:space="preserve">
|
||||
<value>PowerToys Settings</value>
|
||||
<comment>Title of the settings window when running as user</comment>
|
||||
@@ -2722,6 +2728,18 @@ 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>
|
||||
|
||||
@@ -76,16 +76,24 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
GeneralSettingsConfig = settingsRepository.SettingsConfig;
|
||||
|
||||
// To obtain the settings configurations of Fancy zones.
|
||||
ArgumentNullException.ThrowIfNull(settingsRepository);
|
||||
// To obtain the settings configurations of Advanced Paste.
|
||||
ArgumentNullException.ThrowIfNull(advancedPasteSettingsRepository);
|
||||
|
||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
|
||||
|
||||
ArgumentNullException.ThrowIfNull(advancedPasteSettingsRepository);
|
||||
_advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig ?? throw new ArgumentException("SettingsConfig cannot be null", nameof(advancedPasteSettingsRepository));
|
||||
|
||||
_advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig;
|
||||
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();
|
||||
|
||||
AttachConfigurationHandlers();
|
||||
|
||||
@@ -93,7 +101,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
SendConfigMSG = ipcMSGCallBackFunc;
|
||||
|
||||
_additionalActions = _advancedPasteSettings.Properties.AdditionalActions;
|
||||
_customActions = _advancedPasteSettings.Properties.CustomActions.Value;
|
||||
_customActions = _advancedPasteSettings.Properties.CustomActions.Value ?? new ObservableCollection<AdvancedPasteCustomAction>();
|
||||
|
||||
SetupSettingsFileWatcher();
|
||||
|
||||
@@ -469,7 +477,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
public PasteAIConfiguration PasteAIConfiguration
|
||||
{
|
||||
get => _advancedPasteSettings.Properties.PasteAIConfiguration;
|
||||
get
|
||||
{
|
||||
// Ensure PasteAIConfiguration is never null for XAML binding
|
||||
_advancedPasteSettings.Properties.PasteAIConfiguration ??= new PasteAIConfiguration();
|
||||
return _advancedPasteSettings.Properties.PasteAIConfiguration;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (!ReferenceEquals(value, _advancedPasteSettings.Properties.PasteAIConfiguration))
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
public partial class DashboardViewModel : PageViewModelBase
|
||||
{
|
||||
private readonly object _sortLock = new object();
|
||||
|
||||
protected override string ModuleName => "Dashboard";
|
||||
|
||||
private Dispatcher dispatcher;
|
||||
@@ -51,6 +53,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
// Flag to prevent circular updates when a UI toggle triggers settings changes.
|
||||
private bool _isUpdatingFromUI;
|
||||
|
||||
// Flag to prevent toggle operations during sorting to avoid race conditions.
|
||||
private bool _isSorting;
|
||||
|
||||
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
|
||||
|
||||
public AllHotkeyConflictsData AllHotkeyConflictsData
|
||||
@@ -80,15 +85,17 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
get => generalSettingsConfig.DashboardSortOrder;
|
||||
set
|
||||
{
|
||||
if (Set(ref _dashboardSortOrder, value))
|
||||
if (_dashboardSortOrder != value)
|
||||
{
|
||||
_dashboardSortOrder = value;
|
||||
generalSettingsConfig.DashboardSortOrder = value;
|
||||
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(generalSettingsConfig);
|
||||
|
||||
// Save settings to file
|
||||
SettingsUtils.Default.SaveSettings(generalSettingsConfig.ToJsonString());
|
||||
|
||||
SendConfigMSG(outgoing.ToString());
|
||||
|
||||
// Notify UI before sorting so menu updates its checked state
|
||||
OnPropertyChanged(nameof(DashboardSortOrder));
|
||||
|
||||
SortModuleList();
|
||||
}
|
||||
}
|
||||
@@ -103,7 +110,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
dispatcher = Dispatcher.CurrentDispatcher;
|
||||
_settingsRepository = settingsRepository;
|
||||
generalSettingsConfig = settingsRepository.SettingsConfig;
|
||||
generalSettingsConfig.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
|
||||
|
||||
_settingsRepository.SettingsChanged += OnSettingsChanged;
|
||||
|
||||
// Initialize dashboard sort order from settings
|
||||
@@ -128,7 +135,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
generalSettingsConfig = newSettings;
|
||||
generalSettingsConfig.AddEnabledModuleChangeNotification(ModuleEnabledChangedOnSettingsPage);
|
||||
|
||||
// Update local field and notify UI if sort order changed
|
||||
if (_dashboardSortOrder != generalSettingsConfig.DashboardSortOrder)
|
||||
{
|
||||
_dashboardSortOrder = generalSettingsConfig.DashboardSortOrder;
|
||||
OnPropertyChanged(nameof(DashboardSortOrder));
|
||||
}
|
||||
|
||||
ModuleEnabledChangedOnSettingsPage();
|
||||
});
|
||||
}
|
||||
@@ -198,40 +212,58 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
/// Sorts the module list according to the current sort order and updates the AllModules collection.
|
||||
/// On first call, populates AllModules. On subsequent calls, uses Move() to reorder items in-place
|
||||
/// to avoid destroying and recreating UI elements.
|
||||
/// Temporarily disables interaction on all items during sorting to prevent race conditions.
|
||||
/// </summary>
|
||||
private void SortModuleList()
|
||||
{
|
||||
var sortedItems = (DashboardSortOrder switch
|
||||
if (_isSorting)
|
||||
{
|
||||
DashboardSortOrder.ByStatus => _moduleItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label),
|
||||
_ => _moduleItems.OrderBy(x => x.Label), // Default alphabetical
|
||||
}).ToList();
|
||||
|
||||
// If AllModules is empty (first load), just populate it.
|
||||
if (AllModules.Count == 0)
|
||||
{
|
||||
foreach (var item in sortedItems)
|
||||
{
|
||||
AllModules.Add(item);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, update the collection in place using Move to avoid UI glitches.
|
||||
for (int i = 0; i < sortedItems.Count; i++)
|
||||
lock (_sortLock)
|
||||
{
|
||||
var currentItem = sortedItems[i];
|
||||
var currentIndex = AllModules.IndexOf(currentItem);
|
||||
|
||||
if (currentIndex != -1 && currentIndex != i)
|
||||
_isSorting = true;
|
||||
try
|
||||
{
|
||||
AllModules.Move(currentIndex, i);
|
||||
var sortedItems = (DashboardSortOrder switch
|
||||
{
|
||||
DashboardSortOrder.ByStatus => _moduleItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label),
|
||||
_ => _moduleItems.OrderBy(x => x.Label), // Default alphabetical
|
||||
}).ToList();
|
||||
|
||||
// If AllModules is empty (first load), just populate it.
|
||||
if (AllModules.Count == 0)
|
||||
{
|
||||
foreach (var item in sortedItems)
|
||||
{
|
||||
AllModules.Add(item);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, update the collection in place using Move to avoid UI glitches.
|
||||
for (int i = 0; i < sortedItems.Count; i++)
|
||||
{
|
||||
var currentItem = sortedItems[i];
|
||||
var currentIndex = AllModules.IndexOf(currentItem);
|
||||
|
||||
if (currentIndex != -1 && currentIndex != i)
|
||||
{
|
||||
AllModules.Move(currentIndex, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Use dispatcher to reset flag after UI updates complete
|
||||
dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background, () =>
|
||||
{
|
||||
_isSorting = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Notify that DashboardSortOrder changed so the menu updates its checked state.
|
||||
OnPropertyChanged(nameof(DashboardSortOrder));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -279,10 +311,25 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
var dashboardListItem = (DashboardListItem)item;
|
||||
var isEnabled = dashboardListItem.IsEnabled;
|
||||
|
||||
// Ignore toggle operations during sorting to prevent race conditions.
|
||||
// Revert the toggle state since UI already changed due to TwoWay binding.
|
||||
if (_isSorting)
|
||||
{
|
||||
dashboardListItem.UpdateStatus(!isEnabled);
|
||||
return;
|
||||
}
|
||||
|
||||
_isUpdatingFromUI = true;
|
||||
try
|
||||
{
|
||||
Views.ShellPage.UpdateGeneralSettingsCallback(dashboardListItem.Tag, isEnabled);
|
||||
// Send optimized IPC message with only the module status update
|
||||
// Format: {"module_status": {"ModuleName": true/false}}
|
||||
string moduleKey = ModuleHelper.GetModuleKey(dashboardListItem.Tag);
|
||||
string moduleStatusJson = $"{{\"module_status\": {{\"{moduleKey}\": {isEnabled.ToString().ToLowerInvariant()}}}}}";
|
||||
SendConfigMSG(moduleStatusJson);
|
||||
|
||||
// Update local settings config to keep UI in sync
|
||||
ModuleHelper.SetIsModuleEnabled(generalSettingsConfig, dashboardListItem.Tag, isEnabled);
|
||||
|
||||
if (dashboardListItem.Tag == ModuleType.NewPlus && isEnabled == true)
|
||||
{
|
||||
|
||||
@@ -113,6 +113,9 @@ 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);
|
||||
@@ -1083,6 +1086,34 @@ 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);
|
||||
@@ -1154,5 +1185,6 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user