mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
dfe07b16ae
Enhances the modern folder browser to accept and open to a specified initial directory. This improves the user experience by starting the "Browse for Drivers" dialog in the project's 'Drivers' subfolder, reducing the need for manual navigation. The implementation uses the Win32 API to create a shell item from the initial path and set it as the dialog's starting folder.
939 lines
37 KiB
PowerShell
939 lines
37 KiB
PowerShell
# Function to update priorities sequentially in a ListView
|
|
function Update-ListViewPriorities {
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[System.Windows.Controls.ListView]$ListView
|
|
)
|
|
|
|
$currentPriority = 1
|
|
foreach ($item in $ListView.Items) {
|
|
if ($null -ne $item -and $item.PSObject.Properties['Priority']) {
|
|
$item.Priority = $currentPriority
|
|
$currentPriority++
|
|
}
|
|
}
|
|
$ListView.Items.Refresh()
|
|
}
|
|
|
|
# Function to move selected item to the top
|
|
function Move-ListViewItemTop {
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[System.Windows.Controls.ListView]$ListView
|
|
)
|
|
|
|
$selectedItem = $ListView.SelectedItem
|
|
if ($null -eq $selectedItem) { return }
|
|
|
|
$currentIndex = $ListView.Items.IndexOf($selectedItem)
|
|
if ($currentIndex -gt 0) {
|
|
$ListView.Items.RemoveAt($currentIndex)
|
|
$ListView.Items.Insert(0, $selectedItem)
|
|
$ListView.SelectedItem = $selectedItem
|
|
Update-ListViewPriorities -ListView $ListView
|
|
}
|
|
}
|
|
|
|
# Function to move selected item up one position
|
|
function Move-ListViewItemUp {
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[System.Windows.Controls.ListView]$ListView
|
|
)
|
|
|
|
$selectedItem = $ListView.SelectedItem
|
|
if ($null -eq $selectedItem) { return }
|
|
|
|
$currentIndex = $ListView.Items.IndexOf($selectedItem)
|
|
if ($currentIndex -gt 0) {
|
|
$ListView.Items.RemoveAt($currentIndex)
|
|
$ListView.Items.Insert($currentIndex - 1, $selectedItem)
|
|
$ListView.SelectedItem = $selectedItem
|
|
Update-ListViewPriorities -ListView $ListView
|
|
}
|
|
}
|
|
|
|
# Function to move selected item down one position
|
|
function Move-ListViewItemDown {
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[System.Windows.Controls.ListView]$ListView
|
|
)
|
|
|
|
$selectedItem = $ListView.SelectedItem
|
|
if ($null -eq $selectedItem) { return }
|
|
|
|
$currentIndex = $ListView.Items.IndexOf($selectedItem)
|
|
if ($currentIndex -lt ($ListView.Items.Count - 1)) {
|
|
$ListView.Items.RemoveAt($currentIndex)
|
|
$ListView.Items.Insert($currentIndex + 1, $selectedItem)
|
|
$ListView.SelectedItem = $selectedItem
|
|
Update-ListViewPriorities -ListView $ListView
|
|
}
|
|
}
|
|
|
|
# Function to move selected item to the bottom
|
|
function Move-ListViewItemBottom {
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[System.Windows.Controls.ListView]$ListView
|
|
)
|
|
|
|
$selectedItem = $ListView.SelectedItem
|
|
if ($null -eq $selectedItem) { return }
|
|
|
|
$currentIndex = $ListView.Items.IndexOf($selectedItem)
|
|
if ($currentIndex -lt ($ListView.Items.Count - 1)) {
|
|
$ListView.Items.RemoveAt($currentIndex)
|
|
$ListView.Items.Add($selectedItem)
|
|
$ListView.SelectedItem = $selectedItem
|
|
Update-ListViewPriorities -ListView $ListView
|
|
}
|
|
}
|
|
|
|
# Function to update status of a specific item in a ListView
|
|
function Update-ListViewItemStatus {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[object]$WindowObject, # Changed type to [object]
|
|
[Parameter(Mandatory)]
|
|
[object]$ListView, # Changed type to [object]
|
|
[Parameter(Mandatory)]
|
|
[string]$IdentifierProperty,
|
|
[Parameter(Mandatory)]
|
|
[string]$IdentifierValue,
|
|
[Parameter(Mandatory)]
|
|
[string]$StatusProperty,
|
|
[Parameter(Mandatory)]
|
|
[string]$StatusValue
|
|
)
|
|
|
|
# Ensure we are in UI mode and objects are of correct WPF types
|
|
if ($WindowObject -is [System.Windows.Window] -and $ListView -is [System.Windows.Controls.ListView]) {
|
|
# Directly update UI elements as this function is now called on the UI thread
|
|
try {
|
|
$itemToUpdate = $ListView.ItemsSource | Where-Object { $_.$IdentifierProperty -eq $IdentifierValue } | Select-Object -First 1
|
|
if ($null -ne $itemToUpdate) {
|
|
$itemToUpdate.$StatusProperty = $StatusValue
|
|
$ListView.Items.Refresh() # Refresh the view to show the change
|
|
}
|
|
else {
|
|
# Log if item not found (for debugging)
|
|
WriteLog "Update-ListViewItemStatus: Item with $IdentifierProperty '$IdentifierValue' not found in ListView."
|
|
}
|
|
}
|
|
catch {
|
|
WriteLog "Update-ListViewItemStatus: Error updating ListView: $($_.Exception.Message)"
|
|
}
|
|
} # End of if ($WindowObject -is [System.Windows.Window]...)
|
|
else {
|
|
# Log if called in non-UI mode or with incorrect types (should not happen if Invoke-ParallelProcessing $isUiMode is correct)
|
|
WriteLog "Update-ListViewItemStatus: Skipped UI update for $IdentifierValue due to non-UI mode or incorrect object types."
|
|
}
|
|
}
|
|
|
|
# Function to update overall progress bar and status text label
|
|
function Update-OverallProgress {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[object]$WindowObject, # Changed type to [object]
|
|
[Parameter(Mandatory)]
|
|
[int]$CompletedCount,
|
|
[Parameter(Mandatory)]
|
|
[int]$TotalCount,
|
|
[Parameter(Mandatory)]
|
|
[string]$StatusText,
|
|
[Parameter(Mandatory)]
|
|
[string]$ProgressBarName,
|
|
[Parameter(Mandatory)]
|
|
[string]$StatusLabelName
|
|
)
|
|
|
|
# Ensure we are in UI mode and WindowObject is of correct WPF type
|
|
if ($WindowObject -is [System.Windows.Window]) {
|
|
# Directly update UI elements as this function is now called on the UI thread
|
|
try {
|
|
# Find controls by name using the $WindowObject
|
|
$pb = $WindowObject.FindName($ProgressBarName)
|
|
$lbl = $WindowObject.FindName($StatusLabelName)
|
|
|
|
if ($null -eq $pb) {
|
|
WriteLog "Update-OverallProgress: ProgressBar '$ProgressBarName' not found."
|
|
return
|
|
}
|
|
if ($null -eq $lbl) {
|
|
WriteLog "Update-OverallProgress: StatusLabel '$StatusLabelName' not found."
|
|
return
|
|
}
|
|
|
|
# Update the progress bar
|
|
if ($TotalCount -gt 0) {
|
|
$percentComplete = ($CompletedCount / $TotalCount) * 100
|
|
$pb.Value = $percentComplete
|
|
}
|
|
else {
|
|
$pb.Value = 0
|
|
}
|
|
|
|
# Update the status label
|
|
$lbl.Text = $StatusText
|
|
|
|
}
|
|
catch {
|
|
WriteLog "Update-OverallProgress: Error updating progress: $($_.Exception.Message)"
|
|
}
|
|
} # End of if ($WindowObject -is [System.Windows.Window])
|
|
else {
|
|
# Log if called in non-UI mode or with incorrect types
|
|
WriteLog "Update-OverallProgress: Skipped UI update ($StatusText) due to non-UI mode or incorrect WindowObject type."
|
|
}
|
|
}
|
|
|
|
# Helper function to enqueue progress updates to the UI thread
|
|
function Invoke-ProgressUpdate {
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[System.Collections.Concurrent.ConcurrentQueue[hashtable]]$ProgressQueue,
|
|
[Parameter(Mandatory)]
|
|
[string]$Identifier,
|
|
[Parameter(Mandatory)]
|
|
[string]$Status
|
|
)
|
|
$ProgressQueue.Enqueue(@{ Identifier = $Identifier; Status = $Status })
|
|
}
|
|
|
|
# Add a function to create a sortable list view
|
|
function Add-SortableColumn {
|
|
param(
|
|
[System.Windows.Controls.GridView]$gridView,
|
|
[string]$header,
|
|
[string]$binding,
|
|
[int]$width = 'Auto',
|
|
[bool]$isCheckbox = $false,
|
|
[System.Windows.HorizontalAlignment]$headerHorizontalAlignment = [System.Windows.HorizontalAlignment]::Stretch
|
|
)
|
|
|
|
$column = New-Object System.Windows.Controls.GridViewColumn
|
|
$commonPadding = New-Object System.Windows.Thickness(5, 2, 5, 2)
|
|
|
|
$headerControl = New-Object System.Windows.Controls.GridViewColumnHeader
|
|
$headerControl.Tag = $binding # Used for sorting
|
|
|
|
if ($isCheckbox) {
|
|
# Cell template for a column of checkboxes
|
|
$cellTemplate = New-Object System.Windows.DataTemplate
|
|
$gridFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.Grid])
|
|
|
|
$checkBoxFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.CheckBox])
|
|
$checkBoxFactory.SetBinding([System.Windows.Controls.CheckBox]::IsCheckedProperty, (New-Object System.Windows.Data.Binding("IsSelected")))
|
|
$checkBoxFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Center)
|
|
$checkBoxFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
|
|
|
|
$checkBoxFactory.AddHandler([System.Windows.Controls.CheckBox]::ClickEvent, [System.Windows.RoutedEventHandler] {
|
|
param($eventSourceLocal, $eventArgsLocal)
|
|
# Sync logic would be needed here if this column had a header checkbox
|
|
})
|
|
$gridFactory.AppendChild($checkBoxFactory)
|
|
$cellTemplate.VisualTree = $gridFactory
|
|
$column.CellTemplate = $cellTemplate
|
|
}
|
|
else {
|
|
# For regular text columns
|
|
$headerControl.HorizontalContentAlignment = $headerHorizontalAlignment
|
|
$headerControl.Content = $header
|
|
|
|
$headerTextElementFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBlock])
|
|
$headerTextElementFactory.SetValue([System.Windows.Controls.TextBlock]::TextProperty, $header)
|
|
$headerTextBlockPadding = New-Object System.Windows.Thickness($commonPadding.Left, $commonPadding.Top, $commonPadding.Right, $commonPadding.Bottom)
|
|
$headerTextElementFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, $headerTextBlockPadding)
|
|
$headerTextElementFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
|
|
|
|
$headerDataTemplate = New-Object System.Windows.DataTemplate
|
|
$headerDataTemplate.VisualTree = $headerTextElementFactory
|
|
$headerControl.ContentTemplate = $headerDataTemplate
|
|
|
|
$cellTemplate = New-Object System.Windows.DataTemplate
|
|
$textBlockFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBlock])
|
|
$textBlockFactory.SetBinding([System.Windows.Controls.TextBlock]::TextProperty, (New-Object System.Windows.Data.Binding($binding)))
|
|
# Adjust left padding to 0 for cell text to align with header text
|
|
$cellTextBlockPadding = New-Object System.Windows.Thickness(0, $commonPadding.Top, $commonPadding.Right, $commonPadding.Bottom)
|
|
$textBlockFactory.SetValue([System.Windows.Controls.TextBlock]::PaddingProperty, $cellTextBlockPadding)
|
|
$textBlockFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Left)
|
|
$textBlockFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
|
|
|
|
$cellTemplate.VisualTree = $textBlockFactory
|
|
$column.CellTemplate = $cellTemplate
|
|
}
|
|
|
|
$column.Header = $headerControl
|
|
|
|
if ($width -ne 'Auto') {
|
|
$column.Width = $width
|
|
}
|
|
|
|
$gridView.Columns.Add($column)
|
|
}
|
|
|
|
# Function to add a selectable GridViewColumn with a "Select All" header CheckBox
|
|
function Add-SelectableGridViewColumn {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[System.Windows.Controls.ListView]$ListView,
|
|
[Parameter(Mandatory)]
|
|
[psobject]$State,
|
|
[Parameter(Mandatory)]
|
|
[string]$HeaderCheckBoxKeyName,
|
|
[Parameter(Mandatory)]
|
|
[double]$ColumnWidth,
|
|
[string]$IsSelectedPropertyName = "IsSelected"
|
|
)
|
|
|
|
# Ensure the ListView has a GridView
|
|
if ($null -eq $ListView.View -or -not ($ListView.View -is [System.Windows.Controls.GridView])) {
|
|
WriteLog "Add-SelectableGridViewColumn: ListView '$($ListView.Name)' does not have a GridView or View is null. Cannot add column."
|
|
return
|
|
}
|
|
$gridView = $ListView.View
|
|
|
|
# Create the "Select All" CheckBox for the header
|
|
$headerCheckBox = New-Object System.Windows.Controls.CheckBox
|
|
$headerCheckBox.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Center
|
|
|
|
# MODIFICATION: Store the actual ListView object in the header's Tag
|
|
$headerTagObject = [PSCustomObject]@{
|
|
PropertyName = $IsSelectedPropertyName
|
|
ListViewControl = $ListView # Store the object itself
|
|
}
|
|
$headerCheckBox.Tag = $headerTagObject
|
|
|
|
$headerCheckBox.Add_Checked({
|
|
param($senderCheckBoxLocal, $eventArgsCheckedLocal)
|
|
$tagData = $senderCheckBoxLocal.Tag
|
|
$localPropertyName = $tagData.PropertyName
|
|
$actualListView = $tagData.ListViewControl # Get the control directly from the tag
|
|
|
|
$collectionToUpdate = if ($null -ne $actualListView.ItemsSource) { $actualListView.ItemsSource } else { $actualListView.Items }
|
|
if ($null -ne $collectionToUpdate) {
|
|
foreach ($item in $collectionToUpdate) { $item.$($localPropertyName) = $true }
|
|
$actualListView.Items.Refresh()
|
|
}
|
|
})
|
|
|
|
$headerCheckBox.Add_Unchecked({
|
|
param($senderCheckBoxLocal, $eventArgsUncheckedLocal)
|
|
if ($senderCheckBoxLocal.IsChecked -eq $false) {
|
|
$tagData = $senderCheckBoxLocal.Tag
|
|
$localPropertyName = $tagData.PropertyName
|
|
$actualListView = $tagData.ListViewControl # Get the control directly from the tag
|
|
|
|
$collectionToUpdate = if ($null -ne $actualListView.ItemsSource) { $actualListView.ItemsSource } else { $actualListView.Items }
|
|
if ($null -ne $collectionToUpdate) {
|
|
foreach ($item in $collectionToUpdate) { $item.$($localPropertyName) = $false }
|
|
$actualListView.Items.Refresh()
|
|
}
|
|
}
|
|
})
|
|
|
|
$State.Controls[$HeaderCheckBoxKeyName] = $headerCheckBox
|
|
WriteLog "Add-SelectableGridViewColumn: Stored header checkbox in State.Controls with key '$HeaderCheckBoxKeyName'."
|
|
|
|
$selectableColumn = New-Object System.Windows.Controls.GridViewColumn
|
|
$selectableColumn.Header = $headerCheckBox
|
|
$selectableColumn.Width = $ColumnWidth
|
|
|
|
$cellTemplate = New-Object System.Windows.DataTemplate
|
|
$borderFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.Border])
|
|
$borderFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)
|
|
$borderFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Stretch)
|
|
|
|
$checkBoxFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.CheckBox])
|
|
$checkBoxFactory.SetBinding([System.Windows.Controls.CheckBox]::IsCheckedProperty, (New-Object System.Windows.Data.Binding($IsSelectedPropertyName)))
|
|
$checkBoxFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Center)
|
|
$checkBoxFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
|
|
|
|
# MODIFICATION: Store the actual ListView object in the item checkbox's Tag
|
|
$tagObject = [PSCustomObject]@{
|
|
HeaderCheckboxKeyName = $HeaderCheckBoxKeyName
|
|
ListViewControl = $ListView # Store the object itself
|
|
}
|
|
$checkBoxFactory.SetValue([System.Windows.FrameworkElement]::TagProperty, $tagObject)
|
|
|
|
$checkBoxFactory.AddHandler([System.Windows.Controls.CheckBox]::ClickEvent, [System.Windows.RoutedEventHandler] {
|
|
param($eventSourceLocal, $eventArgsLocal)
|
|
$itemCheckBox = $eventSourceLocal -as [System.Windows.Controls.CheckBox]
|
|
$tagData = $itemCheckBox.Tag
|
|
|
|
$headerCheckboxKeyFromTag = $tagData.HeaderCheckboxKeyName
|
|
$targetListView = $tagData.ListViewControl # Get the control directly from the tag
|
|
|
|
# Get the state from the window tag
|
|
$window = [System.Windows.Window]::GetWindow($targetListView)
|
|
if ($null -eq $window -or $null -eq $window.Tag) {
|
|
WriteLog "Add-SelectableGridViewColumn: ERROR - Could not get window or state from window tag."
|
|
return
|
|
}
|
|
$localState = $window.Tag
|
|
|
|
WriteLog "Add-SelectableGridViewColumn: Item Click. ListView: '$($targetListView.Name)', HeaderChkKey: '$headerCheckboxKeyFromTag'"
|
|
|
|
$headerChk = $localState.Controls[$headerCheckboxKeyFromTag]
|
|
if ($null -ne $headerChk) {
|
|
Update-SelectAllHeaderCheckBoxState -ListView $targetListView -HeaderCheckBox $headerChk
|
|
}
|
|
else {
|
|
WriteLog "Add-SelectableGridViewColumn: Error - Could not retrieve header checkbox from state with key '$headerCheckboxKeyFromTag'."
|
|
}
|
|
})
|
|
|
|
$borderFactory.AppendChild($checkBoxFactory)
|
|
$cellTemplate.VisualTree = $borderFactory
|
|
$selectableColumn.CellTemplate = $cellTemplate
|
|
|
|
$gridView.Columns.Insert(0, $selectableColumn)
|
|
WriteLog "Add-SelectableGridViewColumn: Successfully added selectable column to '$($ListView.Name)'."
|
|
}
|
|
|
|
# Function to update the IsChecked state of a "Select All" header CheckBox
|
|
function Update-SelectAllHeaderCheckBoxState {
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[System.Windows.Controls.ListView]$ListView,
|
|
[Parameter(Mandatory)]
|
|
[System.Windows.Controls.CheckBox]$HeaderCheckBox
|
|
)
|
|
|
|
$collectionToInspect = $null
|
|
if ($null -ne $ListView.ItemsSource) {
|
|
$collectionToInspect = @($ListView.ItemsSource)
|
|
}
|
|
elseif ($ListView.HasItems) {
|
|
# Check if Items collection has items and ItemsSource is null
|
|
$collectionToInspect = @($ListView.Items)
|
|
}
|
|
|
|
# If no items to inspect (either ItemsSource was null and Items was empty, or ItemsSource was empty)
|
|
if ($null -eq $collectionToInspect -or $collectionToInspect.Count -eq 0) {
|
|
$HeaderCheckBox.IsChecked = $false
|
|
return
|
|
}
|
|
|
|
$selectedCount = ($collectionToInspect | Where-Object { $_.IsSelected }).Count
|
|
WriteLog "Update-SelectAllHeaderCheckBoxState: Selected count is $selectedCount for ListView '$($ListView.Name)'."
|
|
$totalItemCount = $collectionToInspect.Count # Get the total count from the collection being inspected
|
|
WriteLog "Update-SelectAllHeaderCheckBoxState: Total item count is $totalItemCount for ListView '$($ListView.Name)'."
|
|
|
|
if ($totalItemCount -eq 0) {
|
|
# Handle empty list case specifically
|
|
$HeaderCheckBox.IsChecked = $false
|
|
}
|
|
elseif ($selectedCount -eq $totalItemCount) {
|
|
$HeaderCheckBox.IsChecked = $true
|
|
}
|
|
elseif ($selectedCount -eq 0) {
|
|
$HeaderCheckBox.IsChecked = $false
|
|
}
|
|
else {
|
|
# Indeterminate state
|
|
$HeaderCheckBox.IsChecked = $null
|
|
}
|
|
}
|
|
|
|
# Function to toggle the IsSelected state of the currently selected ListView item
|
|
function Invoke-ListViewItemToggle {
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[System.Windows.Controls.ListView]$ListView,
|
|
[Parameter(Mandatory)]
|
|
[psobject]$State,
|
|
[Parameter(Mandatory)]
|
|
[string]$HeaderCheckBoxKeyName
|
|
)
|
|
|
|
$selectedItem = $ListView.SelectedItem
|
|
if ($null -eq $selectedItem) { return }
|
|
|
|
# Store the current index to restore focus later
|
|
$currentIndex = $ListView.SelectedIndex
|
|
|
|
# Toggle the IsSelected property
|
|
$selectedItem.IsSelected = -not $selectedItem.IsSelected
|
|
$ListView.Items.Refresh()
|
|
|
|
# Update the 'Select All' header checkbox state
|
|
$headerChk = $State.Controls[$HeaderCheckBoxKeyName]
|
|
if ($null -ne $headerChk) {
|
|
Update-SelectAllHeaderCheckBoxState -ListView $ListView -HeaderCheckBox $headerChk
|
|
}
|
|
|
|
# Restore selection and focus to the item that was just toggled
|
|
if ($currentIndex -ge 0 -and $ListView.Items.Count -gt $currentIndex) {
|
|
$ListView.SelectedIndex = $currentIndex
|
|
|
|
# Ensure the UI is updated before trying to find the container
|
|
$ListView.UpdateLayout()
|
|
|
|
$listViewItem = $ListView.ItemContainerGenerator.ContainerFromIndex($currentIndex)
|
|
if ($null -ne $listViewItem) {
|
|
$listViewItem.Focus()
|
|
}
|
|
}
|
|
}
|
|
|
|
# Function to sort ListView items
|
|
function Invoke-ListViewSort {
|
|
param(
|
|
[System.Windows.Controls.ListView]$listView,
|
|
[string]$property,
|
|
[PSCustomObject]$State
|
|
)
|
|
|
|
# Ensure $State.Flags is a hashtable and contains the required sort properties
|
|
if ($State.Flags -is [hashtable]) {
|
|
if (-not $State.Flags.ContainsKey('lastSortProperty')) {
|
|
$State.Flags['lastSortProperty'] = $null
|
|
}
|
|
if (-not $State.Flags.ContainsKey('lastSortAscending')) {
|
|
$State.Flags['lastSortAscending'] = $true # Default to ascending
|
|
}
|
|
}
|
|
else {
|
|
Write-Warning "Invoke-ListViewSort: \$State.Flags is not a hashtable or is null. Sort state may not work correctly."
|
|
# Attempt to initialize if $State.Flags is null or unexpectedly not a hashtable,
|
|
# though this might indicate a deeper issue with $State.Flags initialization.
|
|
if ($null -eq $State.Flags) { $State.Flags = @{} }
|
|
if ($State.Flags -is [hashtable]) { # Check again after potential initialization
|
|
if (-not $State.Flags.ContainsKey('lastSortProperty')) { $State.Flags['lastSortProperty'] = $null }
|
|
if (-not $State.Flags.ContainsKey('lastSortAscending')) { $State.Flags['lastSortAscending'] = $true }
|
|
}
|
|
}
|
|
|
|
# Toggle sort direction if clicking the same column
|
|
if ($State.Flags.lastSortProperty -eq $property) {
|
|
$State.Flags.lastSortAscending = -not $State.Flags.lastSortAscending
|
|
}
|
|
else {
|
|
$State.Flags.lastSortAscending = $true
|
|
}
|
|
$State.Flags.lastSortProperty = $property
|
|
|
|
# Get items from ItemsSource or Items collection
|
|
$currentItemsSource = $listView.ItemsSource
|
|
$itemsToSort = @()
|
|
if ($null -ne $currentItemsSource) {
|
|
$itemsToSort = @($currentItemsSource)
|
|
}
|
|
else {
|
|
$itemsToSort = @($listView.Items)
|
|
}
|
|
|
|
if ($itemsToSort.Count -eq 0) {
|
|
return
|
|
}
|
|
|
|
$selectedItems = @($itemsToSort | Where-Object { $_.IsSelected })
|
|
$unselectedItems = @($itemsToSort | Where-Object { -not $_.IsSelected })
|
|
|
|
# Define the primary sort criterion
|
|
$primarySortDefinition = @{
|
|
Expression = {
|
|
$val = $_.$property
|
|
if ($null -eq $val) { '' } else { $val }
|
|
}
|
|
Ascending = $State.Flags.lastSortAscending
|
|
}
|
|
|
|
$sortCriteria = [System.Collections.Generic.List[hashtable]]::new()
|
|
$sortCriteria.Add($primarySortDefinition)
|
|
|
|
# Determine secondary sort property based on the ListView
|
|
$secondarySortPropertyName = $null
|
|
if ($listView.Name -eq 'lstDriverModels') {
|
|
$secondarySortPropertyName = "Model"
|
|
}
|
|
elseif ($listView.Name -eq 'lstWingetResults') {
|
|
$secondarySortPropertyName = "Name"
|
|
}
|
|
elseif ($listView.Name -eq 'lstAppsScriptVariables') {
|
|
if ($property -eq "Key") {
|
|
$secondarySortPropertyName = "Value"
|
|
}
|
|
elseif ($property -eq "Value") {
|
|
$secondarySortPropertyName = "Key"
|
|
}
|
|
else {
|
|
# Default secondary sort for IsSelected or other properties
|
|
$secondarySortPropertyName = "Key"
|
|
}
|
|
}
|
|
|
|
if ($null -ne $secondarySortPropertyName -and $property -ne $secondarySortPropertyName) {
|
|
$itemsHaveSecondaryProperty = $false
|
|
if ($unselectedItems.Count -gt 0) {
|
|
if ($null -ne $unselectedItems[0].PSObject.Properties[$secondarySortPropertyName]) {
|
|
$itemsHaveSecondaryProperty = $true
|
|
}
|
|
}
|
|
elseif ($selectedItems.Count -gt 0) {
|
|
if ($null -ne $selectedItems[0].PSObject.Properties[$secondarySortPropertyName]) {
|
|
$itemsHaveSecondaryProperty = $true
|
|
}
|
|
}
|
|
|
|
if ($itemsHaveSecondaryProperty) {
|
|
# Create a scriptblock for the secondary sort expression dynamically
|
|
$expressionScriptBlock = [scriptblock]::Create("`$_.$secondarySortPropertyName")
|
|
|
|
$secondarySortDefinition = @{
|
|
Expression = {
|
|
$val = Invoke-Command -ScriptBlock $expressionScriptBlock -ArgumentList $_
|
|
if ($null -eq $val) { '' } else { $val }
|
|
}
|
|
Ascending = $true # Secondary sort always ascending
|
|
}
|
|
$sortCriteria.Add($secondarySortDefinition)
|
|
}
|
|
}
|
|
|
|
$sortedUnselected = $unselectedItems | Sort-Object -Property $sortCriteria.ToArray()
|
|
# Ensure $sortedUnselected is not null before attempting to add its range
|
|
if ($null -eq $sortedUnselected) {
|
|
$sortedUnselected = @()
|
|
}
|
|
|
|
# Combine sorted items: selected items first, then sorted unselected items
|
|
$newSortedList = [System.Collections.Generic.List[object]]::new()
|
|
$newSortedList.AddRange($selectedItems)
|
|
$newSortedList.AddRange($sortedUnselected)
|
|
|
|
# Set the new sorted list as the ItemsSource
|
|
# Try nulling out ItemsSource first to force a more complete refresh
|
|
$listView.ItemsSource = $null
|
|
$listView.ItemsSource = $newSortedList.ToArray()
|
|
}
|
|
|
|
# --------------------------------------------------------------------------
|
|
# SECTION: Modern Folder Picker
|
|
# --------------------------------------------------------------------------
|
|
|
|
# 1) Define a C# class that uses the correct GUIDs for IFileDialog, IFileOpenDialog, and FileOpenDialog,
|
|
# while omitting conflicting "GetResults/GetSelectedItems" from IFileDialog.
|
|
if (-not ("ModernFolderBrowser" -as [type])) {
|
|
$modernFolderBrowserCode = @"
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
|
|
public static class ModernFolderBrowser
|
|
{
|
|
// Flags for IFileDialog
|
|
[Flags]
|
|
private enum FileDialogOptions : uint
|
|
|
|
{
|
|
OverwritePrompt = 0x00000002,
|
|
StrictFileTypes = 0x00000004,
|
|
NoChangeDir = 0x00000008,
|
|
PickFolders = 0x00000020,
|
|
ForceFileSystem = 0x00000040,
|
|
AllNonStorageItems = 0x00000080,
|
|
NoValidate = 0x00000100,
|
|
AllowMultiSelect = 0x00000200,
|
|
PathMustExist = 0x00000800,
|
|
FileMustExist = 0x00001000,
|
|
CreatePrompt = 0x00002000,
|
|
ShareAware = 0x00004000,
|
|
NoReadOnlyReturn = 0x00008000,
|
|
NoTestFileCreate = 0x00010000,
|
|
DontAddToRecent = 0x02000000,
|
|
ForceShowHidden = 0x10000000
|
|
}
|
|
|
|
// IFileDialog (GUID from Windows SDK)
|
|
// - Omitting GetResults / GetSelectedItems to avoid overshadow.
|
|
[ComImport]
|
|
[Guid("42F85136-DB7E-439C-85F1-E4075D135FC8")]
|
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
private interface IFileDialog
|
|
{
|
|
[PreserveSig]
|
|
int Show(IntPtr parent);
|
|
|
|
void SetFileTypes(uint cFileTypes, IntPtr rgFilterSpec);
|
|
void SetFileTypeIndex(uint iFileType);
|
|
void GetFileTypeIndex(out uint piFileType);
|
|
void Advise(IntPtr pfde, out uint pdwCookie);
|
|
void Unadvise(uint dwCookie);
|
|
void SetOptions(FileDialogOptions fos);
|
|
void GetOptions(out FileDialogOptions pfos);
|
|
void SetDefaultFolder(IShellItem psi);
|
|
void SetFolder(IShellItem psi);
|
|
void GetFolder(out IShellItem ppsi);
|
|
void GetCurrentSelection(out IShellItem ppsi);
|
|
void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName);
|
|
void GetFileName(out IntPtr pszName);
|
|
void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
|
|
void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText);
|
|
void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
|
|
void GetResult(out IShellItem ppsi);
|
|
void AddPlace(IShellItem psi, int fdap);
|
|
void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
|
|
void Close(int hr);
|
|
void SetClientGuid(ref Guid guid);
|
|
void ClearClientData();
|
|
void SetFilter(IntPtr pFilter);
|
|
|
|
// NOTE: We intentionally do NOT define GetResults and GetSelectedItems here,
|
|
// because they cause overshadow warnings in IFileOpenDialog.
|
|
}
|
|
|
|
// IFileOpenDialog extends IFileDialog by adding 2 new methods with the same name,
|
|
// which otherwise cause overshadow warnings. We'll define them only here.
|
|
[ComImport]
|
|
[Guid("D57C7288-D4AD-4768-BE02-9D969532D960")]
|
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
private interface IFileOpenDialog : IFileDialog
|
|
{
|
|
// These two come after the parent's vtable:
|
|
void GetResults(out IntPtr ppenum);
|
|
void GetSelectedItems(out IntPtr ppsai);
|
|
}
|
|
|
|
// The coclass for creating an IFileOpenDialog
|
|
[ComImport]
|
|
[Guid("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7")]
|
|
private class FileOpenDialog
|
|
{
|
|
}
|
|
|
|
// IShellItem
|
|
[ComImport]
|
|
[Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")]
|
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
private interface IShellItem
|
|
{
|
|
void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv);
|
|
void GetParent(out IShellItem ppsi);
|
|
void GetDisplayName(uint sigdnName, out IntPtr ppszName);
|
|
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
|
|
void Compare(IShellItem psi, uint hint, out int piOrder);
|
|
}
|
|
|
|
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
private static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IntPtr pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface, IidParameterIndex = 2)] out IShellItem ppv);
|
|
|
|
private const uint SIGDN_FILESYSPATH = 0x80058000;
|
|
private static readonly Guid IID_IShellItem = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE");
|
|
|
|
public static string ShowDialog(string title, IntPtr parentHandle, string initialDirectory)
|
|
{
|
|
// Create COM dialog instance
|
|
IFileOpenDialog dialog = (IFileOpenDialog)(new FileOpenDialog());
|
|
|
|
// Get current options
|
|
FileDialogOptions opts;
|
|
dialog.GetOptions(out opts);
|
|
|
|
// Add flags for picking folders
|
|
opts |= FileDialogOptions.PickFolders | FileDialogOptions.PathMustExist | FileDialogOptions.ForceFileSystem;
|
|
dialog.SetOptions(opts);
|
|
|
|
// Set initial directory if provided
|
|
if (!string.IsNullOrEmpty(initialDirectory))
|
|
{
|
|
try
|
|
{
|
|
Guid iid = IID_IShellItem; // Create a local copy to pass by ref
|
|
if (SHCreateItemFromParsingName(initialDirectory, IntPtr.Zero, ref iid, out IShellItem initialFolder) == 0)
|
|
{
|
|
dialog.SetFolder(initialFolder);
|
|
Marshal.ReleaseComObject(initialFolder);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Ignore errors in setting initial directory (e.g., path doesn't exist)
|
|
}
|
|
}
|
|
|
|
// Set title
|
|
if (!string.IsNullOrEmpty(title))
|
|
{
|
|
dialog.SetTitle(title);
|
|
}
|
|
|
|
// Show the dialog
|
|
int hr = dialog.Show(parentHandle);
|
|
// 0 = S_OK. 1 or 0x800704C7 often means user canceled. Return null if so.
|
|
if (hr != 0)
|
|
{
|
|
if ((uint)hr == 0x800704C7 || hr == 1)
|
|
{
|
|
return null; // Canceled
|
|
}
|
|
else
|
|
{
|
|
Marshal.ThrowExceptionForHR(hr);
|
|
}
|
|
}
|
|
|
|
// Retrieve the selection (IShellItem)
|
|
IShellItem shellItem;
|
|
dialog.GetResult(out shellItem);
|
|
if (shellItem == null) return null;
|
|
|
|
// Convert to file system path
|
|
IntPtr pszPath = IntPtr.Zero;
|
|
shellItem.GetDisplayName(SIGDN_FILESYSPATH, out pszPath);
|
|
if (pszPath == IntPtr.Zero) return null;
|
|
|
|
string folderPath = Marshal.PtrToStringAuto(pszPath);
|
|
Marshal.FreeCoTaskMem(pszPath);
|
|
|
|
return folderPath;
|
|
}
|
|
}
|
|
"@
|
|
Add-Type -TypeDefinition $modernFolderBrowserCode -Language CSharp
|
|
}
|
|
|
|
# 2) Define a PowerShell function that invokes our C# wrapper
|
|
function Show-ModernFolderPicker {
|
|
param(
|
|
[string]$Title = "Select a folder",
|
|
[string]$InitialDirectory
|
|
)
|
|
# For a simple test, pass IntPtr.Zero as the parent window handle
|
|
return [ModernFolderBrowser]::ShowDialog($Title, [IntPtr]::Zero, $InitialDirectory)
|
|
}
|
|
|
|
function Invoke-BrowseAction {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateSet('Folder', 'OpenFile', 'SaveFile')]
|
|
[string]$Type,
|
|
|
|
[string]$Title,
|
|
[string]$Filter,
|
|
[string]$InitialDirectory,
|
|
[string]$FileName,
|
|
[string]$DefaultExt,
|
|
[switch]$AllowNewFile
|
|
)
|
|
|
|
switch ($Type) {
|
|
'Folder' {
|
|
return Show-ModernFolderPicker -Title $Title -InitialDirectory $InitialDirectory
|
|
}
|
|
'OpenFile' {
|
|
$dialog = New-Object Microsoft.Win32.OpenFileDialog
|
|
$dialog.Title = $Title
|
|
if (-not [string]::IsNullOrWhiteSpace($Filter)) { $dialog.Filter = $Filter }
|
|
if ($AllowNewFile) { $dialog.CheckFileExists = $false }
|
|
if (-not [string]::IsNullOrWhiteSpace($InitialDirectory)) {
|
|
$dialog.InitialDirectory = $InitialDirectory
|
|
}
|
|
if ($dialog.ShowDialog()) {
|
|
return $dialog.FileName
|
|
}
|
|
}
|
|
'SaveFile' {
|
|
$dialog = New-Object Microsoft.Win32.SaveFileDialog
|
|
$dialog.Title = $Title
|
|
if (-not [string]::IsNullOrWhiteSpace($Filter)) { $dialog.Filter = $Filter }
|
|
if ($AllowNewFile) { $dialog.CheckFileExists = $false } # This property is obsolete but used in existing code.
|
|
if (-not [string]::IsNullOrWhiteSpace($InitialDirectory)) {
|
|
$dialog.InitialDirectory = $InitialDirectory
|
|
}
|
|
if (-not [string]::IsNullOrWhiteSpace($FileName)) {
|
|
$dialog.FileName = $FileName
|
|
}
|
|
if (-not [string]::IsNullOrWhiteSpace($DefaultExt)) {
|
|
$dialog.DefaultExt = $DefaultExt
|
|
}
|
|
if ($dialog.ShowDialog()) {
|
|
return $dialog.FileName
|
|
}
|
|
}
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Clear-ListViewContent {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[psobject]$State,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[System.Windows.Controls.ListView]$ListViewControl,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ConfirmationTitle,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ConfirmationMessage,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[System.Collections.IList]$BackingDataList,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$StatusMessage,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[System.Windows.Controls.TextBox[]]$TextBoxesToClear,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[scriptblock]$PostClearAction
|
|
)
|
|
|
|
$result = [System.Windows.MessageBox]::Show($ConfirmationMessage, $ConfirmationTitle, [System.Windows.MessageBoxButton]::YesNo, [System.Windows.MessageBoxImage]::Question)
|
|
if ($result -ne [System.Windows.MessageBoxResult]::Yes) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
# If a backing data list is provided, clear it and rebind. This is the preferred method.
|
|
if ($null -ne $BackingDataList) {
|
|
$BackingDataList.Clear()
|
|
$ListViewControl.ItemsSource = $BackingDataList.ToArray()
|
|
}
|
|
# If no backing list, determine how to clear the control.
|
|
else {
|
|
# If ItemsSource is in use, the only valid way to clear is to set it to null or an empty collection.
|
|
if ($null -ne $ListViewControl.ItemsSource) {
|
|
$ListViewControl.ItemsSource = $null
|
|
}
|
|
# If ItemsSource is NOT in use, we can safely clear the Items collection directly (for BYO Apps).
|
|
elseif ($null -ne $ListViewControl.Items) {
|
|
$ListViewControl.Items.Clear()
|
|
}
|
|
}
|
|
|
|
$ListViewControl.Items.Refresh()
|
|
|
|
# Clear any specified textboxes
|
|
if ($null -ne $TextBoxesToClear) {
|
|
foreach ($textBox in $TextBoxesToClear) {
|
|
$textBox.Clear()
|
|
}
|
|
}
|
|
|
|
# Update the status message if provided
|
|
if (-not [string]::IsNullOrWhiteSpace($StatusMessage) -and $null -ne $State.Controls.txtStatus) {
|
|
$State.Controls.txtStatus.Text = $StatusMessage
|
|
}
|
|
|
|
# Execute any post-clear custom actions. The scriptblock will have access to the $State and $ListViewControl variables from this function's scope.
|
|
if ($null -ne $PostClearAction) {
|
|
& $PostClearAction
|
|
}
|
|
}
|
|
catch {
|
|
WriteLog "Error in Clear-ListViewContent for $($ListViewControl.Name): $($_.Exception.Message)"
|
|
[System.Windows.MessageBox]::Show("An error occurred while clearing the list: $($_.Exception.Message)", "Error", "OK", "Error")
|
|
}
|
|
}
|
|
|
|
Export-ModuleMember -Function * |