mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
b7847ebaad
Moves ListView item manipulation functions (`Update-ListViewPriorities`, `Move-ListViewItemTop`, `Move-ListViewItemUp`, `Move-ListViewItemDown`, `Move-ListViewItemBottom`) from the `FFUUI.Core.Applications` module to the `FFUUI.Core.Shared` module. This improves code organization and reusability by centralizing common UI helper logic.
711 lines
28 KiB
PowerShell
711 lines
28 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.Items | 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)]
|
|
[string]$HeaderCheckBoxScriptVariableName,
|
|
[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()
|
|
}
|
|
}
|
|
})
|
|
|
|
Set-Variable -Name $HeaderCheckBoxScriptVariableName -Value $headerCheckBox -Scope Script -Force
|
|
WriteLog "Add-SelectableGridViewColumn: Stored header checkbox in script variable '$HeaderCheckBoxScriptVariableName'."
|
|
|
|
$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]@{
|
|
HeaderCheckboxName = $HeaderCheckBoxScriptVariableName
|
|
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
|
|
|
|
$headerCheckboxNameFromTag = $tagData.HeaderCheckboxName
|
|
$targetListView = $tagData.ListViewControl # Get the control directly from the tag
|
|
|
|
WriteLog "Add-SelectableGridViewColumn: Item Click. ListView: '$($targetListView.Name)', HeaderChkName: '$headerCheckboxNameFromTag'"
|
|
|
|
$headerChk = Get-Variable -Name $headerCheckboxNameFromTag -Scope Script -ValueOnly -ErrorAction SilentlyContinue
|
|
if ($null -ne $headerChk) {
|
|
Update-SelectAllHeaderCheckBoxState -ListView $targetListView -HeaderCheckBox $headerChk
|
|
}
|
|
else {
|
|
WriteLog "Add-SelectableGridViewColumn: Error - Could not retrieve script variable for header checkbox named '$headerCheckboxNameFromTag'."
|
|
}
|
|
})
|
|
|
|
$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
|
|
$totalItemCount = $collectionToInspect.Count # Get the total count from the collection being inspected
|
|
|
|
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 sort ListView items
|
|
function Invoke-ListViewSort {
|
|
param(
|
|
[System.Windows.Controls.ListView]$listView,
|
|
[string]$property,
|
|
[PSCustomObject]$State
|
|
)
|
|
|
|
# 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.
|
|
Add-Type -TypeDefinition @"
|
|
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);
|
|
}
|
|
|
|
private const uint SIGDN_FILESYSPATH = 0x80058000;
|
|
|
|
public static string ShowDialog(string title, IntPtr parentHandle)
|
|
{
|
|
// 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 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;
|
|
}
|
|
}
|
|
"@ -Language CSharp
|
|
|
|
# 2) Define a PowerShell function that invokes our C# wrapper
|
|
function Show-ModernFolderPicker {
|
|
param(
|
|
[string]$Title = "Select a folder"
|
|
)
|
|
# For a simple test, pass IntPtr.Zero as the parent window handle
|
|
return [ModernFolderBrowser]::ShowDialog($Title, [IntPtr]::Zero)
|
|
}
|
|
|
|
Export-ModuleMember -Function * |