diff --git a/AGENTS.md b/AGENTS.md
index db441a2512..362fe22128 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -339,6 +339,7 @@ Do not rename a public type to get under the limit.
- Preserve the existing nullable reference type annotations and context (`enable` is set at project level).
- Do not enable or disable nullable in individual files unless requested.
+- **ThrowHelper (required for BCL throws):** For new and changed code, throw framework exceptions only through `Krypton.Interop.ThrowHelper` (`Source/Krypton Components/Krypton.Interop/General/ThrowHelper.cs`). Prefer `ThrowIfNull` / `ThrowIfNullOrWhiteSpace` for guards, `value ?? ThrowHelper.ThrowArgumentNullException(value)` (or the generic `` overloads) in expression contexts, and the matching `ThrowArgumentException` / `ThrowArgumentOutOfRangeException` / `ThrowInvalidOperationException` / `ThrowNotSupportedException` / `ThrowNotImplementedException` / `ThrowObjectDisposedException` / `ThrowInvalidCastException` / `ThrowWin32Exception` / `ThrowNullReferenceException` helpers when that is the contract. Do **not** write bare `throw new ArgumentNullException(...)`, `throw new ArgumentException(...)`, `throw new ArgumentOutOfRangeException(...)`, `throw new InvalidOperationException(...)`, and the other BCL types `ThrowHelper` already covers. `ThrowHelper` is `internal` in Interop and visible to sibling assemblies via `InternalsVisibleTo`; do not duplicate it, and do not add `ThrowHelper` APIs for product/domain exception types (for example `KryptonUpdateException`) into Interop. Domain or feature exceptions stay as typed `throw new …` (or a **local** cold-path helper next to that feature when the call sites are many). Apply to new and changed throw sites; do not mass-convert unrelated legacy `throw new` in files outside the task unless asked.
- No unneeded `try/catch` blocks if there's no catch handling
- Idioms: use null-propagation and object/collection initializers where consistent
- Prefer [switch expressions](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/switch-expression) for simple value/type dispatch that only returns (or assigns) a value. Keep `switch` statements for complex control flow, multiple statements per arm, or side effects. Prefer a discard arm (`_ => ...`) when exhaustiveness matters. Use only pattern forms already common in this codebase (constant, type, discard, simple property/`when` guards as elsewhere); do not introduce newer pattern syntax that would fight **Public API → Compatibility**. Apply to new and changed code; do not mass-convert unrelated existing code unless asked.
@@ -830,6 +831,7 @@ When adding a **new** example project in [Standard-Toolkit-Demos](https://github
- **PNG** for a static default look of the example in **both** a light and a dark builtin theme (same baseline as Toolkit **UI Screenshots / GIFs**: `Microsoft365Blue` and `Microsoft365BlackDarkMode`). Do not shoot every theme.
- Prefer filenames that keep the pair obvious (`example-light.png` / `example-dark.png`, or `Example-light.png` / `Example-dark.png`). When the category README historically shows a single image, embed both (two image lines under the heading).
- **GIF** when motion is the point (toggle, toast, menu open, fade, scrollbar interaction), matching neighbours such as `KryptonToggleSwitchExample.gif` or `ToastNotificationExample.gif`. Capture light and dark when the motion is theme-sensitive; one GIF is enough when motion is geometry-only.
+- Prefer PrintWindow / HWND capture of the example form (same idea as Toolkit **UI Screenshots / GIFs**); verify the image shows the demo, not the IDE.
#### Where to save the file
diff --git a/Documents/Changelog/Changelog.md b/Documents/Changelog/Changelog.md
index 91b7d2407e..439fe751bc 100644
--- a/Documents/Changelog/Changelog.md
+++ b/Documents/Changelog/Changelog.md
@@ -76,6 +76,7 @@
- A download that drops or is cancelled stays on the progress window so it can be resumed or started again. The whole file is still checked before it is applied.
- Release notes open in their own window, Remind later asks for a date or skips that version, and the progress window can show file name, size, speed, and time remaining.
- The update manifest can limit how many days Remind later offers, and can allow or block postpone, skip, resume, and retry. A critical package blocks postpone unless `allowPostpone` is true and blocks skip unless `allowSkip` is true. `allowRetry` false hides Restart.
+ - The update manifest can add an optional banner (text, colours or gradient, image URL) on the prompt and notes windows, and an optional pulsing form border on the prompt, notes, and progress windows.
- To use, you will need to download the [Krypton.Standard.Toolkit](https://www.nuget.org/packages/Krypton.Standard.Toolkit) NuGet package, as this control is part of the `Krypton.Toolkit.Utilities` assembly.
- Implemented [#3806](https://github.com/Krypton-Suite/Standard-Toolkit/issues/3806), Windows XP Themes
- Windows XP visual-style Extra themes in `Krypton.Themes`: Luna Blue / Olive / Silver, Royale, Royale Noir, Zune, and Embedded, with a dedicated XP renderer in `Krypton.Toolkit` and typed bitmaps from `Krypton.Resources`.
diff --git a/Scripts/UnitTests/Invoke-ApplicationUpdaterRecoveryScreenshot.ps1 b/Scripts/UnitTests/Invoke-ApplicationUpdaterRecoveryScreenshot.ps1
index 340d46b79a..5d366b8028 100644
--- a/Scripts/UnitTests/Invoke-ApplicationUpdaterRecoveryScreenshot.ps1
+++ b/Scripts/UnitTests/Invoke-ApplicationUpdaterRecoveryScreenshot.ps1
@@ -1,11 +1,11 @@
<#
.SYNOPSIS
- Captures the updater progress window after an interrupted download.
+ Captures the updater progress window after an interrupted download in light and dark themes.
.DESCRIPTION
- Loads the Debug net472 utilities build in-process (STA), shows the internal
- progress form with Resume, Restart, and Close, and writes
- Documents/PR/4669-application-updater-recovery.png with PrintWindow.
+ Loads the Debug net472 utilities build in-process (STA), shows the internal progress form
+ with Resume, Restart, and Close under Microsoft365Blue and Microsoft365BlackDarkMode, and
+ writes Documents/PR/4669-application-updater-recovery-light.png and *-dark.png with PrintWindow.
# UnitTest-CI: exclude
@@ -17,7 +17,7 @@ param(
[string]$Configuration = 'Debug',
[string]$TargetFramework = 'net472',
[string]$BinDir,
- [string]$OutputPath
+ [string]$OutputDirectory
)
$ErrorActionPreference = 'Stop'
@@ -25,8 +25,11 @@ $ErrorActionPreference = 'Stop'
$repoRoot = Get-UnitTestRepoRoot
$bin = Get-UnitTestBinDir -RepoRoot $repoRoot -Configuration $Configuration -TargetFramework $TargetFramework -BinDir $BinDir
-if (-not $OutputPath) {
- $OutputPath = Join-Path $repoRoot 'Documents\PR\4669-application-updater-recovery.png'
+if (-not $OutputDirectory) {
+ $OutputDirectory = Join-Path $repoRoot 'Documents\PR'
+}
+if (-not (Test-Path -LiteralPath $OutputDirectory)) {
+ New-Item -ItemType Directory -Path $OutputDirectory | Out-Null
}
Register-UnitTestAssemblyResolver -BinDir $bin
@@ -36,6 +39,11 @@ Initialize-UnitTestNativeInput
[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Interop.dll'))
[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Resources.dll'))
[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Toolkit.dll'))
+$themesDll = Join-Path $bin 'Krypton.Themes.dll'
+if (-not (Test-Path -LiteralPath $themesDll)) {
+ throw "Missing $themesDll - build Krypton.Themes Debug $TargetFramework first."
+}
+[void][System.Reflection.Assembly]::LoadFrom($themesDll)
$utilities = [System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Toolkit.Utilities.dll'))
[System.Windows.Forms.Application]::EnableVisualStyles()
@@ -46,27 +54,40 @@ if (-not $formType -or -not $stringsType) {
}
$flags = [System.Reflection.BindingFlags]'Instance,Public,NonPublic'
-$strings = [System.Activator]::CreateInstance($stringsType)
-$form = [System.Activator]::CreateInstance($formType)
-[void]$formType.GetMethod('Bind', $flags).Invoke($form, @($strings))
-$progressArgs = [object[]]::new(2)
-$progressArgs[0] = [int64]45
-$progressArgs[1] = [Nullable[int64]]100
-[void]$formType.GetMethod('SetProgress', $flags).Invoke($form, $progressArgs)
-[void]$formType.GetMethod('ShowRecovery', $flags).Invoke($form, @($strings.DownloadInterrupted, $true, $true, $strings))
+$manager = New-Object Krypton.Toolkit.KryptonManager
+$themes = @(
+ @{ Mode = [Krypton.Toolkit.PaletteMode]::Microsoft365Blue; Suffix = 'light' },
+ @{ Mode = [Krypton.Toolkit.PaletteMode]::Microsoft365BlackDarkMode; Suffix = 'dark' }
+)
-$form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
-$form.Location = New-Object System.Drawing.Point 80, 80
-$form.TopMost = $true
-$form.Show()
-$form.Activate()
-$form.BringToFront()
-[void][UnitTestNative]::SetForegroundWindow($form.Handle)
-[System.Windows.Forms.Application]::DoEvents()
-Start-Sleep -Milliseconds 500
-[System.Windows.Forms.Application]::DoEvents()
+foreach ($theme in $themes) {
+ $manager.GlobalPaletteMode = $theme.Mode
+ $strings = [System.Activator]::CreateInstance($stringsType)
+ $form = [System.Activator]::CreateInstance($formType)
+ [void]$formType.GetMethod('Bind', $flags).Invoke($form, @($strings, $null))
+ $progressArgs = [object[]]::new(2)
+ $progressArgs[0] = [int64]45
+ $progressArgs[1] = [Nullable[int64]]100
+ [void]$formType.GetMethod('SetProgress', $flags).Invoke($form, $progressArgs)
+ # ShowRecovery returns Task; start the recovery UI without awaiting the choice.
+ $null = $formType.GetMethod('ShowRecovery', $flags).Invoke($form, @($strings.DownloadInterrupted, $true, $true, $strings))
+ [System.Windows.Forms.Application]::DoEvents()
-Save-UnitTestWindowPng -Form $form -Path $OutputPath
-$form.Close()
-$form.Dispose()
-Write-Output "Wrote $OutputPath"
+ $form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
+ $form.Location = New-Object System.Drawing.Point 80, 80
+ $form.TopMost = $true
+ $form.Show()
+ $form.Refresh()
+ $form.Activate()
+ $form.BringToFront()
+ [void][UnitTestNative]::SetForegroundWindow($form.Handle)
+ [System.Windows.Forms.Application]::DoEvents()
+ Start-Sleep -Milliseconds 500
+ [System.Windows.Forms.Application]::DoEvents()
+
+ $outputPath = Join-Path $OutputDirectory "4669-application-updater-recovery-$($theme.Suffix).png"
+ Save-UnitTestWindowPng -Form $form -Path $outputPath
+ $form.Close()
+ $form.Dispose()
+ Write-Output "Wrote $outputPath"
+}
diff --git a/Scripts/UnitTests/Invoke-ApplicationUpdaterWindowScreenshot.ps1 b/Scripts/UnitTests/Invoke-ApplicationUpdaterWindowScreenshot.ps1
index fd00326e88..66cee46549 100644
--- a/Scripts/UnitTests/Invoke-ApplicationUpdaterWindowScreenshot.ps1
+++ b/Scripts/UnitTests/Invoke-ApplicationUpdaterWindowScreenshot.ps1
@@ -1,13 +1,11 @@
<#
.SYNOPSIS
- Captures the updater prompt, notes, remind-later dialog, and progress footer.
+ Captures the updater prompt, notes, remind-later dialog, and progress footer in light and dark themes.
.DESCRIPTION
- Loads the Debug net472 utilities build in-process (STA) and writes
- Documents/PR/4669-application-updater-prompt.png,
- 4669-application-updater-notes.png,
- 4669-application-updater-postpone.png, and
- 4669-application-updater-details.png with PrintWindow.
+ Loads the Debug net472 utilities build in-process (STA), applies Microsoft365Blue and
+ Microsoft365BlackDarkMode, and writes Documents/PR/4669-application-updater-*-light.png
+ and *-dark.png with PrintWindow (Save-UnitTestWindowPng).
# UnitTest-CI: exclude
@@ -27,6 +25,9 @@ $ErrorActionPreference = 'Stop'
$repoRoot = Get-UnitTestRepoRoot
$bin = Get-UnitTestBinDir -RepoRoot $repoRoot -Configuration $Configuration -TargetFramework $TargetFramework -BinDir $BinDir
$pr = Join-Path $repoRoot 'Documents\PR'
+if (-not (Test-Path -LiteralPath $pr)) {
+ New-Item -ItemType Directory -Path $pr | Out-Null
+}
Register-UnitTestAssemblyResolver -BinDir $bin
Add-Type -AssemblyName System.Drawing
@@ -35,6 +36,11 @@ Initialize-UnitTestNativeInput
[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Interop.dll'))
[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Resources.dll'))
[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Toolkit.dll'))
+$themesDll = Join-Path $bin 'Krypton.Themes.dll'
+if (-not (Test-Path -LiteralPath $themesDll)) {
+ throw "Missing $themesDll - build Krypton.Themes Debug $TargetFramework first."
+}
+[void][System.Reflection.Assembly]::LoadFrom($themesDll)
$utilities = [System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Toolkit.Utilities.dll'))
[System.Windows.Forms.Application]::EnableVisualStyles()
@@ -48,39 +54,64 @@ $important = [Enum]::Parse($severityType, 'Important')
$recommended = [Enum]::Parse($severityType, 'Recommended')
$installer = [Enum]::Parse($kindType, 'Installer')
$hash = 'a' * 64
-$ctor = $packageType.GetConstructors() | Where-Object { $_.GetParameters().Count -ge 9 } | Select-Object -First 1
+$ctor = $packageType.GetConstructors() | Sort-Object { $_.GetParameters().Count } -Descending | Select-Object -First 1
+$bannerType = $utilities.GetType('Krypton.Toolkit.Utilities.KryptonUpdateBanner')
+$pulseType = $utilities.GetType('Krypton.Toolkit.Utilities.KryptonUpdatePulse')
+$toolkit = [System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Toolkit.dll'))
+$pulseStyleType = $toolkit.GetType('Krypton.Toolkit.InputPulsingBorderStyle')
+$demoBanner = [System.Activator]::CreateInstance(
+ $bannerType,
+ @('Security update — install now', [System.Drawing.Color]::FromArgb(0x8B, 0, 0), [System.Drawing.Color]::White, $null, [System.Drawing.Color]::FromArgb(0xFF, 0x45, 0)))
+$demoPulse = [System.Activator]::CreateInstance(
+ $pulseType,
+ @($true, [System.Drawing.Color]::FromArgb(0xFF, 0x45, 0), [Enum]::Parse($pulseStyleType, 'All'), [float]1.5))
-function New-Package([string]$version, $severity, [string]$notes) {
+function New-Package([string]$version, $severity, [string]$notes, $banner = $null, $pulse = $null) {
$released = [Nullable[datetime]]([datetime]::SpecifyKind([datetime]'2026-11-03T12:00:00', [DateTimeKind]::Utc))
- $invokeArgs = @(
- [version]$version,
- $severity,
- $installer,
- 'https://example.com/setup-9.9.9.exe',
- $hash,
- $null,
- $notes,
- $null,
- $released)
- while ($invokeArgs.Count -lt $ctor.GetParameters().Count) {
- $invokeArgs += [Type]::Missing
+ $invokeArgs = [System.Collections.Generic.List[object]]::new()
+ [void]$invokeArgs.Add([version]$version)
+ [void]$invokeArgs.Add($severity)
+ [void]$invokeArgs.Add($installer)
+ [void]$invokeArgs.Add('https://example.com/setup-9.9.9.exe')
+ [void]$invokeArgs.Add($hash)
+ [void]$invokeArgs.Add($null)
+ [void]$invokeArgs.Add($notes)
+ [void]$invokeArgs.Add($null)
+ [void]$invokeArgs.Add($released)
+ while ($invokeArgs.Count -lt ($ctor.GetParameters().Count - 2)) {
+ [void]$invokeArgs.Add([Type]::Missing)
}
- return $ctor.Invoke($invokeArgs)
+ [void]$invokeArgs.Add($banner)
+ [void]$invokeArgs.Add($pulse)
+ return $ctor.Invoke($invokeArgs.ToArray())
}
-$newer = New-Package '9.9.9' $important 'Newest local package. Severity is Important.'
+$newer = New-Package '9.9.9' $important 'Newest local package. Severity is Important.' $demoBanner $demoPulse
$older = New-Package '1.5.0' $recommended 'Earlier local package. Severity is Recommended.'
$listType = [System.Collections.Generic.List``1].MakeGenericType($packageType)
$notes = [System.Activator]::CreateInstance($listType)
[void]$notes.Add($newer)
[void]$notes.Add($older)
+$promptType = $utilities.GetType('Krypton.Toolkit.Utilities.VisualApplicationUpdatePromptForm')
+$notesType = $utilities.GetType('Krypton.Toolkit.Utilities.VisualApplicationUpdateNotesForm')
+$postponeType = $utilities.GetType('Krypton.Toolkit.Utilities.VisualApplicationUpdatePostponeForm')
+$progressType = $utilities.GetType('Krypton.Toolkit.Utilities.VisualApplicationUpdateProgressForm')
+
+$themes = @(
+ @{ Mode = [Krypton.Toolkit.PaletteMode]::Microsoft365Blue; Suffix = 'light' },
+ @{ Mode = [Krypton.Toolkit.PaletteMode]::Microsoft365BlackDarkMode; Suffix = 'dark' }
+)
+
+$manager = New-Object Krypton.Toolkit.KryptonManager
+
function Show-Shot($form, [string]$path) {
$form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
$form.Location = New-Object System.Drawing.Point 80, 80
$form.TopMost = $true
$form.Show()
+ $form.Refresh()
$form.Activate()
$form.BringToFront()
[void][UnitTestNative]::SetForegroundWindow($form.Handle)
@@ -93,46 +124,48 @@ function Show-Shot($form, [string]$path) {
Write-Output "Wrote $path"
}
-$promptType = $utilities.GetType('Krypton.Toolkit.Utilities.VisualApplicationUpdatePromptForm')
-$prompt = [System.Activator]::CreateInstance($promptType)
-[void]$promptType.GetMethod('Bind', $flags).Invoke($prompt, @($newer, [version]'1.0.0', $notes, $strings, $true, $true, 7))
-Show-Shot $prompt (Join-Path $pr '4669-application-updater-prompt.png')
+foreach ($theme in $themes) {
+ $manager.GlobalPaletteMode = $theme.Mode
+ $suffix = $theme.Suffix
-$notesType = $utilities.GetType('Krypton.Toolkit.Utilities.VisualApplicationUpdateNotesForm')
-$notesForm = [System.Activator]::CreateInstance($notesType)
-[void]$notesType.GetMethod('Bind', $flags).Invoke($notesForm, @($newer, [version]'1.0.0', $notes, $strings, $true, $true, $true, 7, $null))
-Show-Shot $notesForm (Join-Path $pr '4669-application-updater-notes.png')
+ $prompt = [System.Activator]::CreateInstance($promptType)
+ [void]$promptType.GetMethod('Bind', $flags).Invoke($prompt, @($newer, [version]'1.0.0', $notes, $strings, $true, $true, 7, $null))
+ Show-Shot $prompt (Join-Path $pr "4669-application-updater-prompt-$suffix.png")
-$postponeType = $utilities.GetType('Krypton.Toolkit.Utilities.VisualApplicationUpdatePostponeForm')
-$postpone = [System.Activator]::CreateInstance($postponeType)
-[void]$postponeType.GetMethod('Bind', $flags).Invoke($postpone, @($strings, 7, 365, $true))
-Show-Shot $postpone (Join-Path $pr '4669-application-updater-postpone.png')
+ $notesForm = [System.Activator]::CreateInstance($notesType)
+ [void]$notesType.GetMethod('Bind', $flags).Invoke($notesForm, @($newer, [version]'1.0.0', $notes, $strings, $true, $true, $true, 7, $null, $null))
+ Show-Shot $notesForm (Join-Path $pr "4669-application-updater-notes-$suffix.png")
-$progressType = $utilities.GetType('Krypton.Toolkit.Utilities.VisualApplicationUpdateProgressForm')
-$progress = [System.Activator]::CreateInstance($progressType)
-[void]$progressType.GetMethod('Bind', $flags).Invoke($progress, @($strings))
-[void]$progressType.GetMethod('SetFileName', $flags).Invoke($progress, @('setup-9.9.9.exe'))
-$progress.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
-$progress.Location = New-Object System.Drawing.Point 80, 80
-$progress.TopMost = $true
-$progress.Show()
-$progress.Activate()
-$progress.BringToFront()
-[void][UnitTestNative]::SetForegroundWindow($progress.Handle)
-[System.Windows.Forms.Application]::DoEvents()
-$progressArgs = [object[]]::new(2)
-$progressArgs[0] = [int64]4420000
-$progressArgs[1] = [Nullable[int64]]10000000
-[void]$progressType.GetMethod('SetProgress', $flags).Invoke($progress, $progressArgs)
-Start-Sleep -Milliseconds 400
-$progressArgs[0] = [int64]4500000
-[void]$progressType.GetMethod('SetProgress', $flags).Invoke($progress, $progressArgs)
-[void]$progressType.GetMethod('OnDetails', $flags).Invoke($progress, @($null, [System.EventArgs]::Empty))
-[System.Windows.Forms.Application]::DoEvents()
-Start-Sleep -Milliseconds 300
-[System.Windows.Forms.Application]::DoEvents()
-$detailsPath = Join-Path $pr '4669-application-updater-details.png'
-Save-UnitTestWindowPng -Form $progress -Path $detailsPath -SettleMs 200
-$progress.Close()
-$progress.Dispose()
-Write-Output "Wrote $detailsPath"
+ $postpone = [System.Activator]::CreateInstance($postponeType)
+ [void]$postponeType.GetMethod('Bind', $flags).Invoke($postpone, @($strings, 7, 365, $true))
+ Show-Shot $postpone (Join-Path $pr "4669-application-updater-postpone-$suffix.png")
+
+ $progress = [System.Activator]::CreateInstance($progressType)
+ [void]$progressType.GetMethod('Bind', $flags).Invoke($progress, @($strings, $demoPulse))
+ [void]$progressType.GetMethod('SetFileName', $flags).Invoke($progress, @('setup-9.9.9.exe'))
+ $progress.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
+ $progress.Location = New-Object System.Drawing.Point 80, 80
+ $progress.TopMost = $true
+ $progress.Show()
+ $progress.Refresh()
+ $progress.Activate()
+ $progress.BringToFront()
+ [void][UnitTestNative]::SetForegroundWindow($progress.Handle)
+ [System.Windows.Forms.Application]::DoEvents()
+ $progressArgs = [object[]]::new(2)
+ $progressArgs[0] = [int64]4420000
+ $progressArgs[1] = [Nullable[int64]]10000000
+ [void]$progressType.GetMethod('SetProgress', $flags).Invoke($progress, $progressArgs)
+ Start-Sleep -Milliseconds 400
+ $progressArgs[0] = [int64]4500000
+ [void]$progressType.GetMethod('SetProgress', $flags).Invoke($progress, $progressArgs)
+ [void]$progressType.GetMethod('OnDetails', $flags).Invoke($progress, @($null, [System.EventArgs]::Empty))
+ [System.Windows.Forms.Application]::DoEvents()
+ Start-Sleep -Milliseconds 300
+ [System.Windows.Forms.Application]::DoEvents()
+ $detailsPath = Join-Path $pr "4669-application-updater-details-$suffix.png"
+ Save-UnitTestWindowPng -Form $progress -Path $detailsPath -SettleMs 200
+ $progress.Close()
+ $progress.Dispose()
+ Write-Output "Wrote $detailsPath"
+}
diff --git a/Scripts/UnitTests/Invoke-UpdateManifestEditorScreenshot.ps1 b/Scripts/UnitTests/Invoke-UpdateManifestEditorScreenshot.ps1
index 3163ea7034..cc694315d1 100644
--- a/Scripts/UnitTests/Invoke-UpdateManifestEditorScreenshot.ps1
+++ b/Scripts/UnitTests/Invoke-UpdateManifestEditorScreenshot.ps1
@@ -1,11 +1,11 @@
<#
.SYNOPSIS
- Hosts the update manifest editor and captures a filled package.
+ Hosts the update manifest editor and captures a filled package in light and dark themes.
.DESCRIPTION
- Loads the Debug net472 UpdateManifestEditor build in-process (STA), fills one
- sample package, and writes Documents/PR/4669-update-manifest-editor.png with
- PrintWindow so the IDE cannot cover the form.
+ Loads the Debug net472 UpdateManifestEditor build in-process (STA), fills one sample package,
+ applies Microsoft365Blue and Microsoft365BlackDarkMode, and writes
+ Documents/PR/4669-update-manifest-editor-light.png and *-dark.png with PrintWindow.
# UnitTest-CI: exclude
@@ -17,7 +17,7 @@ param(
[string]$Configuration = 'Debug',
[string]$TargetFramework = 'net472',
[string]$BinDir,
- [string]$OutputPath
+ [string]$OutputDirectory
)
$ErrorActionPreference = 'Stop'
@@ -25,8 +25,11 @@ $ErrorActionPreference = 'Stop'
$repoRoot = Get-UnitTestRepoRoot
$bin = Get-UnitTestBinDir -RepoRoot $repoRoot -Configuration $Configuration -TargetFramework $TargetFramework -BinDir $BinDir
-if (-not $OutputPath) {
- $OutputPath = Join-Path $repoRoot 'Documents\PR\4669-update-manifest-editor.png'
+if (-not $OutputDirectory) {
+ $OutputDirectory = Join-Path $repoRoot 'Documents\PR'
+}
+if (-not (Test-Path -LiteralPath $OutputDirectory)) {
+ New-Item -ItemType Directory -Path $OutputDirectory | Out-Null
}
$samplePath = Join-Path ([System.IO.Path]::GetTempPath()) 'krypton-update-sample.txt'
@@ -40,6 +43,11 @@ Initialize-UnitTestNativeInput
[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Interop.dll'))
[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Resources.dll'))
[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Toolkit.dll'))
+$themesDll = Join-Path $bin 'Krypton.Themes.dll'
+if (-not (Test-Path -LiteralPath $themesDll)) {
+ throw "Missing $themesDll - build Krypton.Themes Debug $TargetFramework first."
+}
+[void][System.Reflection.Assembly]::LoadFrom($themesDll)
$editorPath = Join-Path $bin 'UpdateManifestEditor.exe'
$asm = [System.Reflection.Assembly]::LoadFrom($editorPath)
@@ -50,38 +58,50 @@ if (-not $formType) {
}
$flags = [System.Reflection.BindingFlags]'Instance,NonPublic'
-$form = [System.Activator]::CreateInstance($formType)
-$packages = $formType.GetField('_packages', $flags).GetValue($form)
-$package = $packages[0]
-$package.Version = '9.9.9'
-$package.Severity = 'important'
-$package.PackageKind = 'installer'
-$package.PackageFile = $samplePath
-$package.PackageUrl = 'https://example.com/setup.exe'
-$package.Sha256 = $hash
-$package.InstallerArguments = '/quiet'
-$package.ReleaseNotes = 'Sample package for the manifest editor.'
-$package.ReleasedUtc = '2026-11-03T12:00:00.0000000Z'
-$package.MaximumPostponeDays = '14'
-$package.AllowPostpone = 'false'
-$package.AllowSkip = 'true'
-$package.AllowResume = 'true'
-$package.AllowRetry = 'false'
-[void]$formType.GetMethod('ShowSelection', $flags).Invoke($form, @())
-[void]$formType.GetMethod('RefreshListItem', $flags).Invoke($form, @())
-[void]$formType.GetMethod('UpdatePreview', $flags).Invoke($form, @())
+$manager = New-Object Krypton.Toolkit.KryptonManager
+$themes = @(
+ @{ Mode = [Krypton.Toolkit.PaletteMode]::Microsoft365Blue; Suffix = 'light' },
+ @{ Mode = [Krypton.Toolkit.PaletteMode]::Microsoft365BlackDarkMode; Suffix = 'dark' }
+)
-$form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
-$form.Location = New-Object System.Drawing.Point 80, 80
-$form.TopMost = $true
-$form.Show()
-$form.Activate()
-$form.BringToFront()
-[void][UnitTestNative]::SetForegroundWindow($form.Handle)
-[System.Windows.Forms.Application]::DoEvents()
-Start-Sleep -Milliseconds 500
-[System.Windows.Forms.Application]::DoEvents()
+foreach ($theme in $themes) {
+ $manager.GlobalPaletteMode = $theme.Mode
+ $form = [System.Activator]::CreateInstance($formType)
+ $packages = $formType.GetField('_packages', $flags).GetValue($form)
+ $package = $packages[0]
+ $package.Version = '9.9.9'
+ $package.Severity = 'important'
+ $package.PackageKind = 'installer'
+ $package.PackageFile = $samplePath
+ $package.PackageUrl = 'https://example.com/setup.exe'
+ $package.Sha256 = $hash
+ $package.InstallerArguments = '/quiet'
+ $package.ReleaseNotes = 'Sample package for the manifest editor.'
+ $package.ReleasedUtc = '2026-11-03T12:00:00.0000000Z'
+ $package.MaximumPostponeDays = '14'
+ $package.AllowPostpone = 'false'
+ $package.AllowSkip = 'true'
+ $package.AllowResume = 'true'
+ $package.AllowRetry = 'false'
+ [void]$formType.GetMethod('ShowSelection', $flags).Invoke($form, @())
+ [void]$formType.GetMethod('RefreshListItem', $flags).Invoke($form, @())
+ [void]$formType.GetMethod('UpdatePreview', $flags).Invoke($form, @())
-Save-UnitTestWindowPng -Form $form -Path $OutputPath
-$form.Close()
-Write-Output $OutputPath
+ $form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
+ $form.Location = New-Object System.Drawing.Point 80, 80
+ $form.TopMost = $true
+ $form.Show()
+ $form.Refresh()
+ $form.Activate()
+ $form.BringToFront()
+ [void][UnitTestNative]::SetForegroundWindow($form.Handle)
+ [System.Windows.Forms.Application]::DoEvents()
+ Start-Sleep -Milliseconds 500
+ [System.Windows.Forms.Application]::DoEvents()
+
+ $outputPath = Join-Path $OutputDirectory "4669-update-manifest-editor-$($theme.Suffix).png"
+ Save-UnitTestWindowPng -Form $form -Path $outputPath
+ $form.Close()
+ $form.Dispose()
+ Write-Output "Wrote $outputPath"
+}
diff --git a/Scripts/UnitTests/README.md b/Scripts/UnitTests/README.md
index fa41cf10f6..dfee0ee31e 100644
--- a/Scripts/UnitTests/README.md
+++ b/Scripts/UnitTests/README.md
@@ -108,9 +108,9 @@ Default output folder: `Bin\Debug\net472`.
| `Invoke-SvgPictureBoxScreenshot.ps1` | Hosts `KryptonSVGPictureBoxDemo` (#4657) and writes `Documents/PR/4657-krypton-svg-picture-box.png`. | `exclude` |
| `Invoke-FileOperationDialogScreenshot.ps1` | #4672 copies, conflicts, and deletes through `KryptonFileOperation`, and writes light (`Microsoft365Blue`) / dark (`Microsoft365BlackDarkMode`) progress, conflict, confirmation, and demo PNGs under `Documents/PR/` | `exclude` |
| `Invoke-ApplicationUpdaterScreenshot.ps1` | Hosts `ApplicationUpdaterDemo` (#4669), captures the update prompt and the up-to-date notes window, and checks a local zip extract | `exclude` |
-| `Invoke-ApplicationUpdaterRecoveryScreenshot.ps1` | Shows the updater progress window after an interrupted download (#4669) and writes `Documents/PR/4669-application-updater-recovery.png` | `exclude` |
-| `Invoke-ApplicationUpdaterWindowScreenshot.ps1` | Captures the compact update prompt, release notes, remind-later dialog, and expanded progress footer (#4669) | `exclude` |
-| `Invoke-UpdateManifestEditorScreenshot.ps1` | Hosts the update manifest editor and writes `Documents/PR/4669-update-manifest-editor.png` | `exclude` |
+| `Invoke-ApplicationUpdaterRecoveryScreenshot.ps1` | Shows the updater progress window after an interrupted download (#4669) and writes light/dark `Documents/PR/4669-application-updater-recovery-*.png` | `exclude` |
+| `Invoke-ApplicationUpdaterWindowScreenshot.ps1` | Captures the compact update prompt, release notes, remind-later dialog, and expanded progress footer (#4669) in Microsoft 365 Blue / Black Dark Mode | `exclude` |
+| `Invoke-UpdateManifestEditorScreenshot.ps1` | Hosts the update manifest editor and writes light/dark `Documents/PR/4669-update-manifest-editor-*.png` | `exclude` |
| `Invoke-SnapLayoutOverlayScreenshot.ps1` | Compiles a net472 host beside the Debug `Krypton.Toolkit` build, shows a `KryptonForm`, opens the palette snap menu (#4579), and writes `Documents/PR/4579-snap-layout-overlay.png`. `-CustomCard` adds a 70/30 card and writes `Documents/PR/custom-snap-layouts-menu.png`. `-Ghost` enables `ShowPreviewGhost` and writes `Documents/PR/4579-snap-preview-ghost.png` | `exclude` |
| `Invoke-SnapGroupsAssistScreenshot.ps1` | Two `KryptonForm`s: registers a half/half group and captures the menu + `Group | …` caption; then captures the Assist peer picker. Writes `Documents/PR/4579-snap-groups-assist-group.png` and `…-assist.png` | `exclude` |
| `Invoke-SearchBoxPerformanceScreenshot.ps1` | Hosts `KryptonSearchBoxPerformanceDemo` (#4580) and writes `Documents/PR/4580-searchbox-performance-demo.png` | `exclude` || `Invoke-DialogDpiScreenshot.ps1` | #4465 captures the long input prompt and exception dialog with `Save-UnitTestWindowPng` | `exclude` |
diff --git a/Scripts/UnitTests/UnitTest-ApplicationUpdaterBannerPulse.ps1 b/Scripts/UnitTests/UnitTest-ApplicationUpdaterBannerPulse.ps1
new file mode 100644
index 0000000000..ebb3f6d4ab
--- /dev/null
+++ b/Scripts/UnitTests/UnitTest-ApplicationUpdaterBannerPulse.ps1
@@ -0,0 +1,43 @@
+<#
+.SYNOPSIS
+ Smoke-tests banner/pulse manifest parse.
+
+.DESCRIPTION
+ # UnitTest-CI: exclude
+#>
+[CmdletBinding()]
+param()
+
+$ErrorActionPreference = 'Stop'
+. (Join-Path $PSScriptRoot 'UnitTestCommon.ps1')
+$repoRoot = Get-UnitTestRepoRoot
+$bin = Get-UnitTestBinDir -RepoRoot $repoRoot -Configuration Debug -TargetFramework net472
+Register-UnitTestAssemblyResolver -BinDir $bin
+[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Interop.dll'))
+[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Resources.dll'))
+[void][System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Toolkit.dll'))
+$utilities = [System.Reflection.Assembly]::LoadFrom((Join-Path $bin 'Krypton.Toolkit.Utilities.dll'))
+$parser = $utilities.GetType('Krypton.Toolkit.Utilities.KryptonUpdateManifestParser')
+$parse = $parser.GetMethod('Parse', [System.Reflection.BindingFlags]'NonPublic,Static')
+$json = '{"version":"9.9.9","severity":"important","packageKind":"installer","packageUrl":"https://example.com/a.exe","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","banner":{"text":"Hi","backColor":"#8B0000","foreColor":"#FFFFFF"},"pulse":{"enable":true,"color":"#FF4500","style":"All","speed":1.5}}'
+$catalog = $parse.Invoke($null, @($json))
+$package = $catalog.Packages[0]
+if ($package.Banner.Text -ne 'Hi') { throw 'banner text mismatch' }
+if (-not $package.Pulse.Enable) { throw 'pulse enable mismatch' }
+$bad = '{"version":"1.0.0","packageUrl":"https://example.com/a.exe","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","banner":{"imageUrl":"http://example.com/x.png"}}'
+try {
+ [void]$parse.Invoke($null, @($bad))
+ throw 'http imageUrl should fail'
+}
+catch {
+ $message = $_.Exception.Message
+ if ($_.Exception.InnerException) {
+ $message = $_.Exception.InnerException.Message
+ }
+
+ if ($message -notmatch 'https') {
+ throw
+ }
+}
+
+Write-Output 'PASS banner/pulse parse'
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton AboutBox/General/KryptonAboutBoxUtilities.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton AboutBox/General/KryptonAboutBoxUtilities.cs
index 4ae5a87354..ccdc4de8df 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton AboutBox/General/KryptonAboutBoxUtilities.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton AboutBox/General/KryptonAboutBoxUtilities.cs
@@ -161,7 +161,7 @@ public static DateTime GetBinaryBuildDateTime(Assembly assembly)
stream.Seek(lfanew + 8, SeekOrigin.Begin);
uint stamp = reader.ReadUInt32();
- if (stamp == 0 || stamp == uint.MaxValue)
+ if (stamp is 0 or uint.MaxValue)
{
return null;
}
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Toolkit/KryptonApplicationUpdater.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Toolkit/KryptonApplicationUpdater.cs
index a62773fda3..da59a2952c 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Toolkit/KryptonApplicationUpdater.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Toolkit/KryptonApplicationUpdater.cs
@@ -312,7 +312,7 @@ public Version GetCurrentVersion()
{
if (!KryptonUpdateVersion.TryParse(CurrentVersion, out var parsed))
{
- throw new KryptonUpdateException("CurrentVersion is not a System.Version value.");
+ KryptonUpdateThrowHelper.Throw("CurrentVersion is not a System.Version value.");
}
return parsed;
@@ -495,13 +495,13 @@ public async Task ApplyAsync(KryptonUpdatePackage package, string downloadedPath
var finished = await Task.Run(() => process.WaitForExit(10 * 60 * 1000), cancellationToken).ConfigureAwait(true);
if (!finished)
{
- throw new KryptonUpdateException("The update helper did not finish.");
+ KryptonUpdateThrowHelper.Throw("The update helper did not finish.");
}
if (process.ExitCode != 0)
{
var directory = Path.GetDirectoryName(process.StartInfo.FileName) ?? string.Empty;
- throw new KryptonUpdateException(KryptonUpdateClient.ReadHelperResult(directory));
+ KryptonUpdateThrowHelper.Throw(KryptonUpdateClient.ReadHelperResult(directory));
}
return;
@@ -731,7 +731,7 @@ private async Task DownloadAndApplyAsync(KryptonUpdatePackage package, Cancellat
Report(() =>
{
progressForm = new VisualApplicationUpdateProgressForm();
- progressForm.Bind(Strings);
+ progressForm.Bind(Strings, package.Pulse);
progressForm.SetFileName(PackageFileName(package.PackageUrl));
progressForm.CancelClicked += delegate { CancelDownload(); };
var owner = TryGetOwnerForm();
@@ -886,24 +886,33 @@ private KryptonUpdatePromptChoice ShowPrompt(KryptonUpdatePackage offer, Version
var choice = KryptonUpdatePromptChoice.Dismiss;
var chosen = TimeSpan.Zero;
var days = DefaultPostponeDays(offer.MaximumPostponeDays);
- Report(() =>
+ var bannerImage = LoadBannerImage(offer.Banner);
+ try
{
- using var form = new VisualApplicationUpdatePromptForm();
- var allowPostpone = AllowPostpone && offer.AllowsPostpone;
- form.Bind(offer, current, notes, Strings, allowPostpone, offer.AllowsSkip, days);
- var owner = TryGetOwnerForm();
- if (owner != null)
- {
- form.ShowDialog(owner);
- }
- else
+ Report(() =>
{
- form.ShowDialog();
- }
+ using var form = new VisualApplicationUpdatePromptForm();
+ var allowPostpone = AllowPostpone && offer.AllowsPostpone;
+ form.Bind(offer, current, notes, Strings, allowPostpone, offer.AllowsSkip, days, bannerImage);
+ var owner = TryGetOwnerForm();
+ if (owner != null)
+ {
+ form.ShowDialog(owner);
+ }
+ else
+ {
+ form.ShowDialog();
+ }
+
+ choice = form.Choice;
+ chosen = form.PostponeFor;
+ });
+ }
+ finally
+ {
+ bannerImage?.Dispose();
+ }
- choice = form.Choice;
- chosen = form.PostponeFor;
- });
postponeFor = chosen;
return choice;
}
@@ -915,27 +924,46 @@ private KryptonUpdatePromptChoice ShowNotes(KryptonUpdatePackage? offer, Version
var days = DefaultPostponeDays(offer != null ? offer.MaximumPostponeDays : KryptonUpdatePackage.DefaultMaximumPostponeDays);
var allowPostpone = offer != null && AllowPostpone && offer.AllowsPostpone;
var allowSkip = offer != null && offer.AllowsSkip;
- Report(() =>
+ var bannerImage = LoadBannerImage(offer?.Banner);
+ try
{
- using var form = new VisualApplicationUpdateNotesForm();
- form.Bind(offer, current, notes, Strings, allowApply, allowPostpone, allowSkip, days, failureMessage);
- var owner = TryGetOwnerForm();
- if (owner != null)
- {
- form.ShowDialog(owner);
- }
- else
+ Report(() =>
{
- form.ShowDialog();
- }
+ using var form = new VisualApplicationUpdateNotesForm();
+ form.Bind(offer, current, notes, Strings, allowApply, allowPostpone, allowSkip, days, failureMessage, bannerImage);
+ var owner = TryGetOwnerForm();
+ if (owner != null)
+ {
+ form.ShowDialog(owner);
+ }
+ else
+ {
+ form.ShowDialog();
+ }
+
+ choice = form.Choice;
+ chosen = form.PostponeFor;
+ });
+ }
+ finally
+ {
+ bannerImage?.Dispose();
+ }
- choice = form.Choice;
- chosen = form.PostponeFor;
- });
postponeFor = chosen;
return choice;
}
+ private static Image? LoadBannerImage(KryptonUpdateBanner? banner)
+ {
+ if (banner == null || banner.ImageUrl == null || string.IsNullOrWhiteSpace(banner.ImageUrl))
+ {
+ return null;
+ }
+
+ return KryptonUpdatePresentation.LoadBannerImageAsync(banner.ImageUrl).ConfigureAwait(false).GetAwaiter().GetResult();
+ }
+
private int DefaultPostponeDays(int maximumDays)
{
var limit = maximumDays < 1 ? 1 : maximumDays;
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Visuals/VisualApplicationUpdateNotesForm.Designer.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Visuals/VisualApplicationUpdateNotesForm.Designer.cs
index e924f8b719..540ac3d331 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Visuals/VisualApplicationUpdateNotesForm.Designer.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Visuals/VisualApplicationUpdateNotesForm.Designer.cs
@@ -36,11 +36,17 @@ private void InitializeComponent()
this.lblHeading = new Krypton.Toolkit.KryptonLabel();
this.lblCurrent = new Krypton.Toolkit.KryptonLabel();
this.txtNotes = new Krypton.Toolkit.KryptonTextBox();
+ this.bannerPanel = new Krypton.Toolkit.KryptonPanel();
+ this.picBanner = new System.Windows.Forms.PictureBox();
+ this.lblBanner = new Krypton.Toolkit.KryptonLabel();
((System.ComponentModel.ISupportInitialize)(this.kryptonPanel1)).BeginInit();
this.kryptonPanel1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.kryptonPanel2)).BeginInit();
this.kryptonPanel2.SuspendLayout();
this.table.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.bannerPanel)).BeginInit();
+ this.bannerPanel.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.picBanner)).BeginInit();
this.SuspendLayout();
//
// btnUpdate
@@ -164,6 +170,39 @@ private void InitializeComponent()
this.txtNotes.TabIndex = 2;
this.txtNotes.TabStop = false;
//
+ // bannerPanel
+ //
+ this.bannerPanel.Controls.Add(this.lblBanner);
+ this.bannerPanel.Controls.Add(this.picBanner);
+ this.bannerPanel.Dock = System.Windows.Forms.DockStyle.Top;
+ this.bannerPanel.Location = new System.Drawing.Point(0, 0);
+ this.bannerPanel.Name = "bannerPanel";
+ this.bannerPanel.Padding = new System.Windows.Forms.Padding(12, 8, 12, 8);
+ this.bannerPanel.Size = new System.Drawing.Size(640, 0);
+ this.bannerPanel.TabIndex = 4;
+ this.bannerPanel.Visible = false;
+ //
+ // picBanner
+ //
+ this.picBanner.Dock = System.Windows.Forms.DockStyle.Left;
+ this.picBanner.Location = new System.Drawing.Point(12, 8);
+ this.picBanner.Name = "picBanner";
+ this.picBanner.Size = new System.Drawing.Size(32, 32);
+ this.picBanner.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom;
+ this.picBanner.TabIndex = 0;
+ this.picBanner.TabStop = false;
+ this.picBanner.Visible = false;
+ //
+ // lblBanner
+ //
+ this.lblBanner.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.lblBanner.LabelStyle = Krypton.Toolkit.LabelStyle.BoldPanel;
+ this.lblBanner.Location = new System.Drawing.Point(44, 8);
+ this.lblBanner.Name = "lblBanner";
+ this.lblBanner.Size = new System.Drawing.Size(584, 32);
+ this.lblBanner.TabIndex = 1;
+ this.lblBanner.Values.Text = "";
+ //
// VisualApplicationUpdateNotesForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
@@ -171,6 +210,7 @@ private void InitializeComponent()
this.ClientSize = new System.Drawing.Size(640, 420);
this.Controls.Add(this.kryptonPanel2);
this.Controls.Add(this.kryptonPanel1);
+ this.Controls.Add(this.bannerPanel);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
@@ -185,6 +225,10 @@ private void InitializeComponent()
this.kryptonPanel2.ResumeLayout(false);
this.table.ResumeLayout(false);
this.table.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.bannerPanel)).EndInit();
+ this.bannerPanel.ResumeLayout(false);
+ this.bannerPanel.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.picBanner)).EndInit();
this.ResumeLayout(false);
}
@@ -199,4 +243,7 @@ private void InitializeComponent()
private KryptonLabel lblHeading;
private KryptonLabel lblCurrent;
private KryptonTextBox txtNotes;
+ private KryptonPanel bannerPanel;
+ private System.Windows.Forms.PictureBox picBanner;
+ private KryptonLabel lblBanner;
}
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Visuals/VisualApplicationUpdateNotesForm.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Visuals/VisualApplicationUpdateNotesForm.cs
index 7ceb39be6e..7f32a37cbe 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Visuals/VisualApplicationUpdateNotesForm.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Visuals/VisualApplicationUpdateNotesForm.cs
@@ -19,11 +19,13 @@ internal partial class VisualApplicationUpdateNotesForm : KryptonForm
private KryptonApplicationUpdaterStrings _strings = new KryptonApplicationUpdaterStrings();
private int _defaultDays = 1;
private bool _allowSkip;
+ private int _baseClientHeight;
/// Initializes the notes window.
public VisualApplicationUpdateNotesForm()
{
InitializeComponent();
+ _baseClientHeight = ClientSize.Height;
btnUpdate.Click += OnUpdateNow;
btnRemind.Click += OnRemind;
btnSkip.Click += OnSkip;
@@ -46,7 +48,8 @@ public VisualApplicationUpdateNotesForm()
/// When false, Skip this version is hidden. When postpone is hidden, Skip is shown on this window.
/// Initial day count for the remind-later dialog.
/// When set, replaces the notes and hides Update now.
- internal void Bind(KryptonUpdatePackage? offer, Version current, IReadOnlyList notes, KryptonApplicationUpdaterStrings strings, bool allowApply, bool allowPostpone, bool allowSkip, int defaultDays, string? failureMessage)
+ /// Optional banner image already loaded for .
+ internal void Bind(KryptonUpdatePackage? offer, Version current, IReadOnlyList notes, KryptonApplicationUpdaterStrings strings, bool allowApply, bool allowPostpone, bool allowSkip, int defaultDays, string? failureMessage, Image? bannerImage = null)
{
_offer = offer;
_strings = strings ?? new KryptonApplicationUpdaterStrings();
@@ -67,6 +70,9 @@ internal void Bind(KryptonUpdatePackage? offer, Version current, IReadOnlyListApplies localised labels and shows Cancel for the first transfer.
/// Prompt text.
- internal void Bind(KryptonApplicationUpdaterStrings strings)
+ /// Optional form chrome glow from the package.
+ internal void Bind(KryptonApplicationUpdaterStrings strings, KryptonUpdatePulse? pulse = null)
{
_strings = strings ?? new KryptonApplicationUpdaterStrings();
Text = _strings.WindowTitle;
@@ -63,6 +64,7 @@ internal void Bind(KryptonApplicationUpdaterStrings strings)
btnClose.Text = _strings.Close;
ApplyDetailsButton();
SetFileName(_fileName);
+ KryptonUpdatePresentation.ApplyPulse(this, pulse);
BeginTransfer(_strings.Downloading);
}
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Visuals/VisualApplicationUpdatePromptForm.Designer.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Visuals/VisualApplicationUpdatePromptForm.Designer.cs
index 4035365801..18cdfb5272 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Visuals/VisualApplicationUpdatePromptForm.Designer.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Visuals/VisualApplicationUpdatePromptForm.Designer.cs
@@ -38,11 +38,17 @@ private void InitializeComponent()
this.btnSkip = new Krypton.Toolkit.KryptonButton();
this.kryptonPanel1 = new Krypton.Toolkit.KryptonPanel();
this.kryptonBorderEdge1 = new Krypton.Toolkit.KryptonBorderEdge();
+ this.bannerPanel = new Krypton.Toolkit.KryptonPanel();
+ this.picBanner = new System.Windows.Forms.PictureBox();
+ this.lblBanner = new Krypton.Toolkit.KryptonLabel();
((System.ComponentModel.ISupportInitialize)(this.kryptonPanel)).BeginInit();
this.kryptonPanel.SuspendLayout();
this.table.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.kryptonPanel1)).BeginInit();
this.kryptonPanel1.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.bannerPanel)).BeginInit();
+ this.bannerPanel.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.picBanner)).BeginInit();
this.SuspendLayout();
//
// kryptonPanel
@@ -185,6 +191,39 @@ private void InitializeComponent()
this.kryptonBorderEdge1.Size = new System.Drawing.Size(650, 1);
this.kryptonBorderEdge1.Text = "kryptonBorderEdge1";
//
+ // bannerPanel
+ //
+ this.bannerPanel.Controls.Add(this.lblBanner);
+ this.bannerPanel.Controls.Add(this.picBanner);
+ this.bannerPanel.Dock = System.Windows.Forms.DockStyle.Top;
+ this.bannerPanel.Location = new System.Drawing.Point(0, 0);
+ this.bannerPanel.Name = "bannerPanel";
+ this.bannerPanel.Padding = new System.Windows.Forms.Padding(12, 8, 12, 8);
+ this.bannerPanel.Size = new System.Drawing.Size(650, 0);
+ this.bannerPanel.TabIndex = 3;
+ this.bannerPanel.Visible = false;
+ //
+ // picBanner
+ //
+ this.picBanner.Dock = System.Windows.Forms.DockStyle.Left;
+ this.picBanner.Location = new System.Drawing.Point(12, 8);
+ this.picBanner.Name = "picBanner";
+ this.picBanner.Size = new System.Drawing.Size(32, 32);
+ this.picBanner.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom;
+ this.picBanner.TabIndex = 0;
+ this.picBanner.TabStop = false;
+ this.picBanner.Visible = false;
+ //
+ // lblBanner
+ //
+ this.lblBanner.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.lblBanner.LabelStyle = Krypton.Toolkit.LabelStyle.BoldPanel;
+ this.lblBanner.Location = new System.Drawing.Point(44, 8);
+ this.lblBanner.Name = "lblBanner";
+ this.lblBanner.Size = new System.Drawing.Size(594, 32);
+ this.lblBanner.TabIndex = 1;
+ this.lblBanner.Values.Text = "";
+ //
// VisualApplicationUpdatePromptForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
@@ -192,6 +231,7 @@ private void InitializeComponent()
this.ClientSize = new System.Drawing.Size(650, 230);
this.Controls.Add(this.kryptonPanel);
this.Controls.Add(this.kryptonPanel1);
+ this.Controls.Add(this.bannerPanel);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
@@ -206,6 +246,10 @@ private void InitializeComponent()
((System.ComponentModel.ISupportInitialize)(this.kryptonPanel1)).EndInit();
this.kryptonPanel1.ResumeLayout(false);
this.kryptonPanel1.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.bannerPanel)).EndInit();
+ this.bannerPanel.ResumeLayout(false);
+ this.bannerPanel.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.picBanner)).EndInit();
this.ResumeLayout(false);
}
@@ -223,4 +267,7 @@ private void InitializeComponent()
private Krypton.Toolkit.KryptonButton btnSkip;
private KryptonPanel kryptonPanel1;
private KryptonBorderEdge kryptonBorderEdge1;
+ private KryptonPanel bannerPanel;
+ private System.Windows.Forms.PictureBox picBanner;
+ private KryptonLabel lblBanner;
}
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Visuals/VisualApplicationUpdatePromptForm.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Visuals/VisualApplicationUpdatePromptForm.cs
index b233c8f99b..61b3062bca 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Visuals/VisualApplicationUpdatePromptForm.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Controls Visuals/VisualApplicationUpdatePromptForm.cs
@@ -18,11 +18,14 @@ internal partial class VisualApplicationUpdatePromptForm : KryptonForm
private KryptonApplicationUpdaterStrings _strings = new KryptonApplicationUpdaterStrings();
private int _defaultDays = 1;
private bool _allowSkip;
+ private int _baseClientHeight;
+ private Image? _bannerImage;
/// Initializes the prompt.
public VisualApplicationUpdatePromptForm()
{
InitializeComponent();
+ _baseClientHeight = ClientSize.Height;
btnUpdate.Click += OnUpdateNow;
btnNotes.Click += OnNotes;
btnRemind.Click += OnRemind;
@@ -43,7 +46,8 @@ public VisualApplicationUpdatePromptForm()
/// When false, Remind later is hidden.
/// When false, Skip this version is hidden. When postpone is hidden, Skip is shown on this window.
/// Initial day count for the remind-later dialog.
- internal void Bind(KryptonUpdatePackage offer, Version current, IReadOnlyList notes, KryptonApplicationUpdaterStrings strings, bool allowPostpone, bool allowSkip, int defaultDays)
+ /// Optional banner image already loaded for .
+ internal void Bind(KryptonUpdatePackage offer, Version current, IReadOnlyList notes, KryptonApplicationUpdaterStrings strings, bool allowPostpone, bool allowSkip, int defaultDays, Image? bannerImage = null)
{
_offer = offer;
_current = current ?? new Version(0, 0, 0, 0);
@@ -51,6 +55,7 @@ internal void Bind(KryptonUpdatePackage offer, Version current, IReadOnlyListOptional text strip shown at the top of the update prompt and notes windows.
+public sealed class KryptonUpdateBanner
+{
+ /// Initializes a banner.
+ /// Caption. May be empty when only an image is shown.
+ /// First panel colour. Empty keeps the theme default.
+ /// Caption colour. Empty keeps the theme default.
+ /// HTTPS address or local image path. Null when there is no image.
+ /// Second panel colour for a linear gradient. Empty uses a solid .
+ public KryptonUpdateBanner(string? text, Color backColor, Color foreColor, string? imageUrl, Color backColor2 = default)
+ {
+ Text = text ?? string.Empty;
+ BackColor = backColor;
+ BackColor2 = backColor2;
+ ForeColor = foreColor;
+ if (imageUrl != null && !string.IsNullOrWhiteSpace(imageUrl))
+ {
+ ImageUrl = imageUrl.Trim();
+ }
+ }
+
+ /// Gets the caption. Empty when only an image is shown.
+ public string Text { get; }
+
+ /// Gets the first panel colour. Empty keeps the theme default.
+ public Color BackColor { get; }
+
+ /// Gets the second panel colour for a linear gradient. Empty draws a solid .
+ public Color BackColor2 { get; }
+
+ /// Gets the caption colour. Empty keeps the theme default.
+ public Color ForeColor { get; }
+
+ /// Gets the HTTPS address or local image path, or null.
+ public string? ImageUrl { get; }
+
+ /// Gets whether the banner has text or an image to show.
+ public bool HasContent =>
+ (Text != null && !string.IsNullOrWhiteSpace(Text)) ||
+ (ImageUrl != null && !string.IsNullOrWhiteSpace(ImageUrl));
+}
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/General/KryptonUpdateClient.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/General/KryptonUpdateClient.cs
index 4616d58262..97b74fca2a 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/General/KryptonUpdateClient.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/General/KryptonUpdateClient.cs
@@ -62,13 +62,47 @@ internal static async Task ReadTextAsync(string location, string? bearer
if (!IsHttps(location))
{
- throw new KryptonUpdateException("The update address must be https or a local file.");
+ KryptonUpdateThrowHelper.Throw("The update address must be https or a local file.");
}
using var response = await SendAsync(location, bearerToken, accept, null, cancellationToken).ConfigureAwait(false);
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
+ /// Downloads at most from an https address. Throws when the response is larger.
+ /// HTTPS address.
+ /// Upper bound for the body.
+ /// Cancels the copy.
+ internal static async Task DownloadBytesAsync(string location, int maxBytes, CancellationToken cancellationToken)
+ {
+ if (!IsHttps(location))
+ {
+ KryptonUpdateThrowHelper.Throw("The banner image address must be https or a local file.");
+ }
+
+ using var response = await SendAsync(location, null, null, null, cancellationToken).ConfigureAwait(false);
+ if (response.Content.Headers.ContentLength.HasValue && response.Content.Headers.ContentLength.Value > maxBytes)
+ {
+ KryptonUpdateThrowHelper.Throw("The banner image is larger than " + maxBytes + " bytes.");
+ }
+
+ using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ using var buffer = new MemoryStream(Math.Min(maxBytes, 64 * 1024));
+ var chunk = new byte[8192];
+ int read;
+ while ((read = await stream.ReadAsync(chunk, 0, chunk.Length, cancellationToken).ConfigureAwait(false)) > 0)
+ {
+ if (buffer.Length + read > maxBytes)
+ {
+ KryptonUpdateThrowHelper.Throw("The banner image is larger than " + maxBytes + " bytes.");
+ }
+
+ buffer.Write(chunk, 0, read);
+ }
+
+ return buffer.ToArray();
+ }
+
internal static async Task DownloadPackageAsync(string location, IProgress? progress, CancellationToken cancellationToken)
{
var destination = CreateDownloadPath(location);
@@ -107,7 +141,7 @@ internal static async Task DownloadToAsync(string location, string destination,
if (!IsHttps(location))
{
- throw new KryptonUpdateException("The package address must be https or a local file.");
+ KryptonUpdateThrowHelper.Throw("The package address must be https or a local file.");
}
long existing = 0;
@@ -149,7 +183,7 @@ internal static async Task DownloadToAsync(string location, string destination,
if (response.StatusCode != HttpStatusCode.OK &&
response.StatusCode != HttpStatusCode.PartialContent)
{
- throw new KryptonUpdateException("The server returned " + (int)response.StatusCode + ".");
+ KryptonUpdateThrowHelper.Throw("The server returned " + (int)response.StatusCode + ".");
}
}
@@ -173,7 +207,7 @@ internal static void Verify(string path, string sha256, string? thumbprint)
if (!FixedTimeEquals(expected, actual))
{
- throw new KryptonUpdateException("The downloaded file does not match the expected SHA-256 hash.");
+ KryptonUpdateThrowHelper.Throw("The downloaded file does not match the expected SHA-256 hash.");
}
if (thumbprint == null || string.IsNullOrWhiteSpace(thumbprint))
@@ -191,14 +225,14 @@ internal static void Verify(string path, string sha256, string? thumbprint)
}
catch (Exception exception)
{
- throw new KryptonUpdateException("The downloaded file is not Authenticode-signed.", exception);
+ certificate = KryptonUpdateThrowHelper.Throw("The downloaded file is not Authenticode-signed.", exception);
}
var actualThumb = (certificate.Thumbprint ?? string.Empty).Replace(" ", string.Empty);
var expectedThumb = thumbprint.Replace(" ", string.Empty);
if (!string.Equals(actualThumb, expectedThumb, StringComparison.OrdinalIgnoreCase))
{
- throw new KryptonUpdateException("The Authenticode certificate thumbprint does not match the manifest.");
+ KryptonUpdateThrowHelper.Throw("The Authenticode certificate thumbprint does not match the manifest.");
}
}
@@ -226,7 +260,7 @@ internal static void StartInstaller(string path, string? arguments)
if (Process.Start(start) == null)
{
- throw new KryptonUpdateException("The installer did not start.");
+ KryptonUpdateThrowHelper.Throw("The installer did not start.");
}
}
@@ -247,7 +281,7 @@ internal static Process StartHelper(string zipPath, string destination, string?
});
if (process == null)
{
- throw new KryptonUpdateException("The update helper did not start.");
+ KryptonUpdateThrowHelper.Throw("The update helper did not start.");
}
return process;
@@ -310,7 +344,7 @@ private static async Task SendAsync(string location, string
response.Dispose();
if (next == null)
{
- throw new KryptonUpdateException("The server redirected without a location.");
+ return KryptonUpdateThrowHelper.Throw("The server redirected without a location.");
}
if (!next.IsAbsoluteUri)
@@ -320,7 +354,7 @@ private static async Task SendAsync(string location, string
if (!string.Equals(next.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
- throw new KryptonUpdateException("Refusing a redirect that leaves https.");
+ return KryptonUpdateThrowHelper.Throw("Refusing a redirect that leaves https.");
}
current = next;
@@ -330,13 +364,13 @@ private static async Task SendAsync(string location, string
if (!response.IsSuccessStatusCode)
{
response.Dispose();
- throw new KryptonUpdateException("The server returned " + status + ".", status);
+ return KryptonUpdateThrowHelper.Throw("The server returned " + status + ".", status);
}
return response;
}
- throw new KryptonUpdateException("The server redirected too many times.");
+ return KryptonUpdateThrowHelper.Throw("The server redirected too many times.");
}
internal static string CreateDownloadPath(string location)
@@ -375,7 +409,7 @@ private static string ExtractHelper()
using var stream = assembly.GetManifestResourceStream(HelperResourceName);
if (stream == null)
{
- throw new KryptonUpdateException("The update helper is not embedded in Krypton.Toolkit.Utilities. Rebuild the utilities project.");
+ KryptonUpdateThrowHelper.Throw("The update helper is not embedded in Krypton.Toolkit.Utilities. Rebuild the utilities project.");
}
var directory = Path.Combine(Path.GetTempPath(), "KryptonApplicationUpdater", Guid.NewGuid().ToString("N"));
@@ -439,7 +473,7 @@ private static byte[] ParseSha256(string sha256)
if (hex.Length != 64)
{
- throw new KryptonUpdateException("sha256 must be 64 hex characters.");
+ KryptonUpdateThrowHelper.Throw("sha256 must be 64 hex characters.");
}
var bytes = new byte[32];
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/General/KryptonUpdatePackage.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/General/KryptonUpdatePackage.cs
index bd448a0944..4923a31172 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/General/KryptonUpdatePackage.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/General/KryptonUpdatePackage.cs
@@ -35,6 +35,8 @@ public sealed class KryptonUpdatePackage
/// When null, a critical package cannot be skipped and every other severity can. When set, that choice wins.
/// When null, a partial download can be resumed. When false, Resume is hidden.
/// When null, a failed download can be started again. When false, Restart is hidden.
+ /// Optional prompt and notes banner. Null when the manifest omits it.
+ /// Optional form chrome glow. Null when the manifest omits it.
public KryptonUpdatePackage(
Version version,
KryptonUpdateSeverity severity,
@@ -49,12 +51,14 @@ public KryptonUpdatePackage(
bool? allowPostpone = null,
bool? allowSkip = null,
bool? allowResume = null,
- bool? allowRetry = null)
+ bool? allowRetry = null,
+ KryptonUpdateBanner? banner = null,
+ KryptonUpdatePulse? pulse = null)
{
ThrowHelper.ThrowIfNull(version);
if (maximumPostponeDays.HasValue && maximumPostponeDays.Value < 1)
{
- throw new ArgumentOutOfRangeException(nameof(maximumPostponeDays), maximumPostponeDays.Value, "maximumPostponeDays must be at least 1.");
+ ThrowHelper.ThrowArgumentOutOfRangeException(nameof(maximumPostponeDays), maximumPostponeDays.Value, "maximumPostponeDays must be at least 1.");
}
Version = version;
@@ -75,6 +79,8 @@ public KryptonUpdatePackage(
_allowSkip = allowSkip;
_allowResume = allowResume;
_allowRetry = allowRetry;
+ Banner = banner;
+ Pulse = pulse;
}
/// Gets the package version.
@@ -140,6 +146,12 @@ public KryptonUpdatePackage(
/// Gets the manifest allowRetry value, or null when the file omitted it.
internal bool? AllowRetrySetting => _allowRetry;
+ /// Gets the optional prompt and notes banner, or null when the manifest omits it.
+ public KryptonUpdateBanner? Banner { get; }
+
+ /// Gets the optional form chrome glow, or null when the manifest omits it.
+ public KryptonUpdatePulse? Pulse { get; }
+
/// Returns a copy that downloads from .
/// Resolved HTTPS address or local path.
/// A package with the same metadata and the new address.
@@ -158,7 +170,9 @@ public KryptonUpdatePackage WithPackageUrl(string packageUrl) =>
_allowPostpone,
_allowSkip,
_allowResume,
- _allowRetry);
+ _allowRetry,
+ Banner,
+ Pulse);
}
/// Packages returned by one feed check.
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/General/KryptonUpdatePresentation.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/General/KryptonUpdatePresentation.cs
new file mode 100644
index 0000000000..516dad26d3
--- /dev/null
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/General/KryptonUpdatePresentation.cs
@@ -0,0 +1,185 @@
+#region BSD License
+/*
+ *
+ * New BSD 3-Clause License (https://github.com/Krypton-Suite/Standard-Toolkit/blob/master/LICENSE)
+ * Modifications by Peter Wagner (aka Wagnerp), Simon Coghlan (aka Smurf-IV), Giduac & Ahmed Abdelhameed, tobitege et al. 2026 - 2026. All rights reserved.
+ *
+ */
+#endregion
+
+namespace Krypton.Toolkit.Utilities;
+
+/// Applies manifest banner and pulse settings to the update windows.
+internal static class KryptonUpdatePresentation
+{
+ private const int BannerHeight = 48;
+ private const int BannerImageSize = 32;
+ private const int MaxImageBytes = 512 * 1024;
+
+ /// Turns form chrome pulsing on or off from the package pulse object.
+ /// Prompt, notes, or progress window.
+ /// Manifest pulse, or null when omitted.
+ internal static void ApplyPulse(KryptonForm form, KryptonUpdatePulse? pulse)
+ {
+ if (form == null)
+ {
+ return;
+ }
+
+ var values = form.PulsingBorderValues;
+ if (pulse == null || !pulse.Enable)
+ {
+ values.Enable = false;
+ return;
+ }
+
+ values.Enable = true;
+ values.Animate = true;
+ values.ShowWhen = InputPulsingBorderShowWhen.Always;
+ values.Style = pulse.Style;
+ values.AnimationSpeed = pulse.Speed;
+ if (!pulse.Color.IsEmpty)
+ {
+ values.Colors.Color1 = pulse.Color;
+ values.Colors.Color2 = Lighten(pulse.Color, 0.35f);
+ values.Colors.HighlightColor = Lighten(pulse.Color, 0.55f);
+ }
+ }
+
+ ///
+ /// Shows or hides the banner strip. Returns the client height added when the strip becomes visible.
+ ///
+ /// Top-docked banner panel.
+ /// Caption label inside the host.
+ /// Optional image box. Hidden when there is no image.
+ /// Manifest banner, or null.
+ /// Image already loaded for , or null.
+ /// Pixels added to the client height, or zero when the strip stays hidden.
+ internal static int ApplyBanner(KryptonPanel host, KryptonLabel label, PictureBox image, KryptonUpdateBanner? banner, Image? loadedImage)
+ {
+ if (host == null || label == null || image == null)
+ {
+ return 0;
+ }
+
+ if (banner == null || !banner.HasContent)
+ {
+ host.Visible = false;
+ host.Height = 0;
+ image.Image = null;
+ image.Visible = false;
+ return 0;
+ }
+
+ var height = Scaled(BannerHeight, host);
+ host.Height = height;
+ host.Visible = true;
+ if (!banner.BackColor.IsEmpty || !banner.BackColor2.IsEmpty)
+ {
+ var color1 = banner.BackColor.IsEmpty ? banner.BackColor2 : banner.BackColor;
+ var color2 = banner.BackColor2.IsEmpty ? color1 : banner.BackColor2;
+ host.StateCommon.Color1 = color1;
+ host.StateCommon.Color2 = color2;
+ host.StateCommon.ColorStyle = color2 == color1
+ ? PaletteColorStyle.Solid
+ : PaletteColorStyle.Linear;
+ }
+
+ var text = banner.Text ?? string.Empty;
+ label.Values.Text = text;
+ label.Visible = text != null && !string.IsNullOrWhiteSpace(text);
+ if (!banner.ForeColor.IsEmpty)
+ {
+ label.StateCommon.ShortText.Color1 = banner.ForeColor;
+ }
+
+ if (loadedImage != null)
+ {
+ image.Image = loadedImage;
+ image.Visible = true;
+ image.Size = new Size(Scaled(BannerImageSize, host), Scaled(BannerImageSize, host));
+ }
+ else
+ {
+ image.Image = null;
+ image.Visible = false;
+ }
+
+ return height;
+ }
+
+ /// Loads a banner image from https or a local path. Returns null when the address is missing or the load fails.
+ /// Manifest image address.
+ /// Cancels the download.
+ internal static async Task LoadBannerImageAsync(string? imageUrl, CancellationToken cancellationToken = default)
+ {
+ if (imageUrl == null || string.IsNullOrWhiteSpace(imageUrl))
+ {
+ return null;
+ }
+
+ try
+ {
+ byte[] bytes;
+ if (KryptonUpdateClient.TryGetLocalPath(imageUrl, out var path))
+ {
+ var info = new FileInfo(path);
+ if (!info.Exists || info.Length <= 0 || info.Length > MaxImageBytes)
+ {
+ return null;
+ }
+
+ bytes = File.ReadAllBytes(path);
+ }
+ else if (KryptonUpdateClient.IsHttps(imageUrl))
+ {
+ bytes = await KryptonUpdateClient.DownloadBytesAsync(imageUrl, MaxImageBytes, cancellationToken).ConfigureAwait(true);
+ }
+ else
+ {
+ return null;
+ }
+
+ if (bytes == null || bytes.Length == 0)
+ {
+ return null;
+ }
+
+ using (var stream = new MemoryStream(bytes))
+ {
+ using (var source = Image.FromStream(stream))
+ {
+ return new Bitmap(source);
+ }
+ }
+ }
+ catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or ArgumentException
+ or OutOfMemoryException or KryptonUpdateException or HttpRequestException
+ or TaskCanceledException)
+ {
+ return null;
+ }
+ }
+
+ private static Color Lighten(Color color, float amount)
+ {
+ amount = amount < 0f ? 0f : amount > 1f ? 1f : amount;
+ var r = color.R + (int)((255 - color.R) * amount);
+ var g = color.G + (int)((255 - color.G) * amount);
+ var b = color.B + (int)((255 - color.B) * amount);
+ return Color.FromArgb(color.A, Clamp(r), Clamp(g), Clamp(b));
+ }
+
+ private static int Clamp(int value) => value < 0 ? 0 : value > 255 ? 255 : value;
+
+ private static int Scaled(int value, Control control)
+ {
+ var dpi = control.DeviceDpi;
+ if (dpi <= 0)
+ {
+ dpi = 96;
+ }
+
+ return value * dpi / 96;
+ }
+}
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/General/KryptonUpdatePulse.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/General/KryptonUpdatePulse.cs
new file mode 100644
index 0000000000..3d0ed3087f
--- /dev/null
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/General/KryptonUpdatePulse.cs
@@ -0,0 +1,39 @@
+#region BSD License
+/*
+ *
+ * New BSD 3-Clause License (https://github.com/Krypton-Suite/Standard-Toolkit/blob/master/LICENSE)
+ * Modifications by Peter Wagner (aka Wagnerp), Simon Coghlan (aka Smurf-IV), Giduac & Ahmed Abdelhameed, tobitege et al. 2026 - 2026. All rights reserved.
+ *
+ */
+#endregion
+
+namespace Krypton.Toolkit.Utilities;
+
+/// Optional pulsing form chrome for the update prompt, notes, and progress windows.
+public sealed class KryptonUpdatePulse
+{
+ /// Initializes pulse settings.
+ /// When true, the form chrome glows.
+ /// Primary glow colour. Empty keeps the toolkit default.
+ /// Edge style. Defaults to .
+ /// Animation speed multiplier. Values at or below zero become 1.
+ public KryptonUpdatePulse(bool enable, Color color, InputPulsingBorderStyle style, float speed)
+ {
+ Enable = enable;
+ Color = color;
+ Style = style;
+ Speed = speed <= 0f ? 1f : speed;
+ }
+
+ /// Gets whether the form chrome glows.
+ public bool Enable { get; }
+
+ /// Gets the primary glow colour. Empty keeps the toolkit default.
+ public Color Color { get; }
+
+ /// Gets whether the glow is on the bottom edge or the full border.
+ public InputPulsingBorderStyle Style { get; }
+
+ /// Gets the animation speed multiplier.
+ public float Speed { get; }
+}
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/General/KryptonUpdateThrowHelper.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/General/KryptonUpdateThrowHelper.cs
new file mode 100644
index 0000000000..1d00b73e6d
--- /dev/null
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/General/KryptonUpdateThrowHelper.cs
@@ -0,0 +1,69 @@
+#region BSD License
+/*
+ *
+ * New BSD 3-Clause License (https://github.com/Krypton-Suite/Standard-Toolkit/blob/master/LICENSE)
+ * Modifications by Peter Wagner (aka Wagnerp), Simon Coghlan (aka Smurf-IV), Giduac & Ahmed Abdelhameed, tobitege et al. 2026 - 2026. All rights reserved.
+ *
+ */
+#endregion
+
+namespace Krypton.Toolkit.Utilities;
+
+///
+/// Cold-path helpers for . BCL throws still go through .
+///
+[StackTraceHidden]
+internal static class KryptonUpdateThrowHelper
+{
+ /// Throws with a UI-safe message.
+ /// Text safe to show in the UI.
+ [DoesNotReturn]
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static void Throw(string message) =>
+ throw new KryptonUpdateException(message);
+
+ /// Throws with an inner cause.
+ /// Text safe to show in the UI.
+ /// The underlying failure.
+ [DoesNotReturn]
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static void Throw(string message, Exception innerException) =>
+ throw new KryptonUpdateException(message, innerException);
+
+ /// Throws an HTTP failure. lets a range download tell 416 from other errors.
+ /// Text safe to show in the UI.
+ /// Status code returned by the server.
+ [DoesNotReturn]
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static void Throw(string message, int httpStatus) =>
+ throw new KryptonUpdateException(message, httpStatus);
+
+ /// Throws for expression forms (e.g. switch arms).
+ /// The expected type of the expression.
+ /// Text safe to show in the UI.
+ /// Never returns; the return type exists only for use in expressions.
+ [DoesNotReturn]
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static T Throw(string message) =>
+ throw new KryptonUpdateException(message);
+
+ /// Throws with an inner cause for expression forms.
+ /// The expected type of the expression.
+ /// Text safe to show in the UI.
+ /// The underlying failure.
+ /// Never returns; the return type exists only for use in expressions.
+ [DoesNotReturn]
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static T Throw(string message, Exception innerException) =>
+ throw new KryptonUpdateException(message, innerException);
+
+ /// Throws an HTTP failure for expression forms.
+ /// The expected type of the expression.
+ /// Text safe to show in the UI.
+ /// Status code returned by the server.
+ /// Never returns; the return type exists only for use in expressions.
+ [DoesNotReturn]
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static T Throw(string message, int httpStatus) =>
+ throw new KryptonUpdateException(message, httpStatus);
+}
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Providers/KryptonGitHubReleaseUpdateProvider.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Providers/KryptonGitHubReleaseUpdateProvider.cs
index 8385094f31..69512a5917 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Providers/KryptonGitHubReleaseUpdateProvider.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Providers/KryptonGitHubReleaseUpdateProvider.cs
@@ -47,7 +47,7 @@ public async Task CheckAsync(CancellationToken cancellatio
{
if (string.IsNullOrWhiteSpace(_owner) || string.IsNullOrWhiteSpace(_repository))
{
- throw new KryptonUpdateException("Set GitHubOwner and GitHubRepository.");
+ KryptonUpdateThrowHelper.Throw("Set GitHubOwner and GitHubRepository.");
}
var api = "https://api.github.com/repos/" + Uri.EscapeDataString(_owner.Trim()) + "/" + Uri.EscapeDataString(_repository.Trim()) + "/releases/latest";
@@ -58,7 +58,7 @@ public async Task CheckAsync(CancellationToken cancellatio
}
catch (KryptonUpdateException exception) when (exception.Message.Contains("404"))
{
- throw new KryptonUpdateException("No published GitHub release was found.", exception);
+ json = KryptonUpdateThrowHelper.Throw("No published GitHub release was found.", exception);
}
var release = GitHubRelease.Parse(json);
@@ -74,12 +74,12 @@ public async Task CheckAsync(CancellationToken cancellatio
var hash = HashFromDigest(asset.Digest);
if (hash == null || string.IsNullOrWhiteSpace(hash))
{
- throw new KryptonUpdateException("The release asset has no SHA-256 digest and the release has no krypton-update.json manifest.");
+ KryptonUpdateThrowHelper.Throw("The release asset has no SHA-256 digest and the release has no krypton-update.json manifest.");
}
if (!KryptonUpdateVersion.TryParse(release.TagName, out var version))
{
- throw new KryptonUpdateException("The release tag is not a System.Version value.");
+ KryptonUpdateThrowHelper.Throw("The release tag is not a System.Version value.");
}
var kind = asset.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)
@@ -103,13 +103,13 @@ private static List ResolveManifestPackages(IReadOnlyList<
var asset = FindAsset(assets, package.PackageUrl);
if (asset == null)
{
- throw new KryptonUpdateException("The manifest packageUrl '" + package.PackageUrl + "' is not an https address or a release asset name.");
+ KryptonUpdateThrowHelper.Throw("The manifest packageUrl '" + package.PackageUrl + "' is not an https address or a release asset name.");
}
var hash = string.IsNullOrWhiteSpace(package.Sha256) ? HashFromDigest(asset.Digest) : package.Sha256;
if (hash == null || string.IsNullOrWhiteSpace(hash))
{
- throw new KryptonUpdateException("The manifest package is missing sha256.");
+ KryptonUpdateThrowHelper.Throw("The manifest package is missing sha256.");
}
resolved.Add(new KryptonUpdatePackage(
@@ -126,7 +126,9 @@ private static List ResolveManifestPackages(IReadOnlyList<
package.AllowPostponeSetting,
package.AllowSkipSetting,
package.AllowResumeSetting,
- package.AllowRetrySetting));
+ package.AllowRetrySetting,
+ package.Banner,
+ package.Pulse));
}
return resolved;
@@ -139,7 +141,7 @@ private GitHubAsset SelectPackageAsset(List assets)
var named = FindAsset(assets, _assetName);
if (named == null)
{
- throw new KryptonUpdateException("The release does not contain asset '" + _assetName + "'.");
+ return KryptonUpdateThrowHelper.Throw("The release does not contain asset '" + _assetName + "'.");
}
return named;
@@ -155,7 +157,7 @@ private GitHubAsset SelectPackageAsset(List assets)
if (match != null)
{
- throw new KryptonUpdateException("The release has more than one installer or zip. Set GitHubAssetName.");
+ return KryptonUpdateThrowHelper.Throw("The release has more than one installer or zip. Set GitHubAssetName.");
}
match = asset;
@@ -163,7 +165,7 @@ private GitHubAsset SelectPackageAsset(List assets)
if (match == null)
{
- throw new KryptonUpdateException("The release has no .exe, .msi, or .zip asset.");
+ return KryptonUpdateThrowHelper.Throw("The release has no .exe, .msi, or .zip asset.");
}
return match;
@@ -279,7 +281,7 @@ private static GitHubRelease ParseSerializer(string json)
var data = serializer.DeserializeObject(json) as Dictionary;
if (data == null)
{
- throw new KryptonUpdateException("The GitHub release response was not a JSON object.");
+ return KryptonUpdateThrowHelper.Throw("The GitHub release response was not a JSON object.");
}
var release = new GitHubRelease
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Providers/KryptonJsonManifestUpdateProvider.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Providers/KryptonJsonManifestUpdateProvider.cs
index 3c40cd8dac..68609efc59 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Providers/KryptonJsonManifestUpdateProvider.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Providers/KryptonJsonManifestUpdateProvider.cs
@@ -28,7 +28,7 @@ public async Task CheckAsync(CancellationToken cancellatio
{
if (string.IsNullOrWhiteSpace(_feedUrl))
{
- throw new KryptonUpdateException("Set FeedUrl to an https manifest or a local manifest file.");
+ KryptonUpdateThrowHelper.Throw("Set FeedUrl to an https manifest or a local manifest file.");
}
var json = await KryptonUpdateClient.ReadTextAsync(_feedUrl, null, null, cancellationToken).ConfigureAwait(false);
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Providers/KryptonUpdateManifestParser.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Providers/KryptonUpdateManifestParser.cs
index 139ab7ac50..a53da3daae 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Providers/KryptonUpdateManifestParser.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Application Updater/Providers/KryptonUpdateManifestParser.cs
@@ -18,7 +18,7 @@ internal static KryptonUpdateCatalog Parse(string json)
{
if (string.IsNullOrWhiteSpace(json))
{
- throw new KryptonUpdateException("The update manifest is empty.");
+ KryptonUpdateThrowHelper.Throw("The update manifest is empty.");
}
var raw = ReadRawPackages(json, out var feed);
@@ -35,19 +35,19 @@ private static KryptonUpdatePackage Build(RawPackage raw, FeedPolicy feed)
{
if (!KryptonUpdateVersion.TryParse(raw.Version, out var version))
{
- throw new KryptonUpdateException("The manifest version is not a System.Version value.");
+ KryptonUpdateThrowHelper.Throw("The manifest version is not a System.Version value.");
}
var packageUrl = raw.PackageUrl;
if (packageUrl == null || string.IsNullOrWhiteSpace(packageUrl))
{
- throw new KryptonUpdateException("The manifest is missing packageUrl.");
+ KryptonUpdateThrowHelper.Throw("The manifest is missing packageUrl.");
}
var sha256 = raw.Sha256;
if (sha256 == null || string.IsNullOrWhiteSpace(sha256))
{
- throw new KryptonUpdateException("The manifest is missing sha256.");
+ KryptonUpdateThrowHelper.Throw("The manifest is missing sha256.");
}
DateTime? released = null;
@@ -55,7 +55,7 @@ private static KryptonUpdatePackage Build(RawPackage raw, FeedPolicy feed)
{
if (!DateTime.TryParse(raw.ReleasedUtc, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsed))
{
- throw new KryptonUpdateException("The manifest releasedUtc value is not a time.");
+ KryptonUpdateThrowHelper.Throw("The manifest releasedUtc value is not a time.");
}
released = parsed.ToUniversalTime();
@@ -75,7 +75,97 @@ private static KryptonUpdatePackage Build(RawPackage raw, FeedPolicy feed)
raw.AllowPostpone ?? feed.AllowPostpone,
raw.AllowSkip ?? feed.AllowSkip,
raw.AllowResume ?? feed.AllowResume,
- raw.AllowRetry ?? feed.AllowRetry);
+ raw.AllowRetry ?? feed.AllowRetry,
+ raw.Banner ?? feed.Banner,
+ raw.Pulse ?? feed.Pulse);
+ }
+
+ private static void ValidateImageUrl(string? imageUrl)
+ {
+ if (imageUrl == null || string.IsNullOrWhiteSpace(imageUrl))
+ {
+ return;
+ }
+
+ var trimmed = imageUrl.Trim();
+ if (KryptonUpdateClient.IsHttps(trimmed))
+ {
+ return;
+ }
+
+ if (Uri.TryCreate(trimmed, UriKind.Absolute, out var uri) && uri.Scheme == Uri.UriSchemeHttp)
+ {
+ KryptonUpdateThrowHelper.Throw("The banner imageUrl must be https or a local file.");
+ }
+
+ if (Uri.TryCreate(trimmed, UriKind.Absolute, out uri) && uri.IsFile)
+ {
+ return;
+ }
+
+ // Local paths are accepted at parse time even when the file is not on this machine.
+ if (trimmed.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
+ {
+ KryptonUpdateThrowHelper.Throw("The banner imageUrl is not a valid path or https address.");
+ }
+ }
+
+ private static Color ParseColor(string? text, string name)
+ {
+ if (text == null || string.IsNullOrWhiteSpace(text))
+ {
+ return Color.Empty;
+ }
+
+ var value = text.Trim();
+ if (value.StartsWith("#", StringComparison.Ordinal))
+ {
+ var hex = value.Substring(1);
+ // uint so AARRGGBB values with the high bit set still parse; FromArgb takes the bits as Int32.
+ if (hex.Length is 6 or 8 &&
+ uint.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var argb))
+ {
+ if (hex.Length == 6)
+ {
+ argb |= 0xFF000000u;
+ }
+
+ return Color.FromArgb(unchecked((int)argb));
+ }
+
+ return KryptonUpdateThrowHelper.Throw("The manifest " + name + " value is not a colour.");
+ }
+
+ try
+ {
+ var converted = ColorTranslator.FromHtml(value);
+ if (converted.IsEmpty && !value.Equals("Empty", StringComparison.OrdinalIgnoreCase))
+ {
+ return KryptonUpdateThrowHelper.Throw("The manifest " + name + " value is not a colour.");
+ }
+
+ return converted;
+ }
+ catch (Exception exception) when (exception is ArgumentException or FormatException or KryptonUpdateException)
+ {
+ return KryptonUpdateThrowHelper.Throw("The manifest " + name + " value is not a colour.");
+ }
+ }
+
+ private static InputPulsingBorderStyle ParsePulseStyle(string? text)
+ {
+ if (text == null || string.IsNullOrWhiteSpace(text))
+ {
+ return InputPulsingBorderStyle.All;
+ }
+
+ if (Enum.TryParse(text.Trim(), true, out InputPulsingBorderStyle style) &&
+ style is InputPulsingBorderStyle.All or InputPulsingBorderStyle.Bottom)
+ {
+ return style;
+ }
+
+ return KryptonUpdateThrowHelper.Throw("Unknown pulse style '" + text + "'.");
}
private static KryptonUpdateSeverity ParseSeverity(string? text)
@@ -90,7 +180,7 @@ private static KryptonUpdateSeverity ParseSeverity(string? text)
return severity;
}
- throw new KryptonUpdateException("Unknown update severity '" + text + "'.");
+ return KryptonUpdateThrowHelper.Throw("Unknown update severity '" + text + "'.");
}
private static KryptonUpdatePackageKind ParseKind(string? text, string packageUrl)
@@ -107,7 +197,7 @@ private static KryptonUpdatePackageKind ParseKind(string? text, string packageUr
return KryptonUpdatePackageKind.Installer;
}
- throw new KryptonUpdateException("Unknown packageKind '" + text + "'.");
+ return KryptonUpdateThrowHelper.Throw("Unknown packageKind '" + text + "'.");
}
return packageUrl.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)
@@ -150,7 +240,7 @@ private static RawPackage ReadElement(JsonElement element)
{
if (element.ValueKind != JsonValueKind.Object)
{
- throw new KryptonUpdateException("The update manifest must be a JSON object.");
+ KryptonUpdateThrowHelper.Throw("The update manifest must be a JSON object.");
}
return new RawPackage
@@ -168,7 +258,9 @@ private static RawPackage ReadElement(JsonElement element)
AllowPostpone = GetBool(element, "allowPostpone"),
AllowSkip = GetBool(element, "allowSkip"),
AllowResume = GetBool(element, "allowResume"),
- AllowRetry = GetBool(element, "allowRetry")
+ AllowRetry = GetBool(element, "allowRetry"),
+ Banner = ReadBanner(element),
+ Pulse = ReadPulse(element)
};
}
@@ -179,9 +271,75 @@ private static FeedPolicy ReadPolicy(JsonElement element) =>
AllowPostpone = GetBool(element, "allowPostpone"),
AllowSkip = GetBool(element, "allowSkip"),
AllowResume = GetBool(element, "allowResume"),
- AllowRetry = GetBool(element, "allowRetry")
+ AllowRetry = GetBool(element, "allowRetry"),
+ Banner = ReadBanner(element),
+ Pulse = ReadPulse(element)
};
+ private static KryptonUpdateBanner? ReadBanner(JsonElement element)
+ {
+ if (!element.TryGetProperty("banner", out var banner) || banner.ValueKind == JsonValueKind.Null)
+ {
+ return null;
+ }
+
+ if (banner.ValueKind != JsonValueKind.Object)
+ {
+ KryptonUpdateThrowHelper.Throw("The manifest banner value must be a JSON object.");
+ }
+
+ var imageUrl = GetString(banner, "imageUrl");
+ ValidateImageUrl(imageUrl);
+ var result = new KryptonUpdateBanner(
+ GetString(banner, "text"),
+ ParseColor(GetString(banner, "backColor"), "banner.backColor"),
+ ParseColor(GetString(banner, "foreColor"), "banner.foreColor"),
+ imageUrl,
+ ParseColor(GetString(banner, "backColor2"), "banner.backColor2"));
+ return result.HasContent || !result.BackColor.IsEmpty || !result.BackColor2.IsEmpty || !result.ForeColor.IsEmpty
+ ? result
+ : null;
+ }
+
+ private static KryptonUpdatePulse? ReadPulse(JsonElement element)
+ {
+ if (!element.TryGetProperty("pulse", out var pulse) || pulse.ValueKind == JsonValueKind.Null)
+ {
+ return null;
+ }
+
+ if (pulse.ValueKind != JsonValueKind.Object)
+ {
+ KryptonUpdateThrowHelper.Throw("The manifest pulse value must be a JSON object.");
+ }
+
+ return new KryptonUpdatePulse(
+ GetBool(pulse, "enable") ?? false,
+ ParseColor(GetString(pulse, "color"), "pulse.color"),
+ ParsePulseStyle(GetString(pulse, "style")),
+ GetSingle(pulse, "speed") ?? 1f);
+ }
+
+ private static float? GetSingle(JsonElement element, string name)
+ {
+ if (!element.TryGetProperty(name, out var value) || value.ValueKind == JsonValueKind.Null)
+ {
+ return null;
+ }
+
+ if (value.ValueKind == JsonValueKind.Number && value.TryGetSingle(out var number))
+ {
+ return number;
+ }
+
+ if (value.ValueKind == JsonValueKind.String && float.TryParse(value.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed))
+ {
+ return parsed;
+ }
+
+ return KryptonUpdateThrowHelper.Throw("The manifest " + name + " value is not a number.");
+ }
+
private static int? GetInt32(JsonElement element, string name)
{
if (!element.TryGetProperty(name, out var value) || value.ValueKind == JsonValueKind.Null)
@@ -199,7 +357,7 @@ private static FeedPolicy ReadPolicy(JsonElement element) =>
return parsed;
}
- throw new KryptonUpdateException("The manifest " + name + " value is not a whole number.");
+ return KryptonUpdateThrowHelper.Throw("The manifest " + name + " value is not a whole number.");
}
private static bool? GetBool(JsonElement element, string name)
@@ -224,7 +382,7 @@ private static FeedPolicy ReadPolicy(JsonElement element) =>
return parsed;
}
- throw new KryptonUpdateException("The manifest " + name + " value is not true or false.");
+ return KryptonUpdateThrowHelper.Throw("The manifest " + name + " value is not true or false.");
}
private static string? GetString(JsonElement element, string name)
@@ -260,7 +418,7 @@ private static List ReadWithSerializer(string json, out FeedPolicy f
return list;
}
- throw new KryptonUpdateException("The update manifest must be a JSON object.");
+ return KryptonUpdateThrowHelper.Throw>("The update manifest must be a JSON object.");
}
private static RawPackage ReadDictionary(Dictionary element) =>
@@ -279,7 +437,9 @@ private static RawPackage ReadDictionary(Dictionary element) =>
AllowPostpone = GetBool(element, "allowPostpone"),
AllowSkip = GetBool(element, "allowSkip"),
AllowResume = GetBool(element, "allowResume"),
- AllowRetry = GetBool(element, "allowRetry")
+ AllowRetry = GetBool(element, "allowRetry"),
+ Banner = ReadBanner(element),
+ Pulse = ReadPulse(element)
};
private static FeedPolicy ReadPolicy(Dictionary element) =>
@@ -289,9 +449,72 @@ private static FeedPolicy ReadPolicy(Dictionary element) =>
AllowPostpone = GetBool(element, "allowPostpone"),
AllowSkip = GetBool(element, "allowSkip"),
AllowResume = GetBool(element, "allowResume"),
- AllowRetry = GetBool(element, "allowRetry")
+ AllowRetry = GetBool(element, "allowRetry"),
+ Banner = ReadBanner(element),
+ Pulse = ReadPulse(element)
};
+ private static KryptonUpdateBanner? ReadBanner(Dictionary element)
+ {
+ if (!element.TryGetValue("banner", out var value) || value == null)
+ {
+ return null;
+ }
+
+ if (!(value is Dictionary banner))
+ {
+ return KryptonUpdateThrowHelper.Throw("The manifest banner value must be a JSON object.");
+ }
+
+ var imageUrl = GetString(banner, "imageUrl");
+ ValidateImageUrl(imageUrl);
+ var result = new KryptonUpdateBanner(
+ GetString(banner, "text"),
+ ParseColor(GetString(banner, "backColor"), "banner.backColor"),
+ ParseColor(GetString(banner, "foreColor"), "banner.foreColor"),
+ imageUrl,
+ ParseColor(GetString(banner, "backColor2"), "banner.backColor2"));
+ return result.HasContent || !result.BackColor.IsEmpty || !result.BackColor2.IsEmpty || !result.ForeColor.IsEmpty
+ ? result
+ : null;
+ }
+
+ private static KryptonUpdatePulse? ReadPulse(Dictionary element)
+ {
+ if (!element.TryGetValue("pulse", out var value) || value == null)
+ {
+ return null;
+ }
+
+ if (!(value is Dictionary pulse))
+ {
+ return KryptonUpdateThrowHelper.Throw("The manifest pulse value must be a JSON object.");
+ }
+
+ return new KryptonUpdatePulse(
+ GetBool(pulse, "enable") ?? false,
+ ParseColor(GetString(pulse, "color"), "pulse.color"),
+ ParsePulseStyle(GetString(pulse, "style")),
+ GetSingle(pulse, "speed") ?? 1f);
+ }
+
+ private static float? GetSingle(Dictionary element, string name)
+ {
+ if (!element.TryGetValue(name, out var value) || value == null)
+ {
+ return null;
+ }
+
+ try
+ {
+ return Convert.ToSingle(value, CultureInfo.InvariantCulture);
+ }
+ catch (Exception exception) when (exception is FormatException or InvalidCastException or OverflowException)
+ {
+ return KryptonUpdateThrowHelper.Throw("The manifest " + name + " value is not a number.");
+ }
+ }
+
private static int? GetInt32(Dictionary element, string name)
{
if (!element.TryGetValue(name, out var value) || value == null)
@@ -303,9 +526,9 @@ private static FeedPolicy ReadPolicy(Dictionary element) =>
{
return Convert.ToInt32(value, CultureInfo.InvariantCulture);
}
- catch (Exception exception) when (exception is FormatException || exception is InvalidCastException || exception is OverflowException)
+ catch (Exception exception) when (exception is FormatException or InvalidCastException or OverflowException)
{
- throw new KryptonUpdateException("The manifest " + name + " value is not a whole number.");
+ return KryptonUpdateThrowHelper.Throw("The manifest " + name + " value is not a whole number.");
}
}
@@ -326,7 +549,7 @@ private static FeedPolicy ReadPolicy(Dictionary element) =>
return parsed;
}
- throw new KryptonUpdateException("The manifest " + name + " value is not true or false.");
+ return KryptonUpdateThrowHelper.Throw("The manifest " + name + " value is not true or false.");
}
private static string? GetString(Dictionary element, string name)
@@ -396,6 +619,10 @@ private sealed class RawPackage
public bool? AllowResume { get; set; }
public bool? AllowRetry { get; set; }
+
+ public KryptonUpdateBanner? Banner { get; set; }
+
+ public KryptonUpdatePulse? Pulse { get; set; }
}
private struct FeedPolicy
@@ -409,5 +636,9 @@ private struct FeedPolicy
public bool? AllowResume;
public bool? AllowRetry;
+
+ public KryptonUpdateBanner? Banner;
+
+ public KryptonUpdatePulse? Pulse;
}
}
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Code Editor/Controls Toolkit/KryptonCodeEditor.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Code Editor/Controls Toolkit/KryptonCodeEditor.cs
index c628a2fa3a..6044087fb7 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Code Editor/Controls Toolkit/KryptonCodeEditor.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Code Editor/Controls Toolkit/KryptonCodeEditor.cs
@@ -1,4 +1,4 @@
-#region BSD License
+#region BSD License
/*
*
* New BSD 3-Clause License (https://github.com/Krypton-Suite/Standard-Toolkit/blob/master/LICENSE)
@@ -1438,7 +1438,7 @@ private int GetIndentLevel(string line)
int indent = 0;
foreach (char c in line)
{
- if (c == ' ' || c == '\t')
+ if (c is ' ' or '\t')
{
indent++;
}
@@ -1468,7 +1468,7 @@ private bool IsBlockEnd(string line)
return _language switch
{
Language.CSharp or Language.Cpp => trimmed == "}",
- Language.VbNet => trimmed == "End Sub" || trimmed == "End Function" || trimmed == "End If" || trimmed == "End For" || trimmed == "End While",
+ Language.VbNet => trimmed is "End Sub" or "End Function" or "End If" or "End For" or "End While",
Language.Python => GetIndentLevel(line) == 0 && !string.IsNullOrWhiteSpace(line),
_ => false
};
@@ -1589,7 +1589,7 @@ private void OnRichTextBoxKeyPress(object? sender, KeyPressEventArgs e)
{
ShowAutoComplete();
}
- else if (e.KeyChar == '.' || e.KeyChar == '(')
+ else if (e.KeyChar is '.' or '(')
{
ShowAutoComplete();
}
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Code Editor/Controls Visuals/VisualAutoCompleteForm.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Code Editor/Controls Visuals/VisualAutoCompleteForm.cs
index 3a3c5d3ae0..2ccd9c3a0b 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Code Editor/Controls Visuals/VisualAutoCompleteForm.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Code Editor/Controls Visuals/VisualAutoCompleteForm.cs
@@ -56,7 +56,7 @@ public VisualAutoCompleteForm(KryptonCodeEditor editor)
_editor.RichTextBox.Focus();
e.Handled = true;
}
- else if (e.KeyCode == Keys.Up || e.KeyCode == Keys.Down)
+ else if (e.KeyCode is Keys.Up or Keys.Down)
{
// Let listbox handle navigation
e.Handled = false;
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Combo Box User Control/Designers/Editors/KryptonDropContentEditor.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Combo Box User Control/Designers/Editors/KryptonDropContentEditor.cs
index 2e1e2acb48..6f71a932a6 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Combo Box User Control/Designers/Editors/KryptonDropContentEditor.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Combo Box User Control/Designers/Editors/KryptonDropContentEditor.cs
@@ -180,10 +180,7 @@ public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext? conte
/// Decide whether can be selected as a final value.
private static bool CanCommitItem(object? item) =>
- item is null
- || item is string and NoneEntry
- || item is Control
- || item is Type;
+ item is null or string and NoneEntry or Control or Type;
/// Resolve the picked item to a instance (or ).
private static object? ResolveCommittedValue(object? selected, IDesignerHost host, object? fallback)
@@ -192,7 +189,7 @@ item is null
{
case null:
return fallback;
- case string s when s == NoneEntry:
+ case string and NoneEntry:
return null;
case Control c:
return c;
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Custom Strings/General/KryptonCustomStringsPersistence.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Custom Strings/General/KryptonCustomStringsPersistence.cs
index f93cd84969..c46caa1f83 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Custom Strings/General/KryptonCustomStringsPersistence.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Custom Strings/General/KryptonCustomStringsPersistence.cs
@@ -1015,7 +1015,7 @@ private static List Tokenize(string json)
continue;
}
- if (c == '{' || c == '}' || c == ':' || c == ',')
+ if (c is '{' or '}' or ':' or ',')
{
tokens.Add(c.ToString());
index++;
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Custom Theme Generator/Controls Toolkit/KryptonCustomThemeGenerator.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Custom Theme Generator/Controls Toolkit/KryptonCustomThemeGenerator.cs
index bc27ad1781..6f74a412d8 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Custom Theme Generator/Controls Toolkit/KryptonCustomThemeGenerator.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Custom Theme Generator/Controls Toolkit/KryptonCustomThemeGenerator.cs
@@ -298,7 +298,7 @@ public static void Export(KryptonCustomPaletteBase palette, string filePath, boo
}
internal static bool IsDarkDonor(PaletteMode mode) =>
- mode == PaletteMode.Office2010BlueDarkMode || mode == PaletteMode.Microsoft365BlackDarkMode;
+ mode is PaletteMode.Office2010BlueDarkMode or PaletteMode.Microsoft365BlackDarkMode;
private static KryptonColorSchemeBase CopyDonorScheme(PaletteBase donor)
{
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Multi Select Tree View/Controls Toolkit/KryptonMultiSelectTreeView.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Multi Select Tree View/Controls Toolkit/KryptonMultiSelectTreeView.cs
index 8a2a789b44..cdec5a19ac 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Multi Select Tree View/Controls Toolkit/KryptonMultiSelectTreeView.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Multi Select Tree View/Controls Toolkit/KryptonMultiSelectTreeView.cs
@@ -264,8 +264,7 @@ private void OnTreeViewMouseDown(object? sender, MouseEventArgs e)
}
var hit = TreeView.HitTest(e.Location);
- if (hit.Location == TreeViewHitTestLocations.PlusMinus
- || hit.Location == TreeViewHitTestLocations.StateImage)
+ if (hit.Location is TreeViewHitTestLocations.PlusMinus or TreeViewHitTestLocations.StateImage)
{
return;
}
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Print Preview Control/Controls Toolkit/KryptonPrintPreviewControl.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Print Preview Control/Controls Toolkit/KryptonPrintPreviewControl.cs
index 00a8b6eca7..6d3473c527 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Print Preview Control/Controls Toolkit/KryptonPrintPreviewControl.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Print Preview Control/Controls Toolkit/KryptonPrintPreviewControl.cs
@@ -481,7 +481,7 @@ private void OnMouseMove(object? sender, MouseEventArgs e)
///
private void OnMouseUp(object? sender, MouseEventArgs e)
{
- if (_isPanning && (e.Button == MouseButtons.Middle || e.Button == MouseButtons.Left))
+ if (_isPanning && e.Button is MouseButtons.Middle or MouseButtons.Left)
{
_isPanning = false;
Cursor = Cursors.Default;
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Radial Menu/General/RadialMenuInteractionCore.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Radial Menu/General/RadialMenuInteractionCore.cs
index da43614552..0b74a1843a 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Radial Menu/General/RadialMenuInteractionCore.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Radial Menu/General/RadialMenuInteractionCore.cs
@@ -269,7 +269,7 @@ public RadialHitResult BeginPress(Point clientPoint)
return hit;
}
- if (hit.Kind == RadialHitKind.Sector || hit.Kind == RadialHitKind.OuterRing)
+ if (hit.Kind is RadialHitKind.Sector or RadialHitKind.OuterRing)
{
_pressedIndex = hit.SectorIndex;
_pressedOuterRing = hit.Kind == RadialHitKind.OuterRing;
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Screen Color Picker/Controls Visuals/VisualScreenColorPickerOverlay.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Screen Color Picker/Controls Visuals/VisualScreenColorPickerOverlay.cs
index 5bcdf9a063..cfeb58aa37 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Screen Color Picker/Controls Visuals/VisualScreenColorPickerOverlay.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Screen Color Picker/Controls Visuals/VisualScreenColorPickerOverlay.cs
@@ -162,7 +162,7 @@ protected override void OnMouseWheel(MouseEventArgs e)
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
Keys key = keyData & Keys.KeyCode;
- if (key == Keys.F12 || key == Keys.PrintScreen || key == Keys.Snapshot)
+ if (key is Keys.F12 or Keys.PrintScreen or Keys.Snapshot)
{
CopyVisibleOverlayToClipboard();
return true;
@@ -178,7 +178,7 @@ protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
protected override void WndProc(ref Message m)
{
- if ((m.Msg == WmKeyDown || m.Msg == WmSysKeyDown) && (int)m.WParam == VkSnapshot)
+ if (m.Msg is WmKeyDown or WmSysKeyDown && (int)m.WParam == VkSnapshot)
{
CopyVisibleOverlayToClipboard();
return;
@@ -195,12 +195,12 @@ protected override void OnKeyDown(KeyEventArgs e)
Cancel();
e.Handled = true;
}
- else if (e.KeyCode == Keys.Enter || e.KeyCode == Keys.Space)
+ else if (e.KeyCode is Keys.Enter or Keys.Space)
{
Confirm();
e.Handled = true;
}
- else if (e.KeyCode == Keys.F12 || e.KeyCode == Keys.PrintScreen || e.KeyCode == Keys.Snapshot)
+ else if (e.KeyCode is Keys.F12 or Keys.PrintScreen or Keys.Snapshot)
{
CopyVisibleOverlayToClipboard();
e.Handled = true;
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Search Box/General/SuggestionPopup.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Search Box/General/SuggestionPopup.cs
index 8ecee2b95e..8dc4f28c7f 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Search Box/General/SuggestionPopup.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Search Box/General/SuggestionPopup.cs
@@ -440,7 +440,7 @@ protected override void SetVisibleCore(bool value)
protected override void WndProc(ref Message m)
{
// Prevent activation messages from stealing focus
- if (m.Msg == PI.WM_.ACTIVATE || m.Msg == PI.WM_.MOUSEACTIVATE)
+ if (m.Msg is PI.WM_.ACTIVATE or PI.WM_.MOUSEACTIVATE)
{
m.Result = IntPtr.Zero;
return;
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Splash Screen Manager/Controls Visuals/VisualSplashScreenManagerForm.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Splash Screen Manager/Controls Visuals/VisualSplashScreenManagerForm.cs
index 0ade633eb2..669555c9b1 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Splash Screen Manager/Controls Visuals/VisualSplashScreenManagerForm.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Splash Screen Manager/Controls Visuals/VisualSplashScreenManagerForm.cs
@@ -522,9 +522,7 @@ private static PaletteImageStyle ToPaletteImageStyle(ImageLayout layout) =>
};
private static bool IsForcedCloseReason(CloseReason closeReason) =>
- closeReason == CloseReason.WindowsShutDown
- || closeReason == CloseReason.TaskManagerClosing
- || closeReason == CloseReason.ApplicationExitCall;
+ closeReason is CloseReason.WindowsShutDown or CloseReason.TaskManagerClosing or CloseReason.ApplicationExitCall;
private static double ClampOpacity(double opacity)
{
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton System Information/General/SystemInformationWmi.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton System Information/General/SystemInformationWmi.cs
index 9a59bc479d..bee2de2085 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton System Information/General/SystemInformationWmi.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton System Information/General/SystemInformationWmi.cs
@@ -152,7 +152,7 @@ public static WmiQueryResult Query(
/// Formats a WMI property value for display.
public static string FormatValue(object? value)
{
- if (value == null || value is DBNull)
+ if (value is null or DBNull)
{
return string.Empty;
}
@@ -236,6 +236,5 @@ private static ManagementScope GetScope()
}
private static bool IsAccessDenied(ManagementException ex) =>
- ex.ErrorCode == ManagementStatus.AccessDenied ||
- ex.ErrorCode == ManagementStatus.PrivilegeNotHeld;
+ ex.ErrorCode is ManagementStatus.AccessDenied or ManagementStatus.PrivilegeNotHeld;
}
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Toast/Controls Visuals/Base/VisualToastBaseForm.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Toast/Controls Visuals/Base/VisualToastBaseForm.cs
index 6a1ddcfd30..20f8e35a7b 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Toast/Controls Visuals/Base/VisualToastBaseForm.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton Toast/Controls Visuals/Base/VisualToastBaseForm.cs
@@ -203,7 +203,7 @@ protected void ApplyScaledButtonStripInsets()
foreach (Control cellChild in tlp.Controls)
{
- if (cellChild.Anchor == AnchorStyles.Right || cellChild.Anchor == AnchorStyles.Left)
+ if (cellChild.Anchor is AnchorStyles.Right or AnchorStyles.Left)
{
cellChild.Anchor = AnchorStyles.None;
}
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton ToolStrip/Controls Toolkit/KryptonBlinkingToolStripStatusLabel.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton ToolStrip/Controls Toolkit/KryptonBlinkingToolStripStatusLabel.cs
index a16c5aca1f..760d9f110e 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton ToolStrip/Controls Toolkit/KryptonBlinkingToolStripStatusLabel.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Components/Krypton ToolStrip/Controls Toolkit/KryptonBlinkingToolStripStatusLabel.cs
@@ -423,8 +423,8 @@ private void ApplyVisibilityBlink(bool advanceHardPhase)
private void ApplyTargetColours(Color foreColour, Color backColour)
{
BlinkTarget target = _blinkValues.BlinkTarget;
- bool driveForeground = target == BlinkTarget.Foreground || target == BlinkTarget.Both;
- bool driveBackground = target == BlinkTarget.Background || target == BlinkTarget.Both;
+ bool driveForeground = target is BlinkTarget.Foreground or BlinkTarget.Both;
+ bool driveBackground = target is BlinkTarget.Background or BlinkTarget.Both;
if (driveForeground)
{
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Utilities/Font Awesome/General/FontAwesomeHelper.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Utilities/Font Awesome/General/FontAwesomeHelper.cs
index a0f0d348bf..bbc7acd3e1 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Utilities/Font Awesome/General/FontAwesomeHelper.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Utilities/Font Awesome/General/FontAwesomeHelper.cs
@@ -733,7 +733,7 @@ private static int GetIconUnicodeMapping(string iconName, FontAwesomeStyle style
// Regular, Light, Thin, and Duotone styles use the same Unicode values as Solid
// Fall back to Solid mappings when these styles are requested
- if (style == FontAwesomeStyle.Regular || style == FontAwesomeStyle.Light || style == FontAwesomeStyle.Thin || style == FontAwesomeStyle.Duotone)
+ if (style is FontAwesomeStyle.Regular or FontAwesomeStyle.Light or FontAwesomeStyle.Thin or FontAwesomeStyle.Duotone)
{
return GetIconUnicodeMapping(iconName, FontAwesomeStyle.Solid);
}
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Utilities/WinFormAnimatiion/General/Animator2D.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Utilities/WinFormAnimatiion/General/Animator2D.cs
index 98a9e1a280..344977919c 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Utilities/WinFormAnimatiion/General/Animator2D.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Utilities/WinFormAnimatiion/General/Animator2D.cs
@@ -238,7 +238,7 @@ public virtual AnimatorStatus CurrentStatus
///
public virtual void Pause()
{
- if (CurrentStatus == AnimatorStatus.OnHold || CurrentStatus == AnimatorStatus.Playing)
+ if (CurrentStatus is AnimatorStatus.OnHold or AnimatorStatus.Playing)
{
HorizontalAnimator.Pause();
VerticalAnimator.Pause();
diff --git a/Source/Krypton Components/Krypton.Toolkit.Utilities/Utilities/WinFormAnimatiion/General/Animator3D.cs b/Source/Krypton Components/Krypton.Toolkit.Utilities/Utilities/WinFormAnimatiion/General/Animator3D.cs
index da898448c6..3d0bf92122 100644
--- a/Source/Krypton Components/Krypton.Toolkit.Utilities/Utilities/WinFormAnimatiion/General/Animator3D.cs
+++ b/Source/Krypton Components/Krypton.Toolkit.Utilities/Utilities/WinFormAnimatiion/General/Animator3D.cs
@@ -261,7 +261,7 @@ public virtual AnimatorStatus CurrentStatus
///
public virtual void Pause()
{
- if (CurrentStatus == AnimatorStatus.OnHold || CurrentStatus == AnimatorStatus.Playing)
+ if (CurrentStatus is AnimatorStatus.OnHold or AnimatorStatus.Playing)
{
HorizontalAnimator.Pause();
VerticalAnimator.Pause();
diff --git a/Source/Krypton Components/TestForm/KryptonUtilities/Feature/ApplicationUpdaterDemo.Designer.cs b/Source/Krypton Components/TestForm/KryptonUtilities/Feature/ApplicationUpdaterDemo.Designer.cs
index 2e739deef3..5835f88605 100644
--- a/Source/Krypton Components/TestForm/KryptonUtilities/Feature/ApplicationUpdaterDemo.Designer.cs
+++ b/Source/Krypton Components/TestForm/KryptonUtilities/Feature/ApplicationUpdaterDemo.Designer.cs
@@ -30,6 +30,7 @@ private void InitializeComponent()
this.lblMaxDays = new Krypton.Toolkit.KryptonLabel();
this.nudMaxDays = new Krypton.Toolkit.KryptonNumericUpDown();
this.chkAllowCritical = new Krypton.Toolkit.KryptonCheckBox();
+ this.chkBannerPulse = new Krypton.Toolkit.KryptonCheckBox();
this.flowButtons = new System.Windows.Forms.FlowLayoutPanel();
this.btnCheckNow = new Krypton.Toolkit.KryptonButton();
this.btnScheduled = new Krypton.Toolkit.KryptonButton();
@@ -104,6 +105,7 @@ private void InitializeComponent()
this.flowOptions.Controls.Add(this.lblMaxDays);
this.flowOptions.Controls.Add(this.nudMaxDays);
this.flowOptions.Controls.Add(this.chkAllowCritical);
+ this.flowOptions.Controls.Add(this.chkBannerPulse);
this.flowOptions.Dock = System.Windows.Forms.DockStyle.Fill;
this.flowOptions.Name = "flowOptions";
this.flowOptions.WrapContents = true;
@@ -163,6 +165,14 @@ private void InitializeComponent()
this.chkAllowCritical.Size = new System.Drawing.Size(180, 24);
this.chkAllowCritical.Values.Text = "Allow critical postpone";
//
+ // chkBannerPulse
+ //
+ this.chkBannerPulse.Checked = true;
+ this.chkBannerPulse.CheckState = System.Windows.Forms.CheckState.Checked;
+ this.chkBannerPulse.Name = "chkBannerPulse";
+ this.chkBannerPulse.Size = new System.Drawing.Size(200, 24);
+ this.chkBannerPulse.Values.Text = "Banner and pulse on 9.9.9";
+ //
// flowButtons
//
this.flowButtons.Controls.Add(this.btnCheckNow);
@@ -280,6 +290,7 @@ private void InitializeComponent()
private Krypton.Toolkit.KryptonLabel lblMaxDays;
private Krypton.Toolkit.KryptonNumericUpDown nudMaxDays;
private Krypton.Toolkit.KryptonCheckBox chkAllowCritical;
+ private Krypton.Toolkit.KryptonCheckBox chkBannerPulse;
private System.Windows.Forms.FlowLayoutPanel flowButtons;
private Krypton.Toolkit.KryptonButton btnCheckNow;
private Krypton.Toolkit.KryptonButton btnScheduled;
diff --git a/Source/Krypton Components/TestForm/KryptonUtilities/Feature/ApplicationUpdaterDemo.cs b/Source/Krypton Components/TestForm/KryptonUtilities/Feature/ApplicationUpdaterDemo.cs
index 1843b83a31..e3c3525b7e 100644
--- a/Source/Krypton Components/TestForm/KryptonUtilities/Feature/ApplicationUpdaterDemo.cs
+++ b/Source/Krypton Components/TestForm/KryptonUtilities/Feature/ApplicationUpdaterDemo.cs
@@ -66,6 +66,7 @@ public ApplicationUpdaterDemo()
cmbKind.SelectedIndexChanged += OnSelectionChanged;
nudMaxDays.ValueChanged += OnSelectionChanged;
chkAllowCritical.CheckedChanged += OnSelectionChanged;
+ chkBannerPulse.CheckedChanged += OnSelectionChanged;
}
private void ApplicationUpdaterDemo_Load(object? sender, EventArgs e)
@@ -218,6 +219,12 @@ private void WriteManifest()
policy += ",\"allowPostpone\":true";
}
+ if (chkBannerPulse.Checked)
+ {
+ policy += ",\"banner\":{\"text\":\"Security update — install now\",\"backColor\":\"#8B0000\",\"backColor2\":\"#FF4500\",\"foreColor\":\"#FFFFFF\"}"
+ + ",\"pulse\":{\"enable\":true,\"color\":\"#FF4500\",\"style\":\"All\",\"speed\":1.5}";
+ }
+
var json = "{\"packages\":["
+ PackageJson("1.5.0", "recommended", kind, packagePath, hash, "Earlier local package, listed so the prompt shows more than one version.", "2026-06-01T00:00:00Z", string.Empty)
+ ","
diff --git a/Source/TestHarnesses/UpdateManifestEditor/MainForm.cs b/Source/TestHarnesses/UpdateManifestEditor/MainForm.cs
index 048d3542e3..5354904949 100644
--- a/Source/TestHarnesses/UpdateManifestEditor/MainForm.cs
+++ b/Source/TestHarnesses/UpdateManifestEditor/MainForm.cs
@@ -36,9 +36,100 @@ public MainForm()
cmbAllowSkip.Items.AddRange(new object[] { "default", "allow", "block" });
cmbAllowResume.Items.AddRange(new object[] { "default", "allow", "block" });
cmbAllowRetry.Items.AddRange(new object[] { "default", "allow", "block" });
+ AddPresentationEditorRows();
AddPackage();
}
+ private void AddPresentationEditorRows()
+ {
+ editor.SuspendLayout();
+ editor.Controls.Remove(lblNotes);
+ editor.Controls.Remove(txtNotes);
+ while (editor.RowStyles.Count > 15)
+ {
+ editor.RowStyles.RemoveAt(editor.RowStyles.Count - 1);
+ }
+
+ for (var index = 0; index < 9; index++)
+ {
+ editor.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 32F));
+ }
+
+ editor.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
+ editor.RowCount = 25;
+
+ lblBannerText = CreateEditorLabel("Banner text");
+ txtBannerText = CreateEditorTextBox();
+ lblBannerBackColor = CreateEditorLabel("Banner back");
+ txtBannerBackColor = CreateEditorTextBox();
+ lblBannerBackColor2 = CreateEditorLabel("Banner back 2");
+ txtBannerBackColor2 = CreateEditorTextBox();
+ lblBannerForeColor = CreateEditorLabel("Banner fore");
+ txtBannerForeColor = CreateEditorTextBox();
+ lblBannerImageUrl = CreateEditorLabel("Banner image");
+ txtBannerImageUrl = CreateEditorTextBox();
+ lblPulseEnable = CreateEditorLabel("Pulse");
+ cmbPulseEnable = CreateEditorCombo(new object[] { "default", "on", "off" });
+ lblPulseColor = CreateEditorLabel("Pulse colour");
+ txtPulseColor = CreateEditorTextBox();
+ lblPulseStyle = CreateEditorLabel("Pulse style");
+ cmbPulseStyle = CreateEditorCombo(new object[] { "default", "All", "Bottom" });
+ lblPulseSpeed = CreateEditorLabel("Pulse speed");
+ txtPulseSpeed = CreateEditorTextBox();
+
+ editor.Controls.Add(lblBannerText, 0, 15);
+ editor.Controls.Add(txtBannerText, 1, 15);
+ editor.Controls.Add(lblBannerBackColor, 0, 16);
+ editor.Controls.Add(txtBannerBackColor, 1, 16);
+ editor.Controls.Add(lblBannerBackColor2, 0, 17);
+ editor.Controls.Add(txtBannerBackColor2, 1, 17);
+ editor.Controls.Add(lblBannerForeColor, 0, 18);
+ editor.Controls.Add(txtBannerForeColor, 1, 18);
+ editor.Controls.Add(lblBannerImageUrl, 0, 19);
+ editor.Controls.Add(txtBannerImageUrl, 1, 19);
+ editor.Controls.Add(lblPulseEnable, 0, 20);
+ editor.Controls.Add(cmbPulseEnable, 1, 20);
+ editor.Controls.Add(lblPulseColor, 0, 21);
+ editor.Controls.Add(txtPulseColor, 1, 21);
+ editor.Controls.Add(lblPulseStyle, 0, 22);
+ editor.Controls.Add(cmbPulseStyle, 1, 22);
+ editor.Controls.Add(lblPulseSpeed, 0, 23);
+ editor.Controls.Add(txtPulseSpeed, 1, 23);
+ editor.Controls.Add(lblNotes, 0, 24);
+ editor.Controls.Add(txtNotes, 1, 24);
+ editor.ResumeLayout();
+ }
+
+ private static KryptonLabel CreateEditorLabel(string text)
+ {
+ var label = new KryptonLabel();
+ label.Values.Text = text;
+ return label;
+ }
+
+ private KryptonTextBox CreateEditorTextBox()
+ {
+ var box = new KryptonTextBox
+ {
+ Dock = DockStyle.Fill
+ };
+ box.TextChanged += OnFieldChanged;
+ return box;
+ }
+
+ private KryptonComboBox CreateEditorCombo(object[] items)
+ {
+ var combo = new KryptonComboBox
+ {
+ DropDownStyle = ComboBoxStyle.DropDownList,
+ DropDownWidth = 160,
+ Width = 160
+ };
+ combo.Items.AddRange(items);
+ combo.SelectedIndexChanged += OnFieldChanged;
+ return combo;
+ }
+
private void OnAdd(object? sender, EventArgs e) => AddPackage();
private void OnRemove(object? sender, EventArgs e)
@@ -266,6 +357,15 @@ private void ShowSelection()
SelectCombo(cmbAllowSkip, AllowPostponeLabel(package.AllowSkip), "default");
SelectCombo(cmbAllowResume, AllowPostponeLabel(package.AllowResume), "default");
SelectCombo(cmbAllowRetry, AllowPostponeLabel(package.AllowRetry), "default");
+ txtBannerText.Text = package.BannerText;
+ txtBannerBackColor.Text = package.BannerBackColor;
+ txtBannerBackColor2.Text = package.BannerBackColor2;
+ txtBannerForeColor.Text = package.BannerForeColor;
+ txtBannerImageUrl.Text = package.BannerImageUrl;
+ SelectCombo(cmbPulseEnable, PulseEnableLabel(package.PulseEnable), "default");
+ txtPulseColor.Text = package.PulseColor;
+ SelectCombo(cmbPulseStyle, string.IsNullOrWhiteSpace(package.PulseStyle) ? "default" : package.PulseStyle, "default");
+ txtPulseSpeed.Text = package.PulseSpeed;
txtNotes.Text = package.ReleaseNotes;
_loading = false;
}
@@ -287,6 +387,15 @@ private void ClearFields()
cmbAllowSkip.SelectedIndex = -1;
cmbAllowResume.SelectedIndex = -1;
cmbAllowRetry.SelectedIndex = -1;
+ txtBannerText.Text = string.Empty;
+ txtBannerBackColor.Text = string.Empty;
+ txtBannerBackColor2.Text = string.Empty;
+ txtBannerForeColor.Text = string.Empty;
+ txtBannerImageUrl.Text = string.Empty;
+ cmbPulseEnable.SelectedIndex = -1;
+ txtPulseColor.Text = string.Empty;
+ cmbPulseStyle.SelectedIndex = -1;
+ txtPulseSpeed.Text = string.Empty;
txtNotes.Text = string.Empty;
_loading = false;
}
@@ -313,6 +422,16 @@ private void CommitSelection()
package.AllowSkip = AllowPostponeValue(cmbAllowSkip.SelectedItem as string);
package.AllowResume = AllowPostponeValue(cmbAllowResume.SelectedItem as string);
package.AllowRetry = AllowPostponeValue(cmbAllowRetry.SelectedItem as string);
+ package.BannerText = txtBannerText.Text.Trim();
+ package.BannerBackColor = txtBannerBackColor.Text.Trim();
+ package.BannerBackColor2 = txtBannerBackColor2.Text.Trim();
+ package.BannerForeColor = txtBannerForeColor.Text.Trim();
+ package.BannerImageUrl = txtBannerImageUrl.Text.Trim();
+ package.PulseEnable = PulseEnableValue(cmbPulseEnable.SelectedItem as string);
+ package.PulseColor = txtPulseColor.Text.Trim();
+ var style = cmbPulseStyle.SelectedItem as string;
+ package.PulseStyle = style == null || style == "default" ? string.Empty : style;
+ package.PulseSpeed = txtPulseSpeed.Text.Trim();
package.ReleaseNotes = txtNotes.Text;
}
@@ -444,6 +563,31 @@ private static string AllowPostponeValue(string? label)
return string.Empty;
}
+ private static string PulseEnableLabel(string value)
+ {
+ if (bool.TryParse(value, out var enable))
+ {
+ return enable ? "on" : "off";
+ }
+
+ return "default";
+ }
+
+ private static string PulseEnableValue(string? label)
+ {
+ if (string.Equals(label, "on", StringComparison.OrdinalIgnoreCase))
+ {
+ return "true";
+ }
+
+ if (string.Equals(label, "off", StringComparison.OrdinalIgnoreCase))
+ {
+ return "false";
+ }
+
+ return string.Empty;
+ }
+
private static void SelectCombo(KryptonComboBox combo, string value, string fallback)
{
var wanted = string.IsNullOrWhiteSpace(value) ? fallback : value.Trim();
@@ -460,5 +604,24 @@ private static void SelectCombo(KryptonComboBox combo, string value, string fall
}
private void SetStatus(string message) => lblStatus.Text = message;
+
+ private KryptonLabel lblBannerText;
+ private KryptonTextBox txtBannerText;
+ private KryptonLabel lblBannerBackColor;
+ private KryptonTextBox txtBannerBackColor;
+ private KryptonLabel lblBannerBackColor2;
+ private KryptonTextBox txtBannerBackColor2;
+ private KryptonLabel lblBannerForeColor;
+ private KryptonTextBox txtBannerForeColor;
+ private KryptonLabel lblBannerImageUrl;
+ private KryptonTextBox txtBannerImageUrl;
+ private KryptonLabel lblPulseEnable;
+ private KryptonComboBox cmbPulseEnable;
+ private KryptonLabel lblPulseColor;
+ private KryptonTextBox txtPulseColor;
+ private KryptonLabel lblPulseStyle;
+ private KryptonComboBox cmbPulseStyle;
+ private KryptonLabel lblPulseSpeed;
+ private KryptonTextBox txtPulseSpeed;
}
}
diff --git a/Source/TestHarnesses/UpdateManifestEditor/ManifestJson.cs b/Source/TestHarnesses/UpdateManifestEditor/ManifestJson.cs
index f8c7f90cdf..00678d977c 100644
--- a/Source/TestHarnesses/UpdateManifestEditor/ManifestJson.cs
+++ b/Source/TestHarnesses/UpdateManifestEditor/ManifestJson.cs
@@ -224,6 +224,8 @@ private static void WritePackage(StringBuilder builder, ManifestPackage package,
AddOptionalBool(fields, "allowSkip", package.AllowSkip);
AddOptionalBool(fields, "allowResume", package.AllowResume);
AddOptionalBool(fields, "allowRetry", package.AllowRetry);
+ AddBanner(fields, package);
+ AddPulse(fields, package);
builder.Append(pad).AppendLine("{");
for (var index = 0; index < fields.Count; index++)
@@ -240,6 +242,55 @@ private static void WritePackage(StringBuilder builder, ManifestPackage package,
builder.Append(pad).Append('}');
}
+ private static void AddBanner(List fields, ManifestPackage package)
+ {
+ var parts = new List();
+ AddOptional(parts, "text", package.BannerText);
+ AddOptional(parts, "backColor", package.BannerBackColor);
+ AddOptional(parts, "backColor2", package.BannerBackColor2);
+ AddOptional(parts, "foreColor", package.BannerForeColor);
+ AddOptional(parts, "imageUrl", package.BannerImageUrl);
+ if (parts.Count == 0)
+ {
+ return;
+ }
+
+ fields.Add("\"banner\": { " + string.Join(", ", parts) + " }");
+ }
+
+ private static void AddPulse(List fields, ManifestPackage package)
+ {
+ if (package.PulseEnable == null || string.IsNullOrWhiteSpace(package.PulseEnable))
+ {
+ return;
+ }
+
+ if (!bool.TryParse(package.PulseEnable.Trim(), out var enable))
+ {
+ return;
+ }
+
+ var parts = new List
+ {
+ "\"enable\": " + (enable ? "true" : "false")
+ };
+ AddOptional(parts, "color", package.PulseColor);
+ if (package.PulseStyle != null && !string.IsNullOrWhiteSpace(package.PulseStyle))
+ {
+ parts.Add(Quote("style", package.PulseStyle.Trim()));
+ }
+
+ if (package.PulseSpeed != null
+ && !string.IsNullOrWhiteSpace(package.PulseSpeed)
+ && float.TryParse(package.PulseSpeed.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var speed)
+ && speed > 0f)
+ {
+ parts.Add("\"speed\": " + speed.ToString(CultureInfo.InvariantCulture));
+ }
+
+ fields.Add("\"pulse\": { " + string.Join(", ", parts) + " }");
+ }
+
private static string DisplayVersion(string? text)
{
if (TryNormalizeVersion(text, out var version))
@@ -280,31 +331,53 @@ private static void AddOptionalBool(List fields, string name, string? va
fields.Add("\"" + name + "\": " + (allow ? "true" : "false"));
}
- private static void ApplyRoot(ManifestPackage package, string days, string postpone, string skip, string resume, string retry)
+ private static void ApplyRoot(ManifestPackage package, ManifestPackage root)
{
if (string.IsNullOrWhiteSpace(package.MaximumPostponeDays))
{
- package.MaximumPostponeDays = days;
+ package.MaximumPostponeDays = root.MaximumPostponeDays;
}
if (string.IsNullOrWhiteSpace(package.AllowPostpone))
{
- package.AllowPostpone = postpone;
+ package.AllowPostpone = root.AllowPostpone;
}
if (string.IsNullOrWhiteSpace(package.AllowSkip))
{
- package.AllowSkip = skip;
+ package.AllowSkip = root.AllowSkip;
}
if (string.IsNullOrWhiteSpace(package.AllowResume))
{
- package.AllowResume = resume;
+ package.AllowResume = root.AllowResume;
}
if (string.IsNullOrWhiteSpace(package.AllowRetry))
{
- package.AllowRetry = retry;
+ package.AllowRetry = root.AllowRetry;
+ }
+
+ // Root banner/pulse apply only when the package omitted that object entirely.
+ if (string.IsNullOrWhiteSpace(package.BannerText)
+ && string.IsNullOrWhiteSpace(package.BannerBackColor)
+ && string.IsNullOrWhiteSpace(package.BannerBackColor2)
+ && string.IsNullOrWhiteSpace(package.BannerForeColor)
+ && string.IsNullOrWhiteSpace(package.BannerImageUrl))
+ {
+ package.BannerText = root.BannerText;
+ package.BannerBackColor = root.BannerBackColor;
+ package.BannerBackColor2 = root.BannerBackColor2;
+ package.BannerForeColor = root.BannerForeColor;
+ package.BannerImageUrl = root.BannerImageUrl;
+ }
+
+ if (string.IsNullOrWhiteSpace(package.PulseEnable))
+ {
+ package.PulseEnable = root.PulseEnable;
+ package.PulseColor = root.PulseColor;
+ package.PulseStyle = root.PulseStyle;
+ package.PulseSpeed = root.PulseSpeed;
}
}
@@ -372,15 +445,11 @@ private static IReadOnlyList ReadModern(string json)
&& root.TryGetProperty("packages", out var packages)
&& packages.ValueKind == JsonValueKind.Array)
{
- var rootDays = FormatOptionalInt(root, "maximumPostponeDays");
- var rootPostpone = FormatOptionalBool(root, "allowPostpone");
- var rootSkip = FormatOptionalBool(root, "allowSkip");
- var rootResume = FormatOptionalBool(root, "allowResume");
- var rootRetry = FormatOptionalBool(root, "allowRetry");
+ var rootDefaults = ReadElement(root);
foreach (var item in packages.EnumerateArray())
{
var package = ReadElement(item);
- ApplyRoot(package, rootDays, rootPostpone, rootSkip, rootResume, rootRetry);
+ ApplyRoot(package, rootDefaults);
list.Add(package);
}
@@ -414,10 +483,59 @@ private static ManifestPackage ReadElement(JsonElement element)
AllowPostpone = FormatOptionalBool(element, "allowPostpone"),
AllowSkip = FormatOptionalBool(element, "allowSkip"),
AllowResume = FormatOptionalBool(element, "allowResume"),
- AllowRetry = FormatOptionalBool(element, "allowRetry")
+ AllowRetry = FormatOptionalBool(element, "allowRetry"),
+ BannerText = GetNestedString(element, "banner", "text"),
+ BannerBackColor = GetNestedString(element, "banner", "backColor"),
+ BannerBackColor2 = GetNestedString(element, "banner", "backColor2"),
+ BannerForeColor = GetNestedString(element, "banner", "foreColor"),
+ BannerImageUrl = GetNestedString(element, "banner", "imageUrl"),
+ PulseEnable = GetNestedBool(element, "pulse", "enable"),
+ PulseColor = GetNestedString(element, "pulse", "color"),
+ PulseStyle = GetNestedString(element, "pulse", "style"),
+ PulseSpeed = GetNestedNumber(element, "pulse", "speed")
};
}
+ private static string GetNestedString(JsonElement element, string parent, string name)
+ {
+ if (!element.TryGetProperty(parent, out var objectElement) || objectElement.ValueKind != JsonValueKind.Object)
+ {
+ return string.Empty;
+ }
+
+ return GetString(objectElement, name);
+ }
+
+ private static string GetNestedBool(JsonElement element, string parent, string name)
+ {
+ if (!element.TryGetProperty(parent, out var objectElement) || objectElement.ValueKind != JsonValueKind.Object)
+ {
+ return string.Empty;
+ }
+
+ return FormatOptionalBool(objectElement, name);
+ }
+
+ private static string GetNestedNumber(JsonElement element, string parent, string name)
+ {
+ if (!element.TryGetProperty(parent, out var objectElement) || objectElement.ValueKind != JsonValueKind.Object)
+ {
+ return string.Empty;
+ }
+
+ if (!objectElement.TryGetProperty(name, out var value) || value.ValueKind == JsonValueKind.Null)
+ {
+ return string.Empty;
+ }
+
+ if (value.ValueKind == JsonValueKind.Number && value.TryGetSingle(out var number))
+ {
+ return number.ToString(CultureInfo.InvariantCulture);
+ }
+
+ return value.ValueKind == JsonValueKind.String ? value.GetString() ?? string.Empty : string.Empty;
+ }
+
private static string FormatOptionalInt(JsonElement element, string name)
{
if (!element.TryGetProperty(name, out var value) || value.ValueKind == JsonValueKind.Null)
@@ -473,15 +591,11 @@ private static IReadOnlyList ReadFramework(string json)
var list = new List();
if (data is Dictionary root && root.TryGetValue("packages", out var packages))
{
- var rootDays = FormatOptionalInt(root, "maximumPostponeDays");
- var rootPostpone = FormatOptionalBool(root, "allowPostpone");
- var rootSkip = FormatOptionalBool(root, "allowSkip");
- var rootResume = FormatOptionalBool(root, "allowResume");
- var rootRetry = FormatOptionalBool(root, "allowRetry");
+ var rootDefaults = ReadDictionary(root);
foreach (var item in Enumerate(packages))
{
var package = ReadDictionary(item);
- ApplyRoot(package, rootDays, rootPostpone, rootSkip, rootResume, rootRetry);
+ ApplyRoot(package, rootDefaults);
list.Add(package);
}
@@ -513,9 +627,60 @@ private static ManifestPackage ReadDictionary(Dictionary element
AllowPostpone = FormatOptionalBool(element, "allowPostpone"),
AllowSkip = FormatOptionalBool(element, "allowSkip"),
AllowResume = FormatOptionalBool(element, "allowResume"),
- AllowRetry = FormatOptionalBool(element, "allowRetry")
+ AllowRetry = FormatOptionalBool(element, "allowRetry"),
+ BannerText = GetNestedString(element, "banner", "text"),
+ BannerBackColor = GetNestedString(element, "banner", "backColor"),
+ BannerBackColor2 = GetNestedString(element, "banner", "backColor2"),
+ BannerForeColor = GetNestedString(element, "banner", "foreColor"),
+ BannerImageUrl = GetNestedString(element, "banner", "imageUrl"),
+ PulseEnable = GetNestedBool(element, "pulse", "enable"),
+ PulseColor = GetNestedString(element, "pulse", "color"),
+ PulseStyle = GetNestedString(element, "pulse", "style"),
+ PulseSpeed = GetNestedNumber(element, "pulse", "speed")
};
+ private static string GetNestedString(Dictionary element, string parent, string name)
+ {
+ if (!element.TryGetValue(parent, out var value) || !(value is Dictionary nested))
+ {
+ return string.Empty;
+ }
+
+ return GetString(nested, name);
+ }
+
+ private static string GetNestedBool(Dictionary element, string parent, string name)
+ {
+ if (!element.TryGetValue(parent, out var value) || !(value is Dictionary nested))
+ {
+ return string.Empty;
+ }
+
+ return FormatOptionalBool(nested, name);
+ }
+
+ private static string GetNestedNumber(Dictionary element, string parent, string name)
+ {
+ if (!element.TryGetValue(parent, out var value) || !(value is Dictionary nested))
+ {
+ return string.Empty;
+ }
+
+ if (!nested.TryGetValue(name, out var number) || number == null)
+ {
+ return string.Empty;
+ }
+
+ try
+ {
+ return Convert.ToSingle(number, CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture);
+ }
+ catch (Exception exception) when (exception is FormatException || exception is InvalidCastException || exception is OverflowException)
+ {
+ return Convert.ToString(number, CultureInfo.InvariantCulture) ?? string.Empty;
+ }
+ }
+
private static string FormatOptionalInt(Dictionary element, string name)
{
if (!element.TryGetValue(name, out var value) || value == null)
diff --git a/Source/TestHarnesses/UpdateManifestEditor/ManifestPackage.cs b/Source/TestHarnesses/UpdateManifestEditor/ManifestPackage.cs
index d2a90a3ed5..6010e856d2 100644
--- a/Source/TestHarnesses/UpdateManifestEditor/ManifestPackage.cs
+++ b/Source/TestHarnesses/UpdateManifestEditor/ManifestPackage.cs
@@ -47,6 +47,26 @@ internal sealed class ManifestPackage
/// Empty omits allowRetry. False hides Restart.
public string AllowRetry { get; set; } = string.Empty;
+ public string BannerText { get; set; } = string.Empty;
+
+ public string BannerBackColor { get; set; } = string.Empty;
+
+ public string BannerBackColor2 { get; set; } = string.Empty;
+
+ public string BannerForeColor { get; set; } = string.Empty;
+
+ public string BannerImageUrl { get; set; } = string.Empty;
+
+ /// Empty omits pulse. true or false writes enable.
+ public string PulseEnable { get; set; } = string.Empty;
+
+ public string PulseColor { get; set; } = string.Empty;
+
+ /// Empty defaults to All when pulse is written.
+ public string PulseStyle { get; set; } = string.Empty;
+
+ public string PulseSpeed { get; set; } = string.Empty;
+
public override string ToString()
{
var version = string.IsNullOrWhiteSpace(Version) ? "(new package)" : Version.Trim();