public/dialogs/Show-UiMessageDialog.ps1

function Show-UiMessageDialog {
    <#
    .SYNOPSIS
        Displays a themed message dialog with customizable buttons and icons.
    .DESCRIPTION
        Shows a custom WPF dialog that respects the current theme. Replaces standard MessageBox.
        A message taller than the screen scrolls inside the dialog, which stays away from the
        taskbar with its buttons in view.
    .PARAMETER Title
        Dialog window title.
    .PARAMETER Message
        Message text to display.
    .PARAMETER Buttons
        Button configuration: OK, OKCancel, YesNo, or YesNoCancel.
    .PARAMETER Icon
        Icon type: Info, Warning, Error, Question, or None.
    .PARAMETER PowerShell
        When used, displays the message in a PowerShell console-styled code viewer.
        Uses Consolas font, blue background (#012456), white text, and makes the dialog
        larger (700x500) and resizable with horizontal/vertical scrollbars.
        For source code and command snippets.
    .PARAMETER CustomButtons
        Custom buttons, rendered left to right in declaration order. Pass a
        { New-UiDialogButton ... } definition block, an array of New-UiDialogButton output,
        or the legacy hashtable array where each hashtable has:
        - Label: The button text (required)
        - Value: The value returned when clicked (required)
        - IsDefault: If true, Enter activates this button (optional)
        - IsAccent: If true, button uses accent color (optional)
        - IsCancel: If true, Esc activates this button (optional)
 
        When provided, the -Buttons parameter is ignored.
    .PARAMETER ThemeColors
        Override theme colors for this dialog. Pass a colors hashtable directly.
    .EXAMPLE
        Show-UiMessageDialog -Title 'Confirmation' -Message 'Are you sure?' -Buttons YesNo -Icon Question
    .EXAMPLE
        $answer = Show-UiMessageDialog -Title 'Unsaved Changes' -Message 'Save changes?' -Icon Question -CustomButtons {
            New-UiDialogButton 'Save' -Accent -Default
            New-UiDialogButton 'Discard'
            New-UiDialogButton 'Cancel' -Cancel
        }
    .EXAMPLE
        $result = Show-UiMessageDialog -Title 'Success' -Message 'Operation completed!' -Buttons OK -Icon Info
    .EXAMPLE
        Show-UiMessageDialog -Title 'Source Code' -Message $scriptBlock.ToString() -PowerShell
    .EXAMPLE
        # Legacy hashtable form, still supported
        $buttons = @(
            @{ Label = 'Save'; Value = 'Save'; IsAccent = $true; IsDefault = $true }
            @{ Label = 'Discard'; Value = 'Discard' }
            @{ Label = 'Cancel'; Value = 'Cancel'; IsCancel = $true }
        )
        Show-UiMessageDialog -Title 'Unsaved Changes' -Message 'Save changes?' -CustomButtons $buttons -Icon Question
    #>

    [CmdletBinding()]
    param(
        [string]$Title = 'Message',

        [Parameter(Mandatory)]
        [string]$Message,

        [ValidateSet('OK', 'OKCancel', 'YesNo', 'YesNoCancel')]
        [string]$Buttons = 'OK',

        [ValidateSet('Info', 'Warning', 'Error', 'Question', 'None')]
        [string]$Icon = 'Info',

        [object]$ThemeColors,

        [switch]$PowerShell,

        # Untyped: takes a New-UiDialogButton definition block, an array of definitions, or the legacy hashtable array. [array] would swallow a definition block as a one-element array wrapping the scriptblock.
        [object]$CustomButtons
    )

    Write-Debug "Title='$Title' Buttons='$Buttons' Icon='$Icon' PowerShell=$PowerShell"

    if ($null -ne $CustomButtons) {
        $CustomButtons = ConvertTo-UiDefinitionArray -InputObject $CustomButtons -ParameterName '-CustomButtons' -CallerName 'Show-UiMessageDialog' -AllowObject
    }

    # Calculate width based on button count - each button is ~90px wide
    $buttonCount   = if ($CustomButtons) { $CustomButtons.Count } else { 3 }
    $minForButtons = [Math]::Max(420, ($buttonCount * 90) + 50)

    # Long messages (error dumps, stack traces, joined multi-row failures) get a wide resizable dialog - at 420 a stack line wraps four deep and the whole thing reads like a ransom note.
    # Short prompts keep the compact fixed layout.
    $messageLines = @(if ($Message) { $Message -split "`n" } else { '' })
    $longestLine  = 0
    foreach ($msgLine in $messageLines) { if ($msgLine.Length -gt $longestLine) { $longestLine = $msgLine.Length } }
    $isBigMessage = ($longestLine -gt 80) -or ($messageLines.Count -gt 10)

    # PowerShell mode uses fixed size; standard mode sizes to content
    $dialogParams = @{
        Title         = $Title
        Width         = if ($PowerShell) { 700 } elseif ($isBigMessage) { [Math]::Max(700, $minForButtons) } else { $minForButtons }
        Height        = if ($PowerShell) { 500 } else { 0 }
        MaxHeight     = if ($PowerShell -or $isBigMessage) { 10000 } else { 800 }
        SizeToContent = if ($PowerShell) { 'Manual' } else { 'Height' }
        ResizeMode    = if ($PowerShell -or $isBigMessage) { 'CanResize' } else { 'NoResize' }
        AppIdSuffix   = 'Message'
        ThemeColors   = $ThemeColors
    }

    # Set overlay icon based on message type
    $overlayGlyph = switch ($Icon) {
        'Info'     { [PsUi.ModuleContext]::GetIcon('Info') }
        'Warning'  { [PsUi.ModuleContext]::GetIcon('Alert') }
        'Error'    { [PsUi.ModuleContext]::GetIcon('Error') }
        'Question' { [PsUi.ModuleContext]::GetIcon('Help') }
        default    { [PsUi.ModuleContext]::GetIcon('Info') }
    }
    $dialogParams['OverlayGlyph'] = $overlayGlyph

    # Use standard helper to create the window shell
    $dialog       = New-DialogWindow @dialogParams
    $window       = $dialog.Window
    $contentPanel = $dialog.ContentPanel
    $colors       = $dialog.Colors

    # Color for icon matches overlay
    $iconColor = switch ($Icon) {
        'Info'     { $colors.Accent }
        'Warning'  { $colors.Warning }
        'Error'    { $colors.Error }
        'Question' { $colors.Accent }
        default    { $colors.Accent }
    }

    # Button panel at bottom using Grid for left/right alignment
    $buttonBar = [System.Windows.Controls.Grid]::new()
    $buttonBar.Margin = [System.Windows.Thickness]::new(0, 12, 0, 0)
    [void]$buttonBar.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]@{ Width = '*' })
    [void]$buttonBar.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]@{ Width = 'Auto' })
    [System.Windows.Controls.DockPanel]::SetDock($buttonBar, 'Bottom')
    [void]$contentPanel.Children.Add($buttonBar)

    # Add copy button for informational dialogs (not Question/choice dialogs)
    $showCopyButton = $Icon -in @('Info', 'Warning', 'Error') -or $PowerShell
    if ($showCopyButton) {
        $copyBtn = [System.Windows.Controls.Button]@{
            Height              = 28
            Padding             = [System.Windows.Thickness]::new(10, 4, 10, 4)
            ToolTip             = 'Copy message to clipboard'
            HorizontalAlignment = 'Left'
        }

        # Icon + text content for copy button
        $copyContent = [System.Windows.Controls.StackPanel]@{ Orientation = 'Horizontal' }
        $copyIcon = [System.Windows.Controls.TextBlock]@{
            Text              = [PsUi.ModuleContext]::GetIcon('Copy')
            FontFamily        = [PsUi.ModuleContext]::ActiveIconFontFamily
            FontSize          = 12
            Margin            = [System.Windows.Thickness]::new(0, 0, 6, 0)
            VerticalAlignment = 'Center'
        }
        $copyText = [System.Windows.Controls.TextBlock]@{
            Text              = 'Copy'
            VerticalAlignment = 'Center'
        }
        [void]$copyContent.Children.Add($copyIcon)
        [void]$copyContent.Children.Add($copyText)
        $copyBtn.Content = $copyContent

        # Apply standard styling and hook click
        Set-ButtonStyle -Button $copyBtn
        $copyBtn.Tag = $Message
        $copyBtn.Add_Click({
            [System.Windows.Clipboard]::SetText($this.Tag)

            # Brief visual feedback - change icon to checkmark
            $panel     = $this.Content
            $iconBlock = $panel.Children[0]
            $originalIcon = $iconBlock.Text
            $iconBlock.Text = [PsUi.ModuleContext]::GetIcon('Accept')

            # Reset after 1.5 seconds
            $timer = [System.Windows.Threading.DispatcherTimer]::new()
            $timer.Interval = [TimeSpan]::FromMilliseconds(1500)
            $capturedIconBlock = $iconBlock
            $capturedOriginal  = $originalIcon
            $timer.Add_Tick({
                $capturedIconBlock.Text = $capturedOriginal
                $this.Stop()
            }.GetNewClosure())
            $timer.Start()
        })

        [System.Windows.Controls.Grid]::SetColumn($copyBtn, 0)
        [void]$buttonBar.Children.Add($copyBtn)
    }

    # Right-aligned button panel for action buttons
    $buttonPanel = [System.Windows.Controls.StackPanel]@{
        Orientation         = 'Horizontal'
        HorizontalAlignment = 'Right'
    }
    [System.Windows.Controls.Grid]::SetColumn($buttonPanel, 1)
    [void]$buttonBar.Children.Add($buttonPanel)

    # Build message content area - different for PowerShell vs standard mode
    if ($PowerShell) {

        # PowerShell console-styled TextBox
        $codeBox = [System.Windows.Controls.TextBox]@{

            Text                          = $Message
            IsReadOnly                    = $true
            AcceptsReturn                 = $true
            TextWrapping                  = 'NoWrap'
            VerticalContentAlignment      = 'Top'
            HorizontalScrollBarVisibility = 'Auto'
            VerticalScrollBarVisibility   = 'Auto'
            FontFamily                    = [System.Windows.Media.FontFamily]::new('Consolas')
            FontSize                      = 13
            Background                    = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#012456')
            Foreground                    = [System.Windows.Media.Brushes]::White
            BorderThickness               = [System.Windows.Thickness]::new(1)
            BorderBrush                   = [System.Windows.Media.BrushConverter]::new().ConvertFrom('#1E3A5F')
            Padding                       = [System.Windows.Thickness]::new(5)
            Margin                        = [System.Windows.Thickness]::new(0, 0, 0, 12)

        }

        $codeBox.Resources[[System.Windows.SystemColors]::ControlBrushKey] = $codeBox.Background

        # Add themed context menu for copy/select all
        $codeBox.ContextMenu = New-TextBoxContextMenu -ReadOnly
        [void]$contentPanel.Children.Add($codeBox)
    }
    else {
        # Standard message area with optional icon, wrapped in ScrollViewer
        $scrollViewer = [System.Windows.Controls.ScrollViewer]@{
            VerticalScrollBarVisibility   = 'Auto'
            HorizontalScrollBarVisibility = 'Disabled'
            Padding                       = [System.Windows.Thickness]::new(0, 0, 8, 0)
        }

        $messageGrid = [System.Windows.Controls.Grid]::new()
        [void]$messageGrid.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]::new())
        [void]$messageGrid.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]::new())

        $hasIcon = $Icon -ne 'None'
        if ($hasIcon) {
            $messageGrid.ColumnDefinitions[0].Width = [System.Windows.GridLength]::new(48)
        }
        else {
            $messageGrid.ColumnDefinitions[0].Width = [System.Windows.GridLength]::new(0)
        }
        $messageGrid.ColumnDefinitions[1].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star)

        # Add icon if needed
        if ($hasIcon) {
            $iconBlock = [System.Windows.Controls.TextBlock]@{
                Text                = $overlayGlyph
                FontFamily          = [PsUi.ModuleContext]::ActiveIconFontFamily
                FontSize            = 32
                Foreground          = ConvertTo-UiBrush $iconColor
                VerticalAlignment   = 'Center'
                HorizontalAlignment = 'Center'
                Margin              = [System.Windows.Thickness]::new(0, 0, 12, 0)
            }
            [System.Windows.Controls.Grid]::SetColumn($iconBlock, 0)
            [void]$messageGrid.Children.Add($iconBlock)
        }

        $messageText = [System.Windows.Controls.TextBlock]@{
            Text              = $Message
            FontSize          = 13
            TextWrapping      = 'Wrap'
            Foreground        = ConvertTo-UiBrush $colors.ControlFg
            VerticalAlignment = 'Top'
            Margin            = [System.Windows.Thickness]::new(0, 0, 0, 0)
        }
        [System.Windows.Controls.Grid]::SetColumn($messageText, 1)
        [void]$messageGrid.Children.Add($messageText)

        $scrollViewer.Content = $messageGrid
        [void]$contentPanel.Children.Add($scrollViewer)
    }

    # Add buttons - either custom buttons or standard button sets
    if ($CustomButtons -and $CustomButtons.Count -gt 0) {
        foreach ($btnDef in $CustomButtons) {
            $btn = [System.Windows.Controls.Button]@{
                Content  = $btnDef.Label
                MinWidth = 70
                Height   = 28
                Margin   = [System.Windows.Thickness]::new(6, 0, 0, 0)
                Padding  = [System.Windows.Thickness]::new(14, 4, 14, 4)
            }

            if ($btnDef.IsAccent) {
                Set-ButtonStyle -Button $btn -Accent
            }
            else {
                Set-ButtonStyle -Button $btn
            }

            # Store value AND window reference to avoid closure issues
            $btn.Tag = @{ Value = $btnDef.Value; Window = $window }
            $btn.Add_Click({
                $this.Tag.Window.Tag = $this.Tag.Value
                $this.Tag.Window.Close()
            })

            [void]$buttonPanel.Children.Add($btn)

            if ($btnDef.IsDefault) { $btn.IsDefault = $true }
            if ($btnDef.IsCancel) { $btn.IsCancel = $true }
        }
    }
    else {
        # Standard button configurations - helper to create styled button
        $createButton = {
            param($Text, $IsAccent, $Result, $TargetWindow)
            $btn = [System.Windows.Controls.Button]@{
                Content = $Text
                Width   = 80
                Height  = 28
                Margin  = [System.Windows.Thickness]::new(4, 0, 0, 0)
            }
            if ($IsAccent) { Set-ButtonStyle -Button $btn -Accent }
            else { Set-ButtonStyle -Button $btn }
            $btn.Tag = @{ Result = $Result; Window = $TargetWindow }
            $btn.Add_Click({ $this.Tag.Window.Tag = $this.Tag.Result; $this.Tag.Window.Close() })
            return $btn
        }

        switch ($Buttons) {
            'OK' {
                $okBtn = & $createButton 'OK' $true 'OK' $window
                $okBtn.IsDefault = $true
                [void]$buttonPanel.Children.Add($okBtn)
            }
            'OKCancel' {
                $okBtn = & $createButton 'OK' $true 'OK' $window
                $okBtn.IsDefault = $true
                [void]$buttonPanel.Children.Add($okBtn)

                $cancelBtn = & $createButton 'Cancel' $false 'Cancel' $window
                $cancelBtn.IsCancel = $true
                [void]$buttonPanel.Children.Add($cancelBtn)
            }
            'YesNo' {
                $yesBtn = & $createButton 'Yes' $true 'Yes' $window
                $yesBtn.IsDefault = $true
                [void]$buttonPanel.Children.Add($yesBtn)

                $noBtn = & $createButton 'No' $false 'No' $window
                $noBtn.IsCancel = $true
                [void]$buttonPanel.Children.Add($noBtn)
            }
            'YesNoCancel' {
                $yesBtn = & $createButton 'Yes' $true 'Yes' $window
                $yesBtn.IsDefault = $true
                [void]$buttonPanel.Children.Add($yesBtn)

                $noBtn = & $createButton 'No' $false 'No' $window
                [void]$buttonPanel.Children.Add($noBtn)

                $cancelBtn = & $createButton 'Cancel' $false 'Cancel' $window
                $cancelBtn.IsCancel = $true
                [void]$buttonPanel.Children.Add($cancelBtn)
            }
        }
    }

    # Attach the standard fade-in
    Initialize-UiWindowLoaded -Window $window -TitleBarBackground $colors.HeaderBackground -TitleBarForeground $colors.HeaderForeground

    # CenterOnParent places the dialog before SizeToContent has measured it, a long message grows down past the taskbar
    $fitWindow = $window
    $window.Add_Loaded({
        $source = [System.Windows.PresentationSource]::FromVisual($fitWindow)
        if (!$source) { return }
        $handle = [System.Windows.Interop.WindowInteropHelper]::new($fitWindow).Handle
        $area   = [PsUi.WindowManager]::GetWorkAreaForWindow($handle)

        # Device pixels, not DIPs
        $toDip  = $source.CompositionTarget.TransformFromDevice
        $top    = $toDip.Transform($area.TopLeft).Y
        $bottom = $toDip.Transform($area.BottomRight).Y

        # Without UpdateLayout, ActualHeight still reads the old height
        if ($fitWindow.MaxHeight -gt $bottom - $top) {
            $fitWindow.MaxHeight = $bottom - $top
            $fitWindow.UpdateLayout()
        }
        if ($fitWindow.Top + $fitWindow.ActualHeight -gt $bottom) {
            $fitWindow.Top = $bottom - $fitWindow.ActualHeight
        }

        # CenterOnParent stops at the virtual screen's top, which a taskbar docked up there covers
        if ($fitWindow.Top -lt $top) { $fitWindow.Top = $top }
    }.GetNewClosure())

    # Position and show
    Set-UiDialogPosition -Dialog $window
    Write-Debug "Showing modal dialog"
    [void]$window.ShowDialog()

    $result = $window.Tag
    Write-Debug "Result: $result"
    return $result
}