Private/Gui.ps1

function Add-SldSelectedApp {
    param(
        [Parameter(Mandatory)]$SourceList,
        [Parameter(Mandatory)]$TargetList
    )

    foreach ($item in @($SourceList.SelectedItems)) {
        if (($TargetList.ItemsSource | ForEach-Object Value) -notcontains $item.Value) {
            [void]$TargetList.ItemsSource.Add($item)
        }
    }
}

function Add-SldSuggestedApp {
    param(
        [Parameter(Mandatory)]$SourceList,
        [Parameter(Mandatory)]$TargetList
    )

    if ($SourceList.SelectedItems.Count -eq 0 -and $SourceList.Items.Count -gt 0) {
        $SourceList.SelectedIndex = 0
    }

    Add-SldSelectedApp -SourceList $SourceList -TargetList $TargetList
}

function Remove-SldSelectedApp {
    param([Parameter(Mandatory)]$List)

    foreach ($item in @($List.SelectedItems)) {
        $List.ItemsSource.Remove($item)
    }
}

function ConvertTo-SldFilteredCollection {
    param(
        [Parameter(Mandatory)][object[]]$App,
        [string]$Query
    )

    $collection = [System.Collections.ObjectModel.ObservableCollection[object]]::new()
    $terms = @($Query -split '\s+' | Where-Object { $_ })

    foreach ($entry in $App) {
        $haystack = '{0} {1} {2} {3}' -f $entry.Name, $entry.SourceType, $entry.Value, $entry.Category
        $isMatch = $true
        foreach ($term in $terms) {
            if ($haystack.IndexOf($term, [StringComparison]::OrdinalIgnoreCase) -lt 0) {
                $isMatch = $false
                break
            }
        }

        if ($isMatch) {
            [void]$collection.Add($entry)
        }
    }

    , $collection
}

function Update-SldAvailableApps {
    param(
        [Parameter(Mandatory)]$List,
        [Parameter(Mandatory)]$Status,
        [Parameter(Mandatory)][object[]]$AllApps,
        [string]$Query
    )

    $filtered = ConvertTo-SldFilteredCollection -App $AllApps -Query $Query
    $List.ItemsSource = $filtered

    if ($filtered.Count -gt 0) {
        $List.SelectedIndex = 0
    }

    if ([string]::IsNullOrWhiteSpace($Query)) {
        $Status.Text = "$($filtered.Count) apps available"
    }
    else {
        $Status.Text = "$($filtered.Count) matches for '$Query'"
    }
}

function Move-SldSelectedApp {
    param(
        [Parameter(Mandatory)]$List,
        [Parameter(Mandatory)][ValidateSet('Up', 'Down')]$Direction
    )

    $index = $List.SelectedIndex
    if ($index -lt 0) {
        return
    }

    $newIndex = if ($Direction -eq 'Up') { $index - 1 } else { $index + 1 }
    if ($newIndex -lt 0 -or $newIndex -ge $List.ItemsSource.Count) {
        return
    }

    $item = $List.ItemsSource[$index]
    $List.ItemsSource.RemoveAt($index)
    $List.ItemsSource.Insert($newIndex, $item)
    $List.SelectedIndex = $newIndex
}

function Update-SldPreview {
    param(
        [Parameter(Mandatory)]$List,
        [Parameter(Mandatory)]$Preview,
        [ValidateSet('Start', 'Taskbar')][string]$Mode,
        [bool]$ApplyOnce,
        [bool]$Replace
    )

    try {
        if ($Mode -eq 'Start') {
            $Preview.Text = ConvertTo-SldStartLayoutJson -App @($List.ItemsSource) -ApplyOnce $ApplyOnce
        }
        else {
            $Preview.Text = ConvertTo-SldTaskbarLayoutXml -App @($List.ItemsSource) -Replace:$Replace
        }
    }
    catch {
        $Preview.Text = $_.Exception.Message
    }
}

function Show-SldErrorMessage {
    param(
        [Parameter(Mandatory)]$Window,
        [Parameter(Mandatory)][string]$Message
    )

    [void][System.Windows.MessageBox]::Show($Window, $Message, 'Start Layout Designer', 'OK', 'Error')
}

function Show-SldInfoMessage {
    param(
        [Parameter(Mandatory)]$Window,
        [Parameter(Mandatory)][string]$Message
    )

    [void][System.Windows.MessageBox]::Show($Window, $Message, 'Start Layout Designer', 'OK', 'Information')
}

