Private/BuilderUI.ps1
|
# Builder UI (WPF) used by Show-AutopilotBrandingBuilder. # # The window, its named controls and the UI state live in module-scope variables ($window, $ui, $State). The # helper functions and the event handlers (which run later, while the window is shown) read them from there. $script:window = $null $script:ui = $null $script:State = $null # When set, Show-ABUIMessage doesn't show a message box (used by the automated tests, where a message box would # block forever): it returns the next answer from $ABUIDialogAnswers (e.g. 'Yes'), or throws if there is none. $script:ABUISuppressDialogs = $false $script:ABUIDialogAnswers = New-Object System.Collections.Generic.Queue[string] $script:ABUIDialogLog = New-Object System.Collections.Generic.List[string] function Show-ABUIMessage { # Shows a message box owned by the builder window and returns the result (e.g. 'Yes'). param ( [Parameter(Mandatory = $true)] [string] $Text, [string] $Caption = 'Autopilot Branding Builder', [ValidateSet('OK', 'OKCancel', 'YesNo', 'YesNoCancel')] [string] $Buttons = 'OK', [ValidateSet('None', 'Information', 'Question', 'Warning', 'Error')] [string] $Icon = 'Information' ) if ($script:ABUISuppressDialogs) { $script:ABUIDialogLog.Add("$($Caption): $Text") if ($script:ABUIDialogAnswers.Count -gt 0) { return $script:ABUIDialogAnswers.Dequeue() } throw "Message box ($Caption): $Text" } if ($script:window) { return [System.Windows.MessageBox]::Show($script:window, $Text, $Caption, $Buttons, $Icon) } return [System.Windows.MessageBox]::Show($Text, $Caption, $Buttons, $Icon) } #region Helpers function Invoke-Safely { param ([scriptblock] $ScriptBlock) try { & $ScriptBlock } catch { Show-ABUIMessage -Text ("$_") -Caption 'Autopilot Branding Builder' -Buttons 'OK' -Icon 'Error' | Out-Null } } function Get-BasePath { # Folder that relative paths in the configuration are relative to: the configuration file's folder, or the # module's project template for a configuration that hasn't been saved yet if ($State.ConfigPath) { return (Split-Path -Path $State.ConfigPath -Parent) } return (Get-ABTemplatePath) } function Get-DialogFolder { # Initial folder for file dialogs: the configuration's folder, or Documents (never the module's template folder) if ($State.ConfigPath) { return (Split-Path -Path $State.ConfigPath -Parent) } return [Environment]::GetFolderPath('MyDocuments') } function Get-TaskDefinition([string] $TaskName) { $State.Definitions | Where-Object { $_.Name -eq $TaskName } | Select-Object -First 1 } function New-Brush([string] $Color) { (New-Object System.Windows.Media.BrushConverter).ConvertFromString($Color) } function Format-Value($Value) { if ($null -eq $Value) { return '' } if ($Value -is [bool]) { if ($Value) { return 'True' } else { return 'False' } } if ($Value -is [array]) { return ($Value -join ', ') } return [string]$Value } function Set-Dirty([bool] $Dirty = $true) { $State.Dirty = $Dirty Update-Title } function Update-Title { $name = if ($State.ConfigPath) { $State.ConfigPath } else { '(not saved yet - save to create a project folder)' } $marker = if ($State.Dirty) { ' *' } else { '' } $window.Title = "Autopilot Branding Builder - $name$marker" } function Update-Status { $enabled = @($State.Definitions | Where-Object { $State.Configuration.Tasks[$_.Name].Enabled }).Count $relativeTo = if ($State.ConfigPath) { Get-BasePath } else { 'the default template' } $ui.StatusText.Text = "$enabled of $($State.Definitions.Count) tasks enabled. Paths are relative to $relativeTo" } function Set-TaskEnabled([string] $TaskName, [bool] $Enabled) { $entry = $State.Configuration.Tasks[$TaskName] if ([bool]$entry.Enabled -eq $Enabled) { return } $entry.Enabled = $Enabled $wasLoading = $State.Loading $State.Loading = $true try { if ($State.ListCheckBoxes.ContainsKey($TaskName)) { $State.ListCheckBoxes[$TaskName].IsChecked = $Enabled } if ($State.SelectedTask -eq $TaskName -and $State.DetailEnabledCheckBox) { $State.DetailEnabledCheckBox.IsChecked = $Enabled } } finally { $State.Loading = $wasLoading } Set-Dirty Update-Status } function Set-ParameterValue([string] $TaskName, $Definition, $Value) { try { $converted = ConvertTo-ABParameterValue -Definition $Definition -Value $Value } catch { # Not a valid value for the parameter type (e.g. while typing a number); keep the previous value return } $State.Configuration.Tasks[$TaskName].Parameters[$Definition.Name] = $converted Set-Dirty } function Confirm-Discard { if (-not $State.Dirty) { return $true } $answer = Show-ABUIMessage -Text ('Save changes to the current configuration?') -Caption 'Autopilot Branding Builder' -Buttons 'YesNoCancel' -Icon 'Question' switch ($answer) { 'Yes' { return (Save-Configuration) } 'No' { return $true } default { return $false } } } #endregion #region Configuration load/save function Set-Configuration($Configuration, [string] $Path) { $State.Configuration = $Configuration $State.ConfigPath = $Path # Generate into the configuration's (project's) Output folder by default $ui.OutputPathBox.Text = if ($Path) { Join-Path (Split-Path -Path $Path -Parent) 'Output' } else { '' } $State.Dirty = $false Show-TaskList Update-Title Update-Status } function Save-Configuration([switch] $SaveAs, [string] $Path) { # Saves to -Path if specified (e.g. by the tests), otherwise to the current file or a file chosen in a dialog. # When the configuration moves to another folder, template files it uses are copied there (so saving the default # template creates a project) and other file and folder parameters are rebased. if (-not $Path) { $Path = $State.ConfigPath } else { $SaveAs = $false } if ($SaveAs -or -not $path) { $dialog = New-Object Microsoft.Win32.SaveFileDialog $dialog.Filter = 'JSON configuration (*.json)|*.json|All files (*.*)|*.*' $dialog.DefaultExt = '.json' $dialog.FileName = if ($path) { Split-Path $path -Leaf } else { 'AutopilotBranding.json' } $dialog.InitialDirectory = Get-DialogFolder $dialog.Title = if ($State.ConfigPath) { 'Save the configuration' } else { 'Save the configuration in a project folder (the files it uses are copied there)' } if (-not $dialog.ShowDialog($window)) { return $false } $path = $dialog.FileName } $path = ConvertTo-ABFullPath $path $oldBase = Get-BasePath $newBase = Split-Path -Path $path -Parent if (Test-ABPathUnder -Path $newBase -Folder (Get-ABTemplatePath)) { throw "Don't save into the module's template folder; choose a project folder instead." } $oldOutput = $ui.OutputPathBox.Text if ($oldBase -ne $newBase) { Update-ABAssetBase -Configuration $State.Configuration -TaskDefinitions $State.Definitions -OldBase $oldBase -NewBase $newBase } Save-ABConfiguration -Configuration $State.Configuration -Path $path $State.ConfigPath = $path if (-not $oldOutput -or $oldOutput -eq (Join-Path $oldBase 'Output')) { $ui.OutputPathBox.Text = Join-Path $newBase 'Output' } Set-Dirty $false Update-Status if ($State.SelectedTask) { Show-TaskDetail $State.SelectedTask } $ui.StatusText.Text = "Saved $path" return $true } #endregion #region Task list and details function Show-TaskList { $wasLoading = $State.Loading $State.Loading = $true try { $selected = $State.SelectedTask $ui.TaskList.Items.Clear() $State.ListCheckBoxes = @{} foreach ($definition in $State.Definitions) { $checkBox = New-Object System.Windows.Controls.CheckBox $checkBox.IsChecked = [bool]$State.Configuration.Tasks[$definition.Name].Enabled $checkBox.VerticalAlignment = 'Top' $checkBox.Margin = '0,2,8,0' $checkBox.Tag = $definition.Name $checkBox.ToolTip = 'Run this task' $checkBox.Add_Click({ param($sender, $e) if (-not $State.Loading) { Set-TaskEnabled -TaskName $sender.Tag -Enabled ([bool]$sender.IsChecked) } }) $State.ListCheckBoxes[$definition.Name] = $checkBox $name = New-Object System.Windows.Controls.TextBlock $name.Text = $definition.Name $name.FontWeight = 'SemiBold' $synopsis = New-Object System.Windows.Controls.TextBlock $synopsis.Text = $definition.Synopsis $synopsis.TextWrapping = 'Wrap' $synopsis.FontSize = 11 $synopsis.Foreground = New-Brush '#666666' $text = New-Object System.Windows.Controls.StackPanel $text.Children.Add($name) | Out-Null $text.Children.Add($synopsis) | Out-Null $panel = New-Object System.Windows.Controls.DockPanel [System.Windows.Controls.DockPanel]::SetDock($checkBox, 'Left') $panel.Children.Add($checkBox) | Out-Null $panel.Children.Add($text) | Out-Null $item = New-Object System.Windows.Controls.ListBoxItem $item.Content = $panel $item.Tag = $definition.Name $item.Padding = '4' $ui.TaskList.Items.Add($item) | Out-Null if ($definition.Name -eq $selected) { $ui.TaskList.SelectedItem = $item } } } finally { $State.Loading = $wasLoading } if (-not $ui.TaskList.SelectedItem -and $ui.TaskList.Items.Count -gt 0) { $ui.TaskList.SelectedIndex = 0 } elseif ($ui.TaskList.SelectedItem) { Show-TaskDetail $ui.TaskList.SelectedItem.Tag } } function Add-DetailText([string] $Text, [double] $Size = 13, [string] $Weight = 'Normal', [string] $Color = '#000000', [string] $Margin = '0,0,0,6') { $block = New-Object System.Windows.Controls.TextBlock $block.Text = $Text $block.TextWrapping = 'Wrap' $block.FontSize = $Size $block.FontWeight = $Weight $block.Foreground = New-Brush $Color $block.Margin = $Margin $ui.DetailPanel.Children.Add($block) | Out-Null $block } function New-ParameterEditor([string] $TaskName, $Definition, $Value) { $tag = @{ Task = $TaskName; Parameter = $Definition } if ($Definition.Kind -in 'Boolean', 'Switch') { $checkBox = New-Object System.Windows.Controls.CheckBox $checkBox.Content = 'Enabled' $checkBox.IsChecked = [bool]$Value $checkBox.Tag = $tag $checkBox.Add_Click({ param($sender, $e) if (-not $State.Loading) { Set-ParameterValue -TaskName $sender.Tag.Task -Definition $sender.Tag.Parameter -Value ([bool]$sender.IsChecked) } }) return $checkBox } if ($Definition.ValidateSet) { $combo = New-Object System.Windows.Controls.ComboBox $combo.MinWidth = 200 $combo.HorizontalAlignment = 'Left' foreach ($option in $Definition.ValidateSet) { $combo.Items.Add($option) | Out-Null } $combo.SelectedItem = [string]$Value $combo.Tag = $tag $combo.Add_SelectionChanged({ param($sender, $e) if (-not $State.Loading) { Set-ParameterValue -TaskName $sender.Tag.Task -Definition $sender.Tag.Parameter -Value ([string]$sender.SelectedItem) } }) return $combo } $textBox = New-Object System.Windows.Controls.TextBox $textBox.Tag = $tag if ($Definition.Kind -eq 'StringArray') { $textBox.AcceptsReturn = $true $textBox.TextWrapping = 'NoWrap' $textBox.MinHeight = 90 $textBox.MaxHeight = 260 $textBox.VerticalScrollBarVisibility = 'Auto' $textBox.HorizontalScrollBarVisibility = 'Auto' $textBox.FontFamily = 'Consolas' $textBox.Text = (@($Value) -join "`r`n") } else { $textBox.Text = [string]$Value $textBox.Padding = '2' } $textBox.Add_TextChanged({ param($sender, $e) if (-not $State.Loading) { Set-ParameterValue -TaskName $sender.Tag.Task -Definition $sender.Tag.Parameter -Value $sender.Text } }) if (-not $Definition.AssetKind) { return $textBox } # File or folder: text box plus a Browse button $browse = New-Object System.Windows.Controls.Button $browse.Content = 'Browse...' $browse.Padding = '8,2' $browse.Margin = '4,0,0,0' $browse.Tag = @{ Parameter = $Definition; TextBox = $textBox } $browse.Add_Click({ param($sender, $e) Invoke-Safely { $definition = $sender.Tag.Parameter $box = $sender.Tag.TextBox $base = Get-BasePath $current = $null if ($box.Text) { $current = Resolve-ABAssetPath -Path $box.Text -BasePath $base } $selectedPath = $null if ($definition.AssetKind -eq 'Folder') { $dialog = New-Object System.Windows.Forms.FolderBrowserDialog $dialog.Description = "Select the folder for $($definition.Name)" if ($current -and (Test-Path $current)) { $dialog.SelectedPath = $current } if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $selectedPath = $dialog.SelectedPath } } else { $dialog = New-Object Microsoft.Win32.OpenFileDialog $dialog.Filter = 'All files (*.*)|*.*' $dialog.Title = "Select the file for $($definition.Name)" if ($current -and (Test-Path $current)) { $dialog.InitialDirectory = Split-Path $current -Parent $dialog.FileName = Split-Path $current -Leaf } else { $dialog.InitialDirectory = Get-DialogFolder } if ($dialog.ShowDialog($window)) { $selectedPath = $dialog.FileName } } if ($selectedPath) { Set-AssetFromSelection -TextBox $box -Definition $definition -SelectedPath $selectedPath } } }) $panel = New-Object System.Windows.Controls.DockPanel [System.Windows.Controls.DockPanel]::SetDock($browse, 'Right') $panel.Children.Add($browse) | Out-Null $panel.Children.Add($textBox) | Out-Null return $panel } function Set-AssetFromSelection { # Called after the user browses to a file or folder for a file/folder parameter. If it isn't in the project's # Assets folder, offers to copy it there (confirming before replacing a different file or folder with the same # name), then sets the parameter (through its text box) to the path relative to the project. param ( [Parameter(Mandatory = $true)] $TextBox, [Parameter(Mandatory = $true)] $Definition, [Parameter(Mandatory = $true)] [string] $SelectedPath ) $kind = $Definition.AssetKind.ToLower() if (-not $State.ConfigPath) { Show-ABUIMessage -Text ("This configuration hasn't been saved to a project folder yet, so the $kind can't be copied into the project's Assets folder; it is referenced where it is.`r`n`r`n" + "Saving the configuration creates a project. Browse again afterwards to copy the $kind into its Assets folder. Either way, it's copied into the package when you generate.") ` -Caption 'Copy to Assets' -Icon 'Information' | Out-Null $TextBox.Text = $SelectedPath return } $project = Split-Path -Path $State.ConfigPath -Parent $target = Get-ABProjectAssetTarget -Source $SelectedPath -ProjectPath $project if ($target.InAssets) { $TextBox.Text = $target.Relative return } $answer = Show-ABUIMessage -Text ("$SelectedPath isn't in the project's Assets folder ($($target.AssetsFolder)).`r`n`r`n" + "Copy the $kind there and use the copy?`r`n`r`n" + "Yes: copy it to $($target.Relative)`r`n" + "No: use it where it is (it's still copied into the package when you generate)`r`n" + "Cancel: don't change $($Definition.Name)") -Caption 'Copy to Assets' -Buttons 'YesNoCancel' -Icon 'Question' if ($answer -eq 'No') { $TextBox.Text = Get-ABRelativePath -Path $SelectedPath -BasePath $project return } if ($answer -ne 'Yes') { return } $replace = $false if ($target.Exists -and -not $target.Identical) { $confirm = Show-ABUIMessage -Text ("$($target.Relative) already exists in the project and is different from the selected $kind.`r`n`r`n" + "Replace it with $($SelectedPath)?") -Caption 'Replace file in Assets' -Buttons 'YesNo' -Icon 'Warning' if ($confirm -ne 'Yes') { $ui.StatusText.Text = "$($Definition.Name) not changed" return } $replace = $true } $TextBox.Text = Copy-ABProjectAsset -Source $SelectedPath -ProjectPath $project -Replace:$replace $ui.StatusText.Text = if ($target.Identical) { "Using the identical $kind already in $($target.Relative)" } else { "Copied $SelectedPath to $($target.Relative)" } } function Show-TaskDetail([string] $TaskName) { $State.SelectedTask = $TaskName $State.DetailEnabledCheckBox = $null $ui.DetailPanel.Children.Clear() $definition = Get-TaskDefinition $TaskName if (-not $definition) { return } $entry = $State.Configuration.Tasks[$TaskName] $wasLoading = $State.Loading $State.Loading = $true try { Add-DetailText -Text $definition.Name -Size 22 -Weight 'SemiBold' -Margin '0,0,0,4' | Out-Null Add-DetailText -Text $definition.Synopsis -Size 14 | Out-Null if ($definition.Description) { Add-DetailText -Text $definition.Description -Color '#555555' | Out-Null } $enabled = New-Object System.Windows.Controls.CheckBox $enabled.Content = 'Run this task' $enabled.FontWeight = 'SemiBold' $enabled.Margin = '0,6,0,12' $enabled.IsChecked = [bool]$entry.Enabled $enabled.Tag = $TaskName $enabled.Add_Click({ param($sender, $e) if (-not $State.Loading) { Set-TaskEnabled -TaskName $sender.Tag -Enabled ([bool]$sender.IsChecked) } }) $ui.DetailPanel.Children.Add($enabled) | Out-Null $State.DetailEnabledCheckBox = $enabled Add-DetailText -Text 'Parameters' -Size 16 -Weight 'SemiBold' -Margin '0,4,0,8' | Out-Null if ($definition.Parameters.Count -eq 0) { Add-DetailText -Text 'This task has no parameters.' -Color '#666666' | Out-Null } foreach ($p in $definition.Parameters) { $value = if ($entry.Parameters.Contains($p.Name)) { $entry.Parameters[$p.Name] } else { $p.DefaultValue } $stack = New-Object System.Windows.Controls.StackPanel $header = New-Object System.Windows.Controls.TextBlock $header.TextWrapping = 'Wrap' $nameRun = New-Object System.Windows.Documents.Run ("-$($p.Name)") $nameRun.FontWeight = 'SemiBold' $header.Inlines.Add($nameRun) $typeText = " [$($p.TypeName)]" if ($p.AssetKind) { $typeText += " ($($p.AssetKind.ToLower()))" } $typeRun = New-Object System.Windows.Documents.Run ($typeText) $typeRun.Foreground = New-Brush '#777777' $header.Inlines.Add($typeRun) if ($p.Mandatory) { $requiredRun = New-Object System.Windows.Documents.Run (' required') $requiredRun.Foreground = New-Brush '#B00020' $header.Inlines.Add($requiredRun) } $stack.Children.Add($header) | Out-Null if ($p.HelpText) { $help = New-Object System.Windows.Controls.TextBlock $help.Text = $p.HelpText $help.TextWrapping = 'Wrap' $help.Foreground = New-Brush '#555555' $help.Margin = '0,2,0,4' $stack.Children.Add($help) | Out-Null } $editor = New-ParameterEditor -TaskName $TaskName -Definition $p -Value $value $stack.Children.Add($editor) | Out-Null $defaultText = Format-Value $p.DefaultValue if ($defaultText -and $p.Kind -ne 'Switch') { $default = New-Object System.Windows.Controls.TextBlock $default.Text = "Default: $defaultText" $default.TextWrapping = 'Wrap' $default.FontSize = 11 $default.Foreground = New-Brush '#888888' $default.Margin = '0,2,0,0' $stack.Children.Add($default) | Out-Null } $border = New-Object System.Windows.Controls.Border $border.BorderBrush = New-Brush '#DDDDDD' $border.BorderThickness = '1' $border.CornerRadius = '4' $border.Padding = '10' $border.Margin = '0,0,0,10' $border.Child = $stack $ui.DetailPanel.Children.Add($border) | Out-Null } } finally { $State.Loading = $wasLoading } } #endregion function Initialize-ABBuilderWindow { <# .SYNOPSIS Creates the builder window, wires up its event handlers, and loads the initial configuration. .DESCRIPTION The window, its controls and the UI state are kept in module-scope variables ($script:window, $script:ui, $script:State) so that the event handlers and helper functions can reach them. Show-AutopilotBrandingBuilder calls this and then shows the window. #> [CmdletBinding()] param ( [string] $ConfigPath, [string] $OutputPath ) Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase, System.Windows.Forms $script:State = @{ Definitions = @(Get-ABTaskDefinition) Configuration = $null ConfigPath = $null Dirty = $false Loading = $false SelectedTask = $null ListCheckBoxes = @{} } #region XAML [xml]$xaml = @' <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Autopilot Branding Builder" Height="780" Width="1180" MinHeight="500" MinWidth="800" WindowStartupLocation="CenterScreen" FontSize="13"> <DockPanel> <ToolBarTray DockPanel.Dock="Top"> <ToolBar> <Button x:Name="NewProjectButton" Content="New project..." Padding="8,2" ToolTip="Create a project folder from the default template (configuration and sample files) and open it"/> <Button x:Name="NewButton" Content="New" Padding="8,2" ToolTip="Start a new configuration with all tasks disabled"/> <Button x:Name="OpenButton" Content="Open..." Padding="8,2" ToolTip="Open a JSON configuration file"/> <Button x:Name="SaveButton" Content="Save" Padding="8,2" ToolTip="Save the configuration"/> <Button x:Name="SaveAsButton" Content="Save As..." Padding="8,2" ToolTip="Save the configuration to a new JSON file"/> <Separator/> <Button x:Name="ImportButton" Content="Import Config.xml..." Padding="8,2" ToolTip="Convert a v3 Config.xml file"/> <Separator/> <Button x:Name="ValidateButton" Content="Validate" Padding="8,2" ToolTip="Check the configuration for problems"/> </ToolBar> </ToolBarTray> <StatusBar DockPanel.Dock="Bottom"> <StatusBarItem> <TextBlock x:Name="StatusText"/> </StatusBarItem> </StatusBar> <Border DockPanel.Dock="Bottom" Padding="8" BorderThickness="0,1,0,0" BorderBrush="#DDDDDD"> <DockPanel> <Button x:Name="GenerateButton" DockPanel.Dock="Right" Content="Generate output and .intunewin" Padding="14,4" Margin="8,0,0,0" FontWeight="SemiBold" ToolTip="Generate the output folder and package it as AutopilotBranding.intunewin in the output folder's parent folder"/> <Button x:Name="BrowseOutputButton" DockPanel.Dock="Right" Content="Browse..." Padding="8,2" Margin="4,0,0,0"/> <TextBlock DockPanel.Dock="Left" Text="Output folder:" VerticalAlignment="Center" Margin="0,0,8,0"/> <TextBox x:Name="OutputPathBox" VerticalContentAlignment="Center"/> </DockPanel> </Border> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="340" MinWidth="200"/> <ColumnDefinition Width="5"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <DockPanel Grid.Column="0"> <DockPanel DockPanel.Dock="Top" Margin="8,8,8,4"> <Button x:Name="NoneButton" DockPanel.Dock="Right" Content="None" Padding="6,0" Margin="4,0,0,0"/> <Button x:Name="AllButton" DockPanel.Dock="Right" Content="All" Padding="6,0"/> <TextBlock Text="Configuration tasks" FontWeight="SemiBold" VerticalAlignment="Center"/> </DockPanel> <ListBox x:Name="TaskList" Margin="8,0,0,8" ScrollViewer.HorizontalScrollBarVisibility="Disabled"/> </DockPanel> <GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" Background="#EEEEEE"/> <ScrollViewer Grid.Column="2" VerticalScrollBarVisibility="Auto" Margin="0,8,0,8"> <StackPanel x:Name="DetailPanel" Margin="16,0,16,16"/> </ScrollViewer> </Grid> </DockPanel> </Window> '@ $script:window = [System.Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml)) $script:ui = @{} foreach ($name in 'NewProjectButton', 'NewButton', 'OpenButton', 'SaveButton', 'SaveAsButton', 'ImportButton', 'ValidateButton', 'StatusText', 'GenerateButton', 'BrowseOutputButton', 'OutputPathBox', 'NoneButton', 'AllButton', 'TaskList', 'DetailPanel') { $ui[$name] = $window.FindName($name) } $ui.OutputPathBox.Text = $OutputPath #endregion #region Event handlers $ui.TaskList.Add_SelectionChanged({ param($sender, $e) if ($sender.SelectedItem) { Invoke-Safely { Show-TaskDetail $sender.SelectedItem.Tag } } }) $ui.AllButton.Add_Click({ foreach ($definition in $State.Definitions) { Set-TaskEnabled -TaskName $definition.Name -Enabled $true } }) $ui.NoneButton.Add_Click({ foreach ($definition in $State.Definitions) { Set-TaskEnabled -TaskName $definition.Name -Enabled $false } }) $ui.NewProjectButton.Add_Click({ Invoke-Safely { if (-not (Confirm-Discard)) { return } $dialog = New-Object System.Windows.Forms.FolderBrowserDialog $dialog.Description = 'Select or create the project folder (the default configuration and sample files are copied there)' $dialog.SelectedPath = Get-DialogFolder if ($dialog.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { return } $project = $dialog.SelectedPath $configPath = Join-Path $project 'AutopilotBranding.json' if (Test-Path -LiteralPath $configPath) { Show-ABUIMessage -Text "$project already contains a project; opening it." -Caption 'New project' | Out-Null } else { New-AutopilotBrandingProject -Path $project | Out-Null } Set-Configuration -Configuration (Read-ABConfiguration -Path $configPath -TaskDefinitions $State.Definitions) -Path $configPath $ui.StatusText.Text = "Opened project $project" } }) $ui.NewButton.Add_Click({ Invoke-Safely { if (-not (Confirm-Discard)) { return } $configuration = New-ABConfiguration -TaskDefinitions $State.Definitions foreach ($key in @($configuration.Tasks.Keys)) { $configuration.Tasks[$key].Enabled = $false } Set-Configuration -Configuration $configuration -Path $null } }) $ui.OpenButton.Add_Click({ Invoke-Safely { if (-not (Confirm-Discard)) { return } $dialog = New-Object Microsoft.Win32.OpenFileDialog $dialog.Filter = 'JSON configuration (*.json)|*.json|All files (*.*)|*.*' $dialog.InitialDirectory = Get-DialogFolder if ($dialog.ShowDialog($window)) { $configuration = Read-ABConfiguration -Path $dialog.FileName -TaskDefinitions $State.Definitions Set-Configuration -Configuration $configuration -Path $dialog.FileName } } }) $ui.SaveButton.Add_Click({ Invoke-Safely { Save-Configuration | Out-Null } }) $ui.SaveAsButton.Add_Click({ Invoke-Safely { Save-Configuration -SaveAs | Out-Null } }) $ui.ImportButton.Add_Click({ Invoke-Safely { if (-not (Confirm-Discard)) { return } $dialog = New-Object Microsoft.Win32.OpenFileDialog $dialog.Filter = 'Config.xml (*.xml)|*.xml|All files (*.*)|*.*' $dialog.Title = 'Select a v3 AutopilotBranding Config.xml file' if ($dialog.ShowDialog($window)) { $configuration = ConvertFrom-ABLegacyConfig -Path $dialog.FileName -TaskDefinitions $State.Definitions Set-Configuration -Configuration $configuration -Path $null Set-Dirty Show-ABUIMessage -Text ("Imported $($dialog.FileName).`r`n`r`nFile and folder parameters point at the files next to Config.xml. Review the tasks, then save the configuration.") -Caption 'Autopilot Branding Builder' -Buttons 'OK' -Icon 'Information' | Out-Null } } }) $ui.ValidateButton.Add_Click({ Invoke-Safely { $problems = @(Test-ABConfiguration -Configuration $State.Configuration -BasePath (Get-BasePath) -TaskDefinitions $State.Definitions) if ($problems.Count -eq 0) { Show-ABUIMessage -Text ('No problems found.') -Caption 'Validate' -Buttons 'OK' -Icon 'Information' | Out-Null } else { $text = ($problems | ForEach-Object { "[$($_.Severity)] $($_.Task) -$($_.Parameter): $($_.Message)" }) -join "`r`n" Show-ABUIMessage -Text ($text) -Caption 'Validate' -Buttons 'OK' -Icon 'Warning' | Out-Null } } }) $ui.BrowseOutputButton.Add_Click({ $dialog = New-Object System.Windows.Forms.FolderBrowserDialog $dialog.Description = 'Select the output folder' if ($ui.OutputPathBox.Text -and (Test-Path $ui.OutputPathBox.Text)) { $dialog.SelectedPath = $ui.OutputPathBox.Text } if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $ui.OutputPathBox.Text = $dialog.SelectedPath } }) $ui.GenerateButton.Add_Click({ Invoke-Safely { if (-not $State.ConfigPath) { Show-ABUIMessage -Text 'Save the configuration first: saving it in a folder creates a project there (with the files the configuration uses), and the output is generated from that project.' -Caption 'Generate output' | Out-Null return } $output = $ui.OutputPathBox.Text.Trim() if (-not $output) { throw 'Specify an output folder.' } if (-not [System.IO.Path]::IsPathRooted($output)) { $output = Join-Path (Get-BasePath) $output } $problems = @(Test-ABConfiguration -Configuration $State.Configuration -BasePath (Get-BasePath) -TaskDefinitions $State.Definitions) $errors = @($problems | Where-Object { $_.Severity -eq 'Error' }) if ($errors.Count -gt 0) { $text = ($errors | ForEach-Object { "$($_.Task) -$($_.Parameter): $($_.Message)" }) -join "`r`n" Show-ABUIMessage -Text ("Fix these problems before generating the output:`r`n`r`n$text") -Caption 'Generate output' -Buttons 'OK' -Icon 'Warning' | Out-Null return } if ((Test-Path $output) -and @(Get-ChildItem -Path $output -Force).Count -gt 0) { $answer = Show-ABUIMessage -Text ("The folder $output is not empty. Replace its contents?") -Caption 'Generate output' -Buttons 'YesNo' -Icon 'Question' if ($answer -ne 'Yes') { return } } if (Test-ABPathUnder -Path (ConvertTo-ABFullPath $output) -Folder (Get-ABTemplatePath)) { throw "Don't generate into the module's template folder; choose another output folder." } $packageFolder = Split-Path -Path (ConvertTo-ABFullPath $output) -Parent if (-not $packageFolder) { throw 'The output folder must not be the root of a drive (the .intunewin file is created in its parent folder).' } $window.Cursor = [System.Windows.Input.Cursors]::Wait try { $ui.StatusText.Text = 'Generating output...' $result = New-ABOutput -Configuration $State.Configuration -BasePath (Get-BasePath) -OutputPath $output -Force $ui.StatusText.Text = 'Creating .intunewin package...' $intunewin = New-ABIntuneWinPackage -SourcePath $result.OutputPath -DestinationPath $packageFolder -SetupFile (Split-Path $result.RunnerScript -Leaf) } finally { $window.Cursor = $null } $ui.StatusText.Text = "Output generated in $($result.OutputPath); package created: $intunewin" $message = "Output generated in $($result.OutputPath) ($($result.EnabledTasks.Count) tasks).`r`n`r`nIntune package created: $intunewin" $answer = Show-ABUIMessage -Text ("$message`r`n`r`nOpen the folder?") -Caption 'Generate output' -Buttons 'YesNo' -Icon 'Information' if ($answer -eq 'Yes') { Start-Process explorer.exe -ArgumentList "/select,`"$intunewin`"" } } }) $window.Add_Closing({ param($sender, $e) if (-not (Confirm-Discard)) { $e.Cancel = $true } }) #endregion # Load the initial configuration try { if ($ConfigPath -and (Test-Path $ConfigPath)) { $fullPath = (Resolve-Path $ConfigPath).Path Set-Configuration -Configuration (Read-ABConfiguration -Path $fullPath -TaskDefinitions $State.Definitions) -Path $fullPath } else { # No configuration: start from the module's default template (unsaved; saving it creates a project) if ($ConfigPath) { throw "Configuration file not found: $ConfigPath" } $templateConfig = Join-Path (Get-ABTemplatePath) 'AutopilotBranding.json' Set-Configuration -Configuration (Read-ABConfiguration -Path $templateConfig -TaskDefinitions $State.Definitions) -Path $null } if ($OutputPath) { $ui.OutputPathBox.Text = $OutputPath } } catch { Show-ABUIMessage -Text ("Unable to load $($ConfigPath): $_") -Caption 'Autopilot Branding Builder' -Buttons 'OK' -Icon 'Error' | Out-Null Set-Configuration -Configuration (New-ABConfiguration -TaskDefinitions $State.Definitions) -Path $null } } |