function Show-MainWindow { Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase,System.Windows.Forms | Out-Null # Helper to constrain the maximized window to the monitor working area (respects taskbar). # Required for WindowStyle=None windows — without this the window extends behind the taskbar. 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; } '@ } # 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 $mainBorder = $window.FindName('MainBorder') $titleBarBackground = $window.FindName('TitleBarBackground') $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') $importConfigBtn = $window.FindName('ImportConfigBtn') $exportConfigBtn = $window.FindName('ExportConfigBtn') $windowStateNormal = [System.Windows.WindowState]::Normal $windowStateMaximized = [System.Windows.WindowState]::Maximized $normalWindowShadow = $mainBorder.Effect $initialNormalMaxWidth = 1400.0 $convertScreenPointToDip = { param( [double]$x, [double]$y ) $source = [System.Windows.PresentationSource]::FromVisual($window) if ($null -eq $source -or $null -eq $source.CompositionTarget) { return [System.Windows.Point]::new($x, $y) } return $source.CompositionTarget.TransformFromDevice.Transform([System.Windows.Point]::new($x, $y)) } $convertScreenPixelsToDip = { param( [double]$width, [double]$height ) $topLeft = & $convertScreenPointToDip 0 0 $bottomRight = & $convertScreenPointToDip $width $height return [System.Windows.Size]::new($bottomRight.X - $topLeft.X, $bottomRight.Y - $topLeft.Y) } $getWindowScreen = { $hwnd = (New-Object System.Windows.Interop.WindowInteropHelper($window)).Handle if ($hwnd -eq [IntPtr]::Zero) { return $null } return [System.Windows.Forms.Screen]::FromHandle($hwnd) } $updateWindowChrome = { $chrome = [System.Windows.Shell.WindowChrome]::GetWindowChrome($window) if ($window.WindowState -eq $windowStateMaximized) { $mainBorder.Margin = [System.Windows.Thickness]::new(0) $mainBorder.BorderThickness = [System.Windows.Thickness]::new(0) $mainBorder.CornerRadius = [System.Windows.CornerRadius]::new(0) $mainBorder.Effect = $null $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 { $mainBorder.Margin = [System.Windows.Thickness]::new(0) $mainBorder.BorderThickness = [System.Windows.Thickness]::new(1) $mainBorder.CornerRadius = [System.Windows.CornerRadius]::new(8) $mainBorder.Effect = $normalWindowShadow $titleBarBackground.CornerRadius = [System.Windows.CornerRadius]::new(8, 8, 0, 0) if ($chrome) { $chrome.ResizeBorderThickness = [System.Windows.Thickness]::new(5) } } } $applyInitialWindowSize = { if ($window.WindowState -ne $windowStateNormal) { return } $screen = & $getWindowScreen if ($null -eq $screen) { return } $workingAreaTopLeftDip = & $convertScreenPointToDip $screen.WorkingArea.Left $screen.WorkingArea.Top $workingAreaDip = & $convertScreenPixelsToDip $screen.WorkingArea.Width $screen.WorkingArea.Height $window.Width = [Math]::Min($initialNormalMaxWidth, $workingAreaDip.Width) $window.Left = $workingAreaTopLeftDip.X + (($workingAreaDip.Width - $window.Width) / 2) } $window.Add_SourceInitialized({ & $applyInitialWindowSize & $updateWindowChrome # Register WM_GETMINMAXINFO hook so maximizing respects the working area (taskbar) $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) }) $contentGrid = $window.FindName('ContentGrid') $maxContentWidth = 1600.0 $updateContentMargin = { $w = $window.ActualWidth if ($w -gt $maxContentWidth) { $gutter = [Math]::Floor(($w - $maxContentWidth) / 2) $contentGrid.Margin = [System.Windows.Thickness]::new($gutter, 0, $gutter, 0) } else { $contentGrid.Margin = [System.Windows.Thickness]::new(0) } } $window.Add_SizeChanged({ & $updateContentMargin UpdateTweaksResponsiveColumns }) $window.Add_StateChanged({ & $updateWindowChrome }) $window.Add_LocationChanged({ # Nudge the popup offset to force WPF to recalculate its screen position if ($script:BubblePopup -and $script:BubblePopup.IsOpen) { $script:BubblePopup.HorizontalOffset += 1 $script:BubblePopup.HorizontalOffset -= 1 } }) $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 }) # --- Import/Export Configuration --- $exportConfigBtn.Add_Click({ Export-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox }) $importConfigBtn.Add_Click({ Import-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -OnAppsImported { UpdateAppSelectionStatus; UpdatePresetStates } -OnImportCompleted { $tabControl.SelectedIndex = 3 UpdateNavigationButtons $window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{ Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here' }) | Out-Null } }) $closeBtn.Add_Click({ $window.Close() }) # Ensure closing the main window stops all execution $window.Add_Closing({ $script:CancelRequested = $true }) # Integrated App Selection UI $appsPanel = $window.FindName('AppSelectionPanel') $onlyInstalledAppsBox = $window.FindName('OnlyInstalledAppsBox') $loadingAppsIndicator = $window.FindName('LoadingAppsIndicator') $appSelectionStatus = $window.FindName('AppSelectionStatus') $headerNameBtn = $window.FindName('HeaderNameBtn') $headerDescriptionBtn = $window.FindName('HeaderDescriptionBtn') $headerAppIdBtn = $window.FindName('HeaderAppIdBtn') $sortArrowName = $window.FindName('SortArrowName') $sortArrowDescription = $window.FindName('SortArrowDescription') $sortArrowAppId = $window.FindName('SortArrowAppId') $presetsBtn = $window.FindName('PresetsBtn') $presetsPopup = $window.FindName('PresetsPopup') $presetDefaultApps = $window.FindName('PresetDefaultApps') $presetLastUsed = $window.FindName('PresetLastUsed') $jsonPresetsPanel = $window.FindName('JsonPresetsPanel') $presetsArrow = $window.FindName('PresetsArrow') $clearAppSelectionBtn = $window.FindName('ClearAppSelectionBtn') $tweaksPresetsBtn = $window.FindName('TweaksPresetsBtn') $tweaksPresetsPopup = $window.FindName('TweaksPresetsPopup') $presetDefaultTweaksBtn = $window.FindName('PresetDefaultTweaksBtn') $presetLastUsedTweaksBtn = $window.FindName('PresetLastUsedTweaksBtn') $presetPrivacyTweaksBtn = $window.FindName('PresetPrivacyTweaksBtn') $presetAITweaksBtn = $window.FindName('PresetAITweaksBtn') $tweaksPresetsArrow = $window.FindName('TweaksPresetsArrow') function AttachTriStateClickBehavior { param([System.Windows.Controls.CheckBox]$checkBox) if (-not $checkBox -or -not $checkBox.IsThreeState) { return } if (-not $checkBox.PSObject.Properties['WasIndeterminateBeforeClick']) { Add-Member -InputObject $checkBox -MemberType NoteProperty -Name 'WasIndeterminateBeforeClick' -Value $false } $checkBox.Add_PreviewMouseLeftButtonDown({ $this.WasIndeterminateBeforeClick = ($this.IsChecked -eq [System.Nullable[bool]]$null) }) } function NormalizeCheckboxState { param([System.Windows.Controls.CheckBox]$checkBox) if ($checkBox.PSObject.Properties['WasIndeterminateBeforeClick'] -and $checkBox.WasIndeterminateBeforeClick) { # WPF toggles null -> false before Click handlers fire; restore desired mixed -> checked behavior. $checkBox.WasIndeterminateBeforeClick = $false $checkBox.IsChecked = $true return $true } return ($checkBox.IsChecked -eq $true) } function SetTriStatePresetCheckBoxState { param( [System.Windows.Controls.CheckBox]$CheckBox, [int]$Total, [int]$Selected ) if (-not $CheckBox) { return } if ($Total -eq 0) { $CheckBox.IsEnabled = $false $CheckBox.IsChecked = $false return } $CheckBox.IsEnabled = $true if ($Selected -eq 0) { $CheckBox.IsChecked = $false } elseif ($Selected -eq $Total) { $CheckBox.IsChecked = $true } else { $CheckBox.IsChecked = [System.Nullable[bool]]$null } } function AnimateDropdownArrow { param( [System.Windows.Controls.TextBlock]$arrow, [double]$angle ) if (-not $arrow) { return } $animation = New-Object System.Windows.Media.Animation.DoubleAnimation $animation.To = $angle $animation.Duration = [System.Windows.Duration]::new([System.TimeSpan]::FromMilliseconds(200)) $ease = New-Object System.Windows.Media.Animation.CubicEase $ease.EasingMode = 'EaseOut' $animation.EasingFunction = $ease $arrow.RenderTransform.BeginAnimation([System.Windows.Media.RotateTransform]::AngleProperty, $animation) } # Load JSON-defined presets and build dynamic preset checkboxes $script:JsonPresetCheckboxes = @() foreach ($preset in (LoadAppPresetsFromJson)) { $checkbox = New-Object System.Windows.Controls.CheckBox $checkbox.Content = $preset.Name $checkbox.IsThreeState = $true $checkbox.Style = $window.Resources['PresetCheckBoxStyle'] $checkbox.ToolTip = "Select $($preset.Name)" $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $preset.Name) AttachTriStateClickBehavior -checkBox $checkbox Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'PresetAppIds' -Value $preset.AppIds $jsonPresetsPanel.Children.Add($checkbox) | Out-Null $script:JsonPresetCheckboxes += $checkbox $checkbox.Add_Click({ if ($script:UpdatingPresets) { return } $presetIds = $this.PresetAppIds $check = NormalizeCheckboxState -checkBox $this ApplyPresetToApps -MatchFilter { param($c) (@($c.AppIds) | Where-Object { $presetIds -contains $_ }).Count -gt 0 }.GetNewClosure() -Check $check }) } # Track the last selected checkbox for shift-click range selection $script:MainWindowLastSelectedCheckbox = $null # Guard flag: true while a load is in progress; prevents concurrent loads $script:IsLoadingApps = $false # Flag set when Default Mode is clicked before apps have finished loading $script:PendingDefaultMode = $false # Holds apps data preloaded before ShowDialog() so the first load skips the background job $script:PreloadedAppData = $null # Prevent app import until the apps list has finished initial population. if ($importConfigBtn) { $importConfigBtn.IsEnabled = $false } # Set script-level variable for GUI window reference $script:GuiWindow = $window # Guard flag to prevent preset handlers from firing when we update their state programmatically $script:UpdatingPresets = $false $script:UpdatingTweakPresets = $false # Sort state for the app table $script:SortColumn = 'Name' $script:SortAscending = $true function UpdateSortArrows { $ease = New-Object System.Windows.Media.Animation.CubicEase $ease.EasingMode = 'EaseOut' $arrows = @{ 'Name' = $sortArrowName 'Description' = $sortArrowDescription 'AppId' = $sortArrowAppId } foreach ($col in $arrows.Keys) { $tb = $arrows[$col] # Active column: full opacity, rotate to indicate direction (0 = up/asc, 180 = down/desc) # Inactive columns: dim, reset to 0 if ($col -eq $script:SortColumn) { $targetAngle = if ($script:SortAscending) { 0 } else { 180 } $tb.Opacity = 1.0 } else { $targetAngle = 0 $tb.Opacity = 0.3 } $anim = New-Object System.Windows.Media.Animation.DoubleAnimation $anim.To = $targetAngle $anim.Duration = [System.Windows.Duration]::new([System.TimeSpan]::FromMilliseconds(200)) $anim.EasingFunction = $ease $tb.RenderTransform.BeginAnimation([System.Windows.Media.RotateTransform]::AngleProperty, $anim) } } # Rebuilds $script:AppSearchMatches by scanning appsPanel children in their current order, # collecting any that are still highlighted. Preserves the active match across reorderings. function RebuildAppSearchIndex { param($activeMatch = $null) $newMatches = @() $newActiveIndex = -1 $i = 0 foreach ($child in $appsPanel.Children) { if ($child -is [System.Windows.Controls.CheckBox] -and $child.Background -ne [System.Windows.Media.Brushes]::Transparent) { $newMatches += $child if ($null -ne $activeMatch -and [System.Object]::ReferenceEquals($child, $activeMatch)) { $newActiveIndex = $i } $i++ } } $script:AppSearchMatches = $newMatches $script:AppSearchMatchIndex = if ($newActiveIndex -ge 0) { $newActiveIndex } elseif ($newMatches.Count -gt 0) { 0 } else { -1 } } function SortApps { $children = @($appsPanel.Children) $key = switch ($script:SortColumn) { 'Name' { { $_.AppName } } 'Description' { { $_.AppDescription } } 'AppId' { { $_.AppIdDisplay } } } $sorted = $children | Sort-Object $key -Descending:(-not $script:SortAscending) $appsPanel.Children.Clear() foreach ($checkbox in $sorted) { $appsPanel.Children.Add($checkbox) | Out-Null } UpdateSortArrows # Rebuild search match list in new sorted order so keyboard navigation stays correct if ($script:AppSearchMatches.Count -gt 0) { $activeMatch = if ($script:AppSearchMatchIndex -ge 0 -and $script:AppSearchMatchIndex -lt $script:AppSearchMatches.Count) { $script:AppSearchMatches[$script:AppSearchMatchIndex] } else { $null } RebuildAppSearchIndex -activeMatch $activeMatch } } function SetSortColumn($column) { if ($script:SortColumn -eq $column) { $script:SortAscending = -not $script:SortAscending } else { $script:SortColumn = $column $script:SortAscending = $true } SortApps } 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" if ($appRemovalScopeCombo -and $appRemovalScopeSection -and $appRemovalScopeDescription) { if ($selectedCount -gt 0) { if ($userSelectionCombo.SelectedIndex -ne 2) { $appRemovalScopeCombo.IsEnabled = $true } UpdateAppRemovalScopeDescription } else { $appRemovalScopeCombo.IsEnabled = $false $appRemovalScopeDescription.Text = "No apps selected for removal." } } } # Applies a preset by checking/unchecking apps that match the given filter # When -Exclusive is set, all apps are unchecked first so only matching apps end up selected function ApplyPresetToApps { param ( [scriptblock]$MatchFilter, [bool]$Check, [switch]$Exclusive ) foreach ($child in $appsPanel.Children) { if ($child -is [System.Windows.Controls.CheckBox]) { if ($Exclusive) { $child.IsChecked = (& $MatchFilter $child) } elseif (& $MatchFilter $child) { $child.IsChecked = $Check } } } UpdatePresetStates } # Update preset checkboxes to reflect checked/indeterminate/unchecked state function UpdatePresetStates { $script:UpdatingPresets = $true try { # Helper: count matching and checked apps, set checkbox state function SetPresetState($checkbox, [scriptblock]$MatchFilter) { $total = 0; $checked = 0 foreach ($child in $appsPanel.Children) { if ($child -is [System.Windows.Controls.CheckBox]) { if (& $MatchFilter $child) { $total++ if ($child.IsChecked) { $checked++ } } } } SetTriStatePresetCheckBoxState -CheckBox $checkbox -Total $total -Selected $checked } SetPresetState $presetDefaultApps { param($c) $c.SelectedByDefault -eq $true } foreach ($jsonCb in $script:JsonPresetCheckboxes) { $localIds = $jsonCb.PresetAppIds SetPresetState $jsonCb { param($c) (@($c.AppIds) | Where-Object { $localIds -contains $_ }).Count -gt 0 }.GetNewClosure() } # Last used preset: only update if it's visible (has saved apps) if ($presetLastUsed.Visibility -ne 'Collapsed' -and $script:SavedAppIds) { SetPresetState $presetLastUsed { param($c) (@($c.AppIds) | Where-Object { $script:SavedAppIds -contains $_ }).Count -gt 0 } } } finally { $script:UpdatingPresets = $false } } # 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 = @{} $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 $helpBtn.ToolTip = "Open wiki for more info on '$categoryName' tweaks" $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.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; Label = $feature.Label; Category = $categoryName } } } } # Build a feature-label lookup so GenerateOverview can resolve feature IDs without reloading JSON $script:FeatureLabelLookup = @{} foreach ($f in $featuresJson.Features) { $script:FeatureLabelLookup[$f.FeatureId] = $f.Action + ' ' + $f.Label } } # Helper function to load apps and populate the app list panel function script:LoadAppsWithList($listOfApps) { $script:MainWindowLastSelectedCheckbox = $null $loaderScriptPath = $script:LoadAppsDetailsScriptPath $appsFilePath = $script:AppsListFilePath $onlyInstalled = [bool]$onlyInstalledAppsBox.IsChecked # Use preloaded data if available; otherwise load in background job if (-not $onlyInstalled -and $script:PreloadedAppData) { $rawAppData = $script:PreloadedAppData $script:PreloadedAppData = $null } else { # Load apps details in a background job to keep the UI responsive $rawAppData = Invoke-NonBlocking -ScriptBlock { param($loaderScript, $appsListFilePath, $installedList, $onlyInstalled) $script:AppsListFilePath = $appsListFilePath . $loaderScript LoadAppsDetailsFromJson -OnlyInstalled:$onlyInstalled -InstalledList $installedList -InitialCheckedFromJson:$false } -ArgumentList $loaderScriptPath, $appsFilePath, $listOfApps, $onlyInstalled } $appsToAdd = @($rawAppData | Where-Object { $_ -and ($_.AppId -or $_.FriendlyName) } | Sort-Object -Property FriendlyName) $loadingAppsIndicator.Visibility = 'Collapsed' if ($appsToAdd.Count -eq 0) { $window.FindName('DeploymentApplyBtn').IsEnabled = $true if ($importConfigBtn) { $importConfigBtn.IsEnabled = $true } return } $brushSafe = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#4CAF50') $brushUnsafe = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#F44336') $brushDefault = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#FFC107') $brushSafe.Freeze(); $brushUnsafe.Freeze(); $brushDefault.Freeze() # Create WPF controls; pump the Dispatcher every batch so the spinner keeps animating. $batchSize = 20 for ($i = 0; $i -lt $appsToAdd.Count; $i++) { $app = $appsToAdd[$i] $checkbox = New-Object System.Windows.Controls.CheckBox $automationName = if ($app.FriendlyName) { $app.FriendlyName } elseif ($app.AppIdDisplay) { $app.AppIdDisplay } else { $null } if ($automationName) { $checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $automationName) } $checkbox.Tag = $app.AppIdDisplay $checkbox.IsChecked = $app.IsChecked $checkbox.Style = $window.Resources['AppsPanelCheckBoxStyle'] # Build table row: Recommendation dot | Name | Description | App ID $row = New-Object System.Windows.Controls.Grid $row.Style = $window.Resources['AppTableRowStyle'] $c0 = New-Object System.Windows.Controls.ColumnDefinition; $c0.Width = $window.Resources['AppTableDotColWidth'] $c1 = New-Object System.Windows.Controls.ColumnDefinition; $c1.Width = $window.Resources['AppTableNameColWidth'] $c2 = New-Object System.Windows.Controls.ColumnDefinition; $c2.Width = $window.Resources['AppTableDescColWidth'] $c3 = New-Object System.Windows.Controls.ColumnDefinition; $c3.Width = $window.Resources['AppTableIdColWidth'] $row.ColumnDefinitions.Add($c0); $row.ColumnDefinitions.Add($c1) $row.ColumnDefinitions.Add($c2); $row.ColumnDefinitions.Add($c3) $dot = New-Object System.Windows.Shapes.Ellipse $dot.Style = $window.Resources['AppRecommendationDotStyle'] $dot.Fill = switch ($app.Recommendation) { 'safe' { $brushSafe } 'unsafe' { $brushUnsafe } default { $brushDefault } } $dot.ToolTip = switch ($app.Recommendation) { 'safe' { '[Recommended] Safe to remove for most users' } 'unsafe' { '[Not Recommended] Only remove if you know what you are doing' } default { "[Optional] Remove if you don't need this app" } } [System.Windows.Controls.Grid]::SetColumn($dot, 0) $tbName = New-Object System.Windows.Controls.TextBlock $tbName.Text = $app.FriendlyName $tbName.Style = $window.Resources['AppNameTextStyle'] [System.Windows.Controls.Grid]::SetColumn($tbName, 1) $tbDesc = New-Object System.Windows.Controls.TextBlock $tbDesc.Text = $app.Description $tbDesc.Style = $window.Resources['AppDescTextStyle'] $tbDesc.ToolTip = $app.Description [System.Windows.Controls.Grid]::SetColumn($tbDesc, 2) $tbId = New-Object System.Windows.Controls.TextBlock $tbId.Text = $app.AppIdDisplay $tbId.Style = $window.Resources["AppIdTextStyle"] $tbId.ToolTip = $app.AppIdDisplay [System.Windows.Controls.Grid]::SetColumn($tbId, 3) $row.Children.Add($dot) | Out-Null $row.Children.Add($tbName) | Out-Null $row.Children.Add($tbDesc) | Out-Null $row.Children.Add($tbId) | Out-Null $checkbox.Content = $row Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppName' -Value $app.FriendlyName Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppDescription' -Value $app.Description Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'SelectedByDefault' -Value $app.SelectedByDefault Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIds' -Value @($app.AppId) Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'AppIdDisplay' -Value $app.AppIdDisplay $checkbox.Add_Checked({ UpdateAppSelectionStatus }) $checkbox.Add_Unchecked({ UpdateAppSelectionStatus }) AttachShiftClickBehavior -checkbox $checkbox -appsPanel $appsPanel ` -lastSelectedCheckboxRef ([ref]$script:MainWindowLastSelectedCheckbox) ` -updateStatusCallback { UpdateAppSelectionStatus } $appsPanel.Children.Add($checkbox) | Out-Null if (($i + 1) % $batchSize -eq 0) { DoEvents } } SortApps # If Default Mode was clicked while apps were still loading, apply defaults now if ($script:PendingDefaultMode) { $script:PendingDefaultMode = $false ApplyPresetToApps -MatchFilter { param($c) $c.SelectedByDefault -eq $true } -Exclusive } UpdateAppSelectionStatus # Re-enable Apply button now that the full, correctly-checked app list is ready $window.FindName('DeploymentApplyBtn').IsEnabled = $true if ($importConfigBtn) { $importConfigBtn.IsEnabled = $true } } # Loads apps into the UI function LoadAppsIntoMainUI { # Prevent concurrent loads if ($script:IsLoadingApps) { return } $script:IsLoadingApps = $true if ($importConfigBtn) { $importConfigBtn.IsEnabled = $false } # Show loading indicator and clear existing apps $loadingAppsIndicator.Visibility = 'Visible' $appsPanel.Children.Clear() # Disable Apply button while apps are loading so it can't be clicked with a partial list $window.FindName('DeploymentApplyBtn').IsEnabled = $false # Update navigation buttons to disable Next/Previous UpdateNavigationButtons # Force a render so the loading indicator is visible, then schedule the # actual loading at Background priority so this call returns immediately. # This is critical when called from Add_Loaded: the window must finish # its initialization before we start a nested message pump via DoEvents. $window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Render, [action]{}) $window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ try { $listOfApps = "" if ($onlyInstalledAppsBox.IsChecked -and ($script:WingetInstalled -eq $true)) { $listOfApps = GetInstalledAppsViaWinget -TimeOut 10 -NonBlocking if ($null -eq $listOfApps) { Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null $onlyInstalledAppsBox.IsChecked = $false } } LoadAppsWithList $listOfApps } finally { $script:IsLoadingApps = $false } }) | Out-Null } # Event handlers for app selection $onlyInstalledAppsBox.Add_Checked({ LoadAppsIntoMainUI }) $onlyInstalledAppsBox.Add_Unchecked({ LoadAppsIntoMainUI }) # Animate arrow when popup opens/closes, and lazily update preset states $presetsPopup.Add_Opened({ UpdatePresetStates AnimateDropdownArrow -arrow $presetsArrow -angle 180 }) $presetsPopup.Add_Closed({ AnimateDropdownArrow -arrow $presetsArrow -angle 0 $presetsBtn.IsChecked = $false }) $tweaksPresetsPopup.Add_Opened({ UpdateTweakPresetStates AnimateDropdownArrow -arrow $tweaksPresetsArrow -angle 180 }) $tweaksPresetsPopup.Add_Closed({ AnimateDropdownArrow -arrow $tweaksPresetsArrow -angle 0 $tweaksPresetsBtn.IsChecked = $false }) # Close popup when clicking anywhere outside the popup or the presets button. $window.Add_PreviewMouseDown({ $isAppPopupOpen = $presetsPopup.IsOpen $isTweaksPopupOpen = $tweaksPresetsPopup.IsOpen if (-not $isAppPopupOpen -and -not $isTweaksPopupOpen) { return } if ($isAppPopupOpen -and $null -ne $presetsPopup.Child -and $presetsPopup.Child.IsMouseOver) { return } if ($isTweaksPopupOpen -and $null -ne $tweaksPresetsPopup.Child -and $tweaksPresetsPopup.Child.IsMouseOver) { return } $src = $_.OriginalSource -as [System.Windows.DependencyObject] if ($null -ne $src) { $inAppBtn = $presetsBtn.IsAncestorOf($src) -or [System.Object]::ReferenceEquals($presetsBtn, $src) $inTweaksBtn = $tweaksPresetsBtn.IsAncestorOf($src) -or [System.Object]::ReferenceEquals($tweaksPresetsBtn, $src) if ($isAppPopupOpen -and -not $inAppBtn) { $presetsPopup.IsOpen = $false } if ($isTweaksPopupOpen -and -not $inTweaksBtn) { $tweaksPresetsPopup.IsOpen = $false } } }) # Close the preset menu when the main window loses focus (e.g., user switches to another app). $window.Add_Deactivated({ if ($presetsPopup.IsOpen) { $presetsPopup.IsOpen = $false } if ($tweaksPresetsPopup.IsOpen) { $tweaksPresetsPopup.IsOpen = $false } }) # Toggle popup on button click $presetsBtn.Add_Click({ $presetsPopup.IsOpen = -not $presetsPopup.IsOpen $presetsBtn.IsChecked = $presetsPopup.IsOpen }) $tweaksPresetsBtn.Add_Click({ $tweaksPresetsPopup.IsOpen = -not $tweaksPresetsPopup.IsOpen $tweaksPresetsBtn.IsChecked = $tweaksPresetsPopup.IsOpen }) foreach ($presetCheckBox in @( $presetDefaultApps, $presetLastUsed, $presetDefaultTweaksBtn, $presetLastUsedTweaksBtn, $presetPrivacyTweaksBtn, $presetAITweaksBtn )) { AttachTriStateClickBehavior -checkBox $presetCheckBox } # Preset: Default selection $presetDefaultApps.Add_Click({ if ($script:UpdatingPresets) { return } $check = NormalizeCheckboxState -checkBox $this ApplyPresetToApps -MatchFilter { param($c) $c.SelectedByDefault -eq $true } -Check $check }) # Clear selection button + reset all preset checkboxes $clearAppSelectionBtn.Add_Click({ ApplyPresetToApps -MatchFilter { param($c) $true } -Check $false }) # Column header sort handlers $headerNameBtn.Add_MouseLeftButtonUp({ SetSortColumn 'Name' }) $headerDescriptionBtn.Add_MouseLeftButtonUp({ SetSortColumn 'Description' }) $headerAppIdBtn.Add_MouseLeftButtonUp({ SetSortColumn 'AppId' }) # 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') # Track current search matches and active index for Enter-key navigation $script:AppSearchMatches = @() $script:AppSearchMatchIndex = -1 $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 } } $script:AppSearchMatches = @() $script:AppSearchMatchIndex = -1 if ([string]::IsNullOrWhiteSpace($searchText)) { return } # Find and highlight all matching apps $highlightBrush = $window.Resources["SearchHighlightColor"] $activeHighlightBrush = $window.Resources["SearchHighlightActiveColor"] foreach ($child in $appsPanel.Children) { if ($child -is [System.Windows.Controls.CheckBox] -and $child.Visibility -eq 'Visible') { $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)) { $child.Background = $highlightBrush $script:AppSearchMatches += $child } } } # Scroll to first match and mark it as active if ($script:AppSearchMatches.Count -gt 0) { $script:AppSearchMatchIndex = 0 $script:AppSearchMatches[0].Background = $activeHighlightBrush $scrollViewer = FindParentScrollViewer -element $appsPanel if ($scrollViewer) { ScrollToItemIfNotVisible -scrollViewer $scrollViewer -item $script:AppSearchMatches[0] -container $appsPanel } } }) $appSearchBox.Add_KeyDown({ param($sourceControl, $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 } }) # 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') function UpdateTweaksResponsiveColumns { 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 = @() } # 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) } }) # 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 $highlightBrush = $window.Resources["SearchHighlightColor"] $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($sourceControl, $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 - 1 # Navigation button visibility if ($currentIndex -eq $homeIndex) { $nextBtn.Visibility = 'Collapsed' $previousBtn.Visibility = 'Collapsed' } elseif ($currentIndex -eq $overviewIndex) { $nextBtn.Visibility = 'Collapsed' $previousBtn.Visibility = 'Visible' } else { $nextBtn.Visibility = 'Visible' $previousBtn.Visibility = 'Visible' } # Update progress indicators # Tab indices: 0=Home, 1=App Removal, 2=Tweaks, 3=Deployment Settings $progressIndicator1 = $window.FindName('ProgressIndicator1') # App Removal $progressIndicator2 = $window.FindName('ProgressIndicator2') # Tweaks $progressIndicator3 = $window.FindName('ProgressIndicator3') # Deployment Settings $bottomNavGrid = $window.FindName('BottomNavGrid') # Hide bottom navigation on home page if ($currentIndex -eq 0) { $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 = $window.Resources['ProgressActiveColor'] } else { $progressIndicator1.Fill = $window.Resources['ProgressInactiveColor'] } # Indicator 2 (Tweaks) - tab index 2 if ($currentIndex -ge 2) { $progressIndicator2.Fill = $window.Resources['ProgressActiveColor'] } else { $progressIndicator2.Fill = $window.Resources['ProgressInactiveColor'] } # Indicator 3 (Deployment Settings) - tab index 3 if ($currentIndex -ge 3) { $progressIndicator3.Fill = $window.Resources['ProgressActiveColor'] } else { $progressIndicator3.Fill = $window.Resources['ProgressInactiveColor'] } } # 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 = $window.Resources['ValidationErrorColor'] $successBrush = $window.Resources['ValidationSuccessColor'] if ($username.Length -eq 0) { $usernameValidationMessage.Text = "Please enter a username" $usernameValidationMessage.Foreground = $errorBrush return $false } if ($username -eq $env:USERNAME) { $usernameValidationMessage.Text = "Cannot enter your own username, use 'Current User' option instead" $usernameValidationMessage.Foreground = $errorBrush return $false } $userExists = CheckIfUserExists -Username $username if ($userExists) { if (TestIfUserIsLoggedIn -Username $username) { $usernameValidationMessage.Text = "User '$username' is currently logged in. Please sign out that user first." $usernameValidationMessage.Foreground = $errorBrush return $false } $usernameValidationMessage.Text = "User found: $username" $usernameValidationMessage.Foreground = $successBrush return $true } $usernameValidationMessage.Text = "User not found, please enter a valid username" $usernameValidationMessage.Foreground = $errorBrush return $false } function GenerateOverview { $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 application(s)" } UpdateAppSelectionStatus # 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) { $label = $script:FeatureLabelLookup[$fid] if ($label) { $changesList += $label } } } elseif ($mapping.Type -eq 'feature') { $label = $script:FeatureLabelLookup[$mapping.FeatureId] if (-not $label) { $label = $mapping.Action + ' ' + $mapping.Label } $changesList += $label } } } } return $changesList } function ShowChangesOverview { $changesList = GenerateOverview if ($changesList.Count -eq 0) { Show-MessageBox -Message 'No changes have been selected.' -Title 'Selected Changes' -Button 'OK' -Icon 'Information' return } $message = ($changesList | ForEach-Object { "$([char]0x2022) $_" }) -join "`n" Show-MessageBox -Message $message -Title 'Selected Changes' -Button 'OK' -Icon 'None' -Width 600 } $previousBtn.Add_Click({ Hide-Bubble -Immediate 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 Home Default Mode button - apply defaults and navigate directly to overview $homeDefaultModeBtn = $window.FindName('HomeDefaultModeBtn') $homeDefaultModeBtn.Add_Click({ # Load and apply default settings $defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0" if ($defaultsJson) { ApplySettingsToUiControls -window $window -settingsJson $defaultsJson -uiControlMappings $script:UiControlMappings } # Deselect all apps, then select default apps (defer if apps are still loading in the background) if ($script:IsLoadingApps) { $script:PendingDefaultMode = $true } else { ApplyPresetToApps -MatchFilter { param($c) $c.SelectedByDefault -eq $true } -Exclusive } # Navigate directly to the Deployment Settings tab $tabControl.SelectedIndex = 3 UpdateNavigationButtons # Show contextual hint bubble for the Review Changes link $window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{ Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here' }) | Out-Null }) # Handle Review Changes link button $reviewChangesBtn = $window.FindName('ReviewChangesBtn') $reviewChangesBtn.Add_Click({ Hide-Bubble ShowChangesOverview }) # Handle Apply Changes button - validates and immediately starts applying changes $deploymentApplyBtn = $window.FindName('DeploymentApplyBtn') $deploymentApplyBtn.Add_Click({ if (-not (ValidateOtherUsername)) { $validationMessage = if (-not [string]::IsNullOrWhiteSpace($usernameValidationMessage.Text)) { $usernameValidationMessage.Text } else { "Please enter a valid username." } Show-MessageBox -Message $validationMessage -Title "Invalid Username" -Button 'OK' -Icon 'Warning' | Out-Null return } Hide-Bubble -Immediate # 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.AppIds) } } $selectedApps = @($selectedApps | Where-Object { $_ } | Select-Object -Unique) 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) { 0 { Write-Host "Selected user mode: current user ($(GetUserName))" } 1 { Write-Host "Selected user mode: $($otherUsernameTextBox.Text.Trim())" AddParameter User ($otherUsernameTextBox.Text.Trim()) } 2 { Write-Host "Selected user mode: default user profile (Sysprep)" AddParameter Sysprep } } SaveSettings # Check if user wants to restart explorer $restartExplorerCheckBox = $window.FindName('RestartExplorerCheckBox') $shouldRestartExplorer = $restartExplorerCheckBox -and $restartExplorerCheckBox.IsChecked # Show the apply changes window Show-ApplyModal -Owner $window -RestartExplorer $shouldRestartExplorer # Close the main window after the apply dialog closes $window.Close() }) # Initialize UI elements on window load $window.Add_Loaded({ BuildDynamicTweaks UpdateTweaksResponsiveColumns RefreshTweakPresetSources -defaultSettingsJson $defaultsJson -lastUsedSettingsJson $lastUsedSettingsJson RegisterTweakPresetControlStateHandlers UpdateTweakPresetStates 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 }) function BuildTweakPresetControlMap { param($settingsJson) $presetMap = @{} if (-not $settingsJson -or -not $settingsJson.Settings -or -not $script:UiControlMappings) { return $presetMap } # FeatureId -> control metadata, similar to ApplySettingsToUiControls lookup. $featureIdIndex = @{} foreach ($controlName in $script:UiControlMappings.Keys) { $control = $window.FindName($controlName) if (-not $control -or $control.Visibility -ne 'Visible') { continue } $mapping = $script:UiControlMappings[$controlName] if ($mapping.Type -eq 'group') { $i = 1 foreach ($val in $mapping.Values) { foreach ($fid in $val.FeatureIds) { $featureIdIndex[$fid] = @{ ControlName = $controlName; Control = $control; MappingType = 'group'; Index = $i } } $i++ } } elseif ($mapping.Type -eq 'feature') { $featureIdIndex[$mapping.FeatureId] = @{ ControlName = $controlName; Control = $control; MappingType = 'feature' } } } foreach ($setting in $settingsJson.Settings) { if ($setting.Value -ne $true) { continue } if ($setting.Name -eq 'CreateRestorePoint') { continue } $entry = $featureIdIndex[$setting.Name] if (-not $entry) { continue } if ($presetMap.ContainsKey($entry.ControlName)) { continue } $controlType = if ($entry.Control -is [System.Windows.Controls.CheckBox]) { 'CheckBox' } else { 'ComboBox' } $desiredValue = switch ($entry.MappingType) { 'group' { $entry.Index } default { if ($controlType -eq 'CheckBox') { $true } else { 1 } } } $presetMap[$entry.ControlName] = @{ Control = $entry.Control; ControlType = $controlType; DesiredValue = $desiredValue } } return $presetMap } function BuildCategoryTweakPresetMap { param([string]$Category) $presetMap = @{} if (-not $script:UiControlMappings) { return $presetMap } foreach ($controlName in $script:UiControlMappings.Keys) { $mapping = $script:UiControlMappings[$controlName] if ($mapping.Category -ne $Category) { continue } $control = $window.FindName($controlName) if (-not $control -or $control.Visibility -ne 'Visible') { continue } $controlType = if ($control -is [System.Windows.Controls.CheckBox]) { 'CheckBox' } else { 'ComboBox' } $desiredValue = if ($controlType -eq 'CheckBox') { $true } else { 1 } $presetMap[$controlName] = @{ Control = $control; ControlType = $controlType; DesiredValue = $desiredValue } } return $presetMap } function GetSavedAppIdsFromSettingsJson { param($settingsJson) if (-not $settingsJson -or -not $settingsJson.Settings) { return $null } $appsValue = $null foreach ($setting in $settingsJson.Settings) { if ($setting.Name -eq 'Apps' -and $setting.Value) { $appsValue = $setting.Value break } } if (-not $appsValue) { return $null } $savedAppIds = @() if ($appsValue -is [string]) { $savedAppIds = $appsValue.Split(',') } elseif ($appsValue -is [array]) { $savedAppIds = $appsValue } $savedAppIds = $savedAppIds | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } if ($savedAppIds.Count -eq 0) { return $null } return $savedAppIds } function ApplyTweakPresetMap { param( [hashtable]$PresetMap, [bool]$Check ) if (-not $PresetMap) { $PresetMap = @{} } $wasUpdatingTweakPresets = [bool]$script:UpdatingTweakPresets $script:UpdatingTweakPresets = $true try { foreach ($target in $PresetMap.Values) { $control = $target.Control if (-not $control) { continue } if ($target.ControlType -eq 'CheckBox') { $control.IsChecked = $Check } elseif ($target.ControlType -eq 'ComboBox') { $desiredIndex = [int]$target.DesiredValue if ($Check) { $control.SelectedIndex = $desiredIndex } elseif ($control.SelectedIndex -eq $desiredIndex) { $control.SelectedIndex = 0 } } } } finally { $script:UpdatingTweakPresets = $wasUpdatingTweakPresets } if (-not $wasUpdatingTweakPresets) { UpdateTweakPresetStates } } function SetTweakPresetState { param( [System.Windows.Controls.CheckBox]$PresetCheckBox, [hashtable]$PresetMap ) if (-not $PresetCheckBox) { return } if (-not $PresetMap) { $PresetMap = @{} } $total = $PresetMap.Count $selected = 0 foreach ($target in $PresetMap.Values) { $control = $target.Control if (-not $control) { continue } if ($target.ControlType -eq 'CheckBox' -and $control.IsChecked -eq $true) { $selected++ } elseif ($target.ControlType -eq 'ComboBox' -and $control.SelectedIndex -eq [int]$target.DesiredValue) { $selected++ } } SetTriStatePresetCheckBoxState -CheckBox $PresetCheckBox -Total $total -Selected $selected } function UpdateTweakPresetStates { $script:UpdatingTweakPresets = $true try { SetTweakPresetState -PresetCheckBox $presetDefaultTweaksBtn -PresetMap $script:DefaultTweakPresetMap if ($presetLastUsedTweaksBtn -and $presetLastUsedTweaksBtn.Visibility -ne 'Collapsed') { SetTweakPresetState -PresetCheckBox $presetLastUsedTweaksBtn -PresetMap $script:LastUsedTweakPresetMap } SetTweakPresetState -PresetCheckBox $presetPrivacyTweaksBtn -PresetMap $script:PrivacyTweakPresetMap SetTweakPresetState -PresetCheckBox $presetAITweaksBtn -PresetMap $script:AITweakPresetMap } finally { $script:UpdatingTweakPresets = $false } } function RegisterTweakPresetControlStateHandlers { 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]) { $control.Add_Checked({ if (-not $script:UpdatingTweakPresets) { UpdateTweakPresetStates } }) $control.Add_Unchecked({ if (-not $script:UpdatingTweakPresets) { UpdateTweakPresetStates } }) } elseif ($control -is [System.Windows.Controls.ComboBox]) { $control.Add_SelectionChanged({ if (-not $script:UpdatingTweakPresets) { UpdateTweakPresetStates } }) } } } function RefreshTweakPresetSources { param( $defaultSettingsJson, $lastUsedSettingsJson ) $script:DefaultTweakPresetMap = BuildTweakPresetControlMap -settingsJson $defaultSettingsJson $script:LastUsedTweakPresetMap = BuildTweakPresetControlMap -settingsJson $lastUsedSettingsJson $script:PrivacyTweakPresetMap = BuildCategoryTweakPresetMap -Category 'Privacy & Suggested Content' $script:AITweakPresetMap = BuildCategoryTweakPresetMap -Category 'AI' if ($presetLastUsedTweaksBtn) { $presetLastUsedTweaksBtn.Visibility = if ($script:LastUsedTweakPresetMap.Count -gt 0) { 'Visible' } else { 'Collapsed' } } } $lastUsedSettingsJson = LoadJsonFile -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" -optionalFile $defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0" $script:DefaultTweakPresetMap = @{} $script:LastUsedTweakPresetMap = @{} $script:PrivacyTweakPresetMap = @{} $script:AITweakPresetMap = @{} $script:SavedAppIds = GetSavedAppIdsFromSettingsJson -settingsJson $lastUsedSettingsJson if ($presetDefaultTweaksBtn) { $presetDefaultTweaksBtn.Add_Click({ if ($script:UpdatingTweakPresets) { return } $check = NormalizeCheckboxState -checkBox $this ApplyTweakPresetMap -PresetMap $script:DefaultTweakPresetMap -Check $check }) } if ($presetLastUsedTweaksBtn) { $presetLastUsedTweaksBtn.Add_Click({ if ($script:UpdatingTweakPresets) { return } $check = NormalizeCheckboxState -checkBox $this ApplyTweakPresetMap -PresetMap $script:LastUsedTweakPresetMap -Check $check }) } if ($presetPrivacyTweaksBtn) { $presetPrivacyTweaksBtn.Add_Click({ if ($script:UpdatingTweakPresets) { return } $check = NormalizeCheckboxState -checkBox $this ApplyTweakPresetMap -PresetMap $script:PrivacyTweakPresetMap -Check $check }) } if ($presetAITweaksBtn) { $presetAITweaksBtn.Add_Click({ if ($script:UpdatingTweakPresets) { return } $check = NormalizeCheckboxState -checkBox $this ApplyTweakPresetMap -PresetMap $script:AITweakPresetMap -Check $check }) } # Hide Last used tweak preset by default; it is shown after dynamic controls are built and mappings are resolved. if ($presetLastUsedTweaksBtn) { $presetLastUsedTweaksBtn.Visibility = 'Collapsed' } # Preset: Last used selection (wired to PresetLastUsed checkbox) if ($script:SavedAppIds) { $presetLastUsed.Add_Click({ if ($script:UpdatingPresets) { return } $check = NormalizeCheckboxState -checkBox $this ApplyPresetToApps -MatchFilter { param($c) (@($c.AppIds) | Where-Object { $script:SavedAppIds -contains $_ }).Count -gt 0 } -Check $check }) } else { $script:SavedAppIds = $null $presetLastUsed.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 } } } UpdateTweakPresetStates }) # Preload app data to speed up loading when user navigates to App Removal tab try { $script:PreloadedAppData = LoadAppsDetailsFromJson -OnlyInstalled:$false -InstalledList '' -InitialCheckedFromJson:$false } catch { Write-Warning "Failed to preload apps list: $_" } # Show the window $frame = [System.Windows.Threading.DispatcherFrame]::new() $window.Add_Closed({ $frame.Continue = $false }) $window.Show() | Out-Null [System.Windows.Threading.Dispatcher]::PushFrame($frame) return $null }