mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-06-10 02:26:29 +00:00
Refactor & Clean up Show-MainWindow
This commit is contained in:
@@ -547,14 +547,14 @@
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,8,0,0">
|
||||
<Button x:Name="HomeDefaultModeBtn" Width="227" Height="50" Style="{DynamicResource PrimaryButtonStyle}" Margin="0,0,12,0" AutomationProperties.Name="Default Mode">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="Default Mode" ToolTip="Quickly select the recommended settings" FontWeight="SemiBold" VerticalAlignment="Center" FontSize="17" Margin="0,0,0,2"/>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" VerticalAlignment="Center" Margin="0,0,8,-1"/>
|
||||
<TextBlock Text="Default Mode" ToolTip="Quickly select the recommended settings" FontWeight="SemiBold" VerticalAlignment="Center" FontSize="17" Margin="0,0,0,1"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="HomeStartBtn" Width="227" Height="50" Style="{DynamicResource SecondaryButtonStyle}" AutomationProperties.Name="Custom Setup">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="Custom Setup" ToolTip="Manually select your preferred settings" FontWeight="SemiBold" VerticalAlignment="Center" FontSize="17" Margin="0,0,0,2"/>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,8,-1"/>
|
||||
<TextBlock Text="Custom Setup" ToolTip="Manually select your preferred settings" FontWeight="SemiBold" VerticalAlignment="Center" FontSize="17" Margin="0,0,0,1"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
@@ -140,9 +140,16 @@ function Test-StoreSearchSuggestionsDisabled {
|
||||
$everyoneSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0')
|
||||
|
||||
foreach ($accessRule in @($acl.Access)) {
|
||||
if ($accessRule.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Deny -and
|
||||
(($accessRule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne 0) -and
|
||||
(try { $accessRule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $everyoneSid } catch { $false })) {
|
||||
$isDenyFullControl = $accessRule.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Deny -and
|
||||
(($accessRule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne 0)
|
||||
if (-not $isDenyFullControl) { continue }
|
||||
|
||||
$isEveryone = $false
|
||||
try {
|
||||
$isEveryone = $accessRule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $everyoneSid
|
||||
} catch { }
|
||||
|
||||
if ($isEveryone) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
536
Scripts/GUI/MainWindow-AppSelection.ps1
Normal file
536
Scripts/GUI/MainWindow-AppSelection.ps1
Normal file
@@ -0,0 +1,536 @@
|
||||
# MainWindow-AppSelection.ps1
|
||||
# App-selection panel functions: tri-state helpers, sorting, search/highlight, app loading, preset management, and removal scope.
|
||||
|
||||
function Add-TriStateClickBehavior {
|
||||
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 ConvertTo-NormalizedCheckboxState {
|
||||
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 Set-TriStatePresetCheckBoxState {
|
||||
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 Update-SortArrows {
|
||||
param(
|
||||
[System.Windows.Controls.TextBlock]$SortArrowName,
|
||||
[System.Windows.Controls.TextBlock]$SortArrowDescription,
|
||||
[System.Windows.Controls.TextBlock]$SortArrowAppId
|
||||
)
|
||||
|
||||
$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)
|
||||
}
|
||||
}
|
||||
|
||||
function Update-AppsPanelRebuildSearchIndex {
|
||||
param(
|
||||
[System.Windows.Controls.Panel]$AppsPanel,
|
||||
$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 Update-AppsPanelSort {
|
||||
param(
|
||||
[System.Windows.Controls.Panel]$AppsPanel,
|
||||
[System.Windows.Controls.TextBlock]$SortArrowName,
|
||||
[System.Windows.Controls.TextBlock]$SortArrowDescription,
|
||||
[System.Windows.Controls.TextBlock]$SortArrowAppId
|
||||
)
|
||||
|
||||
$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
|
||||
}
|
||||
Update-SortArrows -SortArrowName $SortArrowName -SortArrowDescription $SortArrowDescription -SortArrowAppId $SortArrowAppId
|
||||
|
||||
# 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 }
|
||||
Update-AppsPanelRebuildSearchIndex -AppsPanel $AppsPanel -ActiveMatch $activeMatch
|
||||
}
|
||||
}
|
||||
|
||||
function Update-AppSelectionStatus {
|
||||
param(
|
||||
[System.Windows.Controls.Panel]$AppsPanel,
|
||||
[System.Windows.Controls.TextBlock]$AppSelectionStatus,
|
||||
[System.Windows.Controls.ComboBox]$AppRemovalScopeCombo,
|
||||
[System.Windows.Controls.Border]$AppRemovalScopeSection,
|
||||
[System.Windows.Controls.TextBlock]$AppRemovalScopeDescription,
|
||||
[System.Windows.Controls.ComboBox]$UserSelectionCombo
|
||||
)
|
||||
|
||||
$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) {
|
||||
$AppRemovalScopeSection.Visibility = 'Visible'
|
||||
if ($UserSelectionCombo.SelectedIndex -ne 2) {
|
||||
$AppRemovalScopeCombo.IsEnabled = $true
|
||||
}
|
||||
Update-AppRemovalScopeDescription -AppRemovalScopeCombo $AppRemovalScopeCombo -AppRemovalScopeDescription $AppRemovalScopeDescription
|
||||
}
|
||||
else {
|
||||
$AppRemovalScopeSection.Visibility = 'Collapsed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Update-AppRemovalScopeDescription {
|
||||
param(
|
||||
[System.Windows.Controls.ComboBox]$AppRemovalScopeCombo,
|
||||
[System.Windows.Controls.TextBlock]$AppRemovalScopeDescription
|
||||
)
|
||||
|
||||
$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. Existing and new users will not be affected."
|
||||
}
|
||||
"Target user only" {
|
||||
$AppRemovalScopeDescription.Text = "Apps will only be removed for the specified target user. Existing and new users will not be affected."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-AppPreset {
|
||||
param(
|
||||
[System.Windows.Controls.Panel]$AppsPanel,
|
||||
[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
|
||||
}
|
||||
}
|
||||
}
|
||||
Update-AppPresetStates -AppsPanel $AppsPanel
|
||||
}
|
||||
|
||||
function Update-AppPresetStates {
|
||||
param([System.Windows.Controls.Panel]$AppsPanel)
|
||||
|
||||
$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++ }
|
||||
}
|
||||
}
|
||||
}
|
||||
Set-TriStatePresetCheckBoxState -CheckBox $CheckBox -Total $total -Selected $checked
|
||||
}
|
||||
|
||||
# Find preset checkboxes via window
|
||||
$window = $script:MainWindow
|
||||
$presetDefaultApps = $window.FindName('PresetDefaultApps')
|
||||
$presetLastUsed = $window.FindName('PresetLastUsed')
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
function Scroll-ToItemIfNotVisible {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
function Find-ParentScrollViewer {
|
||||
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
|
||||
}
|
||||
|
||||
function Load-AppsWithList {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
[System.Windows.Controls.Panel]$AppsPanel,
|
||||
[System.Windows.Controls.CheckBox]$OnlyInstalledAppsBox,
|
||||
[System.Windows.Controls.Border]$LoadingAppsIndicator,
|
||||
[System.Windows.Controls.MenuItem]$ImportConfigBtn,
|
||||
[string]$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({
|
||||
$w = $script:MainWindow
|
||||
Update-AppSelectionStatus -AppsPanel $w.FindName('AppSelectionPanel') `
|
||||
-AppSelectionStatus $w.FindName('AppSelectionStatus') `
|
||||
-AppRemovalScopeCombo $w.FindName('AppRemovalScopeCombo') `
|
||||
-AppRemovalScopeSection $w.FindName('AppRemovalScopeSection') `
|
||||
-AppRemovalScopeDescription $w.FindName('AppRemovalScopeDescription') `
|
||||
-UserSelectionCombo $w.FindName('UserSelectionCombo')
|
||||
})
|
||||
$checkbox.Add_Unchecked({
|
||||
$w = $script:MainWindow
|
||||
Update-AppSelectionStatus -AppsPanel $w.FindName('AppSelectionPanel') `
|
||||
-AppSelectionStatus $w.FindName('AppSelectionStatus') `
|
||||
-AppRemovalScopeCombo $w.FindName('AppRemovalScopeCombo') `
|
||||
-AppRemovalScopeSection $w.FindName('AppRemovalScopeSection') `
|
||||
-AppRemovalScopeDescription $w.FindName('AppRemovalScopeDescription') `
|
||||
-UserSelectionCombo $w.FindName('UserSelectionCombo')
|
||||
})
|
||||
AttachShiftClickBehavior -checkbox $checkbox -appsPanel $AppsPanel `
|
||||
-lastSelectedCheckboxRef ([ref]$script:MainWindowLastSelectedCheckbox) `
|
||||
-updateStatusCallback {
|
||||
$w = $script:MainWindow
|
||||
Update-AppSelectionStatus -AppsPanel $w.FindName('AppSelectionPanel') `
|
||||
-AppSelectionStatus $w.FindName('AppSelectionStatus') `
|
||||
-AppRemovalScopeCombo $w.FindName('AppRemovalScopeCombo') `
|
||||
-AppRemovalScopeSection $w.FindName('AppRemovalScopeSection') `
|
||||
-AppRemovalScopeDescription $w.FindName('AppRemovalScopeDescription') `
|
||||
-UserSelectionCombo $w.FindName('UserSelectionCombo')
|
||||
}
|
||||
|
||||
$AppsPanel.Children.Add($checkbox) | Out-Null
|
||||
|
||||
if (($i + 1) % $batchSize -eq 0) { DoEvents }
|
||||
}
|
||||
|
||||
$sortArrowName = $Window.FindName('SortArrowName')
|
||||
$sortArrowDescription = $Window.FindName('SortArrowDescription')
|
||||
$sortArrowAppId = $Window.FindName('SortArrowAppId')
|
||||
Update-AppsPanelSort -AppsPanel $AppsPanel -SortArrowName $sortArrowName -SortArrowDescription $sortArrowDescription -SortArrowAppId $sortArrowAppId
|
||||
|
||||
# If Default Mode was clicked while apps were still loading, apply defaults now
|
||||
if ($script:PendingDefaultMode) {
|
||||
$script:PendingDefaultMode = $false
|
||||
Invoke-AppPreset -AppsPanel $AppsPanel -MatchFilter { param($c) $c.SelectedByDefault -eq $true } -Exclusive
|
||||
}
|
||||
|
||||
$appSelectionStatusText = $Window.FindName('AppSelectionStatus')
|
||||
$appRemovalScopeCombo = $Window.FindName('AppRemovalScopeCombo')
|
||||
$appRemovalScopeSection = $Window.FindName('AppRemovalScopeSection')
|
||||
$appRemovalScopeDescription = $Window.FindName('AppRemovalScopeDescription')
|
||||
$userSelectionCombo = $Window.FindName('UserSelectionCombo')
|
||||
Update-AppSelectionStatus -AppsPanel $AppsPanel -AppSelectionStatus $appSelectionStatusText `
|
||||
-AppRemovalScopeCombo $appRemovalScopeCombo -AppRemovalScopeSection $appRemovalScopeSection `
|
||||
-AppRemovalScopeDescription $appRemovalScopeDescription -UserSelectionCombo $userSelectionCombo
|
||||
|
||||
# Re-enable Apply button now that the full, correctly-checked app list is ready
|
||||
$Window.FindName('DeploymentApplyBtn').IsEnabled = $true
|
||||
if ($ImportConfigBtn) {
|
||||
$ImportConfigBtn.IsEnabled = $true
|
||||
}
|
||||
}
|
||||
|
||||
function Load-AppsIntoMainUI {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
[System.Windows.Controls.Panel]$AppsPanel,
|
||||
[System.Windows.Controls.CheckBox]$OnlyInstalledAppsBox,
|
||||
[System.Windows.Controls.Border]$LoadingAppsIndicator,
|
||||
[System.Windows.Controls.MenuItem]$ImportConfigBtn
|
||||
)
|
||||
|
||||
# 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
|
||||
Update-NavigationButtons -Window $Window -TabControl $Window.FindName('MainTabControl')
|
||||
|
||||
# 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
|
||||
}
|
||||
}
|
||||
|
||||
Load-AppsWithList -Window $Window -AppsPanel $AppsPanel -OnlyInstalledAppsBox $OnlyInstalledAppsBox `
|
||||
-LoadingAppsIndicator $LoadingAppsIndicator -ImportConfigBtn $ImportConfigBtn -ListOfApps $listOfApps
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to load apps list: $($_.Exception.Message)"
|
||||
$LoadingAppsIndicator.Visibility = 'Collapsed'
|
||||
$Window.FindName('DeploymentApplyBtn').IsEnabled = $true
|
||||
if ($ImportConfigBtn) { $ImportConfigBtn.IsEnabled = $true }
|
||||
}
|
||||
finally {
|
||||
$script:IsLoadingApps = $false
|
||||
}
|
||||
}) | Out-Null
|
||||
}
|
||||
487
Scripts/GUI/MainWindow-Deployment.ps1
Normal file
487
Scripts/GUI/MainWindow-Deployment.ps1
Normal file
@@ -0,0 +1,487 @@
|
||||
# MainWindow-Deployment.ps1
|
||||
# Overview generation, pending tweak actions, feature labels, tweak preset maps, apply logic, user mode state, user selection, and validation.
|
||||
|
||||
function Get-FeatureLabel {
|
||||
param(
|
||||
[string]$FeatureId,
|
||||
$FallbackLabel = $null
|
||||
)
|
||||
|
||||
$label = $script:FeatureLabelLookup[$FeatureId]
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$label)) {
|
||||
return [string]$label
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$FallbackLabel)) {
|
||||
return [string]$FallbackLabel
|
||||
}
|
||||
|
||||
return [string]$FeatureId
|
||||
}
|
||||
|
||||
function Get-UndoFeatureLabel {
|
||||
param(
|
||||
[string]$FeatureId,
|
||||
$FallbackLabel = $null
|
||||
)
|
||||
|
||||
$undoLabel = $script:UndoFeatureLabelLookup[$FeatureId]
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$undoLabel)) {
|
||||
return [string]$undoLabel
|
||||
}
|
||||
|
||||
# Fall back to the regular label (prefixed for undo context)
|
||||
$label = Get-FeatureLabel -FeatureId $FeatureId -FallbackLabel $FallbackLabel
|
||||
return [string]$label
|
||||
}
|
||||
|
||||
function Get-PendingTweakActions {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
[bool]$ShowAppliedTweaksMode
|
||||
)
|
||||
|
||||
$actions = New-Object System.Collections.Generic.List[object]
|
||||
|
||||
if (-not $script:UiControlMappings) {
|
||||
return @($actions.ToArray())
|
||||
}
|
||||
|
||||
foreach ($mappingKey in $script:UiControlMappings.Keys) {
|
||||
$control = $Window.FindName($mappingKey)
|
||||
if (-not $control) { continue }
|
||||
$mapping = $script:UiControlMappings[$mappingKey]
|
||||
|
||||
if ($control -is [System.Windows.Controls.CheckBox] -and $mapping.Type -eq 'feature') {
|
||||
$wasApplied = $false
|
||||
if ($ShowAppliedTweaksMode -and $null -ne $control.PSObject.Properties['SystemState']) {
|
||||
$wasApplied = [bool]$control.SystemState
|
||||
}
|
||||
elseif ($null -ne $control.PSObject.Properties['InitialState']) {
|
||||
$wasApplied = [bool]$control.InitialState
|
||||
}
|
||||
elseif ($null -ne $control.PSObject.Properties['SystemState']) {
|
||||
$wasApplied = [bool]$control.SystemState
|
||||
}
|
||||
$isNowChecked = $control.IsChecked -eq $true
|
||||
|
||||
if (-not $wasApplied -and $isNowChecked) {
|
||||
$actions.Add([PSCustomObject]@{
|
||||
Action = 'Apply'
|
||||
FeatureId = [string]$mapping.FeatureId
|
||||
Label = (Get-FeatureLabel -FeatureId $mapping.FeatureId -FallbackLabel $mapping.Label)
|
||||
})
|
||||
}
|
||||
elseif ($wasApplied -and -not $isNowChecked) {
|
||||
$actions.Add([PSCustomObject]@{
|
||||
Action = 'Undo'
|
||||
FeatureId = [string]$mapping.FeatureId
|
||||
Label = (Get-FeatureLabel -FeatureId $mapping.FeatureId -FallbackLabel $mapping.Label)
|
||||
})
|
||||
}
|
||||
}
|
||||
elseif ($control -is [System.Windows.Controls.ComboBox] -and $mapping.Type -eq 'group') {
|
||||
$wasIndex = 0
|
||||
if ($ShowAppliedTweaksMode -and $null -ne $control.PSObject.Properties['SystemIndex']) {
|
||||
$wasIndex = [int]$control.SystemIndex
|
||||
}
|
||||
elseif ($null -ne $control.PSObject.Properties['InitialIndex']) {
|
||||
$wasIndex = [int]$control.InitialIndex
|
||||
}
|
||||
elseif ($null -ne $control.PSObject.Properties['SystemIndex']) {
|
||||
$wasIndex = [int]$control.SystemIndex
|
||||
}
|
||||
$isNowIndex = $control.SelectedIndex
|
||||
|
||||
if ($wasIndex -eq $isNowIndex) { continue }
|
||||
|
||||
if ($isNowIndex -gt 0 -and $isNowIndex -le $mapping.Values.Count) {
|
||||
$selectedValue = $mapping.Values[$isNowIndex - 1]
|
||||
foreach ($fid in $selectedValue.FeatureIds) {
|
||||
$actions.Add([PSCustomObject]@{
|
||||
Action = 'Apply'
|
||||
FeatureId = [string]$fid
|
||||
Label = (Get-FeatureLabel -FeatureId $fid)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return @($actions.ToArray())
|
||||
}
|
||||
|
||||
function New-Overview {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
[System.Windows.Controls.StackPanel]$AppsPanel,
|
||||
$ShowCurrentlyAppliedTweaksCheckBox
|
||||
)
|
||||
|
||||
$changesList = @()
|
||||
$showAppliedTweaksMode = ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true)
|
||||
|
||||
# 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)"
|
||||
}
|
||||
|
||||
foreach ($tweakAction in @(Get-PendingTweakActions -Window $Window -ShowAppliedTweaksMode:$showAppliedTweaksMode)) {
|
||||
if ($tweakAction.Action -eq 'Undo') {
|
||||
$changesList += "Undo: $($tweakAction.Label)"
|
||||
}
|
||||
else {
|
||||
$changesList += $tweakAction.Label
|
||||
}
|
||||
}
|
||||
|
||||
return $changesList
|
||||
}
|
||||
|
||||
function Invoke-ShowChangesOverview {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
[System.Windows.Controls.StackPanel]$AppsPanel,
|
||||
$ShowCurrentlyAppliedTweaksCheckBox
|
||||
)
|
||||
|
||||
$changesList = New-Overview -Window $Window -AppsPanel $AppsPanel -ShowCurrentlyAppliedTweaksCheckBox $ShowCurrentlyAppliedTweaksCheckBox
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function Build-TweakPresetControlMap {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
$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 Build-CategoryTweakPresetMap {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
[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 Get-SavedAppIdsFromSettingsJson {
|
||||
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 Invoke-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) {
|
||||
Update-TweakPresetStates -Window $script:MainWindow
|
||||
}
|
||||
}
|
||||
|
||||
function Set-TweakPresetCheckBoxState {
|
||||
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++
|
||||
}
|
||||
}
|
||||
|
||||
Set-TriStatePresetCheckBoxState -CheckBox $PresetCheckBox -Total $total -Selected $selected
|
||||
}
|
||||
|
||||
function Update-TweakPresetStates {
|
||||
param([System.Windows.Window]$Window)
|
||||
|
||||
$script:UpdatingTweakPresets = $true
|
||||
try {
|
||||
$presetDefaultTweaksBtn = $Window.FindName('PresetDefaultTweaksBtn')
|
||||
$presetLastUsedTweaksBtn = $Window.FindName('PresetLastUsedTweaksBtn')
|
||||
$presetPrivacyTweaksBtn = $Window.FindName('PresetPrivacyTweaksBtn')
|
||||
$presetAITweaksBtn = $Window.FindName('PresetAITweaksBtn')
|
||||
|
||||
Set-TweakPresetCheckBoxState -PresetCheckBox $presetDefaultTweaksBtn -PresetMap $script:DefaultTweakPresetMap
|
||||
if ($presetLastUsedTweaksBtn -and $presetLastUsedTweaksBtn.Visibility -ne 'Collapsed') {
|
||||
Set-TweakPresetCheckBoxState -PresetCheckBox $presetLastUsedTweaksBtn -PresetMap $script:LastUsedTweakPresetMap
|
||||
}
|
||||
Set-TweakPresetCheckBoxState -PresetCheckBox $presetPrivacyTweaksBtn -PresetMap $script:PrivacyTweakPresetMap
|
||||
Set-TweakPresetCheckBoxState -PresetCheckBox $presetAITweaksBtn -PresetMap $script:AITweakPresetMap
|
||||
}
|
||||
finally {
|
||||
$script:UpdatingTweakPresets = $false
|
||||
}
|
||||
}
|
||||
|
||||
function Register-TweakPresetControlStateHandlers {
|
||||
param([System.Windows.Window]$Window)
|
||||
|
||||
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) { Update-TweakPresetStates -Window $script:MainWindow } })
|
||||
$control.Add_Unchecked({ if (-not $script:UpdatingTweakPresets) { Update-TweakPresetStates -Window $script:MainWindow } })
|
||||
}
|
||||
elseif ($control -is [System.Windows.Controls.ComboBox]) {
|
||||
$control.Add_SelectionChanged({ if (-not $script:UpdatingTweakPresets) { Update-TweakPresetStates -Window $script:MainWindow } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Initialize-TweakPresetSources {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
$DefaultSettingsJson,
|
||||
$LastUsedSettingsJson
|
||||
)
|
||||
|
||||
$script:DefaultTweakPresetMap = Build-TweakPresetControlMap -Window $Window -SettingsJson $DefaultSettingsJson
|
||||
$script:LastUsedTweakPresetMap = Build-TweakPresetControlMap -Window $Window -SettingsJson $LastUsedSettingsJson
|
||||
$script:PrivacyTweakPresetMap = Build-CategoryTweakPresetMap -Window $Window -Category 'Privacy & Suggested Content'
|
||||
$script:AITweakPresetMap = Build-CategoryTweakPresetMap -Window $Window -Category 'AI'
|
||||
|
||||
$presetLastUsedTweaksBtn = $Window.FindName('PresetLastUsedTweaksBtn')
|
||||
if ($presetLastUsedTweaksBtn) {
|
||||
$presetLastUsedTweaksBtn.Visibility = if ($script:LastUsedTweakPresetMap.Count -gt 0) { 'Visible' } else { 'Collapsed' }
|
||||
}
|
||||
}
|
||||
|
||||
function Update-AppliedTweaksUserModeState {
|
||||
param(
|
||||
[System.Windows.Controls.CheckBox]$ShowCurrentlyAppliedTweaksCheckBox,
|
||||
[System.Windows.Controls.ComboBox]$UserSelectionCombo
|
||||
)
|
||||
|
||||
# Show/hide detect applied tweaks checkbox based on user mode
|
||||
if ($ShowCurrentlyAppliedTweaksCheckBox) {
|
||||
if ($UserSelectionCombo.SelectedIndex -eq 0) {
|
||||
$ShowCurrentlyAppliedTweaksCheckBox.Visibility = 'Visible'
|
||||
}
|
||||
else {
|
||||
$ShowCurrentlyAppliedTweaksCheckBox.Visibility = 'Collapsed'
|
||||
}
|
||||
}
|
||||
|
||||
# Enable/disable user mode combo based on params only (not checkbox)
|
||||
if ($script:Params.ContainsKey('Sysprep') -or $script:Params.ContainsKey('User')) {
|
||||
$UserSelectionCombo.IsEnabled = $false
|
||||
}
|
||||
else {
|
||||
$UserSelectionCombo.IsEnabled = $true
|
||||
}
|
||||
}
|
||||
|
||||
function Update-UserSelectionDescription {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
[System.Windows.Controls.ComboBox]$UserSelectionCombo,
|
||||
[System.Windows.Controls.TextBox]$OtherUsernameTextBox,
|
||||
[System.Windows.Controls.TextBlock]$UserSelectionDescription
|
||||
)
|
||||
|
||||
switch ($UserSelectionCombo.SelectedIndex) {
|
||||
0 {
|
||||
$currentUserName = GetUserName
|
||||
if ([string]::IsNullOrWhiteSpace($currentUserName)) {
|
||||
$UserSelectionDescription.Text = "The currently logged-in user profile"
|
||||
}
|
||||
else {
|
||||
$UserSelectionDescription.Text = "The currently logged-in user profile: $currentUserName"
|
||||
}
|
||||
}
|
||||
1 {
|
||||
$targetUserName = $OtherUsernameTextBox.Text.Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($targetUserName)) {
|
||||
$UserSelectionDescription.Text = "A different user profile on this system"
|
||||
}
|
||||
else {
|
||||
$UserSelectionDescription.Text = "A different user profile on this system: $targetUserName"
|
||||
}
|
||||
}
|
||||
default {
|
||||
$UserSelectionDescription.Text = "The default user template, affecting all new users created after this point. Useful for Sysprep deployment."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Test-OtherUsername {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
[System.Windows.Controls.ComboBox]$UserSelectionCombo,
|
||||
[System.Windows.Controls.TextBox]$OtherUsernameTextBox,
|
||||
[System.Windows.Controls.TextBlock]$UsernameValidationMessage
|
||||
)
|
||||
|
||||
# Only validate if "Other User" is selected
|
||||
if ($UserSelectionCombo.SelectedIndex -ne 1) {
|
||||
return $true
|
||||
}
|
||||
|
||||
$errorBrush = $Window.Resources['ValidationErrorColor']
|
||||
$successBrush = $Window.Resources['ValidationSuccessColor']
|
||||
$validationResult = Test-TargetUserName -UserName $OtherUsernameTextBox.Text
|
||||
|
||||
$UsernameValidationMessage.Text = $validationResult.Message
|
||||
if ($validationResult.IsValid) {
|
||||
$UsernameValidationMessage.Foreground = $successBrush
|
||||
return $true
|
||||
}
|
||||
|
||||
$UsernameValidationMessage.Foreground = $errorBrush
|
||||
return $false
|
||||
}
|
||||
72
Scripts/GUI/MainWindow-Navigation.ps1
Normal file
72
Scripts/GUI/MainWindow-Navigation.ps1
Normal file
@@ -0,0 +1,72 @@
|
||||
# MainWindow-Navigation.ps1
|
||||
# Wizard navigation helpers: tab navigation buttons and progress indicators.
|
||||
|
||||
function Update-NavigationButtons {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
[System.Windows.Controls.TabControl]$TabControl
|
||||
)
|
||||
|
||||
$currentIndex = $TabControl.SelectedIndex
|
||||
$totalTabs = $TabControl.Items.Count
|
||||
|
||||
$previousBtn = $Window.FindName('PreviousBtn')
|
||||
$nextBtn = $Window.FindName('NextBtn')
|
||||
|
||||
$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']
|
||||
}
|
||||
}
|
||||
511
Scripts/GUI/MainWindow-TweaksBuilder.ps1
Normal file
511
Scripts/GUI/MainWindow-TweaksBuilder.ps1
Normal file
@@ -0,0 +1,511 @@
|
||||
# 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
|
||||
$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.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
|
||||
}
|
||||
215
Scripts/GUI/MainWindow-WindowChrome.ps1
Normal file
215
Scripts/GUI/MainWindow-WindowChrome.ps1
Normal file
@@ -0,0 +1,215 @@
|
||||
# MainWindow-WindowChrome.ps1
|
||||
# Window sizing, DPI-aware coordinate conversion, maximized-window taskbar-constraint helpers, and UI animations.
|
||||
|
||||
function Register-MaximizedWindowHelper {
|
||||
if (-not ([System.Management.Automation.PSTypeName]'Win11Debloat.MaximizedWindowHelper').Type) {
|
||||
Add-Type -Namespace Win11Debloat -Name MaximizedWindowHelper `
|
||||
-ReferencedAssemblies 'PresentationFramework','System.Windows.Forms','System.Drawing' `
|
||||
-MemberDefinition @'
|
||||
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
|
||||
private struct MINMAXINFO {
|
||||
public POINT ptReserved, ptMaxSize, ptMaxPosition, ptMinTrackSize, ptMaxTrackSize;
|
||||
}
|
||||
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
|
||||
private struct POINT { public int x, y; }
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||
private static extern System.IntPtr MonitorFromWindow(System.IntPtr hwnd, uint dwFlags);
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
|
||||
private static extern bool GetMonitorInfo(System.IntPtr hMonitor, ref MONITORINFO lpmi);
|
||||
|
||||
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
|
||||
private struct RECT {
|
||||
public int Left, Top, Right, Bottom;
|
||||
}
|
||||
|
||||
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
|
||||
private struct MONITORINFO {
|
||||
public int cbSize;
|
||||
public RECT rcMonitor;
|
||||
public RECT rcWork;
|
||||
public uint dwFlags;
|
||||
}
|
||||
|
||||
public static System.IntPtr WmGetMinMaxInfoHook(
|
||||
System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled) {
|
||||
if (msg == 0x0024) { // WM_GETMINMAXINFO
|
||||
var mmi = (MINMAXINFO)System.Runtime.InteropServices.Marshal.PtrToStructure(
|
||||
lParam, typeof(MINMAXINFO));
|
||||
|
||||
const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
|
||||
var monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
|
||||
var monitorInfo = new MONITORINFO();
|
||||
monitorInfo.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(MONITORINFO));
|
||||
|
||||
if (monitor != System.IntPtr.Zero && GetMonitorInfo(monitor, ref monitorInfo)) {
|
||||
mmi.ptMaxPosition.x = monitorInfo.rcWork.Left - monitorInfo.rcMonitor.Left;
|
||||
mmi.ptMaxPosition.y = monitorInfo.rcWork.Top - monitorInfo.rcMonitor.Top;
|
||||
mmi.ptMaxSize.x = monitorInfo.rcWork.Right - monitorInfo.rcWork.Left;
|
||||
mmi.ptMaxSize.y = monitorInfo.rcWork.Bottom - monitorInfo.rcWork.Top;
|
||||
}
|
||||
else {
|
||||
var screen = System.Windows.Forms.Screen.FromHandle(hwnd);
|
||||
var wa = screen.WorkingArea;
|
||||
var bounds = screen.Bounds;
|
||||
mmi.ptMaxPosition.x = wa.Left - bounds.Left;
|
||||
mmi.ptMaxPosition.y = wa.Top - bounds.Top;
|
||||
mmi.ptMaxSize.x = wa.Width;
|
||||
mmi.ptMaxSize.y = wa.Height;
|
||||
}
|
||||
|
||||
System.Runtime.InteropServices.Marshal.StructureToPtr(mmi, lParam, true);
|
||||
}
|
||||
return System.IntPtr.Zero;
|
||||
}
|
||||
'@
|
||||
}
|
||||
}
|
||||
|
||||
# Convert screen-pixel coordinates to WPF device-independent pixels (DIP)
|
||||
function ConvertTo-ScreenPointToDip {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
[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))
|
||||
}
|
||||
|
||||
# Convert screen-pixel size to WPF device-independent size
|
||||
function ConvertTo-ScreenPixelsToDip {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
[double]$Width,
|
||||
[double]$Height
|
||||
)
|
||||
|
||||
$topLeft = ConvertTo-ScreenPointToDip -Window $Window -X 0 -Y 0
|
||||
$bottomRight = ConvertTo-ScreenPointToDip -Window $Window -X $Width -Y $Height
|
||||
return [System.Windows.Size]::new($bottomRight.X - $topLeft.X, $bottomRight.Y - $topLeft.Y)
|
||||
}
|
||||
|
||||
# Get the screen that currently contains the window
|
||||
function Get-WindowScreen {
|
||||
param([System.Windows.Window]$Window)
|
||||
|
||||
$hwnd = (New-Object System.Windows.Interop.WindowInteropHelper($Window)).Handle
|
||||
if ($hwnd -eq [IntPtr]::Zero) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return [System.Windows.Forms.Screen]::FromHandle($hwnd)
|
||||
}
|
||||
|
||||
# Update window border/corner chrome when transitioning between Normal and Maximized
|
||||
function Update-MainWindowChrome {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
[System.Windows.Controls.Border]$MainBorder,
|
||||
[System.Windows.Controls.Border]$TitleBarBackground,
|
||||
[object]$NormalWindowShadow
|
||||
)
|
||||
|
||||
$windowStateMaximized = [System.Windows.WindowState]::Maximized
|
||||
$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) }
|
||||
}
|
||||
}
|
||||
|
||||
# Set the initial window size and center on screen (normal state only)
|
||||
function Set-MainWindowInitialSize {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
[double]$InitialNormalMaxWidth = 1400.0
|
||||
)
|
||||
|
||||
if ($Window.WindowState -ne [System.Windows.WindowState]::Normal) {
|
||||
return
|
||||
}
|
||||
|
||||
$screen = Get-WindowScreen -Window $Window
|
||||
if ($null -eq $screen) {
|
||||
return
|
||||
}
|
||||
|
||||
$workingAreaTopLeftDip = ConvertTo-ScreenPointToDip -Window $Window -X $screen.WorkingArea.Left -Y $screen.WorkingArea.Top
|
||||
$workingAreaDip = ConvertTo-ScreenPixelsToDip -Window $Window -Width $screen.WorkingArea.Width -Height $screen.WorkingArea.Height
|
||||
$Window.Width = [Math]::Min($InitialNormalMaxWidth, $workingAreaDip.Width)
|
||||
$Window.Left = $workingAreaTopLeftDip.X + (($workingAreaDip.Width - $Window.Width) / 2)
|
||||
}
|
||||
|
||||
# Update the content grid margin to constrain max content width
|
||||
function Update-MainWindowContentMargin {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
[System.Windows.Controls.Grid]$ContentGrid,
|
||||
[double]$MaxContentWidth = 1600.0
|
||||
)
|
||||
|
||||
$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)
|
||||
}
|
||||
}
|
||||
|
||||
# Vertically center the home content panel
|
||||
function Update-MainWindowHomeContentPosition {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
[System.Windows.Controls.Panel]$HomeContentPanel
|
||||
)
|
||||
|
||||
if ($HomeContentPanel) {
|
||||
$availableHeight = $Window.ActualHeight - 32 # subtract title bar height
|
||||
if ($availableHeight -gt 0) {
|
||||
$topMargin = ($availableHeight - 584) * 0.5
|
||||
$HomeContentPanel.Margin = [System.Windows.Thickness]::new(0, $topMargin, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Start-DropdownArrowAnimation {
|
||||
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)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -331,6 +331,11 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
||||
. "$PSScriptRoot/Scripts/GUI/Show-RestoreBackupWindow.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/RestoreBackupDialogFeatureLists.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/Show-RestoreBackupDialog.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/MainWindow-WindowChrome.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/MainWindow-AppSelection.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/MainWindow-TweaksBuilder.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/MainWindow-Navigation.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/MainWindow-Deployment.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/Show-MainWindow.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/Show-AboutDialog.ps1"
|
||||
. "$PSScriptRoot/Scripts/GUI/Show-Bubble.ps1"
|
||||
|
||||
Reference in New Issue
Block a user