2026-02-15 23:08:54 +01:00
function Show-MainWindow {
Add-Type -AssemblyName PresentationFramework , PresentationCore , WindowsBase , System . Windows . Forms | Out-Null
# Get current Windows build version
$WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' CurrentBuild
$usesDarkMode = GetSystemUsesDarkMode
# Load XAML from file
$xaml = Get-Content -Path $script:MainWindowSchema -Raw
$reader = [ System.Xml.XmlReader ] :: Create ( [ System.IO.StringReader ] :: new ( $xaml ) )
try {
$window = [ System.Windows.Markup.XamlReader ] :: Load ( $reader )
}
finally {
$reader . Close ( )
}
SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode
# Get named elements
$titleBar = $window . FindName ( 'TitleBar' )
$kofiBtn = $window . FindName ( 'KofiBtn' )
$menuBtn = $window . FindName ( 'MenuBtn' )
$closeBtn = $window . FindName ( 'CloseBtn' )
$menuDocumentation = $window . FindName ( 'MenuDocumentation' )
$menuReportBug = $window . FindName ( 'MenuReportBug' )
$menuLogs = $window . FindName ( 'MenuLogs' )
$menuAbout = $window . FindName ( 'MenuAbout' )
# Title bar event handlers
$titleBar . Add_MouseLeftButtonDown ( {
if ( $_ . OriginalSource -is [ System.Windows.Controls.Grid ] -or $_ . OriginalSource -is [ System.Windows.Controls.Border ] -or $_ . OriginalSource -is [ System.Windows.Controls.TextBlock ] ) {
$window . DragMove ( )
}
} )
$kofiBtn . Add_Click ( {
Start-Process " https://ko-fi.com/raphire "
} )
$menuBtn . Add_Click ( {
$menuBtn . ContextMenu . PlacementTarget = $menuBtn
$menuBtn . ContextMenu . Placement = [ System.Windows.Controls.Primitives.PlacementMode ] :: Bottom
$menuBtn . ContextMenu . IsOpen = $true
} )
$menuDocumentation . Add_Click ( {
Start-Process " https://github.com/Raphire/Win11Debloat/wiki "
} )
$menuReportBug . Add_Click ( {
Start-Process " https://github.com/Raphire/Win11Debloat/issues "
} )
$menuLogs . Add_Click ( {
$logsFolder = Join-Path $PSScriptRoot " ../../Logs "
if ( Test-Path $logsFolder ) {
Start-Process " explorer.exe " -ArgumentList $logsFolder
}
else {
Show-MessageBox -Message " No logs folder found at: $logsFolder " -Title " Logs " -Button 'OK' -Icon 'Information'
}
} )
$menuAbout . Add_Click ( {
Show-AboutDialog -Owner $window
} )
$closeBtn . Add_Click ( {
$window . Close ( )
} )
# Ensure closing the main window stops all execution
$window . Add_Closing ( {
$script:CancelRequested = $true
} )
# Implement window resize functionality
$resizeLeft = $window . FindName ( 'ResizeLeft' )
$resizeRight = $window . FindName ( 'ResizeRight' )
$resizeTop = $window . FindName ( 'ResizeTop' )
$resizeBottom = $window . FindName ( 'ResizeBottom' )
$resizeTopLeft = $window . FindName ( 'ResizeTopLeft' )
$resizeTopRight = $window . FindName ( 'ResizeTopRight' )
$resizeBottomLeft = $window . FindName ( 'ResizeBottomLeft' )
$resizeBottomRight = $window . FindName ( 'ResizeBottomRight' )
$script:resizing = $false
$script:resizeEdges = $null
$script:resizeStart = $null
$script:windowStart = $null
$script:resizeElement = $null
$resizeHandler = {
param ( $sender , $e )
$script:resizing = $true
$script:resizeElement = $sender
$script:resizeStart = [ System.Windows.Forms.Cursor ] :: Position
$script:windowStart = @ {
Left = $window . Left
Top = $window . Top
Width = $window . ActualWidth
Height = $window . ActualHeight
}
# Parse direction tag into edge flags for cleaner resize logic
$direction = $sender . Tag
$script:resizeEdges = @ {
Left = $direction -match 'Left'
Right = $direction -match 'Right'
Top = $direction -match 'Top'
Bottom = $direction -match 'Bottom'
}
$sender . CaptureMouse ( )
$e . Handled = $true
}
$moveHandler = {
param ( $sender , $e )
if ( -not $script:resizing ) { return }
$current = [ System.Windows.Forms.Cursor ] :: Position
$deltaX = $current . X - $script:resizeStart . X
$deltaY = $current . Y - $script:resizeStart . Y
# Handle horizontal resize
if ( $script:resizeEdges . Left ) {
$newWidth = [ Math ] :: Max ( $window . MinWidth , $script:windowStart . Width - $deltaX )
if ( $newWidth -ne $window . Width ) {
$window . Left = $script:windowStart . Left + ( $script:windowStart . Width - $newWidth )
$window . Width = $newWidth
}
}
elseif ( $script:resizeEdges . Right ) {
$window . Width = [ Math ] :: Max ( $window . MinWidth , $script:windowStart . Width + $deltaX )
}
# Handle vertical resize
if ( $script:resizeEdges . Top ) {
$newHeight = [ Math ] :: Max ( $window . MinHeight , $script:windowStart . Height - $deltaY )
if ( $newHeight -ne $window . Height ) {
$window . Top = $script:windowStart . Top + ( $script:windowStart . Height - $newHeight )
$window . Height = $newHeight
}
}
elseif ( $script:resizeEdges . Bottom ) {
$window . Height = [ Math ] :: Max ( $window . MinHeight , $script:windowStart . Height + $deltaY )
}
$e . Handled = $true
}
$releaseHandler = {
param ( $sender , $e )
if ( $script:resizing -and $script:resizeElement ) {
$script:resizing = $false
$script:resizeEdges = $null
$script:resizeElement . ReleaseMouseCapture ( )
$script:resizeElement = $null
$e . Handled = $true
}
}
# Set tags and add event handlers for resize borders
$resizeLeft . Tag = 'Left'
$resizeLeft . Add_PreviewMouseLeftButtonDown ( $resizeHandler )
$resizeLeft . Add_MouseMove ( $moveHandler )
$resizeLeft . Add_MouseLeftButtonUp ( $releaseHandler )
$resizeRight . Tag = 'Right'
$resizeRight . Add_PreviewMouseLeftButtonDown ( $resizeHandler )
$resizeRight . Add_MouseMove ( $moveHandler )
$resizeRight . Add_MouseLeftButtonUp ( $releaseHandler )
$resizeTop . Tag = 'Top'
$resizeTop . Add_PreviewMouseLeftButtonDown ( $resizeHandler )
$resizeTop . Add_MouseMove ( $moveHandler )
$resizeTop . Add_MouseLeftButtonUp ( $releaseHandler )
$resizeBottom . Tag = 'Bottom'
$resizeBottom . Add_PreviewMouseLeftButtonDown ( $resizeHandler )
$resizeBottom . Add_MouseMove ( $moveHandler )
$resizeBottom . Add_MouseLeftButtonUp ( $releaseHandler )
$resizeTopLeft . Tag = 'TopLeft'
$resizeTopLeft . Add_PreviewMouseLeftButtonDown ( $resizeHandler )
$resizeTopLeft . Add_MouseMove ( $moveHandler )
$resizeTopLeft . Add_MouseLeftButtonUp ( $releaseHandler )
$resizeTopRight . Tag = 'TopRight'
$resizeTopRight . Add_PreviewMouseLeftButtonDown ( $resizeHandler )
$resizeTopRight . Add_MouseMove ( $moveHandler )
$resizeTopRight . Add_MouseLeftButtonUp ( $releaseHandler )
$resizeBottomLeft . Tag = 'BottomLeft'
$resizeBottomLeft . Add_PreviewMouseLeftButtonDown ( $resizeHandler )
$resizeBottomLeft . Add_MouseMove ( $moveHandler )
$resizeBottomLeft . Add_MouseLeftButtonUp ( $releaseHandler )
$resizeBottomRight . Tag = 'BottomRight'
$resizeBottomRight . Add_PreviewMouseLeftButtonDown ( $resizeHandler )
$resizeBottomRight . Add_MouseMove ( $moveHandler )
$resizeBottomRight . Add_MouseLeftButtonUp ( $releaseHandler )
# Integrated App Selection UI
$appsPanel = $window . FindName ( 'AppSelectionPanel' )
$onlyInstalledAppsBox = $window . FindName ( 'OnlyInstalledAppsBox' )
$loadingAppsIndicator = $window . FindName ( 'LoadingAppsIndicator' )
$appSelectionStatus = $window . FindName ( 'AppSelectionStatus' )
$defaultAppsBtn = $window . FindName ( 'DefaultAppsBtn' )
$clearAppSelectionBtn = $window . FindName ( 'ClearAppSelectionBtn' )
# Track the last selected checkbox for shift-click range selection
$script:MainWindowLastSelectedCheckbox = $null
# Track current app loading operation to prevent race conditions
$script:CurrentAppLoadTimer = $null
$script:CurrentAppLoadJob = $null
$script:CurrentAppLoadJobStartTime = $null
# Apply Tab UI Elements
$consoleOutput = $window . FindName ( 'ConsoleOutput' )
$consoleScrollViewer = $window . FindName ( 'ConsoleScrollViewer' )
$finishBtn = $window . FindName ( 'FinishBtn' )
$finishBtnText = $window . FindName ( 'FinishBtnText' )
# Set script-level variables for Write-ToConsole function
$script:GuiConsoleOutput = $consoleOutput
$script:GuiConsoleScrollViewer = $consoleScrollViewer
$script:GuiWindow = $window
# Updates app selection status text in the App Selection tab
function UpdateAppSelectionStatus {
$selectedCount = 0
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] -and $child . IsChecked ) {
$selectedCount + +
}
}
$appSelectionStatus . Text = " $selectedCount app(s) selected for removal "
}
# Dynamically builds Tweaks UI from Features.json
function BuildDynamicTweaks {
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion " 1.0 "
if ( -not $featuresJson ) {
Show-MessageBox -Message " Unable to load Features.json file! " -Title " Error " -Button 'OK' -Icon 'Error' | Out-Null
Exit
}
# Column containers
$col0 = $window . FindName ( 'Column0Panel' )
$col1 = $window . FindName ( 'Column1Panel' )
$col2 = $window . FindName ( 'Column2Panel' )
$columns = @ ( $col0 , $col1 , $col2 ) | Where-Object { $_ -ne $null }
# Clear all columns for fully dynamic panel creation
foreach ( $col in $columns ) {
if ( $col ) { $col . Children . Clear ( ) }
}
$script:UiControlMappings = @ { }
$script:CategoryCardMap = @ { }
function CreateLabeledCombo($parent , $labelText , $comboName , $items ) {
# If only 2 items (No Change + one option), use a checkbox instead
if ( $items . Count -eq 2 ) {
$checkbox = New-Object System . Windows . Controls . CheckBox
$checkbox . Content = $labelText
$checkbox . Name = $comboName
$checkbox . SetValue ( [ System.Windows.Automation.AutomationProperties ] :: NameProperty , $labelText )
$checkbox . IsChecked = $false
$checkbox . Style = $window . Resources [ " FeatureCheckboxStyle " ]
$parent . Children . Add ( $checkbox ) | Out-Null
# Register the checkbox with the window's name scope
try {
[ System.Windows.NameScope ] :: SetNameScope ( $checkbox , [ System.Windows.NameScope ] :: GetNameScope ( $window ) )
$window . RegisterName ( $comboName , $checkbox )
}
catch {
# Name might already be registered, ignore
}
return $checkbox
}
# Otherwise use a combobox for multiple options
# Wrap label in a Border for search highlighting
$lblBorder = New-Object System . Windows . Controls . Border
$lblBorder . Style = $window . Resources [ 'LabelBorderStyle' ]
$lblBorderName = " $comboName ` _LabelBorder "
$lblBorder . Name = $lblBorderName
$lbl = New-Object System . Windows . Controls . TextBlock
$lbl . Text = $labelText
$lbl . Style = $window . Resources [ 'LabelStyle' ]
$labelName = " $comboName ` _Label "
$lbl . Name = $labelName
$lblBorder . Child = $lbl
$parent . Children . Add ( $lblBorder ) | Out-Null
# Register the label border with the window's name scope
try {
[ System.Windows.NameScope ] :: SetNameScope ( $lblBorder , [ System.Windows.NameScope ] :: GetNameScope ( $window ) )
$window . RegisterName ( $lblBorderName , $lblBorder )
}
catch {
# Name might already be registered, ignore
}
$combo = New-Object System . Windows . Controls . ComboBox
$combo . Name = $comboName
$combo . SetValue ( [ System.Windows.Automation.AutomationProperties ] :: NameProperty , $labelText )
foreach ( $it in $items ) { $cbItem = New-Object System . Windows . Controls . ComboBoxItem ; $cbItem . Content = $it ; $combo . Items . Add ( $cbItem ) | Out-Null }
$combo . SelectedIndex = 0
$parent . Children . Add ( $combo ) | Out-Null
# Register the combo box with the window's name scope
try {
[ System.Windows.NameScope ] :: SetNameScope ( $combo , [ System.Windows.NameScope ] :: GetNameScope ( $window ) )
$window . RegisterName ( $comboName , $combo )
}
catch {
# Name might already be registered, ignore
}
return $combo
}
function GetWikiUrlForCategory($category ) {
if ( -not $category ) { return 'https://github.com/Raphire/Win11Debloat/wiki/Features' }
$slug = $category . ToLowerInvariant ( )
$slug = $slug -replace '&' , ''
$slug = $slug -replace '[^a-z0-9\s-]' , ''
$slug = $slug -replace '\s' , '-'
return " https://github.com/Raphire/Win11Debloat/wiki/Features# $slug "
}
function GetOrCreateCategoryCard($categoryObj ) {
$categoryName = $categoryObj . Name
$categoryIcon = $categoryObj . Icon
if ( $script:CategoryCardMap . ContainsKey ( $categoryName ) ) { return $script:CategoryCardMap [ $categoryName ] }
# Create a new card Border + StackPanel and add to shortest column
$target = $columns | Sort-Object @ { Expression = { $_ . Children . Count } ; Ascending = $true } , @ { Expression = { $columns . IndexOf ( $_ ) } ; Ascending = $true } | Select-Object -First 1
$border = New-Object System . Windows . Controls . Border
$border . Style = $window . Resources [ 'CategoryCardBorderStyle' ]
$border . Tag = 'DynamicCategory'
$panel = New-Object System . Windows . Controls . StackPanel
$safe = ( $categoryName -replace '[^a-zA-Z0-9_]' , '_' )
$panel . Name = " Category_{0}_Panel " -f $safe
$headerRow = New-Object System . Windows . Controls . StackPanel
$headerRow . Orientation = 'Horizontal'
# Add category icon
$icon = New-Object System . Windows . Controls . TextBlock
# Convert HTML entity to character (e.g.,  -> actual character)
if ( $categoryIcon -match '&#x([0-9A-Fa-f]+);' ) {
$hexValue = [ Convert ] :: ToInt32 ( $matches [ 1 ] , 16 )
$icon . Text = [ char ] $hexValue
}
$icon . Style = $window . Resources [ 'CategoryHeaderIcon' ]
$headerRow . Children . Add ( $icon ) | Out-Null
$header = New-Object System . Windows . Controls . TextBlock
$header . Text = $categoryName
$header . Style = $window . Resources [ 'CategoryHeaderTextBlock' ]
$headerRow . Children . Add ( $header ) | Out-Null
$helpIcon = New-Object System . Windows . Controls . TextBlock
$helpIcon . Text = '(?)'
$helpIcon . Style = $window . Resources [ 'CategoryHelpLinkTextStyle' ]
$helpBtn = New-Object System . Windows . Controls . Button
$helpBtn . Content = $helpIcon
$helpBtn . ToolTip = " Open wiki for more info on ' $categoryName ' tweaks "
$helpBtn . Tag = ( GetWikiUrlForCategory -category $categoryName )
$helpBtn . Style = $window . Resources [ 'CategoryHelpLinkButtonStyle' ]
$helpBtn . Add_Click ( {
param ( $sender , $e )
if ( $sender . Tag ) { Start-Process $sender . Tag }
} )
$headerRow . Children . Add ( $helpBtn ) | Out-Null
$panel . Children . Add ( $headerRow ) | Out-Null
$border . Child = $panel
$target . Children . Add ( $border ) | Out-Null
$script:CategoryCardMap [ $categoryName ] = $panel
return $panel
}
# Determine categories present (from lists and features)
$categoriesPresent = @ { }
if ( $featuresJson . UiGroups ) {
foreach ( $g in $featuresJson . UiGroups ) { if ( $g . Category ) { $categoriesPresent [ $g . Category ] = $true } }
}
foreach ( $f in $featuresJson . Features ) { if ( $f . Category ) { $categoriesPresent [ $f . Category ] = $true } }
# Create cards in the order defined in Features.json Categories (if present)
$orderedCategories = @ ( )
if ( $featuresJson . Categories ) {
foreach ( $c in $featuresJson . Categories ) {
$categoryName = if ( $c -is [ string ] ) { $c } else { $c . Name }
if ( $categoriesPresent . ContainsKey ( $categoryName ) ) {
# Store the full category object (or create one with default icon for string categories)
$categoryObj = if ( $c -is [ string ] ) { @ { Name = $c ; Icon = '' } } else { $c }
$orderedCategories + = $categoryObj
}
}
} else {
# For backward compatibility, create category objects from keys
foreach ( $catName in $categoriesPresent . Keys ) {
$orderedCategories + = @ { Name = $catName ; Icon = '' }
}
}
foreach ( $categoryObj in $orderedCategories ) {
$categoryName = $categoryObj . Name
# Create/get card for this category
$panel = GetOrCreateCategoryCard -categoryObj $categoryObj
if ( -not $panel ) { continue }
# Collect groups and features for this category, then sort by priority
$categoryItems = @ ( )
# Add any groups for this category
if ( $featuresJson . UiGroups ) {
$groupIndex = 0
foreach ( $group in $featuresJson . UiGroups ) {
if ( $group . Category -ne $categoryName ) { $groupIndex + + ; continue }
$categoryItems + = [ PSCustomObject ] @ {
Type = 'group'
Data = $group
Priority = if ( $null -ne $group . Priority ) { $group . Priority } else { [ int ] :: MaxValue }
OriginalIndex = $groupIndex
}
$groupIndex + +
}
}
# Add individual features for this category
$featureIndex = 0
foreach ( $feature in $featuresJson . Features ) {
if ( $feature . Category -ne $categoryName ) { $featureIndex + + ; continue }
# Check version and feature compatibility using Features.json
if ( ( $feature . MinVersion -and $WinVersion -lt $feature . MinVersion ) -or ( $feature . MaxVersion -and $WinVersion -gt $feature . MaxVersion ) -or ( $feature . FeatureId -eq 'DisableModernStandbyNetworking' -and ( -not $script:ModernStandbySupported ) ) ) {
$featureIndex + + ; continue
}
# Skip if feature part of a group
$inGroup = $false
if ( $featuresJson . UiGroups ) {
foreach ( $g in $featuresJson . UiGroups ) { foreach ( $val in $g . Values ) { if ( $val . FeatureIds -contains $feature . FeatureId ) { $inGroup = $true ; break } } ; if ( $inGroup ) { break } }
}
if ( $inGroup ) { $featureIndex + + ; continue }
$categoryItems + = [ PSCustomObject ] @ {
Type = 'feature'
Data = $feature
Priority = if ( $null -ne $feature . Priority ) { $feature . Priority } else { [ int ] :: MaxValue }
OriginalIndex = $featureIndex
}
$featureIndex + +
}
# Sort by priority first, then by original index for items with same/no priority
$sortedItems = $categoryItems | Sort-Object -Property Priority , OriginalIndex
# Render sorted items
foreach ( $item in $sortedItems ) {
if ( $item . Type -eq 'group' ) {
$group = $item . Data
$items = @ ( 'No Change' ) + ( $group . Values | ForEach-Object { $_ . Label } )
$comboName = 'Group_{0}Combo' -f $group . GroupId
$combo = CreateLabeledCombo -parent $panel -labelText $group . Label -comboName $comboName -items $items
# attach tooltip from UiGroups if present
if ( $group . ToolTip ) {
$tipBlock = New-Object System . Windows . Controls . TextBlock
$tipBlock . Text = $group . ToolTip
$tipBlock . TextWrapping = 'Wrap'
$tipBlock . MaxWidth = 420
$combo . ToolTip = $tipBlock
$lblBorderObj = $null
try { $lblBorderObj = $window . FindName ( " $comboName ` _LabelBorder " ) } catch { }
if ( $lblBorderObj ) { $lblBorderObj . ToolTip = $tipBlock }
}
$script:UiControlMappings [ $comboName ] = @ { Type = 'group' ; Values = $group . Values ; Label = $group . Label }
}
elseif ( $item . Type -eq 'feature' ) {
$feature = $item . Data
$opt = 'Apply'
if ( $feature . FeatureId -match '^Disable' ) { $opt = 'Disable' } elseif ( $feature . FeatureId -match '^Enable' ) { $opt = 'Enable' }
$items = @ ( 'No Change' , $opt )
$comboName = ( " Feature_{0}_Combo " -f $feature . FeatureId ) -replace '[^a-zA-Z0-9_]' , ''
$combo = CreateLabeledCombo -parent $panel -labelText ( $feature . Action + ' ' + $feature . Label ) -comboName $comboName -items $items
# attach tooltip from Features.json if present
if ( $feature . ToolTip ) {
$tipBlock = New-Object System . Windows . Controls . TextBlock
$tipBlock . Text = $feature . ToolTip
$tipBlock . TextWrapping = 'Wrap'
$tipBlock . MaxWidth = 420
$combo . ToolTip = $tipBlock
$lblBorderObj = $null
try { $lblBorderObj = $window . FindName ( " $comboName ` _LabelBorder " ) } catch { }
if ( $lblBorderObj ) { $lblBorderObj . ToolTip = $tipBlock }
}
$script:UiControlMappings [ $comboName ] = @ { Type = 'feature' ; FeatureId = $feature . FeatureId ; Action = $feature . Action }
}
}
}
}
# Helper function to complete app loading with the WinGet list
function script: LoadAppsWithList($listOfApps ) {
$appsToAdd = LoadAppsDetailsFromJson -OnlyInstalled: $onlyInstalledAppsBox . IsChecked -InstalledList $listOfApps -InitialCheckedFromJson: $false
# Reset the last selected checkbox when loading a new list
$script:MainWindowLastSelectedCheckbox = $null
# Sort apps alphabetically and add to panel
2026-02-18 00:26:10 +01:00
$appsToAdd | Sort-Object -Property FriendlyName | ForEach-Object {
2026-02-15 23:08:54 +01:00
$checkbox = New-Object System . Windows . Controls . CheckBox
2026-02-18 00:26:10 +01:00
$checkbox . SetValue ( [ System.Windows.Automation.AutomationProperties ] :: NameProperty , $_ . FriendlyName )
2026-02-15 23:08:54 +01:00
$checkbox . Tag = $_ . AppId
$checkbox . IsChecked = $_ . IsChecked
$checkbox . Style = $window . Resources [ " AppsPanelCheckBoxStyle " ]
2026-02-18 00:26:10 +01:00
# Build table row content: App Name | Description | App ID
$row = New-Object System . Windows . Controls . Grid
$c0 = New-Object System . Windows . Controls . ColumnDefinition ; $c0 . Width = [ System.Windows.GridLength ] :: new ( 160 )
$c1 = New-Object System . Windows . Controls . ColumnDefinition ; $c1 . Width = [ System.Windows.GridLength ] :: new ( 1 , [ System.Windows.GridUnitType ] :: Star )
$c2 = New-Object System . Windows . Controls . ColumnDefinition ; $c2 . Width = [ System.Windows.GridLength ] :: new ( 286 )
$row . ColumnDefinitions . Add ( $c0 ) ; $row . ColumnDefinitions . Add ( $c1 ) ; $row . ColumnDefinitions . Add ( $c2 )
$tbName = New-Object System . Windows . Controls . TextBlock
$tbName . Text = $_ . FriendlyName
$tbName . Style = $window . Resources [ " AppNameTextStyle " ]
[ System.Windows.Controls.Grid ] :: SetColumn ( $tbName , 0 )
$tbDesc = New-Object System . Windows . Controls . TextBlock
$tbDesc . Text = $_ . Description
$tbDesc . Style = $window . Resources [ " AppDescTextStyle " ]
$tbDesc . ToolTip = $_ . Description
[ System.Windows.Controls.Grid ] :: SetColumn ( $tbDesc , 1 )
$tbId = New-Object System . Windows . Controls . TextBlock
$tbId . Text = $_ . AppId
$tbId . Style = $window . Resources [ " AppIdTextStyle " ]
$tbId . ToolTip = $_ . AppId
[ System.Windows.Controls.Grid ] :: SetColumn ( $tbId , 2 )
$row . Children . Add ( $tbName ) | Out-Null
$row . Children . Add ( $tbDesc ) | Out-Null
$row . Children . Add ( $tbId ) | Out-Null
$checkbox . Content = $row
2026-02-15 23:08:54 +01:00
# Store metadata in checkbox for later use
2026-02-18 00:26:10 +01:00
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name " AppName " -Value $_ . FriendlyName
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name " AppDescription " -Value $_ . Description
2026-02-15 23:08:54 +01:00
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name " SelectedByDefault " -Value $_ . SelectedByDefault
# Add event handler to update status
$checkbox . Add_Checked ( { UpdateAppSelectionStatus } )
$checkbox . Add_Unchecked ( { UpdateAppSelectionStatus } )
# Attach shift-click behavior for range selection
AttachShiftClickBehavior -checkbox $checkbox -appsPanel $appsPanel -lastSelectedCheckboxRef ( [ ref ] $script:MainWindowLastSelectedCheckbox ) -updateStatusCallback { UpdateAppSelectionStatus }
$appsPanel . Children . Add ( $checkbox ) | Out-Null
}
# Hide loading indicator and navigation blocker, update status
$loadingAppsIndicator . Visibility = 'Collapsed'
UpdateAppSelectionStatus
}
# Loads apps into the UI
function LoadAppsIntoMainUI {
# Cancel any existing load operation to prevent race conditions
if ( $script:CurrentAppLoadTimer -and $script:CurrentAppLoadTimer . IsEnabled ) {
$script:CurrentAppLoadTimer . Stop ( )
}
if ( $script:CurrentAppLoadJob ) {
Remove-Job -Job $script:CurrentAppLoadJob -Force -ErrorAction SilentlyContinue
}
$script:CurrentAppLoadTimer = $null
$script:CurrentAppLoadJob = $null
$script:CurrentAppLoadJobStartTime = $null
# Show loading indicator and navigation blocker, clear existing apps immediately
$loadingAppsIndicator . Visibility = 'Visible'
$appsPanel . Children . Clear ( )
# Update navigation buttons to disable Next/Previous
UpdateNavigationButtons
# Force UI to update and render all changes (loading indicator, blocker, disabled buttons)
$window . Dispatcher . Invoke ( [ System.Windows.Threading.DispatcherPriority ] :: Render , [ action ] { } )
# Schedule the actual loading work to run after UI has updated
$window . Dispatcher . BeginInvoke ( [ System.Windows.Threading.DispatcherPriority ] :: Background , [ action ] {
$listOfApps = " "
if ( $onlyInstalledAppsBox . IsChecked -and ( $script:WingetInstalled -eq $true ) ) {
# Start job to get list of installed apps via WinGet (async helper)
$asyncJob = GetInstalledAppsViaWinget -Async
$script:CurrentAppLoadJob = $asyncJob . Job
$script:CurrentAppLoadJobStartTime = $asyncJob . StartTime
# Create timer to poll job status without blocking UI
$script:CurrentAppLoadTimer = New-Object System . Windows . Threading . DispatcherTimer
$script:CurrentAppLoadTimer . Interval = [ TimeSpan ] :: FromMilliseconds ( 100 )
$script:CurrentAppLoadTimer . Add_Tick ( {
# Check if this timer was cancelled (another load started)
if ( -not $script:CurrentAppLoadJob -or -not $script:CurrentAppLoadTimer -or -not $script:CurrentAppLoadJobStartTime ) {
if ( $script:CurrentAppLoadTimer ) { $script:CurrentAppLoadTimer . Stop ( ) }
return
}
$elapsed = ( Get-Date ) - $script:CurrentAppLoadJobStartTime
# Check if job is complete or timed out (10 seconds)
if ( $script:CurrentAppLoadJob . State -eq 'Completed' ) {
$script:CurrentAppLoadTimer . Stop ( )
$listOfApps = Receive-Job -Job $script:CurrentAppLoadJob
Remove-Job -Job $script:CurrentAppLoadJob -ErrorAction SilentlyContinue
$script:CurrentAppLoadJob = $null
$script:CurrentAppLoadTimer = $null
$script:CurrentAppLoadJobStartTime = $null
# Continue with loading apps
LoadAppsWithList $listOfApps
}
elseif ( $elapsed . TotalSeconds -gt 10 -or $script:CurrentAppLoadJob . State -eq 'Failed' ) {
$script:CurrentAppLoadTimer . Stop ( )
Remove-Job -Job $script:CurrentAppLoadJob -Force -ErrorAction SilentlyContinue
$script:CurrentAppLoadJob = $null
$script:CurrentAppLoadTimer = $null
$script:CurrentAppLoadJobStartTime = $null
# Show error that the script was unable to get list of apps from WinGet
Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
$onlyInstalledAppsBox . IsChecked = $false
# Continue with loading all apps (unchecked now)
LoadAppsWithList " "
}
} )
$script:CurrentAppLoadTimer . Start ( )
return # Exit here, timer will continue the work
}
# If checkbox is not checked or winget not installed, load all apps immediately
LoadAppsWithList $listOfApps
} ) | Out-Null
}
# Event handlers for app selection
$onlyInstalledAppsBox . Add_Checked ( {
LoadAppsIntoMainUI
} )
$onlyInstalledAppsBox . Add_Unchecked ( {
LoadAppsIntoMainUI
} )
# Quick selection buttons - only select apps actually in those categories
$defaultAppsBtn . Add_Click ( {
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] ) {
if ( $child . SelectedByDefault -eq $true ) {
$child . IsChecked = $true
} else {
$child . IsChecked = $false
}
}
}
} )
$clearAppSelectionBtn . Add_Click ( {
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] ) {
$child . IsChecked = $false
}
}
} )
# Helper function to scroll to an item if it's not visible, centering it in the viewport
function ScrollToItemIfNotVisible {
param (
[ System.Windows.Controls.ScrollViewer ] $scrollViewer ,
[ System.Windows.UIElement ] $item ,
[ System.Windows.UIElement ] $container
)
if ( -not $scrollViewer -or -not $item -or -not $container ) { return }
try {
$itemPosition = $item . TransformToAncestor ( $container ) . Transform ( [ System.Windows.Point ] :: new ( 0 , 0 ) ) . Y
$viewportHeight = $scrollViewer . ViewportHeight
$itemHeight = $item . ActualHeight
$currentOffset = $scrollViewer . VerticalOffset
# Check if the item is currently visible in the viewport
$itemTop = $itemPosition - $currentOffset
$itemBottom = $itemTop + $itemHeight
$isVisible = ( $itemTop -ge 0 ) -and ( $itemBottom -le $viewportHeight )
# Only scroll if the item is not visible
if ( -not $isVisible ) {
# Center the item in the viewport
$targetOffset = $itemPosition - ( $viewportHeight / 2 ) + ( $itemHeight / 2 )
$scrollViewer . ScrollToVerticalOffset ( [ Math ] :: Max ( 0 , $targetOffset ) )
}
}
catch {
# Fallback to simple bring into view
$item . BringIntoView ( )
}
}
# Helper function to find the parent ScrollViewer of an element
function FindParentScrollViewer {
param ( [ System.Windows.UIElement ] $element )
$parent = [ System.Windows.Media.VisualTreeHelper ] :: GetParent ( $element )
while ( $null -ne $parent ) {
if ( $parent -is [ System.Windows.Controls.ScrollViewer ] ) {
return $parent
}
$parent = [ System.Windows.Media.VisualTreeHelper ] :: GetParent ( $parent )
}
return $null
}
# App Search Box functionality
$appSearchBox = $window . FindName ( 'AppSearchBox' )
$appSearchPlaceholder = $window . FindName ( 'AppSearchPlaceholder' )
2026-02-18 00:26:10 +01:00
# Track current search matches and active index for Enter-key navigation
$script:AppSearchMatches = @ ( )
$script:AppSearchMatchIndex = -1
2026-02-15 23:08:54 +01:00
$appSearchBox . Add_TextChanged ( {
$searchText = $appSearchBox . Text . ToLower ( ) . Trim ( )
# Show/hide placeholder
$appSearchPlaceholder . Visibility = if ( [ string ] :: IsNullOrWhiteSpace ( $appSearchBox . Text ) ) { 'Visible' } else { 'Collapsed' }
# Clear all highlights first
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] ) {
$child . Background = [ System.Windows.Media.Brushes ] :: Transparent
}
}
2026-02-18 00:26:10 +01:00
$script:AppSearchMatches = @ ( )
$script:AppSearchMatchIndex = -1
2026-02-15 23:08:54 +01:00
if ( [ string ] :: IsNullOrWhiteSpace ( $searchText ) ) { return }
# Find and highlight all matching apps
2026-02-18 00:26:10 +01:00
$highlightBrush = $window . Resources [ " SearchHighlightColor " ]
$activeHighlightBrush = $window . Resources [ " SearchHighlightActiveColor " ]
2026-02-15 23:08:54 +01:00
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] -and $child . Visibility -eq 'Visible' ) {
2026-02-18 00:26:10 +01:00
$appName = if ( $child . AppName ) { $child . AppName } else { '' }
$appId = if ( $child . Tag ) { $child . Tag . ToString ( ) } else { '' }
$appDesc = if ( $child . AppDescription ) { $child . AppDescription } else { '' }
if ( $appName . ToLower ( ) . Contains ( $searchText ) -or $appId . ToLower ( ) . Contains ( $searchText ) -or $appDesc . ToLower ( ) . Contains ( $searchText ) ) {
2026-02-15 23:08:54 +01:00
$child . Background = $highlightBrush
2026-02-18 00:26:10 +01:00
$script:AppSearchMatches + = $child
2026-02-15 23:08:54 +01:00
}
}
}
2026-02-18 00:26:10 +01:00
# Scroll to first match and mark it as active
if ( $script:AppSearchMatches . Count -gt 0 ) {
$script:AppSearchMatchIndex = 0
$script:AppSearchMatches [ 0 ] . Background = $activeHighlightBrush
2026-02-15 23:08:54 +01:00
$scrollViewer = FindParentScrollViewer -element $appsPanel
if ( $scrollViewer ) {
2026-02-18 00:26:10 +01:00
ScrollToItemIfNotVisible -scrollViewer $scrollViewer -item $script:AppSearchMatches [ 0 ] -container $appsPanel
2026-02-15 23:08:54 +01:00
}
}
} )
2026-02-18 00:26:10 +01:00
$appSearchBox . Add_KeyDown ( {
param ( $sender , $e )
if ( $e . Key -eq [ System.Windows.Input.Key ] :: Enter -and $script:AppSearchMatches . Count -gt 0 ) {
# Reset background of current active match
$script:AppSearchMatches [ $script:AppSearchMatchIndex ] . Background = $window . Resources [ " SearchHighlightColor " ]
# Advance to next match (wrapping)
$script:AppSearchMatchIndex = ( $script:AppSearchMatchIndex + 1 ) % $script:AppSearchMatches . Count
# Highlight new active match
$script:AppSearchMatches [ $script:AppSearchMatchIndex ] . Background = $window . Resources [ " SearchHighlightActiveColor " ]
$scrollViewer = FindParentScrollViewer -element $appsPanel
if ( $scrollViewer ) {
ScrollToItemIfNotVisible -scrollViewer $scrollViewer -item $script:AppSearchMatches [ $script:AppSearchMatchIndex ] -container $appsPanel
}
$e . Handled = $true
}
} )
2026-02-15 23:08:54 +01:00
# Tweak Search Box functionality
$tweakSearchBox = $window . FindName ( 'TweakSearchBox' )
$tweakSearchPlaceholder = $window . FindName ( 'TweakSearchPlaceholder' )
$tweakSearchBorder = $window . FindName ( 'TweakSearchBorder' )
$tweaksScrollViewer = $window . FindName ( 'TweaksScrollViewer' )
$tweaksGrid = $window . FindName ( 'TweaksGrid' )
$col0 = $window . FindName ( 'Column0Panel' )
$col1 = $window . FindName ( 'Column1Panel' )
$col2 = $window . FindName ( 'Column2Panel' )
# Monitor scrollbar visibility and adjust searchbar margin
$tweaksScrollViewer . Add_ScrollChanged ( {
if ( $tweaksScrollViewer . ScrollableHeight -gt 0 ) {
# The 17px accounts for the scrollbar width + some padding
$tweakSearchBorder . Margin = [ System.Windows.Thickness ] :: new ( 0 , 0 , 17 , 0 )
} else {
$tweakSearchBorder . Margin = [ System.Windows.Thickness ] :: new ( 0 , 0 , 0 , 0 )
}
} )
# Helper function to clear all tweak highlights
function ClearTweakHighlights {
$columns = @ ( $col0 , $col1 , $col2 ) | Where-Object { $_ -ne $null }
foreach ( $column in $columns ) {
foreach ( $card in $column . Children ) {
if ( $card -is [ System.Windows.Controls.Border ] -and $card . Child -is [ System.Windows.Controls.StackPanel ] ) {
foreach ( $control in $card . Child . Children ) {
if ( $control -is [ System.Windows.Controls.CheckBox ] -or
( $control -is [ System.Windows.Controls.Border ] -and $control . Name -like '*_LabelBorder' ) ) {
$control . Background = [ System.Windows.Media.Brushes ] :: Transparent
}
}
}
}
}
}
# Helper function to check if a ComboBox contains matching items
function ComboBoxContainsMatch {
param ( [ System.Windows.Controls.ComboBox ] $comboBox , [ string ] $searchText )
foreach ( $item in $comboBox . Items ) {
$itemText = if ( $item -is [ System.Windows.Controls.ComboBoxItem ] ) { $item . Content . ToString ( ) . ToLower ( ) } else { $item . ToString ( ) . ToLower ( ) }
if ( $itemText . Contains ( $searchText ) ) { return $true }
}
return $false
}
$tweakSearchBox . Add_TextChanged ( {
$searchText = $tweakSearchBox . Text . ToLower ( ) . Trim ( )
# Show/hide placeholder
$tweakSearchPlaceholder . Visibility = if ( [ string ] :: IsNullOrWhiteSpace ( $tweakSearchBox . Text ) ) { 'Visible' } else { 'Collapsed' }
# Clear all highlights
ClearTweakHighlights
if ( [ string ] :: IsNullOrWhiteSpace ( $searchText ) ) { return }
# Find and highlight all matching tweaks
$firstMatch = $null
2026-02-18 00:26:10 +01:00
$highlightBrush = $window . Resources [ " SearchHighlightColor " ]
2026-02-15 23:08:54 +01:00
$columns = @ ( $col0 , $col1 , $col2 ) | Where-Object { $_ -ne $null }
foreach ( $column in $columns ) {
foreach ( $card in $column . Children ) {
if ( $card -is [ System.Windows.Controls.Border ] -and $card . Child -is [ System.Windows.Controls.StackPanel ] ) {
$controlsList = @ ( $card . Child . Children )
for ( $i = 0 ; $i -lt $controlsList . Count ; $i + + ) {
$control = $controlsList [ $i ]
$matchFound = $false
$controlToHighlight = $null
if ( $control -is [ System.Windows.Controls.CheckBox ] ) {
if ( $control . Content . ToString ( ) . ToLower ( ) . Contains ( $searchText ) ) {
$matchFound = $true
$controlToHighlight = $control
}
}
elseif ( $control -is [ System.Windows.Controls.Border ] -and $control . Name -like '*_LabelBorder' ) {
$labelText = if ( $control . Child ) { $control . Child . Text . ToLower ( ) } else { " " }
$comboBox = if ( $i + 1 -lt $controlsList . Count -and $controlsList [ $i + 1 ] -is [ System.Windows.Controls.ComboBox ] ) { $controlsList [ $i + 1 ] } else { $null }
# Check label text or combo box items
if ( $labelText . Contains ( $searchText ) -or ( $comboBox -and ( ComboBoxContainsMatch -comboBox $comboBox -searchText $searchText ) ) ) {
$matchFound = $true
$controlToHighlight = $control
}
}
if ( $matchFound -and $controlToHighlight ) {
$controlToHighlight . Background = $highlightBrush
if ( $null -eq $firstMatch ) { $firstMatch = $controlToHighlight }
}
}
}
}
}
# Scroll to first match if not visible
if ( $firstMatch -and $tweaksScrollViewer ) {
ScrollToItemIfNotVisible -scrollViewer $tweaksScrollViewer -item $firstMatch -container $tweaksGrid
}
} )
# Add Ctrl+F keyboard shortcut to focus search box on current tab
$window . Add_KeyDown ( {
param ( $sender , $e )
# Check if Ctrl+F was pressed
if ( $e . Key -eq [ System.Windows.Input.Key ] :: F -and
( [ System.Windows.Input.Keyboard ] :: Modifiers -band [ System.Windows.Input.ModifierKeys ] :: Control ) ) {
$currentTab = $tabControl . SelectedItem
# Focus AppSearchBox if on App Removal tab
if ( $currentTab . Header -eq " App Removal " -and $appSearchBox ) {
$appSearchBox . Focus ( )
$e . Handled = $true
}
# Focus TweakSearchBox if on Tweaks tab
elseif ( $currentTab . Header -eq " Tweaks " -and $tweakSearchBox ) {
$tweakSearchBox . Focus ( )
$e . Handled = $true
}
}
} )
# Wizard Navigation
$tabControl = $window . FindName ( 'MainTabControl' )
$previousBtn = $window . FindName ( 'PreviousBtn' )
$nextBtn = $window . FindName ( 'NextBtn' )
$userSelectionCombo = $window . FindName ( 'UserSelectionCombo' )
$userSelectionDescription = $window . FindName ( 'UserSelectionDescription' )
$otherUserPanel = $window . FindName ( 'OtherUserPanel' )
$otherUsernameTextBox = $window . FindName ( 'OtherUsernameTextBox' )
$usernameTextBoxPlaceholder = $window . FindName ( 'UsernameTextBoxPlaceholder' )
$usernameValidationMessage = $window . FindName ( 'UsernameValidationMessage' )
$appRemovalScopeCombo = $window . FindName ( 'AppRemovalScopeCombo' )
$appRemovalScopeDescription = $window . FindName ( 'AppRemovalScopeDescription' )
$appRemovalScopeSection = $window . FindName ( 'AppRemovalScopeSection' )
$appRemovalScopeCurrentUser = $window . FindName ( 'AppRemovalScopeCurrentUser' )
$appRemovalScopeTargetUser = $window . FindName ( 'AppRemovalScopeTargetUser' )
# Navigation button handlers
function UpdateNavigationButtons {
$currentIndex = $tabControl . SelectedIndex
$totalTabs = $tabControl . Items . Count
$homeIndex = 0
$overviewIndex = $totalTabs - 2
$applyIndex = $totalTabs - 1
# Navigation button visibility
if ( $currentIndex -eq $homeIndex ) {
$nextBtn . Visibility = 'Collapsed'
$previousBtn . Visibility = 'Collapsed'
} elseif ( $currentIndex -eq $overviewIndex ) {
$nextBtn . Visibility = 'Collapsed'
$previousBtn . Visibility = 'Visible'
} elseif ( $currentIndex -eq $applyIndex ) {
$nextBtn . Visibility = 'Collapsed'
$previousBtn . Visibility = 'Collapsed'
} else {
$nextBtn . Visibility = 'Visible'
$previousBtn . Visibility = 'Visible'
}
# Update progress indicators
# Tab indices: 0=Home, 1=App Removal, 2=Tweaks, 3=Overview, 4=Apply
$blueColor = " #0067c0 "
$greyColor = " #808080 "
$progressIndicator1 = $window . FindName ( 'ProgressIndicator1' ) # App Removal
$progressIndicator2 = $window . FindName ( 'ProgressIndicator2' ) # Tweaks
$progressIndicator3 = $window . FindName ( 'ProgressIndicator3' ) # Overview
$bottomNavGrid = $window . FindName ( 'BottomNavGrid' )
# Hide bottom navigation on home page and apply tab
if ( $currentIndex -eq 0 -or $currentIndex -eq $applyIndex ) {
$bottomNavGrid . Visibility = 'Collapsed'
} else {
$bottomNavGrid . Visibility = 'Visible'
}
# Update indicator colors based on current tab
# Indicator 1 (App Removal) - tab index 1
if ( $currentIndex -ge 1 ) {
$progressIndicator1 . Fill = $blueColor
} else {
$progressIndicator1 . Fill = $greyColor
}
# Indicator 2 (Tweaks) - tab index 2
if ( $currentIndex -ge 2 ) {
$progressIndicator2 . Fill = $blueColor
} else {
$progressIndicator2 . Fill = $greyColor
}
# Indicator 3 (Overview) - tab index 3
if ( $currentIndex -ge 3 ) {
$progressIndicator3 . Fill = $blueColor
} else {
$progressIndicator3 . Fill = $greyColor
}
}
# Update user selection description and show/hide other user panel
$userSelectionCombo . Add_SelectionChanged ( {
switch ( $userSelectionCombo . SelectedIndex ) {
0 {
$userSelectionDescription . Text = " Changes will be applied to the currently logged-in user profile. "
$otherUserPanel . Visibility = 'Collapsed'
$usernameValidationMessage . Text = " "
# Show "Current user only" option, hide "Target user only" option
$appRemovalScopeCurrentUser . Visibility = 'Visible'
$appRemovalScopeTargetUser . Visibility = 'Collapsed'
# Enable app removal scope selection for current user
$appRemovalScopeCombo . IsEnabled = $true
$appRemovalScopeCombo . SelectedIndex = 0
}
1 {
$userSelectionDescription . Text = " Changes will be applied to a different user profile on this system. "
$otherUserPanel . Visibility = 'Visible'
$usernameValidationMessage . Text = " "
# Hide "Current user only" option, show "Target user only" option
$appRemovalScopeCurrentUser . Visibility = 'Collapsed'
$appRemovalScopeTargetUser . Visibility = 'Visible'
# Enable app removal scope selection for other user
$appRemovalScopeCombo . IsEnabled = $true
$appRemovalScopeCombo . SelectedIndex = 0
}
2 {
$userSelectionDescription . Text = " Changes will be applied to the default user template, affecting all new users created after this point. Useful for Sysprep deployment. "
$otherUserPanel . Visibility = 'Collapsed'
$usernameValidationMessage . Text = " "
# Hide other user options since they don't apply to default user template
$appRemovalScopeCurrentUser . Visibility = 'Collapsed'
$appRemovalScopeTargetUser . Visibility = 'Collapsed'
# Lock app removal scope to "All users" when applying to sysprep
$appRemovalScopeCombo . IsEnabled = $false
$appRemovalScopeCombo . SelectedIndex = 0
}
}
} )
# Helper function to update app removal scope description
function UpdateAppRemovalScopeDescription {
$selectedItem = $appRemovalScopeCombo . SelectedItem
if ( $selectedItem ) {
switch ( $selectedItem . Content ) {
" All users " {
$appRemovalScopeDescription . Text = " Apps will be removed for all users and from the Windows image to prevent reinstallation for new users. "
}
" Current user only " {
$appRemovalScopeDescription . Text = " Apps will only be removed for the current user. Other users and new users will not be affected. "
}
" Target user only " {
$appRemovalScopeDescription . Text = " Apps will only be removed for the specified target user. Other users and new users will not be affected. "
}
}
}
}
# Update app removal scope description
$appRemovalScopeCombo . Add_SelectionChanged ( {
UpdateAppRemovalScopeDescription
} )
$otherUsernameTextBox . Add_TextChanged ( {
# Show/hide placeholder
if ( [ string ] :: IsNullOrWhiteSpace ( $otherUsernameTextBox . Text ) ) {
$usernameTextBoxPlaceholder . Visibility = 'Visible'
} else {
$usernameTextBoxPlaceholder . Visibility = 'Collapsed'
}
ValidateOtherUsername
} )
function ValidateOtherUsername {
# Only validate if "Other User" is selected
if ( $userSelectionCombo . SelectedIndex -ne 1 ) {
return $true
}
$username = $otherUsernameTextBox . Text . Trim ( )
$errorBrush = [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #c42b1c " ) )
$successBrush = [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #28a745 " ) )
if ( $username . Length -eq 0 ) {
$usernameValidationMessage . Text = " [X] Please enter a username "
$usernameValidationMessage . Foreground = $errorBrush
return $false
}
if ( $username -eq $env:USERNAME ) {
$usernameValidationMessage . Text = " [X] Cannot enter your own username, use 'Current User' option instead "
$usernameValidationMessage . Foreground = $errorBrush
return $false
}
$userExists = CheckIfUserExists -Username $username
if ( $userExists ) {
$usernameValidationMessage . Text = " [OK] User found: $username "
$usernameValidationMessage . Foreground = $successBrush
return $true
}
$usernameValidationMessage . Text = " [X] User not found, please enter a valid username "
$usernameValidationMessage . Foreground = $errorBrush
return $false
}
function GenerateOverview {
# Load Features.json
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion " 1.0 "
$overviewChangesPanel = $window . FindName ( 'OverviewChangesPanel' )
$overviewChangesPanel . Children . Clear ( )
$changesList = @ ( )
# Collect selected apps
$selectedAppsCount = 0
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] -and $child . IsChecked ) {
$selectedAppsCount + +
}
}
if ( $selectedAppsCount -gt 0 ) {
$changesList + = " Remove $selectedAppsCount selected application(s) "
}
# Update app removal scope section based on whether apps are selected
if ( $selectedAppsCount -gt 0 ) {
# Enable app removal scope selection (unless locked by sysprep mode)
if ( $userSelectionCombo . SelectedIndex -ne 2 ) {
$appRemovalScopeCombo . IsEnabled = $true
}
$appRemovalScopeSection . Opacity = 1.0
UpdateAppRemovalScopeDescription
}
else {
# Disable app removal scope selection when no apps selected
$appRemovalScopeCombo . IsEnabled = $false
$appRemovalScopeSection . Opacity = 0.5
$appRemovalScopeDescription . Text = " No apps selected for removal. "
}
# Collect all ComboBox/CheckBox selections from dynamically created controls
if ( $script:UiControlMappings ) {
foreach ( $mappingKey in $script:UiControlMappings . Keys ) {
$control = $window . FindName ( $mappingKey )
$isSelected = $false
# Check if it's a checkbox or combobox
if ( $control -is [ System.Windows.Controls.CheckBox ] ) {
$isSelected = $control . IsChecked -eq $true
}
elseif ( $control -is [ System.Windows.Controls.ComboBox ] ) {
$isSelected = $control . SelectedIndex -gt 0
}
if ( $control -and $isSelected ) {
$mapping = $script:UiControlMappings [ $mappingKey ]
if ( $mapping . Type -eq 'group' ) {
# For combobox: SelectedIndex 0 = No Change, so subtract 1 to index into Values
$selectedValue = $mapping . Values [ $control . SelectedIndex - 1 ]
foreach ( $fid in $selectedValue . FeatureIds ) {
$feature = $featuresJson . Features | Where-Object { $_ . FeatureId -eq $fid }
if ( $feature ) { $changesList + = ( $feature . Action + ' ' + $feature . Label ) }
}
}
elseif ( $mapping . Type -eq 'feature' ) {
$feature = $featuresJson . Features | Where-Object { $_ . FeatureId -eq $mapping . FeatureId }
if ( $feature ) { $changesList + = ( $feature . Action + ' ' + $feature . Label ) }
}
}
}
}
if ( $changesList . Count -eq 0 ) {
$textBlock = New-Object System . Windows . Controls . TextBlock
$textBlock . Text = " No changes selected "
$textBlock . Style = $window . Resources [ " OverviewNoChangesTextStyle " ]
$overviewChangesPanel . Children . Add ( $textBlock ) | Out-Null
}
else {
foreach ( $change in $changesList ) {
$bullet = New-Object System . Windows . Controls . TextBlock
$bullet . Text = " - $change "
$bullet . Style = $window . Resources [ " OverviewChangeBulletStyle " ]
$overviewChangesPanel . Children . Add ( $bullet ) | Out-Null
}
}
}
$previousBtn . Add_Click ( {
if ( $tabControl . SelectedIndex -gt 0 ) {
$tabControl . SelectedIndex - -
UpdateNavigationButtons
}
} )
$nextBtn . Add_Click ( {
if ( $tabControl . SelectedIndex -lt ( $tabControl . Items . Count - 1 ) ) {
$tabControl . SelectedIndex + +
UpdateNavigationButtons
}
} )
# Handle Home Start button
$homeStartBtn = $window . FindName ( 'HomeStartBtn' )
$homeStartBtn . Add_Click ( {
# Navigate to first tab after home (App Removal)
$tabControl . SelectedIndex = 1
UpdateNavigationButtons
} )
# Handle Overview Apply Changes button - validates and immediately starts applying changes
$overviewApplyBtn = $window . FindName ( 'OverviewApplyBtn' )
$overviewApplyBtn . Add_Click ( {
if ( -not ( ValidateOtherUsername ) ) {
Show-MessageBox -Message " Please enter a valid username. " -Title " Invalid Username " -Button 'OK' -Icon 'Warning' | Out-Null
return
}
# App Removal - collect selected apps from integrated UI
$selectedApps = @ ( )
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] -and $child . IsChecked ) {
$selectedApps + = $child . Tag
}
}
if ( $selectedApps . Count -gt 0 ) {
# Check if Microsoft Store is selected
if ( $selectedApps -contains " Microsoft.WindowsStore " ) {
$result = Show-MessageBox -Message 'Are you sure you wish to uninstall the Microsoft Store? This app cannot easily be reinstalled.' -Title 'Are you sure?' -Button 'YesNo' -Icon 'Warning'
if ( $result -eq 'No' ) {
return
}
}
AddParameter 'RemoveApps'
AddParameter 'Apps' ( $selectedApps -join ',' )
# Add app removal target parameter based on selection
$selectedScopeItem = $appRemovalScopeCombo . SelectedItem
if ( $selectedScopeItem ) {
switch ( $selectedScopeItem . Content ) {
" All users " {
AddParameter 'AppRemovalTarget' 'AllUsers'
}
" Current user only " {
AddParameter 'AppRemovalTarget' 'CurrentUser'
}
" Target user only " {
# Use the target username from Other User panel
AddParameter 'AppRemovalTarget' ( $otherUsernameTextBox . Text . Trim ( ) )
}
}
}
}
# Apply dynamic tweaks selections
if ( $script:UiControlMappings ) {
foreach ( $mappingKey in $script:UiControlMappings . Keys ) {
$control = $window . FindName ( $mappingKey )
$isSelected = $false
$selectedIndex = 0
# Check if it's a checkbox or combobox
if ( $control -is [ System.Windows.Controls.CheckBox ] ) {
$isSelected = $control . IsChecked -eq $true
$selectedIndex = if ( $isSelected ) { 1 } else { 0 }
}
elseif ( $control -is [ System.Windows.Controls.ComboBox ] ) {
$isSelected = $control . SelectedIndex -gt 0
$selectedIndex = $control . SelectedIndex
}
if ( $control -and $isSelected ) {
$mapping = $script:UiControlMappings [ $mappingKey ]
if ( $mapping . Type -eq 'group' ) {
if ( $selectedIndex -gt 0 -and $selectedIndex -le $mapping . Values . Count ) {
$selectedValue = $mapping . Values [ $selectedIndex - 1 ]
foreach ( $fid in $selectedValue . FeatureIds ) {
AddParameter $fid
}
}
}
elseif ( $mapping . Type -eq 'feature' ) {
AddParameter $mapping . FeatureId
}
}
}
}
$controlParamsCount = 0
foreach ( $Param in $script:ControlParams ) {
if ( $script:Params . ContainsKey ( $Param ) ) {
$controlParamsCount + +
}
}
# Check if any changes were selected
$totalChanges = $script:Params . Count - $controlParamsCount
# Apps parameter does not count as a change itself
if ( $script:Params . ContainsKey ( 'Apps' ) ) {
$totalChanges = $totalChanges - 1
}
if ( $totalChanges -eq 0 ) {
Show-MessageBox -Message 'No changes have been selected, please select at least one option to proceed.' -Title 'No Changes Selected' -Button 'OK' -Icon 'Information'
return
}
# Check RestorePointCheckBox
$restorePointCheckBox = $window . FindName ( 'RestorePointCheckBox' )
if ( $restorePointCheckBox -and $restorePointCheckBox . IsChecked ) {
AddParameter 'CreateRestorePoint'
}
# Store selected user mode
switch ( $userSelectionCombo . SelectedIndex ) {
1 { AddParameter User ( $otherUsernameTextBox . Text . Trim ( ) ) }
2 { AddParameter Sysprep }
}
SaveSettings
# Navigate to Apply tab (last tab) and start applying changes
$tabControl . SelectedIndex = $tabControl . Items . Count - 1
# Clear console and set initial status
$consoleOutput . Text = " "
Write-ToConsole " Applying changes to $( if ( $script:Params . ContainsKey ( " Sysprep " ) ) { " default user template " } else { " user $( GetUserName ) " } ) "
Write-ToConsole " Total changes to apply: $totalChanges "
Write-ToConsole " "
# Run changes in background to keep UI responsive
$window . Dispatcher . BeginInvoke ( [ System.Windows.Threading.DispatcherPriority ] :: Background , [ action ] {
try {
ExecuteAllChanges
# Check if user wants to restart explorer (from checkbox)
$restartExplorerCheckBox = $window . FindName ( 'RestartExplorerCheckBox' )
if ( $restartExplorerCheckBox -and $restartExplorerCheckBox . IsChecked -and -not $script:CancelRequested ) {
RestartExplorer
}
Write-ToConsole " "
if ( $script:CancelRequested ) {
Write-ToConsole " Script execution was cancelled by the user. Some changes may not have been applied. "
} else {
Write-ToConsole " All changes have been applied. Please check the output above for any errors. "
}
$finishBtn . Dispatcher . Invoke ( [ action ] {
$finishBtn . IsEnabled = $true
$finishBtnText . Text = " Close Win11Debloat "
} )
}
catch {
Write-ToConsole " Error: $( $_ . Exception . Message ) "
$finishBtn . Dispatcher . Invoke ( [ action ] {
$finishBtn . IsEnabled = $true
$finishBtnText . Text = " Close Win11Debloat "
} )
}
} )
} )
# Initialize UI elements on window load
$window . Add_Loaded ( {
BuildDynamicTweaks
LoadAppsIntoMainUI
# Update Current User label with username
if ( $userSelectionCombo -and $userSelectionCombo . Items . Count -gt 0 ) {
$currentUserItem = $userSelectionCombo . Items [ 0 ]
if ( $currentUserItem -is [ System.Windows.Controls.ComboBoxItem ] ) {
$currentUserItem . Content = " Current User ( $( GetUserName ) ) "
}
}
# Disable Restart Explorer option if NoRestartExplorer parameter is set
$restartExplorerCheckBox = $window . FindName ( 'RestartExplorerCheckBox' )
if ( $restartExplorerCheckBox -and $script:Params . ContainsKey ( " NoRestartExplorer " ) ) {
$restartExplorerCheckBox . IsChecked = $false
$restartExplorerCheckBox . IsEnabled = $false
}
# Force Apply Changes To setting if Sysprep or User parameters are set
if ( $script:Params . ContainsKey ( " Sysprep " ) ) {
$userSelectionCombo . SelectedIndex = 2
$userSelectionCombo . IsEnabled = $false
}
elseif ( $script:Params . ContainsKey ( " User " ) ) {
$userSelectionCombo . SelectedIndex = 1
$userSelectionCombo . IsEnabled = $false
$otherUsernameTextBox . Text = $script:Params . Item ( " User " )
$otherUsernameTextBox . IsEnabled = $false
}
UpdateNavigationButtons
} )
# Add event handler for tab changes
$tabControl . Add_SelectionChanged ( {
# Regenerate overview when switching to Overview tab
if ( $tabControl . SelectedIndex -eq ( $tabControl . Items . Count - 2 ) ) {
GenerateOverview
}
UpdateNavigationButtons
} )
# Handle Load Defaults button
$loadDefaultsBtn = $window . FindName ( 'LoadDefaultsBtn' )
$loadDefaultsBtn . Add_Click ( {
$defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion " 1.0 "
if ( -not $defaultsJson ) {
Show-MessageBox -Message " Failed to load default settings file " -Title " Error " -Button 'OK' -Icon 'Error'
return
}
ApplySettingsToUiControls -window $window -settingsJson $defaultsJson -uiControlMappings $script:UiControlMappings
} )
# Handle Load Last Used settings and Load Last Used apps
$loadLastUsedBtn = $window . FindName ( 'LoadLastUsedBtn' )
$loadLastUsedAppsBtn = $window . FindName ( 'LoadLastUsedAppsBtn' )
$lastUsedSettingsJson = LoadJsonFile -filePath $script:SavedSettingsFilePath -expectedVersion " 1.0 " -optionalFile
$hasSettings = $false
$appsSetting = $null
if ( $lastUsedSettingsJson -and $lastUsedSettingsJson . Settings ) {
foreach ( $s in $lastUsedSettingsJson . Settings ) {
# Only count as hasSettings if a setting other than RemoveApps/Apps is present and true
if ( $s . Value -eq $true -and $s . Name -ne 'RemoveApps' -and $s . Name -ne 'Apps' ) { $hasSettings = $true }
if ( $s . Name -eq 'Apps' -and $s . Value ) { $appsSetting = $s . Value }
}
}
# Show option to load last used settings if they exist
if ( $hasSettings ) {
$loadLastUsedBtn . Add_Click ( {
try {
ApplySettingsToUiControls -window $window -settingsJson $lastUsedSettingsJson -uiControlMappings $script:UiControlMappings
}
catch {
Show-MessageBox -Message " Failed to load last used settings: $_ " -Title " Error " -Button 'OK' -Icon 'Error'
}
} )
}
else {
$loadLastUsedBtn . Visibility = 'Collapsed'
}
# Show option to load last used apps if they exist
if ( $appsSetting -and $appsSetting . ToString ( ) . Trim ( ) . Length -gt 0 ) {
$loadLastUsedAppsBtn . Add_Click ( {
try {
$savedApps = @ ( )
if ( $appsSetting -is [ string ] ) { $savedApps = $appsSetting . Split ( ',' ) }
elseif ( $appsSetting -is [ array ] ) { $savedApps = $appsSetting }
$savedApps = $savedApps | ForEach-Object { $_ . Trim ( ) } | Where-Object { $_ -ne '' }
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] ) {
if ( $savedApps -contains $child . Tag ) { $child . IsChecked = $true } else { $child . IsChecked = $false }
}
}
}
catch {
Show-MessageBox -Message " Failed to load last used app selection: $_ " -Title " Error " -Button 'OK' -Icon 'Error'
}
} )
}
else {
$loadLastUsedAppsBtn . Visibility = 'Collapsed'
}
# Clear All Tweaks button
$clearAllTweaksBtn = $window . FindName ( 'ClearAllTweaksBtn' )
$clearAllTweaksBtn . Add_Click ( {
# Reset all ComboBoxes to index 0 (No Change) and uncheck all CheckBoxes
if ( $script:UiControlMappings ) {
foreach ( $comboName in $script:UiControlMappings . Keys ) {
$control = $window . FindName ( $comboName )
if ( $control -is [ System.Windows.Controls.CheckBox ] ) {
$control . IsChecked = $false
}
elseif ( $control -is [ System.Windows.Controls.ComboBox ] ) {
$control . SelectedIndex = 0
}
}
}
# Also uncheck RestorePointCheckBox
$restorePointCheckBox = $window . FindName ( 'RestorePointCheckBox' )
if ( $restorePointCheckBox ) {
$restorePointCheckBox . IsChecked = $false
}
} )
# Finish (Close Win11Debloat) button handler
$finishBtn . Add_Click ( {
$window . Close ( )
} )
# Show the window
return $window . ShowDialog ( )
}