mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-08-09 09:31:15 +10:00
Speed up runspace and tab initialization (#4793)
* Add startup performance tracing * Lazy initialize non-default tabs * Render install app entries incrementally * Defer and cache toggle status checks * Clean up runspace invocation ownership * Defer GUI runspace pool startup * Defer status taskbar asset rendering * Record final speed verification * Fix deferred install render timer callback * Add dispatcher smoke coverage for install rendering * Replace install render timer with dispatcher callbacks * Gate performance tracing behind compile switch * Clean up analyzer warnings in speed changes * Speed up runspace and tab initialization
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
function Close-WinUtilRunspacePool {
|
||||
if ($null -eq $sync -or -not $sync.ContainsKey("runspace") -or $null -eq $sync.runspace) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if ($sync.runspace.RunspacePoolStateInfo.State -notin @(
|
||||
[System.Management.Automation.Runspaces.RunspacePoolState]::Closed,
|
||||
[System.Management.Automation.Runspaces.RunspacePoolState]::Closing,
|
||||
[System.Management.Automation.Runspaces.RunspacePoolState]::Broken
|
||||
)) {
|
||||
$sync.runspace.Close()
|
||||
}
|
||||
} finally {
|
||||
$sync.runspace.Dispose()
|
||||
$sync.Remove("runspace")
|
||||
}
|
||||
}
|
||||
@@ -2,29 +2,39 @@ Function Get-WinUtilToggleStatus ($ToggleSwitch) {
|
||||
|
||||
$ToggleSwitchReg = $sync.configs.tweaks.$ToggleSwitch.registry
|
||||
|
||||
if ($null -eq $sync.ToggleStatusCache) {
|
||||
$sync.ToggleStatusCache = @{}
|
||||
}
|
||||
|
||||
if ($sync.ToggleStatusCache.ContainsKey($ToggleSwitch)) {
|
||||
return [bool]$sync.ToggleStatusCache[$ToggleSwitch]
|
||||
}
|
||||
|
||||
if (-not (Get-PSDrive -Name HKU -ErrorAction SilentlyContinue)) {
|
||||
New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS | Out-Null
|
||||
}
|
||||
|
||||
foreach ($regentry in $ToggleSwitchReg) {
|
||||
|
||||
if (-not (Test-Path $regentry.Path)) {
|
||||
New-Item -Path $regentry.Path -Force | Out-Null
|
||||
if (Test-Path $regentry.Path) {
|
||||
$regstate = (Get-ItemProperty -Path $regentry.Path).$($regentry.Name)
|
||||
} else {
|
||||
$regstate = $null
|
||||
}
|
||||
|
||||
$regstate = (Get-ItemProperty -Path $regentry.Path).$($regentry.Name)
|
||||
|
||||
if ($null -eq $regstate) {
|
||||
switch ($regentry.DefaultState) {
|
||||
switch ([string]$regentry.DefaultState) {
|
||||
"true" { $regstate = $regentry.Value }
|
||||
"false" { $regstate = $regentry.OriginalValue }
|
||||
}
|
||||
}
|
||||
|
||||
if ($regstate -ne $regentry.Value) {
|
||||
$sync.ToggleStatusCache[$ToggleSwitch] = $false
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
$sync.ToggleStatusCache[$ToggleSwitch] = $true
|
||||
return $true
|
||||
}
|
||||
|
||||
@@ -13,11 +13,13 @@ function Initialize-InstallAppEntry {
|
||||
$appKey
|
||||
)
|
||||
|
||||
$app = $sync.configs.applicationsHashtable.$appKey
|
||||
|
||||
# Create the outer Border for the application type
|
||||
$border = New-Object Windows.Controls.Border
|
||||
$border.Style = $sync.Form.Resources.AppEntryBorderStyle
|
||||
$border.Tag = $appKey
|
||||
$border.ToolTip = $Apps.$appKey.description
|
||||
$border.ToolTip = $app.description
|
||||
$border.Add_MouseLeftButtonUp({
|
||||
$childCheckbox = ($this.Child | Where-Object {$_.Template.TargetType -eq [System.Windows.Controls.Checkbox]})[0]
|
||||
$childCheckBox.isChecked = -not $childCheckbox.IsChecked
|
||||
@@ -61,10 +63,10 @@ function Initialize-InstallAppEntry {
|
||||
# Create the TextBlock for the application name
|
||||
$appName = New-Object Windows.Controls.TextBlock
|
||||
$appName.Style = $sync.Form.Resources.AppEntryNameStyle
|
||||
$appName.Text = $Apps.$appKey.content
|
||||
$appName.Text = $app.content
|
||||
|
||||
# Add FOSS label after the name if FOSS
|
||||
if ($Apps.$appKey.foss -eq $true) {
|
||||
if ($app.foss -eq $true) {
|
||||
$fossRun = [System.Windows.Documents.Run]::new(" $([char]0x25CF)")
|
||||
$fossRun.Foreground = [Windows.Media.SolidColorBrush]::new([Windows.Media.Color]::FromRgb(110, 255, 114))
|
||||
$fossRun.FontSize = 11.5
|
||||
@@ -74,10 +76,13 @@ function Initialize-InstallAppEntry {
|
||||
$checkBox.Content = $appName
|
||||
|
||||
# Add accessibility properties to make the elements screen reader friendly
|
||||
$checkBox.SetValue([Windows.Automation.AutomationProperties]::NameProperty, $Apps.$appKey.content)
|
||||
$border.SetValue([Windows.Automation.AutomationProperties]::NameProperty, $Apps.$appKey.content)
|
||||
$checkBox.SetValue([Windows.Automation.AutomationProperties]::NameProperty, $app.content)
|
||||
$border.SetValue([Windows.Automation.AutomationProperties]::NameProperty, $app.content)
|
||||
|
||||
$border.Child = $checkBox
|
||||
if ($sync.selectedApps -contains $appKey) {
|
||||
$checkBox.IsChecked = $true
|
||||
}
|
||||
# Add the border to the corresponding Category
|
||||
$TargetElement.Children.Add($border) | Out-Null
|
||||
return $checkbox
|
||||
|
||||
@@ -16,7 +16,7 @@ function Initialize-InstallCategoryAppList {
|
||||
$Apps
|
||||
)
|
||||
|
||||
# Pre-group apps by category
|
||||
# Pre-group apps by category before creating WPF controls.
|
||||
$appsByCategory = @{}
|
||||
foreach ($appKey in $Apps.Keys) {
|
||||
$category = $Apps.$appKey.Category
|
||||
@@ -25,6 +25,8 @@ function Initialize-InstallCategoryAppList {
|
||||
}
|
||||
$appsByCategory[$category] += $appKey
|
||||
}
|
||||
$sync.InstallAppRenderQueue = [System.Collections.Queue]::new()
|
||||
|
||||
foreach ($category in $($appsByCategory.Keys | Sort-Object)) {
|
||||
# Create a container for category label + apps
|
||||
$categoryContainer = New-Object Windows.Controls.StackPanel
|
||||
@@ -52,10 +54,10 @@ function Initialize-InstallCategoryAppList {
|
||||
|
||||
# Add click handler to toggle category visibility
|
||||
$toggleButton.Add_MouseLeftButtonUp({
|
||||
param($sender, $e)
|
||||
param($categoryToggle)
|
||||
|
||||
# Find the parent StackPanel (categoryContainer)
|
||||
$categoryContainer = $sender.Parent
|
||||
$categoryContainer = $categoryToggle.Parent
|
||||
if ($categoryContainer -and $categoryContainer.Children.Count -ge 2) {
|
||||
# The WrapPanel is the second child
|
||||
$wrapPanel = $categoryContainer.Children[1]
|
||||
@@ -64,11 +66,11 @@ function Initialize-InstallCategoryAppList {
|
||||
if ($wrapPanel.Visibility -eq [Windows.Visibility]::Visible) {
|
||||
$wrapPanel.Visibility = [Windows.Visibility]::Collapsed
|
||||
# Change - to +
|
||||
$sender.Content = $sender.Content -replace "^- ", "+ "
|
||||
$categoryToggle.Content = $categoryToggle.Content -replace "^- ", "+ "
|
||||
} else {
|
||||
$wrapPanel.Visibility = [Windows.Visibility]::Visible
|
||||
# Change + to -
|
||||
$sender.Content = $sender.Content -replace "^\+ ", "- "
|
||||
$categoryToggle.Content = $categoryToggle.Content -replace "^\+ ", "- "
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -89,9 +91,12 @@ function Initialize-InstallCategoryAppList {
|
||||
# Add the entire category container to the target element
|
||||
$null = $TargetElement.Items.Add($categoryContainer)
|
||||
|
||||
# Add apps to the wrap panel
|
||||
$appsByCategory[$category] | Sort-Object | ForEach-Object {
|
||||
$sync.$_ = $(Initialize-InstallAppEntry -TargetElement $wrapPanel -AppKey $_)
|
||||
}
|
||||
$sync.InstallAppRenderQueue.Enqueue([pscustomobject]@{
|
||||
Category = $category
|
||||
TargetElement = $wrapPanel
|
||||
AppKeys = @($appsByCategory[$category] | Sort-Object)
|
||||
})
|
||||
}
|
||||
|
||||
Start-WinUtilInstallAppRendering
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
function Initialize-WinUtilRunspacePool {
|
||||
if ($sync.runspace -and $sync.runspace.RunspacePoolStateInfo.State -eq [System.Management.Automation.Runspaces.RunspacePoolState]::Opened) {
|
||||
return $sync.runspace
|
||||
}
|
||||
|
||||
if ($sync.runspace) {
|
||||
Close-WinUtilRunspacePool
|
||||
}
|
||||
|
||||
# Set the maximum number of threads for the RunspacePool to the number of threads on the machine.
|
||||
$maxthreads = [Math]::Max([int]$env:NUMBER_OF_PROCESSORS, 1)
|
||||
|
||||
# Create a new session state for parsing variables into our runspace.
|
||||
$hashVars = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'sync', $sync, $null
|
||||
$offlineVar = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList 'PARAM_OFFLINE', $PARAM_OFFLINE, $null
|
||||
$initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
|
||||
|
||||
$initialSessionState.Variables.Add($hashVars)
|
||||
$initialSessionState.Variables.Add($offlineVar)
|
||||
|
||||
# Get every WinUtil/WPF function and add it to the session state.
|
||||
$functions = Get-ChildItem function:\ | Where-Object { $_.Name -imatch 'winutil|WPF' }
|
||||
foreach ($function in $functions) {
|
||||
$functionDefinition = Get-Content function:\$($function.Name)
|
||||
$functionEntry = New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $function.Name, $functionDefinition
|
||||
$initialSessionState.Commands.Add($functionEntry)
|
||||
}
|
||||
|
||||
$sync.runspace = [runspacefactory]::CreateRunspacePool(
|
||||
1, # Minimum thread count
|
||||
$maxthreads, # Maximum thread count
|
||||
$initialSessionState, # Initial session state
|
||||
$Host # Machine to create runspaces on
|
||||
)
|
||||
|
||||
$sync.runspace.Open()
|
||||
Write-WinUtilPerformanceCheckpoint -Name "Runspace pool initialized"
|
||||
return $sync.runspace
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
function Initialize-WinUtilTabContent {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$TabName
|
||||
)
|
||||
|
||||
if ($null -eq $sync.InitializedTabs) {
|
||||
$sync.InitializedTabs = @{}
|
||||
}
|
||||
|
||||
if ($sync.InitializedTabs[$TabName]) {
|
||||
return
|
||||
}
|
||||
|
||||
switch ($TabName) {
|
||||
"Install" {
|
||||
Invoke-WPFUIElements -configVariable $sync.configs.appnavigation -targetGridName "appscategory" -columncount 1
|
||||
Initialize-WPFUI -targetGridName "appscategory"
|
||||
Write-WinUtilPerformanceCheckpoint -Name "App navigation UI created"
|
||||
|
||||
Initialize-WPFUI -targetGridName "appspanel"
|
||||
Write-WinUtilPerformanceCheckpoint -Name "Install UI created"
|
||||
}
|
||||
"Tweaks" {
|
||||
Invoke-WPFUIElements -configVariable $sync.configs.tweaks -targetGridName "tweakspanel" -columncount 2
|
||||
Write-WinUtilPerformanceCheckpoint -Name "Tweaks UI created"
|
||||
}
|
||||
"Config" {
|
||||
Invoke-WPFUIElements -configVariable $sync.configs.feature -targetGridName "featurespanel" -columncount 2
|
||||
Write-WinUtilPerformanceCheckpoint -Name "Features UI created"
|
||||
}
|
||||
"AppX" {
|
||||
Invoke-WPFUIElements -configVariable $sync.configs.appx -targetGridName "appxpanel" -columncount 2
|
||||
Write-WinUtilPerformanceCheckpoint -Name "AppX UI created"
|
||||
}
|
||||
"Win11 Creator" {
|
||||
if ($sync.Form -and $sync.Form.Dispatcher) {
|
||||
$sync.Form.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ Invoke-WinUtilISOCheckExistingWork }) | Out-Null
|
||||
}
|
||||
Write-WinUtilPerformanceCheckpoint -Name "Win11 ISO tab initialized"
|
||||
}
|
||||
}
|
||||
|
||||
$sync.InitializedTabs[$TabName] = $true
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
function Initialize-WinUtilTaskbarOverlayAssets {
|
||||
param(
|
||||
[bool]$IncludeLogo = $true,
|
||||
[bool]$IncludeStatusAssets = $true
|
||||
)
|
||||
|
||||
if ($IncludeLogo -and -not $sync["logorender"]) {
|
||||
$sync["logorender"] = (Invoke-WinUtilAssets -Type "Logo" -Size 90 -Render)
|
||||
Write-WinUtilPerformanceCheckpoint -Name "Taskbar logo asset rendered"
|
||||
}
|
||||
|
||||
if ($IncludeStatusAssets -and -not $sync["checkmarkrender"]) {
|
||||
$sync["checkmarkrender"] = (Invoke-WinUtilAssets -Type "checkmark" -Size 512 -Render)
|
||||
Write-WinUtilPerformanceCheckpoint -Name "Taskbar checkmark asset rendered"
|
||||
}
|
||||
|
||||
if ($IncludeStatusAssets -and -not $sync["warningrender"]) {
|
||||
$sync["warningrender"] = (Invoke-WinUtilAssets -Type "warning" -Size 512 -Render)
|
||||
Write-WinUtilPerformanceCheckpoint -Name "Taskbar warning asset rendered"
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,17 @@ function Invoke-WinUtilAssets {
|
||||
[switch]$render
|
||||
)
|
||||
|
||||
if ($render -and $null -ne $sync) {
|
||||
if ($null -eq $sync.RenderedAssetCache) {
|
||||
$sync.RenderedAssetCache = @{}
|
||||
}
|
||||
|
||||
$cacheKey = "$(([string]$type).ToLowerInvariant())|$Size"
|
||||
if ($sync.RenderedAssetCache.ContainsKey($cacheKey)) {
|
||||
return $sync.RenderedAssetCache[$cacheKey]
|
||||
}
|
||||
}
|
||||
|
||||
# Create the Viewbox and set its size
|
||||
$LogoViewbox = New-Object Windows.Controls.Viewbox
|
||||
$LogoViewbox.Width = $Size
|
||||
@@ -191,6 +202,13 @@ C 21.36,47.14 28.67,50.71 30.01,52.63
|
||||
$bitmapImage.StreamSource = $imageStream
|
||||
$bitmapImage.CacheOption = [Windows.Media.Imaging.BitmapCacheOption]::OnLoad
|
||||
$bitmapImage.EndInit()
|
||||
if ($bitmapImage.CanFreeze) {
|
||||
$bitmapImage.Freeze()
|
||||
}
|
||||
|
||||
if ($null -ne $sync -and $sync.ContainsKey("RenderedAssetCache")) {
|
||||
$sync.RenderedAssetCache[$cacheKey] = $bitmapImage
|
||||
}
|
||||
|
||||
return $bitmapImage
|
||||
} else {
|
||||
|
||||
@@ -61,12 +61,21 @@ function Set-WinUtilTaskbaritem {
|
||||
if ($overlay) {
|
||||
switch ($overlay) {
|
||||
'logo' {
|
||||
if (-not $sync["logorender"]) {
|
||||
Initialize-WinUtilTaskbarOverlayAssets -IncludeLogo $true -IncludeStatusAssets $false
|
||||
}
|
||||
$sync["Form"].taskbarItemInfo.Overlay = $sync["logorender"]
|
||||
}
|
||||
'checkmark' {
|
||||
if (-not $sync["checkmarkrender"]) {
|
||||
Initialize-WinUtilTaskbarOverlayAssets -IncludeLogo $false -IncludeStatusAssets $true
|
||||
}
|
||||
$sync["Form"].taskbarItemInfo.Overlay = $sync["checkmarkrender"]
|
||||
}
|
||||
'warning' {
|
||||
if (-not $sync["warningrender"]) {
|
||||
Initialize-WinUtilTaskbarOverlayAssets -IncludeLogo $false -IncludeStatusAssets $true
|
||||
}
|
||||
$sync["Form"].taskbarItemInfo.Overlay = $sync["warningrender"]
|
||||
}
|
||||
'None' {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
function Invoke-WinUtilInstallAppRenderBatch {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$CategoryBatch
|
||||
)
|
||||
|
||||
foreach ($appKey in $CategoryBatch.AppKeys) {
|
||||
$sync.$appKey = Initialize-InstallAppEntry -TargetElement $CategoryBatch.TargetElement -AppKey $appKey
|
||||
}
|
||||
|
||||
if ($sync.currentTab -eq "Install" -and $sync.SearchBar -and -not [string]::IsNullOrWhiteSpace($sync.SearchBar.Text)) {
|
||||
Find-AppsByNameOrDescription -SearchString $sync.SearchBar.Text
|
||||
}
|
||||
}
|
||||
|
||||
function Complete-WinUtilInstallAppRendering {
|
||||
$sync.InstallAppEntriesRendered = $true
|
||||
Write-WinUtilPerformanceCheckpoint -Name "Install app entries rendered"
|
||||
}
|
||||
|
||||
function Invoke-WinUtilInstallAppRenderNextBatch {
|
||||
if ($sync.InstallAppRenderQueue.Count -gt 0) {
|
||||
$categoryBatch = $sync.InstallAppRenderQueue.Dequeue()
|
||||
Invoke-WinUtilInstallAppRenderBatch -CategoryBatch $categoryBatch
|
||||
}
|
||||
|
||||
if ($sync.InstallAppRenderQueue.Count -gt 0) {
|
||||
$sync.Form.Dispatcher.BeginInvoke(
|
||||
[System.Windows.Threading.DispatcherPriority]::Background,
|
||||
[action]{ Invoke-WinUtilInstallAppRenderNextBatch }
|
||||
) | Out-Null
|
||||
return
|
||||
}
|
||||
|
||||
Complete-WinUtilInstallAppRendering
|
||||
}
|
||||
|
||||
function Start-WinUtilInstallAppRendering {
|
||||
if ($null -eq $sync.InstallAppRenderQueue) {
|
||||
return
|
||||
}
|
||||
|
||||
$sync.InstallAppEntriesRendered = $false
|
||||
|
||||
if ($sync.Form -and $sync.Form.Dispatcher) {
|
||||
$sync.Form.Dispatcher.BeginInvoke(
|
||||
[System.Windows.Threading.DispatcherPriority]::Background,
|
||||
[action]{ Invoke-WinUtilInstallAppRenderNextBatch }
|
||||
) | Out-Null
|
||||
return
|
||||
}
|
||||
|
||||
while ($sync.InstallAppRenderQueue.Count -gt 0) {
|
||||
$categoryBatch = $sync.InstallAppRenderQueue.Dequeue()
|
||||
Invoke-WinUtilInstallAppRenderBatch -CategoryBatch $categoryBatch
|
||||
}
|
||||
|
||||
Complete-WinUtilInstallAppRendering
|
||||
}
|
||||
@@ -30,30 +30,67 @@ function Invoke-WPFRunspace {
|
||||
$ParameterList
|
||||
)
|
||||
|
||||
if (-not ("WinUtilRunspaceCleanup" -as [type])) {
|
||||
Add-Type @"
|
||||
using System;
|
||||
using System.Management.Automation;
|
||||
|
||||
public sealed class WinUtilRunspaceCleanupState
|
||||
{
|
||||
public PowerShell PowerShell { get; set; }
|
||||
public IAsyncResult Handle { get; set; }
|
||||
}
|
||||
|
||||
public static class WinUtilRunspaceCleanup
|
||||
{
|
||||
public static void Cleanup(object state, bool timedOut)
|
||||
{
|
||||
var cleanupState = state as WinUtilRunspaceCleanupState;
|
||||
if (cleanupState == null || cleanupState.PowerShell == null || cleanupState.Handle == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
cleanupState.PowerShell.EndInvoke(cleanupState.Handle);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
cleanupState.PowerShell.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
"@
|
||||
}
|
||||
|
||||
Initialize-WinUtilRunspacePool | Out-Null
|
||||
|
||||
# Create a PowerShell instance
|
||||
$script:powershell = [powershell]::Create()
|
||||
$powershell = [powershell]::Create()
|
||||
|
||||
# Add Scriptblock and Arguments to runspace
|
||||
[void]$script:powershell.AddScript($ScriptBlock)
|
||||
[void]$script:powershell.AddArgument($ArgumentList)
|
||||
[void]$powershell.AddScript($ScriptBlock)
|
||||
[void]$powershell.AddArgument($ArgumentList)
|
||||
|
||||
foreach ($parameter in $ParameterList) {
|
||||
[void]$script:powershell.AddParameter($parameter[0], $parameter[1])
|
||||
[void]$powershell.AddParameter($parameter[0], $parameter[1])
|
||||
}
|
||||
|
||||
$script:powershell.RunspacePool = $sync.runspace
|
||||
$powershell.RunspacePool = $sync.runspace
|
||||
|
||||
# Execute the RunspacePool
|
||||
$script:handle = $script:powershell.BeginInvoke()
|
||||
$handle = $powershell.BeginInvoke()
|
||||
|
||||
$cleanupState = [WinUtilRunspaceCleanupState]::new()
|
||||
$cleanupState.PowerShell = $powershell
|
||||
$cleanupState.Handle = $handle
|
||||
$cleanupCallback = [System.Threading.WaitOrTimerCallback][WinUtilRunspaceCleanup]::Cleanup
|
||||
[System.Threading.ThreadPool]::RegisterWaitForSingleObject($handle.AsyncWaitHandle, $cleanupCallback, $cleanupState, -1, $true) | Out-Null
|
||||
|
||||
# Clean up the RunspacePool threads when they are complete, and invoke the garbage collector to clean up the memory
|
||||
if ($script:handle.IsCompleted) {
|
||||
$script:powershell.EndInvoke($script:handle)
|
||||
$script:powershell.Dispose()
|
||||
$sync.runspace.Dispose()
|
||||
$sync.runspace.Close()
|
||||
[System.GC]::Collect()
|
||||
}
|
||||
# Return the handle
|
||||
return $handle
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ function Invoke-WPFTab {
|
||||
}
|
||||
}
|
||||
$sync.currentTab = $sync.$tabNav.Items[$tabNumber].Header
|
||||
Initialize-WinUtilTabContent -TabName $sync.currentTab
|
||||
|
||||
# Always reset the filter for the current tab
|
||||
if ($sync.currentTab -eq "Install") {
|
||||
|
||||
Reference in New Issue
Block a user