2026-06-10 17:40:31 +02:00
# MainWindow-TweaksBuilder.ps1
# Dynamic tweaks UI construction from Features.json, tweak state management, selection clear, and search/highlight.
function Build-DynamicTweaks {
param (
[ System.Windows.Window ] $Window ,
[ int ] $WinVersion
)
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion " 1.0 "
if ( -not $featuresJson ) {
throw " Unable to load Features.json file. The GUI cannot continue without feature definitions. "
}
# 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 = @ { }
$script:TweaksCompactMode = $null
$script:TweaksCardsMovedFromCol2 = @ ( )
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 ( $item in $items ) { $comboItem = New-Object System . Windows . Controls . ComboBoxItem ; $comboItem . Content = $item ; $combo . Items . Add ( $comboItem ) | 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
2026-06-17 16:35:36 +02:00
$helpBtn . ToolTip = " Open the wiki for more info on ' $categoryName ' tweaks "
2026-06-10 17:40:31 +02:00
$helpBtn . Tag = ( GetWikiUrlForCategory -category $categoryName )
$helpBtn . Style = $Window . Resources [ 'CategoryHelpLinkButtonStyle' ]
$helpBtn . Add_Click ( {
param ( $button , $e )
if ( $button . Tag ) { Start-Process $button . 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 ; Category = $categoryName }
}
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 . Label -comboName $comboName -items $items
# attach tooltip from Features.json if present, and include the disabled-state reason
if ( $feature . ToolTip -or $feature . DisableWhenApplied -eq $true ) {
$tooltipText = $feature . ToolTip
if ( $feature . DisableWhenApplied -eq $true ) {
$tooltipText = " This tweak is already applied and cannot be undone automatically. Visit the Win11Debloat wiki for instructions on how to manually revert this change. "
}
$tipBlock = New-Object System . Windows . Controls . TextBlock
$tipBlock . Text = $tooltipText
$tipBlock . TextWrapping = 'Wrap'
$tipBlock . MaxWidth = 420
$combo . ToolTip = $tipBlock
[ System.Windows.Controls.ToolTipService ] :: SetShowOnDisabled ( $combo , $true )
$lblBorderObj = $null
try { $lblBorderObj = $Window . FindName ( " $comboName ` _LabelBorder " ) } catch { }
if ( $lblBorderObj ) { $lblBorderObj . ToolTip = $tipBlock }
}
$script:UiControlMappings [ $comboName ] = @ { Type = 'feature' ; FeatureId = $feature . FeatureId ; Label = $feature . Label ; Category = $categoryName }
}
}
}
# Build a feature-label lookup so GenerateOverview can resolve feature IDs without reloading JSON
$script:FeatureLabelLookup = @ { }
$script:UndoFeatureLabelLookup = @ { }
foreach ( $f in $featuresJson . Features ) {
$script:FeatureLabelLookup [ $f . FeatureId ] = $f . Label
$script:UndoFeatureLabelLookup [ $f . FeatureId ] = $f . UndoLabel
}
}
function Update-CurrentTweakSystemState {
param (
[ System.Windows.Window ] $Window ,
[ bool ] $ApplyToUi
)
if ( -not $script:UiControlMappings ) { return }
if ( -not $script:Features ) { return }
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion " 1.0 "
if ( -not $featuresJson ) { return }
$groupMap = @ { }
if ( $featuresJson . UiGroups ) {
foreach ( $g in $featuresJson . UiGroups ) {
$groupMap [ $g . GroupId ] = $g
}
}
foreach ( $controlName in $script:UiControlMappings . Keys ) {
$control = $Window . FindName ( $controlName )
if ( -not $control ) { continue }
$mapping = $script:UiControlMappings [ $controlName ]
if ( $control -is [ System.Windows.Controls.CheckBox ] -and $mapping . Type -eq 'feature' ) {
$applied = $false
try { $applied = [ bool ] ( Test-FeatureApplied -FeatureId $mapping . FeatureId ) } catch { }
$featureObj = $script:Features [ $mapping . FeatureId ]
$disableWhenApplied = $featureObj -and $featureObj . DisableWhenApplied -eq $true
Add-Member -InputObject $control -MemberType NoteProperty -Name 'SystemState' -Value $applied -Force
Add-Member -InputObject $control -MemberType NoteProperty -Name 'DisableWhenApplied' -Value $disableWhenApplied -Force
if ( $ApplyToUi ) {
$control . IsChecked = $applied
$control . IsEnabled = -not ( $applied -and $disableWhenApplied )
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialState' -Value $applied -Force
}
}
elseif ( $control -is [ System.Windows.Controls.ComboBox ] -and $mapping . Type -eq 'group' ) {
$groupId = $null
if ( $controlName -match '^Group_(.+)Combo$' ) { $groupId = $matches [ 1 ] }
$activeIndex = 0
if ( $groupId -and $groupMap . ContainsKey ( $groupId ) ) {
try { $activeIndex = Get-CurrentGroupActiveIndex -Group $groupMap [ $groupId ] } catch { }
}
Add-Member -InputObject $control -MemberType NoteProperty -Name 'SystemIndex' -Value $activeIndex -Force
if ( $ApplyToUi ) {
$control . SelectedIndex = $activeIndex
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialIndex' -Value $activeIndex -Force
}
}
}
}
function Load-CurrentTweakStateIntoUI {
param ( [ System.Windows.Window ] $Window )
Update-CurrentTweakSystemState -Window $Window -ApplyToUi: $true
}
function Reset-TweaksToSystemState {
param (
[ System.Windows.Window ] $Window ,
[ bool ] $LoadSystemState
)
if ( -not $script:UiControlMappings ) { return }
foreach ( $controlName in $script:UiControlMappings . Keys ) {
$control = $Window . FindName ( $controlName )
if ( -not $control ) { continue }
if ( $control -is [ System.Windows.Controls.CheckBox ] ) {
if ( $LoadSystemState ) {
# Set checkbox to the currently applied state from registry
$applied = if ( $null -ne $control . PSObject . Properties [ 'SystemState' ] ) { [ bool ] $control . SystemState } else { $false }
$disableWhenApplied = $null -ne $control . PSObject . Properties [ 'DisableWhenApplied' ] -and [ bool ] $control . DisableWhenApplied
$control . IsChecked = $applied
$control . IsEnabled = -not ( $applied -and $disableWhenApplied )
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialState' -Value $applied -Force
}
else {
# Clear the checkbox
$control . IsChecked = $false
$control . IsEnabled = $true
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialState' -Value $false -Force
}
}
elseif ( $control -is [ System.Windows.Controls.ComboBox ] ) {
if ( $LoadSystemState ) {
# Set combobox to the currently applied state from registry
$idx = if ( $null -ne $control . PSObject . Properties [ 'SystemIndex' ] ) { [ int ] $control . SystemIndex } else { 0 }
$control . SelectedIndex = $idx
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialIndex' -Value $idx -Force
}
else {
# Reset to first item (No Change)
$control . SelectedIndex = 0
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialIndex' -Value 0 -Force
}
}
}
}
function Update-TweaksResponsiveColumns {
param ( [ System.Windows.Window ] $Window )
$tweaksGrid = $Window . FindName ( 'TweaksGrid' )
$col0 = $Window . FindName ( 'Column0Panel' )
$col1 = $Window . FindName ( 'Column1Panel' )
$col2 = $Window . FindName ( 'Column2Panel' )
if ( -not $tweaksGrid -or -not $col0 -or -not $col1 -or -not $col2 ) { return }
if ( $tweaksGrid . ColumnDefinitions . Count -lt 3 ) { return }
if ( $null -eq $script:TweaksCardsMovedFromCol2 ) { $script:TweaksCardsMovedFromCol2 = @ ( ) }
$useTwoColumns = $Window . ActualWidth -lt 1200
if ( $script:TweaksCompactMode -eq $useTwoColumns ) { return }
$script:TweaksCompactMode = $useTwoColumns
if ( $useTwoColumns ) {
$tweaksGrid . ColumnDefinitions [ 0 ] . Width = [ System.Windows.GridLength ] :: new ( 1 , [ System.Windows.GridUnitType ] :: Star )
$tweaksGrid . ColumnDefinitions [ 1 ] . Width = [ System.Windows.GridLength ] :: new ( 1 , [ System.Windows.GridUnitType ] :: Star )
$tweaksGrid . ColumnDefinitions [ 2 ] . Width = [ System.Windows.GridLength ] :: new ( 0 )
$col2 . Visibility = 'Collapsed'
# Move third-column cards once when entering compact mode.
$cardsToMove = @ ( $col2 . Children ) | Where-Object { $_ -is [ System.Windows.UIElement ] }
$script:TweaksCardsMovedFromCol2 = @ ( $cardsToMove )
$col2 . Children . Clear ( )
$targetColumns = @ ( $col0 , $col1 )
foreach ( $card in $cardsToMove ) {
$target = $targetColumns |
Sort-Object @ { Expression = { $_ . Children . Count } ; Ascending = $true } , @ { Expression = { $targetColumns . IndexOf ( $_ ) } ; Ascending = $true } |
Select-Object -First 1
$target . Children . Add ( $card ) | Out-Null
}
return
}
$tweaksGrid . ColumnDefinitions [ 0 ] . Width = [ System.Windows.GridLength ] :: new ( 1 , [ System.Windows.GridUnitType ] :: Star )
$tweaksGrid . ColumnDefinitions [ 1 ] . Width = [ System.Windows.GridLength ] :: new ( 1 , [ System.Windows.GridUnitType ] :: Star )
$tweaksGrid . ColumnDefinitions [ 2 ] . Width = [ System.Windows.GridLength ] :: new ( 1 , [ System.Windows.GridUnitType ] :: Star )
$col2 . Visibility = 'Visible'
foreach ( $card in ( @ ( $script:TweaksCardsMovedFromCol2 ) | Where-Object { $_ -is [ System.Windows.UIElement ] } ) ) {
if ( $col0 . Children . Contains ( $card ) ) {
$col0 . Children . Remove ( $card ) | Out-Null
}
elseif ( $col1 . Children . Contains ( $card ) ) {
$col1 . Children . Remove ( $card ) | Out-Null
}
$col2 . Children . Add ( $card ) | Out-Null
}
$script:TweaksCardsMovedFromCol2 = @ ( )
}
function Clear-TweakSelections {
param ( [ System.Windows.Window ] $Window )
if ( -not $script:UiControlMappings ) { return }
foreach ( $controlName in $script:UiControlMappings . Keys ) {
$control = $Window . FindName ( $controlName )
if ( $control -is [ System.Windows.Controls.CheckBox ] ) {
$control . IsChecked = $false
$control . IsEnabled = $true
}
elseif ( $control -is [ System.Windows.Controls.ComboBox ] ) {
$control . SelectedIndex = 0
}
}
}
function Clear-TweakHighlights {
param ( [ System.Windows.Window ] $Window )
$col0 = $Window . FindName ( 'Column0Panel' )
$col1 = $Window . FindName ( 'Column1Panel' )
$col2 = $Window . FindName ( 'Column2Panel' )
$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
}
}
}
}
}
}
function Test-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
}