Enhance user interaction with new dialog features and improve default browser handling

This commit is contained in:
Arne Moerman
2026-05-08 14:03:56 +02:00
parent 34ea1eb4b2
commit c26898d4d2
6 changed files with 498 additions and 38 deletions
+7 -2
View File
@@ -4,14 +4,18 @@ A unified framework for managing Windows automation tasks with a modular, extens
## Quick Start ## Quick Start
Clone or extract the repo to any location, then:
```powershell ```powershell
# Navigate to the root folder # Navigate to the repo folder (any path works)
cd C:\Tools\ArnePowershellAutomation cd <your-repo-path>
# Run the main menu # Run the main menu
.\configure.ps1 .\configure.ps1
``` ```
All paths are relative to `configure.ps1`, so the repo is portable.
## Architecture ## Architecture
### Entry Point: `configure.ps1` ### Entry Point: `configure.ps1`
@@ -218,6 +222,7 @@ When you select a feature for configuration, you'll be prompted for each setting
- **Windows 11 Only for Toasts**: Toast notifications require WinRT (Windows 10+), but full Windows 11 support is assumed. Win10 may work but is untested. - **Windows 11 Only for Toasts**: Toast notifications require WinRT (Windows 10+), but full Windows 11 support is assumed. Win10 may work but is untested.
- **Scheduled Task Security**: The runner task executes as the current user with Limited RunLevel (no elevation). Features cannot perform system-wide administrative tasks; they're limited to user-level operations. - **Scheduled Task Security**: The runner task executes as the current user with Limited RunLevel (no elevation). Features cannot perform system-wide administrative tasks; they're limited to user-level operations.
- **DNS Suffix Matching**: Network detection relies on `Get-DnsClient` adapter DNS suffix. Make sure your network adapter is configured with the correct suffix for accurate detection. - **DNS Suffix Matching**: Network detection relies on `Get-DnsClient` adapter DNS suffix. Make sure your network adapter is configured with the correct suffix for accurate detection.
- **Manual Execution & Repetition Timer**: Executing the runner manually via menu option 5 ("Execute runner now") runs the task immediately but does **not** start the 2-minute repetition timer. The repetition timer only activates on logon/restart. For continuous testing, you may need to restart or wait for next logon.
## Extending with New Features ## Extending with New Features
+200 -12
View File
@@ -180,19 +180,30 @@ function Read-OperationResult {
# Feature discovery # Feature discovery
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
function Get-Features { function Get-FeatureFiles {
<# $featuresDir = Join-Path $script:InternalRoot 'features'
.SYNOPSIS if (-not (Test-Path $featuresDir)) {
Dot-sources every .ps1 in internal\features\ and collects their $FeatureMeta. return @()
Returns an ordered list of [PSCustomObject]@{ File; Meta }. }
#>
$featuresDir = Join-Path $script:InternalRoot 'features' return @(Get-ChildItem -Path $featuresDir -Filter '*.ps1' -ErrorAction SilentlyContinue |
$featureFiles = Get-ChildItem -Path $featuresDir -Filter '*.ps1' -ErrorAction SilentlyContinue | Sort-Object Name)
Sort-Object Name }
function Get-FeatureCacheSignature {
param([System.IO.FileInfo[]]$FeatureFiles)
return (($FeatureFiles | ForEach-Object {
'{0}:{1}' -f $_.FullName, $_.LastWriteTimeUtc.Ticks
}) -join '|')
}
function Load-FeaturesFromFiles {
param([System.IO.FileInfo[]]$FeatureFiles)
$features = [System.Collections.Generic.List[PSCustomObject]]::new() $features = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($file in $featureFiles) { foreach ($file in $FeatureFiles) {
try { try {
. $file.FullName # defines $FeatureMeta + Invoke-Feature . $file.FullName # defines $FeatureMeta + Invoke-Feature
$meta = $FeatureMeta # copy before the next iteration overwrites it $meta = $FeatureMeta # copy before the next iteration overwrites it
@@ -208,7 +219,140 @@ function Get-Features {
} }
} }
return $features return @($features)
}
function Complete-FeaturePrewarm {
param([switch]$Wait)
if (-not $script:FeaturesPrewarmTask) {
return $false
}
$task = $script:FeaturesPrewarmTask
if ($Wait) {
$null = $task.Handle.AsyncWaitHandle.WaitOne()
}
if (-not $task.Handle.IsCompleted) {
return $false
}
try {
$result = $task.PowerShell.EndInvoke($task.Handle)
if ($result.Count -gt 0) {
$payload = $result[0]
$script:FeaturesCache = @($payload.Features)
$script:FeaturesCacheSignature = [string]$payload.Signature
foreach ($errMsg in @($payload.Errors)) {
Write-Log -Level Warn -Message $errMsg -Feature 'Configure'
}
}
}
catch {
Write-Log -Level Warn -Message "Feature prewarm completion failed: $_" -Feature 'Configure'
}
finally {
try { $task.PowerShell.Dispose() } catch {}
$script:FeaturesPrewarmTask = $null
}
return $true
}
function Start-FeaturePrewarmAsync {
if ($script:FeaturesPrewarmTask) {
return
}
$featureFiles = Get-FeatureFiles
if ($featureFiles.Count -eq 0) {
$script:FeaturesCache = @()
$script:FeaturesCacheSignature = ''
return
}
$ps = [powershell]::Create()
$scriptBlock = @'
param([string]$FeaturesDir)
$featureFiles = Get-ChildItem -Path $FeaturesDir -Filter '*.ps1' -ErrorAction SilentlyContinue |
Sort-Object Name
$signature = (($featureFiles | ForEach-Object {
'{0}:{1}' -f $_.FullName, $_.LastWriteTimeUtc.Ticks
}) -join '|')
$features = [System.Collections.Generic.List[PSCustomObject]]::new()
$errors = [System.Collections.Generic.List[string]]::new()
foreach ($file in $featureFiles) {
try {
. $file.FullName
$meta = $FeatureMeta
$features.Add([PSCustomObject]@{
File = $file.FullName
Meta = $meta
})
}
catch {
$errors.Add(("Failed to load feature '{0}' during prewarm: {1}" -f $file.Name, $_.Exception.Message))
}
}
[PSCustomObject]@{
Signature = $signature
Features = @($features)
Errors = @($errors)
}
'@
$null = $ps.AddScript($scriptBlock).AddArgument((Join-Path $script:InternalRoot 'features'))
$handle = $ps.BeginInvoke()
$script:FeaturesPrewarmTask = @{
PowerShell = $ps
Handle = $handle
}
}
function Get-Features {
<#
.SYNOPSIS
Dot-sources every .ps1 in internal\features\ and collects their $FeatureMeta.
Returns an ordered list of [PSCustomObject]@{ File; Meta }.
#>
param([switch]$Refresh)
$featureFiles = Get-FeatureFiles
$cacheSignature = Get-FeatureCacheSignature -FeatureFiles $featureFiles
if (-not $Refresh -and $script:FeaturesCache -and $script:FeaturesCacheSignature -eq $cacheSignature) {
return $script:FeaturesCache
}
if (-not $Refresh) {
[void](Complete-FeaturePrewarm)
if ($script:FeaturesCache -and $script:FeaturesCacheSignature -eq $cacheSignature) {
return $script:FeaturesCache
}
if ($script:FeaturesPrewarmTask) {
Write-Hint 'Loading feature metadata, please wait...'
[void](Complete-FeaturePrewarm -Wait)
if ($script:FeaturesCache -and $script:FeaturesCacheSignature -eq $cacheSignature) {
return $script:FeaturesCache
}
}
}
$script:FeaturesCache = Load-FeaturesFromFiles -FeatureFiles $featureFiles
$script:FeaturesCacheSignature = $cacheSignature
return $script:FeaturesCache
} }
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
@@ -395,7 +539,10 @@ function Register-Runner {
throw "Register-ScheduledTask returned without creating '$($script:TaskName)'." throw "Register-ScheduledTask returned without creating '$($script:TaskName)'."
} }
Write-Host " Task '$($script:TaskName)' registered successfully." -ForegroundColor Green # Start the task so runner begins executing
Start-ScheduledTask -TaskName $script:TaskName -TaskPath $script:TaskPath -ErrorAction Stop
Write-Host " Task '$($script:TaskName)' registered and started successfully." -ForegroundColor Green
Write-Host '' Write-Host ''
Write-Hint (" Triggers: at logon + every {0} minutes" -f $script:RunnerIntervalMinutes) Write-Hint (" Triggers: at logon + every {0} minutes" -f $script:RunnerIntervalMinutes)
Write-Hint " Runs: hidden PowerShell, current user, no elevation" Write-Hint " Runs: hidden PowerShell, current user, no elevation"
@@ -604,10 +751,46 @@ function Show-FeatureMenu {
} }
} }
function Initialize-FeatureCache {
try {
Start-FeaturePrewarmAsync
Write-Log -Level Info -Message 'Feature cache prewarm started in background.' -Feature 'Configure'
}
catch {
Write-Log -Level Warn -Message "Feature cache prewarm failed: $_" -Feature 'Configure'
}
}
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
# Main menu # Main menu
# ═════════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════════
function Invoke-RunnerNow {
$task = Get-RunnerTask
if (-not $task) {
Write-Host " ERROR: Task is not registered. Use option 2 to register first." -ForegroundColor Red
Write-Host ''
Pause-ForKey
return
}
try {
Write-Host ''
Write-Host " Executing runner task now..." -ForegroundColor Cyan
Start-ScheduledTask -TaskName $script:TaskName -TaskPath $script:TaskPath -ErrorAction Stop
Write-Host " Task execution started (runs async). Check logs in a moment." -ForegroundColor Green
Write-Hint " Note: The 2-minute repetition timer only starts on next logon/restart."
Write-Log -Level Info -Message "Runner task invoked manually from configure menu." -Feature 'Configure'
}
catch {
Write-Host " ERROR: Failed to start task: $_" -ForegroundColor Red
Write-Log -Level Error -Message "Failed to start runner task manually: $_" -Feature 'Configure'
}
Write-Host ''
Pause-ForKey
}
function Show-MainMenu { function Show-MainMenu {
while ($true) { while ($true) {
Write-Header 'PowerShell Automation Center' Write-Header 'PowerShell Automation Center'
@@ -615,6 +798,7 @@ function Show-MainMenu {
Write-Host ' 2 Register runner' Write-Host ' 2 Register runner'
Write-Host ' 3 Remove registered runner' Write-Host ' 3 Remove registered runner'
Write-Host ' 4 Configure features' Write-Host ' 4 Configure features'
Write-Host ' 5 Execute runner now'
Write-Host '' Write-Host ''
Write-Hint ' Q Exit' Write-Hint ' Q Exit'
Write-Host '' Write-Host ''
@@ -627,6 +811,7 @@ function Show-MainMenu {
'2' { Register-Runner } '2' { Register-Runner }
'3' { Remove-Runner } '3' { Remove-Runner }
'4' { Show-FeatureMenu } '4' { Show-FeatureMenu }
'5' { Invoke-RunnerNow }
{ $_ -in 'q', 'Q' } { { $_ -in 'q', 'Q' } {
Clear-Host Clear-Host
return return
@@ -679,5 +864,8 @@ if ($AutoRemoveRunner) {
} }
} }
if (-not $AutoRegisterRunner -and -not $AutoRemoveRunner) {
Initialize-FeatureCache
}
Show-MainMenu Show-MainMenu
+130 -16
View File
@@ -1,4 +1,105 @@
# ── Feature metadata ────────────────────────────────────────────────────────── # ── Browser detection helper ──────────────────────────────────────────────────
function Get-InstalledBrowsers {
$knownBrowsers = @(
'ChromeHTML',
'FirefoxURL',
'OperaGXStable',
'Opera GXStable',
'SafariHTML',
'MSEdgeHTM',
'BraveHTML',
'Vivaldi',
'IEexplore'
)
$installed = @()
foreach ($progId in $knownBrowsers) {
if (Test-Path "HKLM:\SOFTWARE\Classes\$progId" -ErrorAction SilentlyContinue) {
$installed += $progId
}
}
return @($installed | Sort-Object)
}
function Get-BrowserSearchText {
param([string]$ProgId)
if (-not $ProgId) { return 'browser' }
switch -Regex ($ProgId) {
'^Opera\s?GXStable$' { return 'opera gx' }
'^ChromeHTML$' { return 'chrome' }
'^FirefoxURL' { return 'firefox' }
'^MSEdgeHTM$' { return 'edge' }
'^BraveHTML$' { return 'brave' }
'^Vivaldi$' { return 'vivaldi' }
default { return $ProgId }
}
}
function Invoke-DefaultBrowserGuidedChange {
param([Parameter(Mandatory)][string]$TargetProgId)
$regPath = 'HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice'
try {
$searchText = Get-BrowserSearchText -ProgId $TargetProgId
Stop-Process -ErrorAction Ignore -Name SystemSettings
Start-Process 'ms-settings:defaultapps'
$ps = Get-Process -ErrorAction Stop SystemSettings
do {
Start-Sleep -Milliseconds 100
$ps.Refresh()
} while ([int]$ps.MainWindowHandle -eq 0)
Start-Sleep -Milliseconds 200
$shell = New-Object -ComObject WScript.Shell
foreach ($i in 1..5) {
$shell.SendKeys('{TAB}')
Start-Sleep -Milliseconds 100
}
$shell.SendKeys($searchText)
Start-Sleep -Seconds 1
$shell.SendKeys('{TAB}')
Start-Sleep -Milliseconds 100
$shell.SendKeys('{ENTER}')
Start-Sleep -Milliseconds 200
$shell.SendKeys('{ENTER}')
Start-Sleep -Milliseconds 200
$shell.SendKeys('%{F4}')
Start-Sleep -Milliseconds 300
$browser = (Get-ItemProperty -Path $regPath -Name 'ProgId' -ErrorAction Stop).ProgId
if ($browser -eq $TargetProgId) {
Write-Log -Level Info -Message "Default browser successfully changed to '$browser'." -Feature 'DefaultBrowser'
return $true
}
Write-Log -Level Warn -Message "Browser change attempted, but current ProgId is '$browser' (expected '$TargetProgId')." -Feature 'DefaultBrowser'
return $false
}
catch {
Write-Log -Level Error -Message "Failed during guided default-browser change: $_" -Feature 'DefaultBrowser'
return $false
}
}
# ── Feature metadata ──────────────────────────────────────────────────────────
$installedBrowsers = Get-InstalledBrowsers
$browserListText = if ($installedBrowsers.Count -gt 0) {
"Detected on this system: " + ($installedBrowsers -join ', ')
} else {
"No known browsers detected. Examples: ChromeHTML, FirefoxURL, OperaGXStable, SafariHTML"
}
$FeatureMeta = @{ $FeatureMeta = @{
Name = 'DefaultBrowser' Name = 'DefaultBrowser'
@@ -9,7 +110,7 @@ $FeatureMeta = @{
Label = 'Target ProgId' Label = 'Target ProgId'
Type = 'string' Type = 'string'
Default = 'OperaGXStable' Default = 'OperaGXStable'
Description = 'ProgId of the desired default browser (e.g. OperaGXStable, ChromeHTML, FirefoxURL-308046B0AF4A39CB)' Description = $browserListText
} }
) )
} }
@@ -23,16 +124,10 @@ function Invoke-Feature {
) )
if (-not $State) { $State = @{} } if (-not $State) { $State = @{} }
if (-not $State.ContainsKey('lastShownDate')) { $State['lastShownDate'] = $null } if (-not $State.ContainsKey('ignoreUntilDate')) { $State['ignoreUntilDate'] = $null }
$today = (Get-Date).ToString('yyyy-MM-dd') $today = (Get-Date).ToString('yyyy-MM-dd')
# Only notify once per day
if ($State['lastShownDate'] -eq $today) {
Write-Log -Level Info -Message 'Already checked today, skipping.' -Feature 'DefaultBrowser'
return $State
}
$regPath = 'HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice' $regPath = 'HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice'
try { try {
@@ -49,7 +144,14 @@ function Invoke-Feature {
Write-Log -Level Info ` Write-Log -Level Info `
-Message "Default browser OK: '$currentProgId'." ` -Message "Default browser OK: '$currentProgId'." `
-Feature 'DefaultBrowser' -Feature 'DefaultBrowser'
$State['lastShownDate'] = $today $State['ignoreUntilDate'] = $null
return $State
}
if ($State['ignoreUntilDate'] -eq $today) {
Write-Log -Level Info `
-Message "Mismatch ignored for today (current '$currentProgId', expected '$($Config['targetProgId'])')." `
-Feature 'DefaultBrowser'
return $State return $State
} }
@@ -57,17 +159,29 @@ function Invoke-Feature {
-Message "Default browser mismatch — found '$currentProgId', expected '$($Config['targetProgId'])'." ` -Message "Default browser mismatch — found '$currentProgId', expected '$($Config['targetProgId'])'." `
-Feature 'DefaultBrowser' -Feature 'DefaultBrowser'
# Note: Windows 11 blocks programmatic default-browser changes via registry hash protection.
# We guide the user to the Settings page instead.
Show-ToastNotification ` Show-ToastNotification `
-Title 'Default Browser' ` -Title 'Default Browser' `
-Body ("Default browser is '$currentProgId'. Click below to set it to $($Config['targetProgId']).") ` -Body ("Default browser is '$currentProgId'. Do you want to change it now?") `
-Buttons @( -Buttons @(
@{ Label = 'Open Default Apps Settings'; Action = 'ms-settings:defaultapps' }, @{ Label = 'Yes'; Action = 'ms-settings:defaultapps' },
@{ Label = 'Dismiss'; Action = 'dismiss' } @{ Label = 'No'; Action = 'dismiss' }
) )
$State['lastShownDate'] = $today $decision = Show-ConfirmationDialog `
-Title 'Default Browser' `
-Message ("Default browser is '$currentProgId', expected '$($Config['targetProgId'])'.`n`nDo you want to change it now?") `
-Feature 'DefaultBrowser' `
-Default 'No'
if ($decision -eq 'Yes') {
[void](Invoke-DefaultBrowserGuidedChange -TargetProgId $Config['targetProgId'])
$State['ignoreUntilDate'] = $null
}
else {
$State['ignoreUntilDate'] = $today
Write-Log -Level Info -Message 'User selected No; mismatch notifications suppressed until tomorrow.' -Feature 'DefaultBrowser'
}
return $State return $State
} }
+55 -8
View File
@@ -45,6 +45,7 @@ function Invoke-Feature {
if (-not $State) { $State = @{} } if (-not $State) { $State = @{} }
if (-not $State.ContainsKey('lastShownDate')) { $State['lastShownDate'] = $null } if (-not $State.ContainsKey('lastShownDate')) { $State['lastShownDate'] = $null }
if (-not $State.ContainsKey('snoozeUntil')) { $State['snoozeUntil'] = $null }
$today = (Get-Date).ToString('yyyy-MM-dd') $today = (Get-Date).ToString('yyyy-MM-dd')
@@ -54,6 +55,21 @@ function Invoke-Feature {
return $State return $State
} }
if ($State['snoozeUntil']) {
try {
$snoozeUntil = [datetime]::Parse($State['snoozeUntil'])
if ((Get-Date) -lt $snoozeUntil) {
Write-Log -Level Info `
-Message ("Snoozed until {0}, skipping." -f $snoozeUntil.ToString('HH:mm:ss')) `
-Feature 'SandwichReminder'
return $State
}
}
catch {
Write-Log -Level Warn -Message "Invalid snoozeUntil value '$($State['snoozeUntil'])', ignoring." -Feature 'SandwichReminder'
}
}
# Parse reminder time # Parse reminder time
try { try {
$targetTime = [datetime]::ParseExact($Config['reminderTime'], 'HH:mm', $null) $targetTime = [datetime]::ParseExact($Config['reminderTime'], 'HH:mm', $null)
@@ -86,17 +102,48 @@ function Invoke-Feature {
return $State return $State
} }
Write-Log -Level Info -Message 'Showing sandwich reminder toast.' -Feature 'SandwichReminder' Write-Log -Level Info -Message 'Showing sandwich reminder confirmation dialog.' -Feature 'SandwichReminder'
Show-ToastNotification ` $decision = Show-MultiChoiceDialog `
-Title 'Sandwich Order' ` -Title 'Sandwich Order' `
-Body 'Do you want to order a sandwich today?' ` -Message 'Do you want to order a sandwich today?' `
-Buttons @( -Feature 'SandwichReminder' `
@{ Label = 'Yes, order now!'; Action = $Config['url'] }, -DefaultValue 'snooze15' `
@{ Label = 'No thanks'; Action = 'dismiss' } -Choices @(
@{ Label = 'Order now'; Value = 'order' },
@{ Label = 'Snooze 15 min'; Value = 'snooze15' },
@{ Label = 'Snooze 1h'; Value = 'snooze60' },
@{ Label = 'No'; Value = 'no' }
) )
$State['lastShownDate'] = $today if ($decision -eq 'order') {
try {
Start-Process $Config['url']
Write-Log -Level Info -Message "Opened sandwich order URL: $($Config['url'])" -Feature 'SandwichReminder'
}
catch {
Write-Log -Level Error -Message "Failed to open sandwich order URL '$($Config['url'])': $_" -Feature 'SandwichReminder'
}
$State['lastShownDate'] = $today
$State['snoozeUntil'] = $null
}
elseif ($decision -eq 'snooze60') {
$until = (Get-Date).AddHours(1)
$State['snoozeUntil'] = $until.ToString('o')
Write-Log -Level Info -Message ("User snoozed sandwich reminder for 1 hour (until {0})." -f $until.ToString('HH:mm:ss')) -Feature 'SandwichReminder'
}
elseif ($decision -eq 'no') {
$State['lastShownDate'] = $today
$State['snoozeUntil'] = $null
Write-Log -Level Info -Message 'User selected No for sandwich reminder; skipping for rest of day.' -Feature 'SandwichReminder'
}
else {
$until = (Get-Date).AddMinutes(15)
$State['snoozeUntil'] = $until.ToString('o')
Write-Log -Level Info -Message ("User snoozed sandwich reminder for 15 minutes (until {0})." -f $until.ToString('HH:mm:ss')) -Feature 'SandwichReminder'
}
return $State return $State
} }
+105
View File
@@ -0,0 +1,105 @@
# PromptHelper.ps1 — shared foreground/system-modal confirmation dialog helper.
function Show-ConfirmationDialog {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$Title,
[Parameter(Mandatory)][string]$Message,
[string]$Feature = 'PromptHelper',
[string]$Default = 'No'
)
try {
$shell = New-Object -ComObject WScript.Shell
# 4=YesNo, 32=Question icon, 4096=System modal, 65536=Foreground
$result = $shell.Popup($Message, 0, $Title, 4 + 32 + 4096 + 65536)
switch ($result) {
6 { return 'Yes' }
7 { return 'No' }
default { return $Default }
}
}
catch {
Write-Log -Level Error -Message "Failed to show confirmation dialog '$Title': $_" -Feature $Feature
return $Default
}
}
function Show-MultiChoiceDialog {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$Title,
[Parameter(Mandatory)][string]$Message,
[Parameter(Mandatory)][object[]]$Choices,
[string]$Feature = 'PromptHelper',
[string]$DefaultValue = ''
)
try {
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null
Add-Type -AssemblyName System.Drawing -ErrorAction SilentlyContinue | Out-Null
if ($Choices.Count -lt 1 -or $Choices.Count -gt 4) {
throw 'Show-MultiChoiceDialog expects 1 to 4 choices.'
}
$form = New-Object System.Windows.Forms.Form
$form.Text = $Title
$form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
$form.TopMost = $true
$form.Width = 480
$form.Height = 190
$form.MinimizeBox = $false
$form.MaximizeBox = $false
$form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
$label = New-Object System.Windows.Forms.Label
$label.AutoSize = $false
$label.Left = 12
$label.Top = 12
$label.Width = 440
$label.Height = 80
$label.Text = $Message
$form.Controls.Add($label)
$form.Tag = $DefaultValue
$gap = 10
$availableWidth = $form.ClientSize.Width - 24 - ($gap * ($Choices.Count - 1))
$buttonWidth = [Math]::Min(130, [Math]::Floor($availableWidth / $Choices.Count))
$totalWidth = ($buttonWidth * $Choices.Count) + ($gap * ($Choices.Count - 1))
$startX = [Math]::Max(12, [int](($form.ClientSize.Width - $totalWidth) / 2))
for ($i = 0; $i -lt $Choices.Count; $i++) {
$choice = $Choices[$i]
$button = New-Object System.Windows.Forms.Button
$button.Left = $startX + ($i * ($buttonWidth + $gap))
$button.Top = 105
$button.Width = $buttonWidth
$button.Height = 30
$button.Text = [string]$choice.Label
$button.Tag = [string]$choice.Value
$button.Add_Click({
param($sender, $eventArgs)
$dlg = $sender.FindForm()
$dlg.Tag = [string]$sender.Tag
$dlg.DialogResult = [System.Windows.Forms.DialogResult]::OK
$dlg.Close()
})
$form.Controls.Add($button)
if ($i -eq 0) {
$form.AcceptButton = $button
}
}
[void]$form.ShowDialog()
$result = [string]$form.Tag
$form.Dispose()
return $result
}
catch {
Write-Log -Level Error -Message "Failed to show multi-choice dialog '$Title': $_" -Feature $Feature
return $DefaultValue
}
}
+1
View File
@@ -10,6 +10,7 @@ $script:InternalRoot = $PSScriptRoot # runner.ps1 lives in internal\
. (Join-Path $InternalRoot 'lib\Logging.ps1') . (Join-Path $InternalRoot 'lib\Logging.ps1')
. (Join-Path $InternalRoot 'lib\NetworkUtils.ps1') . (Join-Path $InternalRoot 'lib\NetworkUtils.ps1')
. (Join-Path $InternalRoot 'lib\ToastHelper.ps1') . (Join-Path $InternalRoot 'lib\ToastHelper.ps1')
. (Join-Path $InternalRoot 'lib\PromptHelper.ps1')
. (Join-Path $InternalRoot 'lib\Config.ps1') . (Join-Path $InternalRoot 'lib\Config.ps1')
Write-Log -Level Info -Message '────── Runner started ──────' -Feature 'Runner' Write-Log -Level Info -Message '────── Runner started ──────' -Feature 'Runner'