Compare commits

...

6 Commits

Author SHA1 Message Date
Jeffrey
9ee0126259 Bump version 2026-06-24 22:13:48 +02:00
Jeffrey
e23ecf36d6 Update minimum window sizing 2026-06-24 22:00:44 +02:00
Jeffrey
32dc3d6bdf Fix maximized window sizing (#673) 2026-06-24 21:45:53 +02:00
Jeffrey
f76adc5054 Add docstrings 2026-06-24 20:55:17 +02:00
Jeffrey
95b583606d Update start menu backup/restore with timestamped filenames (#672) 2026-06-24 17:32:45 +02:00
Jeffrey
693b805114 Simplify Window management (#671) 2026-06-24 14:40:48 +02:00
7 changed files with 180 additions and 199 deletions

View File

@@ -2,13 +2,13 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework" xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
Title="Win11Debloat" Title="Win11Debloat"
MinWidth="860" MinHeight="600" MinWidth="860" MinHeight="640"
ResizeMode="CanResize" ResizeMode="CanResize"
SnapsToDevicePixels="True" SnapsToDevicePixels="True"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
WindowStyle="None" WindowStyle="None"
AllowsTransparency="True" AllowsTransparency="False"
Background="Transparent" Background="{DynamicResource AppBorderColor}"
Foreground="{DynamicResource AppFgColor}"> Foreground="{DynamicResource AppFgColor}">
<shell:WindowChrome.WindowChrome> <shell:WindowChrome.WindowChrome>
<shell:WindowChrome ResizeBorderThickness="5" <shell:WindowChrome ResizeBorderThickness="5"
@@ -464,7 +464,7 @@
<Grid> <Grid>
<StackPanel x:Name="HomeContentPanel" HorizontalAlignment="Center" VerticalAlignment="Top"> <StackPanel x:Name="HomeContentPanel" HorizontalAlignment="Center" VerticalAlignment="Top">
<!-- Logo --> <!-- Logo -->
<Viewbox Width="250" Height="250" Margin="0,0,0,24" HorizontalAlignment="Center"> <Viewbox Width="250" Height="250" Margin="0,0,0,16" HorizontalAlignment="Center">
<Grid Width="250" Height="250"> <Grid Width="250" Height="250">
<!-- Windows logo style icon --> <!-- Windows logo style icon -->
<Path x:Name="LogoFallback" Data="M0,0 L80,0 L80,80 L0,80 Z M90,0 L170,0 L170,80 L90,80 Z M0,90 L80,90 L80,170 L0,170 Z M90,90 L170,90 L170,170 L90,170 Z" <Path x:Name="LogoFallback" Data="M0,0 L80,0 L80,80 L0,80 Z M90,0 L170,0 L170,80 L90,80 Z M0,90 L80,90 L80,170 L0,170 Z M90,90 L170,90 L170,170 L90,170 Z"
@@ -483,7 +483,7 @@
<!-- Title --> <!-- Title -->
<TextBlock Text="Welcome to Win11Debloat" FontSize="40" FontWeight="SemiBold" Foreground="{DynamicResource AppFgColor}" HorizontalAlignment="Center"/> <TextBlock Text="Welcome to Win11Debloat" FontSize="40" FontWeight="SemiBold" Foreground="{DynamicResource AppFgColor}" HorizontalAlignment="Center"/>
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource AppFgColor}" FontSize="20" HorizontalAlignment="Center" Margin="0,8,0,64"> <TextBlock TextWrapping="Wrap" Foreground="{DynamicResource AppFgColor}" FontSize="20" HorizontalAlignment="Center" Margin="0,4,0,48">
<Run Text="Your clean Windows experience is just a few clicks away!"/> <Run Text="Your clean Windows experience is just a few clicks away!"/>
</TextBlock> </TextBlock>

View File

@@ -113,11 +113,15 @@ function ReplaceStartMenu {
return return
} }
$backupBinFile = $startMenuBinFile + ".bak" $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$backupFileName = "Win11Debloat-StartBackup-$timestamp.bak"
$startMenuDir = Split-Path $startMenuBinFile -Parent
$backupBinFile = Join-Path $startMenuDir $backupFileName
if (Test-Path $startMenuBinFile) { if (Test-Path $startMenuBinFile) {
# Backup current start menu file # Backup current start menu file
Move-Item -Path $startMenuBinFile -Destination $backupBinFile -Force Copy-Item -Path $startMenuBinFile -Destination $backupBinFile -Force
Write-Verbose "Start menu backup for user $userName saved to $backupFileName"
} }
else { else {
Write-Host "Unable to find original start2.bin file for user $userName, no backup was created for this user" -ForegroundColor Yellow Write-Host "Unable to find original start2.bin file for user $userName, no backup was created for this user" -ForegroundColor Yellow
@@ -189,6 +193,55 @@ function GetStartMenuUserNameFromPath {
} }
<#
.SYNOPSIS
Returns the path to the latest start menu backup file for the given scope.
.DESCRIPTION
Resolves the LocalState folder for the specified scope and returns the
full path to the most recent Win11Debloat-StartBackup-*.bak file, or
$null if no backup exists.
For CurrentUser, uses $env:LOCALAPPDATA directly. For AllUsers, scans
every user profile.
.PARAMETER Scope
The scope to check: CurrentUser or AllUsers.
.EXAMPLE
$backupPath = Get-StartMenuBackupPath -Scope 'CurrentUser'
.EXAMPLE
$backupPath = Get-StartMenuBackupPath -Scope 'AllUsers'
#>
function Get-StartMenuBackupPath {
param(
[Parameter(Mandatory)]
[ValidateSet('CurrentUser', 'AllUsers')]
[string]$Scope
)
if ($Scope -eq 'CurrentUser') {
$localStateDir = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$latestBackup = Get-ChildItem -Path (Join-Path $localStateDir 'Win11Debloat-StartBackup-*.bak') -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
Select-Object -First 1
if ($latestBackup) { return $latestBackup.FullName }
return $null
}
else {
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
foreach ($startMenuPath in $usersStartMenuPaths) {
$latestBackup = Get-ChildItem -Path (Join-Path $startMenuPath.FullName 'Win11Debloat-StartBackup-*.bak') -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
Select-Object -First 1
if ($latestBackup) { return $latestBackup.FullName }
}
return $null
}
}
<# <#
.SYNOPSIS .SYNOPSIS
@@ -204,14 +257,14 @@ function GetStartMenuUserNameFromPath {
The full path to the user's start2.bin file to restore. The full path to the user's start2.bin file to restore.
.PARAMETER BackupFilePath .PARAMETER BackupFilePath
Path to the backup file to restore from. If omitted, defaults to Path to the backup file to restore from. If omitted, automatically
StartMenuBinFile with a .bak extension. finds the latest Win11Debloat-StartBackup-*.bak file.
.EXAMPLE .EXAMPLE
RestoreStartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" RestoreStartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
.EXAMPLE .EXAMPLE
RestoreStartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -BackupFilePath "C:\Backups\start2.bin" RestoreStartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
#> #>
function RestoreStartMenuFromBackup { function RestoreStartMenuFromBackup {
param( param(
@@ -222,12 +275,28 @@ function RestoreStartMenuFromBackup {
$userName = GetStartMenuUserNameFromPath -StartMenuBinFile $StartMenuBinFile $userName = GetStartMenuUserNameFromPath -StartMenuBinFile $StartMenuBinFile
$backupBinFile = if ([string]::IsNullOrWhiteSpace($BackupFilePath)) { $backupBinFile = if ([string]::IsNullOrWhiteSpace($BackupFilePath)) {
$StartMenuBinFile + '.bak' # Auto-detect latest backup in the same folder as the start2.bin
$startMenuDir = Split-Path $StartMenuBinFile -Parent
$latestBackup = Get-ChildItem -Path (Join-Path $startMenuDir 'Win11Debloat-StartBackup-*.bak') -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
Select-Object -First 1
if ($latestBackup) { $latestBackup.FullName } else { $null }
} }
else { else {
$BackupFilePath $BackupFilePath
} }
$currentBinBackup = $StartMenuBinFile + '.restore.bak' $restoreTimestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$restoreBackupFileName = "Win11Debloat-StartRestore-$restoreTimestamp.bak"
$currentBinBackup = Join-Path (Split-Path $StartMenuBinFile -Parent) $restoreBackupFileName
if ([string]::IsNullOrWhiteSpace($backupBinFile)) {
return [PSCustomObject]@{
UserName = $userName
Result = $false
Message = "No start menu backup file found for user $userName."
}
}
if ($script:Params.ContainsKey("WhatIf")) { if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Restore start menu for user $userName from backup $backupBinFile" -ForegroundColor Cyan Write-Host "[WhatIf] Restore start menu for user $userName from backup $backupBinFile" -ForegroundColor Cyan
@@ -272,37 +341,26 @@ function RestoreStartMenuFromBackup {
Restores the start menu for the current target user from a backup. Restores the start menu for the current target user from a backup.
.DESCRIPTION .DESCRIPTION
Resolves the start2.bin path for the current user (or the user specified Resolves the start2.bin path for the currently logged-in user, then
via the -User parameter), then delegates to RestoreStartMenuFromBackup. delegates to RestoreStartMenuFromBackup.
Returns early with a warning if the user's start menu path cannot
be resolved.
.PARAMETER BackupFilePath .PARAMETER BackupFilePath
Path to the backup file to restore from. If omitted, defaults to Path to the backup file to restore from. If omitted, automatically
the .bak file alongside the current start2.bin. finds the latest Win11Debloat-StartBackup-*.bak file.
.EXAMPLE .EXAMPLE
RestoreStartMenu RestoreStartMenu
.EXAMPLE .EXAMPLE
RestoreStartMenu -BackupFilePath "C:\Backups\start2.bin" RestoreStartMenu -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
#> #>
function RestoreStartMenu { function RestoreStartMenu {
param( param(
[string]$BackupFilePath [string]$BackupFilePath
) )
$targetUserName = GetUserName $targetUserName = $env:USERNAME
$startMenuBinFile = GetStartMenuBinPathForUser -UserName $targetUserName $startMenuBinFile = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
if ([string]::IsNullOrWhiteSpace($startMenuBinFile)) {
Write-Host "Unable to resolve start menu path for user $targetUserName, nothing to restore" -ForegroundColor Yellow
return [PSCustomObject]@{
UserName = $targetUserName
Result = $false
Message = "Could not resolve start menu path for user $targetUserName."
}
}
Write-Host "Restoring start menu for user $targetUserName from backup..." Write-Host "Restoring start menu for user $targetUserName from backup..."
@@ -315,19 +373,21 @@ function RestoreStartMenu {
.DESCRIPTION .DESCRIPTION
Iterates over every existing user profile and restores each user's Iterates over every existing user profile and restores each user's
start2.bin from its .bak backup. For the Default user profile, removes start2.bin from the latest backup in their LocalState folder. For the
the start2.bin file (which was previously copied from a template) so Default user profile, removes the start2.bin file (which was previously
that new profiles revert to the system default start menu. copied from a template) so that new profiles revert to the system
default start menu.
.PARAMETER BackupFilePath .PARAMETER BackupFilePath
Path to the backup file to restore from. If omitted, defaults to Path to the backup file to restore from. If omitted, automatically
the .bak file alongside each user's current start2.bin. finds the latest Win11Debloat-StartBackup-*.bak in each user's
LocalState folder.
.EXAMPLE .EXAMPLE
RestoreStartMenuForAllUsers RestoreStartMenuForAllUsers
.EXAMPLE .EXAMPLE
RestoreStartMenuForAllUsers -BackupFilePath "C:\Backups\start2.bin" RestoreStartMenuForAllUsers -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
#> #>
function RestoreStartMenuForAllUsers { function RestoreStartMenuForAllUsers {
param( param(

View File

@@ -1,71 +1,5 @@
# MainWindow-WindowChrome.ps1 # MainWindow-WindowChrome.ps1
# Window sizing, DPI-aware coordinate conversion, maximized-window taskbar-constraint helpers, and UI animations. # Window sizing, DPI-aware coordinate conversion, and UI animations.
function Register-MaximizedWindowHelper {
if (-not ([System.Management.Automation.PSTypeName]'Win11Debloat.MaximizedWindowHelper').Type) {
Add-Type -Namespace Win11Debloat -Name MaximizedWindowHelper `
-ReferencedAssemblies 'PresentationFramework','System.Windows.Forms','System.Drawing' `
-MemberDefinition @'
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct MINMAXINFO {
public POINT ptReserved, ptMaxSize, ptMaxPosition, ptMinTrackSize, ptMaxTrackSize;
}
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct POINT { public int x, y; }
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern System.IntPtr MonitorFromWindow(System.IntPtr hwnd, uint dwFlags);
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
private static extern bool GetMonitorInfo(System.IntPtr hMonitor, ref MONITORINFO lpmi);
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct RECT {
public int Left, Top, Right, Bottom;
}
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
private struct MONITORINFO {
public int cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
}
public static System.IntPtr WmGetMinMaxInfoHook(
System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled) {
if (msg == 0x0024) { // WM_GETMINMAXINFO
var mmi = (MINMAXINFO)System.Runtime.InteropServices.Marshal.PtrToStructure(
lParam, typeof(MINMAXINFO));
const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
var monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
var monitorInfo = new MONITORINFO();
monitorInfo.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(MONITORINFO));
if (monitor != System.IntPtr.Zero && GetMonitorInfo(monitor, ref monitorInfo)) {
mmi.ptMaxPosition.x = monitorInfo.rcWork.Left - monitorInfo.rcMonitor.Left;
mmi.ptMaxPosition.y = monitorInfo.rcWork.Top - monitorInfo.rcMonitor.Top;
mmi.ptMaxSize.x = monitorInfo.rcWork.Right - monitorInfo.rcWork.Left;
mmi.ptMaxSize.y = monitorInfo.rcWork.Bottom - monitorInfo.rcWork.Top;
}
else {
var screen = System.Windows.Forms.Screen.FromHandle(hwnd);
var wa = screen.WorkingArea;
var bounds = screen.Bounds;
mmi.ptMaxPosition.x = wa.Left - bounds.Left;
mmi.ptMaxPosition.y = wa.Top - bounds.Top;
mmi.ptMaxSize.x = wa.Width;
mmi.ptMaxSize.y = wa.Height;
}
System.Runtime.InteropServices.Marshal.StructureToPtr(mmi, lParam, true);
}
return System.IntPtr.Zero;
}
'@
}
}
# Convert screen-pixel coordinates to WPF device-independent pixels (DIP) # Convert screen-pixel coordinates to WPF device-independent pixels (DIP)
function ConvertTo-ScreenPointToDip { function ConvertTo-ScreenPointToDip {
@@ -118,16 +52,35 @@ function Update-MainWindowChrome {
) )
$windowStateMaximized = [System.Windows.WindowState]::Maximized $windowStateMaximized = [System.Windows.WindowState]::Maximized
$chrome = [System.Windows.Shell.WindowChrome]::GetWindowChrome($Window)
if ($Window.WindowState -eq $windowStateMaximized) { if ($Window.WindowState -eq $windowStateMaximized) {
$MainBorder.Margin = [System.Windows.Thickness]::new(0) $chrome = [System.Windows.Shell.WindowChrome]::GetWindowChrome($Window)
$resizeBorder = if ($chrome) { $chrome.ResizeBorderThickness } else { [System.Windows.SystemParameters]::WindowResizeBorderThickness }
# Compute margins using screen bounds vs working area
$marginLeft = $resizeBorder.Left
$marginTop = $resizeBorder.Top
$marginRight = $resizeBorder.Right
$marginBottom = $resizeBorder.Bottom
$screen = Get-WindowScreen -Window $Window
if ($screen) {
$workTL = ConvertTo-ScreenPointToDip -Window $Window -X $screen.WorkingArea.Left -Y $screen.WorkingArea.Top
$workSize = ConvertTo-ScreenPixelsToDip -Window $Window -Width $screen.WorkingArea.Width -Height $screen.WorkingArea.Height
$screenTL = ConvertTo-ScreenPointToDip -Window $Window -X $screen.Bounds.Left -Y $screen.Bounds.Top
$screenSize = ConvertTo-ScreenPixelsToDip -Window $Window -Width $screen.Bounds.Width -Height $screen.Bounds.Height
$marginLeft += ($workTL.X - $screenTL.X)
$marginTop += ($workTL.Y - $screenTL.Y)
$marginRight += ($screenTL.X + $screenSize.Width) - ($workTL.X + $workSize.Width)
$marginBottom += ($screenTL.Y + $screenSize.Height) - ($workTL.Y + $workSize.Height)
}
$MainBorder.Margin = [System.Windows.Thickness]::new($marginLeft, $marginTop, $marginRight, $marginBottom)
$MainBorder.BorderThickness = [System.Windows.Thickness]::new(0) $MainBorder.BorderThickness = [System.Windows.Thickness]::new(0)
$MainBorder.CornerRadius = [System.Windows.CornerRadius]::new(0) $MainBorder.CornerRadius = [System.Windows.CornerRadius]::new(0)
$MainBorder.Effect = $null $MainBorder.Effect = $null
$TitleBarBackground.CornerRadius = [System.Windows.CornerRadius]::new(0) $TitleBarBackground.CornerRadius = [System.Windows.CornerRadius]::new(0)
# Zero out resize borders when maximized so the entire title bar row is draggable
if ($chrome) { $chrome.ResizeBorderThickness = [System.Windows.Thickness]::new(0) }
} }
else { else {
$MainBorder.Margin = [System.Windows.Thickness]::new(0) $MainBorder.Margin = [System.Windows.Thickness]::new(0)
@@ -135,7 +88,6 @@ function Update-MainWindowChrome {
$MainBorder.CornerRadius = [System.Windows.CornerRadius]::new(8) $MainBorder.CornerRadius = [System.Windows.CornerRadius]::new(8)
$MainBorder.Effect = $NormalWindowShadow $MainBorder.Effect = $NormalWindowShadow
$TitleBarBackground.CornerRadius = [System.Windows.CornerRadius]::new(8, 8, 0, 0) $TitleBarBackground.CornerRadius = [System.Windows.CornerRadius]::new(8, 8, 0, 0)
if ($chrome) { $chrome.ResizeBorderThickness = [System.Windows.Thickness]::new(5) }
} }
} }

View File

@@ -1,3 +1,23 @@
<#
.SYNOPSIS
Hides the currently displayed bubble popup.
.DESCRIPTION
Closes the bubble popup with a smooth fade-out animation (220ms). If the
-Immediate switch is used, the popup is closed instantly without animation.
This function is called automatically by Show-Bubble's timer and can also
be invoked manually to dismiss the bubble early.
.PARAMETER Immediate
If specified, the bubble popup is closed instantly without a fade-out
animation. Any pending close timer is also stopped.
.EXAMPLE
Hide-Bubble
.EXAMPLE
Hide-Bubble -Immediate
#>
function Hide-Bubble { function Hide-Bubble {
param ( param (
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
@@ -37,6 +57,34 @@ function Hide-Bubble {
$bubblePanel.BeginAnimation([System.Windows.UIElement]::OpacityProperty, $fadeOut) $bubblePanel.BeginAnimation([System.Windows.UIElement]::OpacityProperty, $fadeOut)
} }
<#
.SYNOPSIS
Displays a transient bubble popup hint anchored above a target control.
.DESCRIPTION
Shows a WPF popup styled as a speech bubble above the specified target
control. The bubble fades in with a animation, displays for a configurable
duration, then fades out. Any previously shown bubble is dismissed
immediately before showing the new one.
.PARAMETER TargetControl
The WPF Control above which the bubble popup will be placed. This
parameter is mandatory.
.PARAMETER Message
The text message to display inside the bubble. Defaults to
'View the selected changes here'.
.PARAMETER DurationSeconds
The number of seconds the bubble remains visible before auto-hiding.
The minimum value is 1 second. Defaults to 5 seconds.
.EXAMPLE
Show-Bubble -TargetControl $myButton
.EXAMPLE
Show-Bubble -TargetControl $myButton -Message 'Changes saved!' -DurationSeconds 3
#>
function Show-Bubble { function Show-Bubble {
param ( param (
[Parameter(Mandatory=$true)] [Parameter(Mandatory=$true)]

View File

@@ -1,71 +1,6 @@
function Show-MainWindow { function Show-MainWindow {
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase,System.Windows.Forms | Out-Null Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase,System.Windows.Forms | Out-Null
# ---- Constrain maximized window to taskbar work area ----
if (-not ([System.Management.Automation.PSTypeName]'Win11Debloat.MaximizedWindowHelper').Type) {
Add-Type -Namespace Win11Debloat -Name MaximizedWindowHelper `
-ReferencedAssemblies 'PresentationFramework','System.Windows.Forms','System.Drawing' `
-MemberDefinition @'
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct MINMAXINFO {
public POINT ptReserved, ptMaxSize, ptMaxPosition, ptMinTrackSize, ptMaxTrackSize;
}
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct POINT { public int x, y; }
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern System.IntPtr MonitorFromWindow(System.IntPtr hwnd, uint dwFlags);
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
private static extern bool GetMonitorInfo(System.IntPtr hMonitor, ref MONITORINFO lpmi);
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct RECT {
public int Left, Top, Right, Bottom;
}
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
private struct MONITORINFO {
public int cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
}
public static System.IntPtr WmGetMinMaxInfoHook(
System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled) {
if (msg == 0x0024) { // WM_GETMINMAXINFO
var mmi = (MINMAXINFO)System.Runtime.InteropServices.Marshal.PtrToStructure(
lParam, typeof(MINMAXINFO));
const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
var monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
var monitorInfo = new MONITORINFO();
monitorInfo.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(MONITORINFO));
if (monitor != System.IntPtr.Zero && GetMonitorInfo(monitor, ref monitorInfo)) {
mmi.ptMaxPosition.x = monitorInfo.rcWork.Left - monitorInfo.rcMonitor.Left;
mmi.ptMaxPosition.y = monitorInfo.rcWork.Top - monitorInfo.rcMonitor.Top;
mmi.ptMaxSize.x = monitorInfo.rcWork.Right - monitorInfo.rcWork.Left;
mmi.ptMaxSize.y = monitorInfo.rcWork.Bottom - monitorInfo.rcWork.Top;
}
else {
var screen = System.Windows.Forms.Screen.FromHandle(hwnd);
var wa = screen.WorkingArea;
var bounds = screen.Bounds;
mmi.ptMaxPosition.x = wa.Left - bounds.Left;
mmi.ptMaxPosition.y = wa.Top - bounds.Top;
mmi.ptMaxSize.x = wa.Width;
mmi.ptMaxSize.y = wa.Height;
}
System.Runtime.InteropServices.Marshal.StructureToPtr(mmi, lParam, true);
}
return System.IntPtr.Zero;
}
'@
}
$WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' CurrentBuild $WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' CurrentBuild
$usesDarkMode = GetSystemUsesDarkMode $usesDarkMode = GetSystemUsesDarkMode
@@ -123,12 +58,6 @@
$window.Add_SourceInitialized({ $window.Add_SourceInitialized({
& $applyInitialWindowSize & $applyInitialWindowSize
& $updateWindowChrome & $updateWindowChrome
$hwndHelper = New-Object System.Windows.Interop.WindowInteropHelper($window)
$hwndSource = [System.Windows.Interop.HwndSource]::FromHwnd($hwndHelper.Handle)
$hookMethod = [Win11Debloat.MaximizedWindowHelper].GetMethod('WmGetMinMaxInfoHook')
$hook = [System.Delegate]::CreateDelegate([System.Windows.Interop.HwndSourceHook], $hookMethod)
$hwndSource.AddHook($hook)
}) })
$window.Add_SizeChanged({ $window.Add_SizeChanged({

View File

@@ -196,6 +196,10 @@ function Show-RestoreBackupDialog {
$primaryActionBtn.Visibility = 'Visible' $primaryActionBtn.Visibility = 'Visible'
$primaryActionBtn.IsDefault = $true $primaryActionBtn.IsDefault = $true
$chooseRegistryBtn.IsDefault = $false $chooseRegistryBtn.IsDefault = $false
# Show intro panel so user can configure scope & auto-detect
$startMenuAutoBackupCheck.IsChecked = $true
$state.SelectedStartMenuBackupFilePath = $null
& $refreshStartMenuUi & $refreshStartMenuUi
} }
@@ -323,27 +327,14 @@ function Show-RestoreBackupDialog {
} }
if (-not $useManualBackupFile) { if (-not $useManualBackupFile) {
$autoBackupExists = $false $scopeInfo = & $getStartMenuScopeInfo
if ($scope -eq 'AllUsers') { $autoBackupPath = Get-StartMenuBackupPath -Scope $scopeInfo.Scope
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" if ($null -eq $autoBackupPath) {
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue $scopeText = $scopeInfo.SummaryText
foreach ($startMenuPath in $usersStartMenuPaths) { Show-MessageBox -Owner $window -Title 'No Backup Found' -Message "No Start Menu backup file was found for $scopeText. Uncheck 'Automatically find Start Menu backup' to select a backup file manually." -Button 'OK' -Icon 'Warning' | Out-Null
if (Test-Path -LiteralPath (Join-Path $startMenuPath.FullName 'start2.bin.bak')) {
$autoBackupExists = $true
break
}
}
}
else {
$autoBackupPath = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin.bak"
$autoBackupExists = Test-Path -LiteralPath $autoBackupPath
}
if (-not $autoBackupExists) {
$scopeText = (& $getStartMenuScopeInfo).SummaryText
Show-MessageBox -Owner $window -Title 'No Backup Found' -Message "No Start Menu backup file was found. You can uncheck the 'Automatically find Start Menu backup' option to select a backup file manually." -Button 'OK' -Icon 'Warning' | Out-Null
return return
} }
$state.SelectedStartMenuBackupFilePath = if ($scopeInfo.Scope -eq 'CurrentUser') { $autoBackupPath } else { $null }
} }
$window.Tag = @{ $window.Tag = @{
@@ -377,6 +368,7 @@ function Show-RestoreBackupDialog {
}) })
$startMenuScopeCombo.Add_SelectionChanged({ $startMenuScopeCombo.Add_SelectionChanged({
$state.SelectedStartMenuBackupFilePath = $null
& $refreshStartMenuUi & $refreshStartMenuUi
}) })

View File

@@ -137,7 +137,7 @@ if (-not $isAdmin) {
} }
# Define script-level variables & paths # Define script-level variables & paths
$script:Version = "2026.06.14" $script:Version = "2026.06.24"
$configPath = Join-Path $PSScriptRoot 'Config' $configPath = Join-Path $PSScriptRoot 'Config'
$logsPath = Join-Path $PSScriptRoot 'Logs' $logsPath = Join-Path $PSScriptRoot 'Logs'
$schemasPath = Join-Path $PSScriptRoot 'Schemas' $schemasPath = Join-Path $PSScriptRoot 'Schemas'