Win11Debloat/Scripts/GUI/MainWindow-AppSelection.ps1

587 lines
24 KiB
PowerShell

# 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."
}
"Target user only" {
$AppRemovalScopeDescription.Text = "Apps will only be removed for the specified target user."
}
}
}
}
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 Set-PresetState($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')
Set-PresetState $presetDefaultApps { param($c) $c.SelectedByDefault -eq $true }
foreach ($jsonCb in $script:JsonPresetCheckboxes) {
$localIds = $jsonCb.PresetAppIds
Set-PresetState $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) {
Set-PresetState $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
}
<#
.SYNOPSIS
Loads application details and adds their interactive checkboxes to the main window.
.PARAMETER Window
The main application window and resource owner.
.PARAMETER AppsPanel
The panel populated with application checkboxes.
.PARAMETER OnlyInstalledAppsBox
The filter control that determines whether only installed apps are loaded.
.PARAMETER LoadingAppsIndicator
The loading indicator shown while application details are prepared.
.PARAMETER ImportConfigBtn
The optional import control re-enabled after loading completes.
.PARAMETER ListOfApps
An optional pre-fetched list of installed WinGet applications.
#>
function Add-AppsToMainWindow {
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,
[object[]]$ListOfApps
)
$script:MainWindowLastSelectedCheckbox = $null
$loaderScriptPath = $script:LoadAppsDetailsScriptPath
$helperScriptPath = $script:TestAppInWingetListScriptPath
$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.
# The helper is dot-sourced inside the job because the runspace
# does not inherit the parent scope's dot-sourced functions.
$rawAppData = Invoke-NonBlocking -ScriptBlock {
param($loaderScript, $helperScript, $appsListFilePath, $installedList, $onlyInstalled)
$script:AppsListFilePath = $appsListFilePath
. $helperScript
. $loaderScript
Import-AppDetailsFromJson -OnlyInstalled:$onlyInstalled -InstalledList $installedList -InitialCheckedFromJson:$false
} -ArgumentList $loaderScriptPath, $helperScriptPath, $appsFilePath, $ListOfApps, $onlyInstalled
}
$appsToAdd = @($rawAppData | Where-Object { $_ -and ($_.AppId -or $_.FriendlyName) } | Sort-Object -Property FriendlyName)
$LoadingAppsIndicator.Visibility = 'Collapsed'
if ($appsToAdd.Count -eq 0) {
$OnlyInstalledAppsBox.IsHitTestVisible = $true
$Window.FindName('DeploymentApplyBtn').IsEnabled = $true
if ($ImportConfigBtn) {
$ImportConfigBtn.IsEnabled = $true
}
return
}
$brushSafe = $Window.Resources['AppRecommendationSafeColor']
$brushDefault = $Window.Resources['AppRecommendationOptionalColor']
$brushUnsafe = $Window.Resources['AppRecommendationUnsafeColor']
# 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] Can be safely removed 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')
})
Attach-ShiftClickBehavior -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) { Invoke-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 controls now that the full, correctly-checked app list is ready
$OnlyInstalledAppsBox.IsHitTestVisible = $true
$Window.FindName('DeploymentApplyBtn').IsEnabled = $true
if ($ImportConfigBtn) {
$ImportConfigBtn.IsEnabled = $true
}
}
<#
.SYNOPSIS
Starts asynchronous loading of application checkboxes for the main window.
.PARAMETER Window
The main application window.
.PARAMETER AppsPanel
The panel that receives application checkboxes.
.PARAMETER OnlyInstalledAppsBox
The installed-applications filter control.
.PARAMETER LoadingAppsIndicator
The loading indicator shown until loading completes.
.PARAMETER ImportConfigBtn
The optional import control disabled while loading is in progress.
#>
function Initialize-MainWindowApps {
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 controls while apps are loading so they can't be interacted with mid-load
$Window.FindName('DeploymentApplyBtn').IsEnabled = $false
$OnlyInstalledAppsBox.IsHitTestVisible = $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 Invoke-DoEvents.
$Window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Render, [action] {})
$Window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action] {
try {
$listOfApps = $null
if ($OnlyInstalledAppsBox.IsChecked -and ($script:WingetInstalled -eq $true)) {
Write-Host "Retrieving installed apps via winget..."
$listOfApps = Get-WingetInstalledApps -TimeOut 20 -NonBlocking
if ($null -eq $listOfApps) {
Write-Warning "WinGet returned no data (command timed out or failed)"
Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
$OnlyInstalledAppsBox.IsChecked = $false
}
}
Add-AppsToMainWindow -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'
$OnlyInstalledAppsBox.IsHitTestVisible = $true
$Window.FindName('DeploymentApplyBtn').IsEnabled = $true
if ($ImportConfigBtn) { $ImportConfigBtn.IsEnabled = $true }
}
finally {
$script:IsLoadingApps = $false
}
}) | Out-Null
}