mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 10:19:36 -06:00
Add FFU.Common and FFUUI.Core module manifests and shared UI functions
- Created module manifest for FFU.Common with initial version 0.0.1. - Created module manifest for FFUUI.Core with initial version 0.0.1. - Implemented shared UI functions in FFUUI.Shared.psm1, including: - Update-ListViewItemStatus: Updates the status of items in a ListView. - Update-OverallProgress: Updates a progress bar and status label. - Invoke-ProgressUpdate: Enqueues progress updates to the UI thread. - Add-SortableColumn: Adds sortable columns to a ListView. - Add-SelectableGridViewColumn: Adds a selectable column with a "Select All" checkbox. - Update-SelectAllHeaderCheckBoxState: Updates the state of the header checkbox. - Invoke-ListViewSort: Sorts ListView items based on specified properties. - Show-ModernFolderPicker: Displays a modern folder picker dialog.
This commit is contained in:
@@ -0,0 +1,618 @@
|
||||
# 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 *
|
||||
Reference in New Issue
Block a user