private/Controls/Add-StatusBarAutoWiring.ps1
|
$script:StatusBarDefaults = @{ MaxMessageChars = 120 SuccessTimeout = 2 WarningTimeout = 5 ErrorTimeout = 8 CancelTimeout = 3 } function Add-StatusBarAutoWiring { <# .SYNOPSIS Routes executor lifecycle events to any registered status bar that opted in. #> [CmdletBinding()] param( [Parameter(Mandatory)] $Executor, [Parameter(Mandatory)] $Session, [string]$ActionName ) if (!$Executor -or !$Session) { return } # Collect every bar that opted in to auto-routing $targets = [System.Collections.Generic.List[object]]::new() foreach ($key in @($Session.SafeVariables.Keys)) { $candidate = $Session.GetControl($key) if (!$candidate -or $candidate.Tag -isnot [hashtable]) { continue } if (!$candidate.Tag['IsStatusBar']) { continue } if (!$candidate.Tag['AutoProgress'] -and !$candidate.Tag['AutoCancel'] -and !$candidate.Tag['Intercept']) { continue } $targets.Add($candidate) } if ($targets.Count -eq 0) { return } # Reset severity synchronously before the executor starts. OnStarted fires from a ThreadPool via BeginInvoke, and the dispatch gap is long enough for WPF to paint one frame of stale tint (e.g. leftover green from a prior Success). foreach ($bar in $targets) { $barMeta = if ($bar.Tag -is [hashtable]) { $bar.Tag } else { @{} } if ($barMeta.SeverityTimer) { $barMeta.SeverityTimer.Stop() } if ($barMeta.Severity -and $barMeta.Severity -ne 'Info') { $barMeta.Severity = 'Info' Set-StatusBarSeverityVisual -Bar $bar -Severity Info } } # Snapshot defaults so closures inside the handlers don't chase $script: at invoke time $successTimeout = $script:StatusBarDefaults.SuccessTimeout $warningTimeout = $script:StatusBarDefaults.WarningTimeout $errorTimeout = $script:StatusBarDefaults.ErrorTimeout $cancelTimeout = $script:StatusBarDefaults.CancelTimeout $maxChars = $script:StatusBarDefaults.MaxMessageChars foreach ($bar in $targets) { $capturedBar = $bar $capturedProgress = $bar.Tag['ProgressBar'] $capturedText = $bar.Tag['StatusText'] $capturedCancel = $bar.Tag['CancelButton'] # Small label above the progress bar $capturedProgressLabel = $bar.Tag['ProgressLabel'] # Intercept state for badge tracking $capturedIntercept = [bool]$bar.Tag['Intercept'] $capturedCaptureHost = [bool]$bar.Tag['CaptureHost'] $capturedCaptureVerbose = [bool]$bar.Tag['CaptureVerbose'] $capturedCaptureDebug = [bool]$bar.Tag['CaptureDebug'] $capturedNoOutputOnly = [bool]$bar.Tag['NoOutputOnly'] $capturedPersist = [bool]$bar.Tag['Persist'] $capturedMaxMessages = if ($bar.Tag['MaxMessages']) { $bar.Tag['MaxMessages'] } else { 100 } $capturedExecutor = $Executor # The $meta guard repeats in every handler. Tag is always the New-UiStatusBar hashtable (if it ever isn't, something larger is broken). Same reference, mutations write through. # OnStarted: reveal Cancel button and progress bar (severity already reset above) $revealOnStart = { Invoke-OnUIThread { $meta = if ($capturedBar.Tag -is [hashtable]) { $capturedBar.Tag } else { @{} } if ($capturedCancel) { $capturedCancel.Visibility = [System.Windows.Visibility]::Visible $capturedCancel.IsEnabled = $true } # Reset progress label if ($capturedProgressLabel) { $capturedProgressLabel.Text = '' $capturedProgressLabel.Visibility = [System.Windows.Visibility]::Collapsed } # Reset progress bar state but keep it hidden until Write-Progress fires. A ManualBar bar belongs to an explicit Set-UiStatusBar call - its value and sweep stay put across unrelated actions. if ($capturedProgress -and !$meta.ManualBar) { $capturedProgress.Value = 0 $capturedProgress.IsIndeterminate = $false $capturedProgress.Visibility = [System.Windows.Visibility]::Hidden } # Reset intercept badges for the new action (unless -Persist keeps them) if ($capturedIntercept -and !$capturedPersist) { Reset-StatusBarBadges -Meta $meta } } }.GetNewClosure() $Executor.add_OnStarted({ & $revealOnStart }.GetNewClosure()) # RaiseOnStarted snapshots its handler list on the worker thread at slot acquire; attach after ExecuteAsync (Invoke-UiAction does) and the raise is already gone. # Compensate for mid-flight executors; the reveal is idempotent, double fire is free. if ($Executor.IsRunning) { & $revealOnStart } # OnProgress: drive embedded bar text + fill from Write-Progress $Executor.add_OnProgress({ param($record) Invoke-OnUIThread { $meta = if ($capturedBar.Tag -is [hashtable]) { $capturedBar.Tag } else { @{} } if ($record.RecordType -eq 'Completed') { # An explicit -Completed record ends any manual hold on the bar too $meta.ManualBar = $false if ($capturedText) { $capturedText.Text = ''; $capturedText.ToolTip = $null } if ($capturedProgress) { $capturedProgress.IsIndeterminate = $false $capturedProgress.Value = 0 $capturedProgress.Visibility = [System.Windows.Visibility]::Hidden } if ($capturedProgressLabel) { $capturedProgressLabel.Text = '' $capturedProgressLabel.Visibility = [System.Windows.Visibility]::Collapsed } Set-StatusBarSeverityVisual -Bar $capturedBar -Severity Success $meta.Severity = 'Success' if ($meta.SeverityTimer) { $meta.SeverityTimer.Stop() $meta.SeverityTimer.Interval = [TimeSpan]::FromSeconds($successTimeout) $meta.SeverityTimer.Start() } return } # In-progress record: pick the most descriptive text and update fill. Real records reclaim the bar from any manual hold - the hold protects a value the lifecycle isn't driving, and now it is. $meta.ManualBar = $false $progressText = if ($record.StatusDescription) { $record.StatusDescription } elseif ($record.Activity) { $record.Activity } else { $null } if ($capturedText -and $progressText) { $capturedText.Text = $progressText } # Update the small label above the progress bar if ($capturedProgressLabel -and $progressText) { $capturedProgressLabel.Text = $progressText $capturedProgressLabel.Visibility = [System.Windows.Visibility]::Visible } if ($capturedProgress) { # Reveal the bar on first real progress record $capturedProgress.Visibility = [System.Windows.Visibility]::Visible if ($record.PercentComplete -ge 0) { $capturedProgress.IsIndeterminate = $false $pct = $record.PercentComplete if ($pct -gt 100) { $pct = 100 } $capturedProgress.Value = $pct } else { # Activity-only progress: indeterminate mode $capturedProgress.IsIndeterminate = $true } } } }.GetNewClosure()) # OnHostObserved, not OnHostBatch: the observer copy fires in every routing mode, so windowed buttons feed the console badge too. Subscribing OnHostBatch here would starve the output window's own subscription and see nothing in queue mode anyway. if ($capturedCaptureHost) { $Executor.add_OnHostObserved({ param($batch) Invoke-OnUIThread { if ($batch.Count -eq 0 -or !$capturedText) { return } if ($capturedNoOutputOnly -and $capturedExecutor.UseQueueMode) { return } $meta = if ($capturedBar.Tag -is [hashtable]) { $capturedBar.Tag } else { @{} } # Update the status text with the latest message in the batch $lastRecord = $batch[$batch.Count - 1] $capturedText.Text = $lastRecord.Message $capturedText.ToolTip = if ($lastRecord.Message.Length -gt 60) { $lastRecord.Message } else { $null } try { Add-StatusBarHistoryEntry -Bar $capturedBar -Message $lastRecord.Message -Kind 'Info' } catch { Write-Debug "OnHostBatch ledger entry failed: $_" } # Accumulate every record in the console badge popup if ($null -ne $meta.HostMessages) { foreach ($record in $batch) { if (!$record.Message) { continue } $entry = @{ Time = [DateTime]::Now; Message = $record.Message } if ($meta.HostMessages.Count -ge $capturedMaxMessages) { $meta.HostMessages.RemoveAt(0) } $meta.HostMessages.Add($entry) # Foreground inherited from popup container's TextElement.Foreground $msgBlock = [System.Windows.Controls.TextBlock]@{ Text = "[$($entry.Time.ToString('HH:mm:ss'))] $($record.Message)" TextWrapping = 'Wrap' Margin = [System.Windows.Thickness]::new(4, 1, 4, 1) FontSize = 11 FontFamily = [System.Windows.Media.FontFamily]::new('Consolas') } [void]$meta.HostPopup.MessagePanel.Children.Add($msgBlock) # Trim popup panel if over limit if ($meta.HostPopup.MessagePanel.Children.Count -gt $capturedMaxMessages) { $meta.HostPopup.MessagePanel.Children.RemoveAt(0) } } $count = $meta.HostMessages.Count $meta.HostBadge.CountText.Text = "$count" $meta.HostBadge.Badge.Visibility = [System.Windows.Visibility]::Visible $meta.HostBadge.Badge.Opacity = 1.0 $meta.HostBadge.Badge.ToolTip = "$count console message$(if ($count -ne 1) { 's' })" $meta.HostPopup.HeaderText.Text = "$count Message$(if ($count -ne 1) { 's' })" } } }.GetNewClosure()) } # Verbose, badge and popup only. No tint, no status text. Records only arrive when the action's $VerbosePreference lets Write-Verbose emit, same as a console. if ($capturedCaptureVerbose) { $Executor.add_OnVerbose({ param($message) if ([string]::IsNullOrEmpty($message)) { return } Invoke-OnUIThread { $meta = if ($capturedBar.Tag -is [hashtable]) { $capturedBar.Tag } else { @{} } if ($null -eq $meta.VerboseMessages) { return } if ($capturedNoOutputOnly -and $capturedExecutor.UseQueueMode) { return } $entry = @{ Time = [DateTime]::Now; Message = $message } if ($meta.VerboseMessages.Count -ge $capturedMaxMessages) { $meta.VerboseMessages.RemoveAt(0) } $meta.VerboseMessages.Add($entry) $count = $meta.VerboseMessages.Count $meta.VerboseBadge.CountText.Text = "$count" $meta.VerboseBadge.Badge.Opacity = 1.0 $meta.VerboseBadge.Badge.ToolTip = "$count Verbose line$(if ($count -ne 1) { 's' }) - click to view" $msgBlock = [System.Windows.Controls.TextBlock]@{ Text = "[$($entry.Time.ToString('HH:mm:ss'))] $message" TextWrapping = 'Wrap' Margin = [System.Windows.Thickness]::new(4, 1, 4, 1) FontSize = 11 FontFamily = [System.Windows.Media.FontFamily]::new('Consolas') } [void]$meta.VerbosePopup.MessagePanel.Children.Add($msgBlock) $meta.VerbosePopup.HeaderText.Text = "$count Verbose line$(if ($count -ne 1) { 's' })" if ($meta.VerbosePopup.MessagePanel.Children.Count -gt $capturedMaxMessages) { $meta.VerbosePopup.MessagePanel.Children.RemoveAt(0) } } }.GetNewClosure()) } # Debugm, identical, gated by $DebugPreference instead if ($capturedCaptureDebug) { $Executor.add_OnDebug({ param($message) if ([string]::IsNullOrEmpty($message)) { return } Invoke-OnUIThread { $meta = if ($capturedBar.Tag -is [hashtable]) { $capturedBar.Tag } else { @{} } if ($null -eq $meta.DebugMessages) { return } if ($capturedNoOutputOnly -and $capturedExecutor.UseQueueMode) { return } $entry = @{ Time = [DateTime]::Now; Message = $message } if ($meta.DebugMessages.Count -ge $capturedMaxMessages) { $meta.DebugMessages.RemoveAt(0) } $meta.DebugMessages.Add($entry) $count = $meta.DebugMessages.Count $meta.DebugBadge.CountText.Text = "$count" $meta.DebugBadge.Badge.Opacity = 1.0 $meta.DebugBadge.Badge.ToolTip = "$count Debug line$(if ($count -ne 1) { 's' }) - click to view" $msgBlock = [System.Windows.Controls.TextBlock]@{ Text = "[$($entry.Time.ToString('HH:mm:ss'))] $message" TextWrapping = 'Wrap' Margin = [System.Windows.Thickness]::new(4, 1, 4, 1) FontSize = 11 FontFamily = [System.Windows.Media.FontFamily]::new('Consolas') } [void]$meta.DebugPopup.MessagePanel.Children.Add($msgBlock) $meta.DebugPopup.HeaderText.Text = "$count Debug line$(if ($count -ne 1) { 's' })" if ($meta.DebugPopup.MessagePanel.Children.Count -gt $capturedMaxMessages) { $meta.DebugPopup.MessagePanel.Children.RemoveAt(0) } } }.GetNewClosure()) } # OnError: the big ol' heavyweight. Tints severity, truncates the message for the status text, extracts every diagnostic field PSErrorRecord offers, and builds a popup entry. $Executor.add_OnError({ param($errorRecord) $fullErrorMsg = if ($errorRecord -and $errorRecord.Message) { $errorRecord.Message } elseif ($errorRecord) { $errorRecord.ToString() } else { 'Unknown error' } # Clamp the cut so a small/absent $maxChars can't drive Substring negative ("Length cannot be less than zero" spam on the dispatcher that made me bald(er)). $limit = if ([int]$maxChars -gt 3) { [int]$maxChars } else { 120 } $errorMessage = $fullErrorMsg if ($null -ne $errorMessage -and $errorMessage.Length -gt $limit) { $errorMessage = $errorMessage.Substring(0, $limit - 3) + '...' } # Extract every diagnostic field PSErrorRecord has to offer. "Unknown error" is never helpful, so go a little overboard here. Exception type first. $exceptionType = $null if ($errorRecord.RawRecord -and $errorRecord.RawRecord.Exception) { $exceptionType = $errorRecord.RawRecord.Exception.GetType().Name } # Location file:line, category, FQEID - whatever the record actually populated $locationParts = [System.Collections.Generic.List[string]]::new() if ($exceptionType) { $locationParts.Add($exceptionType) } if ($errorRecord.ScriptName) { $fileName = [System.IO.Path]::GetFileName($errorRecord.ScriptName) if ($errorRecord.LineNumber -gt 0) { $locationParts.Add("${fileName}:$($errorRecord.LineNumber)") } else { $locationParts.Add($fileName) } } elseif ($errorRecord.LineNumber -gt 0) { $locationParts.Add("line $($errorRecord.LineNumber)") } if ($errorRecord.Category -and $errorRecord.Category -ne 'NotSpecified') { $locationParts.Add($errorRecord.Category) } if ($errorRecord.FullyQualifiedErrorId) { $locationParts.Add($errorRecord.FullyQualifiedErrorId) } $detailLine = if ($locationParts.Count -gt 0) { $locationParts -join ' | ' } else { $null } $codeLine = if ($errorRecord.Line) { $errorRecord.Line.Trim() } else { $null } # First line of ScriptStackTrace gives the call site without the full trace noise $stackLine = $null if ($errorRecord.ScriptStackTrace) { $stackLine = ($errorRecord.ScriptStackTrace -split "`n")[0].Trim() } # TargetObject reveals what the cmdlet was actually operating on when it blew up $targetLine = $null if ($errorRecord.TargetObject) { $targetStr = $errorRecord.TargetObject.ToString() if ($targetStr -and $targetStr -ne $errorRecord.Message -and $targetStr.Length -le $maxChars) { $targetLine = $targetStr } } # Inner exception is often the actual root cause hiding behind the wrapper $innerLine = $null if ($errorRecord.InnerException) { $innerType = $null if ($errorRecord.RawRecord -and $errorRecord.RawRecord.Exception -and $errorRecord.RawRecord.Exception.InnerException) { $innerType = $errorRecord.RawRecord.Exception.InnerException.GetType().Name } $innerLine = if ($innerType) { "${innerType}: $($errorRecord.InnerException)" } else { $errorRecord.InnerException } } Invoke-OnUIThread { $meta = if ($capturedBar.Tag -is [hashtable]) { $capturedBar.Tag } else { @{} } if ($capturedText) { $capturedText.Text = $errorMessage $capturedText.ToolTip = if ($fullErrorMsg.Length -gt 60) { $fullErrorMsg } else { $null } } try { Add-StatusBarHistoryEntry -Bar $capturedBar -Message $errorMessage -Kind 'Error' } catch { Write-Debug "OnError ledger entry failed: $_" } if ($capturedProgress -and !$meta.ManualBar) { $capturedProgress.IsIndeterminate = $false if ($capturedProgress.Value -le 0) { $capturedProgress.Visibility = [System.Windows.Visibility]::Hidden } } if ($capturedCancel) { $capturedCancel.Visibility = [System.Windows.Visibility]::Hidden } Set-StatusBarSeverityVisual -Bar $capturedBar -Severity Error $meta.Severity = 'Error' if ($meta.SeverityTimer) { $meta.SeverityTimer.Stop() $meta.SeverityTimer.Interval = [TimeSpan]::FromSeconds($errorTimeout) $meta.SeverityTimer.Start() } # Accumulate in error badge when Intercept is active if ($capturedIntercept -and $null -ne $meta.ErrorMessages) { if ($capturedNoOutputOnly -and $capturedExecutor.UseQueueMode) { return } $entry = @{ Time = [DateTime]::Now Message = $fullErrorMsg Detail = $detailLine Code = $codeLine Stack = $stackLine Target = $targetLine Inner = $innerLine } if ($meta.ErrorMessages.Count -ge $capturedMaxMessages) { $meta.ErrorMessages.RemoveAt(0) } $meta.ErrorMessages.Add($entry) $count = $meta.ErrorMessages.Count $meta.ErrorBadge.CountText.Text = "$count" $meta.ErrorBadge.Badge.Opacity = 1.0 $meta.ErrorBadge.Badge.ToolTip = "$count Error$(if ($count -ne 1) { 's' }) - click to view" # Build popup entry: message, location, offending code, stack, target, inner exception. # Text foreground inherits from the popup's outerBorder, so entries track theme automatically. $entryPanel = [System.Windows.Controls.StackPanel]::new() $summaryBlock = [System.Windows.Controls.TextBlock]@{ Text = "[$($entry.Time.ToString('HH:mm:ss'))] $fullErrorMsg" TextWrapping = 'Wrap' Margin = [System.Windows.Thickness]::new(4, 2, 4, 0) FontSize = 12 } [void]$entryPanel.Children.Add($summaryBlock) if ($detailLine) { $detailBlock = [System.Windows.Controls.TextBlock]@{ Text = " $detailLine" TextWrapping = 'Wrap' Margin = [System.Windows.Thickness]::new(4, 0, 4, 0) FontSize = 10 Opacity = 0.65 } [void]$entryPanel.Children.Add($detailBlock) } # Show the offending code line when available if ($codeLine) { $codeBlock = [System.Windows.Controls.TextBlock]@{ Text = " > $codeLine" TextWrapping = 'Wrap' Margin = [System.Windows.Thickness]::new(4, 0, 4, 0) FontSize = 10 FontFamily = [System.Windows.Media.FontFamily]::new('Consolas') Opacity = 0.5 } [void]$entryPanel.Children.Add($codeBlock) } # Show first line of stack trace for call-site context if ($stackLine) { $stackBlock = [System.Windows.Controls.TextBlock]@{ Text = " $stackLine" TextWrapping = 'Wrap' Margin = [System.Windows.Thickness]::new(4, 0, 4, 0) FontSize = 9.5 FontFamily = [System.Windows.Media.FontFamily]::new('Consolas') Opacity = 0.4 } [void]$entryPanel.Children.Add($stackBlock) } # Show what the cmdlet was operating on if ($targetLine) { $targetBlock = [System.Windows.Controls.TextBlock]@{ Text = " Target: $targetLine" TextWrapping = 'Wrap' Margin = [System.Windows.Thickness]::new(4, 0, 4, 0) FontSize = 10 FontFamily = [System.Windows.Media.FontFamily]::new('Consolas') Opacity = 0.45 } [void]$entryPanel.Children.Add($targetBlock) } # Show root cause when the exception wraps another if ($innerLine) { $innerBlock = [System.Windows.Controls.TextBlock]@{ Text = " Inner: $innerLine" TextWrapping = 'Wrap' Margin = [System.Windows.Thickness]::new(4, 0, 4, 2) FontSize = 10 FontFamily = [System.Windows.Media.FontFamily]::new('Consolas') Opacity = 0.45 } [void]$entryPanel.Children.Add($innerBlock) } [void]$meta.ErrorPopup.MessagePanel.Children.Add($entryPanel) $meta.ErrorPopup.HeaderText.Text = "$count Error$(if ($count -ne 1) { 's' })" # Trim popup panel if over limit if ($meta.ErrorPopup.MessagePanel.Children.Count -gt $capturedMaxMessages) { $meta.ErrorPopup.MessagePanel.Children.RemoveAt(0) } } } }.GetNewClosure()) # OnWarning: tint Warning with truncated message and accumulate badge $Executor.add_OnWarning({ param($warningMessage) $fullWarnMsg = $warningMessage $shown = $warningMessage $warnLimit = if ([int]$maxChars -gt 3) { [int]$maxChars } else { 120 } if ($null -ne $shown -and $shown.Length -gt $warnLimit) { $shown = $shown.Substring(0, $warnLimit - 3) + '...' } Invoke-OnUIThread { if ($capturedText -and $shown) { $capturedText.Text = $shown $capturedText.ToolTip = if ($fullWarnMsg.Length -gt 60) { $fullWarnMsg } else { $null } try { Add-StatusBarHistoryEntry -Bar $capturedBar -Message $shown -Kind 'Warning' } catch { Write-Debug "OnWarning ledger entry failed: $_" } } Set-StatusBarSeverityVisual -Bar $capturedBar -Severity Warning $meta = if ($capturedBar.Tag -is [hashtable]) { $capturedBar.Tag } else { @{} } $meta.Severity = 'Warning' if ($meta.SeverityTimer) { $meta.SeverityTimer.Stop() $meta.SeverityTimer.Interval = [TimeSpan]::FromSeconds($warningTimeout) $meta.SeverityTimer.Start() } # Accumulate in warning badge when Intercept is active if ($capturedIntercept -and $null -ne $meta.WarningMessages) { if ($capturedNoOutputOnly -and $capturedExecutor.UseQueueMode) { return } $entry = @{ Time = [DateTime]::Now; Message = $fullWarnMsg } if ($meta.WarningMessages.Count -ge $capturedMaxMessages) { $meta.WarningMessages.RemoveAt(0) } $meta.WarningMessages.Add($entry) $count = $meta.WarningMessages.Count $meta.WarningBadge.CountText.Text = "$count" $meta.WarningBadge.Badge.Opacity = 1.0 $meta.WarningBadge.Badge.ToolTip = "$count Warning$(if ($count -ne 1) { 's' }) - click to view" # Foreground inherited from popup container's TextElement.Foreground $msgBlock = [System.Windows.Controls.TextBlock]@{ Text = "[$($entry.Time.ToString('HH:mm:ss'))] $fullWarnMsg" TextWrapping = 'Wrap' Margin = [System.Windows.Thickness]::new(4, 2, 4, 2) FontSize = 12 } [void]$meta.WarningPopup.MessagePanel.Children.Add($msgBlock) $meta.WarningPopup.HeaderText.Text = "$count Warning$(if ($count -ne 1) { 's' })" # Trim popup panel if over limit if ($meta.WarningPopup.MessagePanel.Children.Count -gt $capturedMaxMessages) { $meta.WarningPopup.MessagePanel.Children.RemoveAt(0) } } } }.GetNewClosure()) # OnCancelled: tint Warning briefly and reset the running visuals $Executor.add_OnCancelled({ Invoke-OnUIThread { $meta = if ($capturedBar.Tag -is [hashtable]) { $capturedBar.Tag } else { @{} } if ($capturedText) { $capturedText.Text = 'Cancelled'; $capturedText.ToolTip = $null } try { Add-StatusBarHistoryEntry -Bar $capturedBar -Message 'Cancelled' -Kind 'Warning' } catch { Write-Debug "OnCancelled ledger entry failed: $_" } if ($capturedProgress -and !$meta.ManualBar) { $capturedProgress.IsIndeterminate = $false if ($capturedProgress.Value -le 0) { $capturedProgress.Visibility = [System.Windows.Visibility]::Hidden } } if ($capturedCancel) { $capturedCancel.Visibility = [System.Windows.Visibility]::Hidden } if ($capturedProgressLabel) { $capturedProgressLabel.Text = '' $capturedProgressLabel.Visibility = [System.Windows.Visibility]::Collapsed } Set-StatusBarSeverityVisual -Bar $capturedBar -Severity Warning $meta.Severity = 'Warning' if ($meta.SeverityTimer) { $meta.SeverityTimer.Stop() $meta.SeverityTimer.Interval = [TimeSpan]::FromSeconds($cancelTimeout) $meta.SeverityTimer.Start() } } }.GetNewClosure()) # OnComplete: hide Cancel and progress bar, drop indeterminate. Leave text, severity, and a ManualBar hold alone so the user's last Write-Status / Set-UiStatusBar wins. $Executor.add_OnComplete({ Invoke-OnUIThread { $meta = if ($capturedBar.Tag -is [hashtable]) { $capturedBar.Tag } else { @{} } if ($capturedCancel) { $capturedCancel.Visibility = [System.Windows.Visibility]::Hidden } if ($capturedProgressLabel) { $capturedProgressLabel.Text = '' $capturedProgressLabel.Visibility = [System.Windows.Visibility]::Collapsed } if ($capturedProgress -and !$meta.ManualBar) { $capturedProgress.IsIndeterminate = $false if ($capturedProgress.Value -le 0) { $capturedProgress.Visibility = [System.Windows.Visibility]::Hidden } } } }.GetNewClosure()) } } |