function Show-SldWindow {
    Add-Type -AssemblyName PresentationFramework
    Add-Type -AssemblyName PresentationCore
    Add-Type -AssemblyName WindowsBase

    $xamlPath = Join-Path $script:ModuleRoot 'Views\MainWindow.xaml'
    [xml]$xaml = Get-Content -LiteralPath $xamlPath -Raw
    $reader = New-Object System.Xml.XmlNodeReader $xaml
    $window = [Windows.Markup.XamlReader]::Load($reader)

    $availableStartList = $window.FindName('AvailableStartList')
    $startSearchBox = $window.FindName('StartSearchBox')
    $startSearchStatus = $window.FindName('StartSearchStatus')
    $selectedStartList = $window.FindName('SelectedStartList')
    $startPreview = $window.FindName('StartPreview')
    $applyOnceCheck = $window.FindName('ApplyOnceCheck')
    $refreshStartButton = $window.FindName('RefreshStartButton')
    $addStartButton = $window.FindName('AddStartButton')
    $removeStartButton = $window.FindName('RemoveStartButton')
    $startUpButton = $window.FindName('StartUpButton')
    $startDownButton = $window.FindName('StartDownButton')
    $importStartButton = $window.FindName('ImportStartButton')
    $exportStartButton = $window.FindName('ExportStartButton')
    $deployStartButton = $window.FindName('DeployStartButton')

    $availableTaskbarList = $window.FindName('AvailableTaskbarList')
    $taskbarSearchBox = $window.FindName('TaskbarSearchBox')
    $taskbarSearchStatus = $window.FindName('TaskbarSearchStatus')
    $selectedTaskbarList = $window.FindName('SelectedTaskbarList')
    $taskbarPreview = $window.FindName('TaskbarPreview')
    $replaceTaskbarCheck = $window.FindName('ReplaceTaskbarCheck')
    $refreshTaskbarButton = $window.FindName('RefreshTaskbarButton')
    $addTaskbarButton = $window.FindName('AddTaskbarButton')
    $removeTaskbarButton = $window.FindName('RemoveTaskbarButton')
    $taskbarUpButton = $window.FindName('TaskbarUpButton')
    $taskbarDownButton = $window.FindName('TaskbarDownButton')
    $importTaskbarButton = $window.FindName('ImportTaskbarButton')
    $exportTaskbarButton = $window.FindName('ExportTaskbarButton')
    $deployTaskbarButton = $window.FindName('DeployTaskbarButton')

    $appState = [pscustomobject]@{
        AllApps = @()
    }
    $loadApps = {
        $loadedApps = New-Object System.Collections.Generic.List[object]
        foreach ($app in Get-SldCandidateApps -IncludeCommonWindowsApps -IncludeDesktopShortcuts -IncludePackagedApps) {
            if ($app.IconPath) {
                $app | Add-Member -NotePropertyName IconSource -NotePropertyValue (Get-SldImageSource -IconPath $app.IconPath) -Force
            }
            else {
                $app | Add-Member -NotePropertyName IconSource -NotePropertyValue $null -Force
            }
            [void]$loadedApps.Add($app)
        }

        $appState.AllApps = @($loadedApps.ToArray())
        Update-SldAvailableApps -List $availableStartList -Status $startSearchStatus -AllApps $appState.AllApps -Query $startSearchBox.Text
        Update-SldAvailableApps -List $availableTaskbarList -Status $taskbarSearchStatus -AllApps $appState.AllApps -Query $taskbarSearchBox.Text
    }

    $selectedStartList.ItemsSource = [System.Collections.ObjectModel.ObservableCollection[object]]::new()
    $selectedTaskbarList.ItemsSource = [System.Collections.ObjectModel.ObservableCollection[object]]::new()
    & $loadApps

    $refreshStartButton.Add_Click({ & $loadApps })
    $refreshTaskbarButton.Add_Click({ & $loadApps })

    $startSearchBox.Add_TextChanged({
            Update-SldAvailableApps -List $availableStartList -Status $startSearchStatus -AllApps $appState.AllApps -Query $startSearchBox.Text
        })
    $taskbarSearchBox.Add_TextChanged({
            Update-SldAvailableApps -List $availableTaskbarList -Status $taskbarSearchStatus -AllApps $appState.AllApps -Query $taskbarSearchBox.Text
        })
    $startSearchBox.Add_KeyDown({
            if ($_.Key -eq [System.Windows.Input.Key]::Enter) {
                Add-SldSuggestedApp -SourceList $availableStartList -TargetList $selectedStartList
                Update-SldPreview -List $selectedStartList -Preview $startPreview -Mode Start -ApplyOnce $applyOnceCheck.IsChecked
                $_.Handled = $true
            }
        })
    $taskbarSearchBox.Add_KeyDown({
            if ($_.Key -eq [System.Windows.Input.Key]::Enter) {
                Add-SldSuggestedApp -SourceList $availableTaskbarList -TargetList $selectedTaskbarList
                Update-SldPreview -List $selectedTaskbarList -Preview $taskbarPreview -Mode Taskbar -Replace $replaceTaskbarCheck.IsChecked
                $_.Handled = $true
            }
        })

    $availableStartList.Add_MouseDoubleClick({
            Add-SldSuggestedApp -SourceList $availableStartList -TargetList $selectedStartList
            Update-SldPreview -List $selectedStartList -Preview $startPreview -Mode Start -ApplyOnce $applyOnceCheck.IsChecked
        })
    $selectedStartList.Add_MouseDoubleClick({
            Remove-SldSelectedApp -List $selectedStartList
            Update-SldPreview -List $selectedStartList -Preview $startPreview -Mode Start -ApplyOnce $applyOnceCheck.IsChecked
        })
    $addStartButton.Add_Click({
            Add-SldSuggestedApp -SourceList $availableStartList -TargetList $selectedStartList
            Update-SldPreview -List $selectedStartList -Preview $startPreview -Mode Start -ApplyOnce $applyOnceCheck.IsChecked
        })
    $removeStartButton.Add_Click({
            Remove-SldSelectedApp -List $selectedStartList
            Update-SldPreview -List $selectedStartList -Preview $startPreview -Mode Start -ApplyOnce $applyOnceCheck.IsChecked
        })
    $startUpButton.Add_Click({
            Move-SldSelectedApp -List $selectedStartList -Direction Up
            Update-SldPreview -List $selectedStartList -Preview $startPreview -Mode Start -ApplyOnce $applyOnceCheck.IsChecked
        })
    $startDownButton.Add_Click({
            Move-SldSelectedApp -List $selectedStartList -Direction Down
            Update-SldPreview -List $selectedStartList -Preview $startPreview -Mode Start -ApplyOnce $applyOnceCheck.IsChecked
        })
    $applyOnceCheck.Add_Click({
            Update-SldPreview -List $selectedStartList -Preview $startPreview -Mode Start -ApplyOnce $applyOnceCheck.IsChecked
        })
    $importStartButton.Add_Click({
            $dialog = New-Object Microsoft.Win32.OpenFileDialog
            $dialog.Filter = 'JSON files (*.json)|*.json|All files (*.*)|*.*'
            if ($dialog.ShowDialog()) {
                try {
                    $json = Get-Content -LiteralPath $dialog.FileName -Raw
                    $imported = ConvertFrom-SldStartLayoutJson -Json $json -AvailableApp $appState.AllApps
                    $selectedStartList.ItemsSource = $imported.Apps
                    $applyOnceCheck.IsChecked = $imported.ApplyOnce
                    Update-SldPreview -List $selectedStartList -Preview $startPreview -Mode Start -ApplyOnce $applyOnceCheck.IsChecked
                }
                catch {
                    Show-SldErrorMessage -Window $window -Message "Could not import Start layout JSON.`r`n$($_.Exception.Message)"
                }
            }
        })
    $exportStartButton.Add_Click({
            $dialog = New-Object Microsoft.Win32.SaveFileDialog
            $dialog.Filter = 'JSON files (*.json)|*.json|All files (*.*)|*.*'
            $dialog.FileName = 'StartLayout.json'
            if ($dialog.ShowDialog()) {
                ConvertTo-SldStartLayoutJson -App @($selectedStartList.ItemsSource) -ApplyOnce $applyOnceCheck.IsChecked |
                Set-Content -LiteralPath $dialog.FileName -Encoding UTF8
            }
        })
    $deployStartButton.Add_Click({
            try {
                $json = ConvertTo-SldStartLayoutJson -App @($selectedStartList.ItemsSource) -ApplyOnce $applyOnceCheck.IsChecked
                $result = Set-SldStartPinsPolicy -Json $json
                $startPreview.Text = $json
                $applyOnceNote = if ($applyOnceCheck.IsChecked) { "`r`n`r`nNote: Apply once is enabled, so Windows may not reapply later changes after the first successful application." } else { "" }
                Show-SldInfoMessage -Window $window -Message "Start pins policy deployed for the current user.`r`n`r`nPolicy: $($result.Policy)`r`nFile: $($result.Path)`r`n`r`nSign out and sign in again for the Start layout to apply.$applyOnceNote"
            }
            catch {
                Show-SldErrorMessage -Window $window -Message "Could not deploy Start pins policy.`r`n$($_.Exception.Message)"
            }
        })

    $availableTaskbarList.Add_MouseDoubleClick({
            Add-SldSuggestedApp -SourceList $availableTaskbarList -TargetList $selectedTaskbarList
            Update-SldPreview -List $selectedTaskbarList -Preview $taskbarPreview -Mode Taskbar -Replace $replaceTaskbarCheck.IsChecked
        })
    $selectedTaskbarList.Add_MouseDoubleClick({
            Remove-SldSelectedApp -List $selectedTaskbarList
            Update-SldPreview -List $selectedTaskbarList -Preview $taskbarPreview -Mode Taskbar -Replace $replaceTaskbarCheck.IsChecked
        })
    $addTaskbarButton.Add_Click({
            Add-SldSuggestedApp -SourceList $availableTaskbarList -TargetList $selectedTaskbarList
            Update-SldPreview -List $selectedTaskbarList -Preview $taskbarPreview -Mode Taskbar -Replace $replaceTaskbarCheck.IsChecked
        })
    $removeTaskbarButton.Add_Click({
            Remove-SldSelectedApp -List $selectedTaskbarList
            Update-SldPreview -List $selectedTaskbarList -Preview $taskbarPreview -Mode Taskbar -Replace $replaceTaskbarCheck.IsChecked
        })
    $taskbarUpButton.Add_Click({
            Move-SldSelectedApp -List $selectedTaskbarList -Direction Up
            Update-SldPreview -List $selectedTaskbarList -Preview $taskbarPreview -Mode Taskbar -Replace $replaceTaskbarCheck.IsChecked
        })
    $taskbarDownButton.Add_Click({
            Move-SldSelectedApp -List $selectedTaskbarList -Direction Down
            Update-SldPreview -List $selectedTaskbarList -Preview $taskbarPreview -Mode Taskbar -Replace $replaceTaskbarCheck.IsChecked
        })
    $replaceTaskbarCheck.Add_Click({
            Update-SldPreview -List $selectedTaskbarList -Preview $taskbarPreview -Mode Taskbar -Replace $replaceTaskbarCheck.IsChecked
        })
    $importTaskbarButton.Add_Click({
            $dialog = New-Object Microsoft.Win32.OpenFileDialog
            $dialog.Filter = 'XML files (*.xml)|*.xml|All files (*.*)|*.*'
            if ($dialog.ShowDialog()) {
                try {
                    $xml = Get-Content -LiteralPath $dialog.FileName -Raw
                    $imported = ConvertFrom-SldTaskbarLayoutXml -Xml $xml -AvailableApp $appState.AllApps
                    $selectedTaskbarList.ItemsSource = $imported.Apps
                    $replaceTaskbarCheck.IsChecked = $imported.Replace
                    Update-SldPreview -List $selectedTaskbarList -Preview $taskbarPreview -Mode Taskbar -Replace $replaceTaskbarCheck.IsChecked
                }
                catch {
                    Show-SldErrorMessage -Window $window -Message "Could not import taskbar XML.`r`n$($_.Exception.Message)"
                }
            }
        })
    $exportTaskbarButton.Add_Click({
            $dialog = New-Object Microsoft.Win32.SaveFileDialog
            $dialog.Filter = 'XML files (*.xml)|*.xml|All files (*.*)|*.*'
            $dialog.FileName = 'TaskbarLayout.xml'
            if ($dialog.ShowDialog()) {
                ConvertTo-SldTaskbarLayoutXml -App @($selectedTaskbarList.ItemsSource) -Replace:$replaceTaskbarCheck.IsChecked |
                Set-Content -LiteralPath $dialog.FileName -Encoding UTF8
            }
        })
    $deployTaskbarButton.Add_Click({
            try {
                $xml = ConvertTo-SldTaskbarLayoutXml -App @($selectedTaskbarList.ItemsSource) -Replace:$replaceTaskbarCheck.IsChecked
                $result = Set-SldTaskbarLayoutPolicy -Xml $xml
                $taskbarPreview.Text = $xml
                Show-SldInfoMessage -Window $window -Message "Taskbar layout policy deployed for the current user.`r`n`r`nPolicy: $($result.Policy)`r`nFile: $($result.Path)`r`n`r`nSign out and sign in again for the taskbar layout to apply."
            }
            catch {
                Show-SldErrorMessage -Window $window -Message "Could not deploy taskbar layout policy.`r`n$($_.Exception.Message)"
            }
        })

    Update-SldPreview -List $selectedStartList -Preview $startPreview -Mode Start -ApplyOnce $applyOnceCheck.IsChecked
    Update-SldPreview -List $selectedTaskbarList -Preview $taskbarPreview -Mode Taskbar -Replace $replaceTaskbarCheck.IsChecked

    [void]$window.ShowDialog()
}