From db952355f33ff9e8b1a02c624d91e21be73f7901 Mon Sep 17 00:00:00 2001
From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com>
Date: Tue, 23 Jun 2026 17:06:24 -0700
Subject: [PATCH] Add unified FFU disk layout controls
Consolidate disk size, required partition drive letters, Recovery, and data partition settings into the Hyper-V Disk Layout UI. Add capacity validation, Fill Remaining handling, Recovery remove/restore, data partition reorder/remove controls, and persist CreateRecoveryPartition through config and VHDX cache checks.
Update FFU optimization so automatic partition selection targets the Fill Remaining partition, falling back to Windows when none is selected. Refresh sample config and docs for the new Disk Layout parameters.
---
FFUDevelopment/BuildFFUVM.ps1 | 116 +++-
FFUDevelopment/BuildFFUVM_UI.ps1 | 9 +-
FFUDevelopment/BuildFFUVM_UI.xaml | 159 ++---
.../FFUUI.Core/FFUUI.Core.Config.psm1 | 563 +++++++++++++++++-
.../FFUUI.Core/FFUUI.Core.Handlers.psm1 | 114 +++-
.../FFUUI.Core/FFUUI.Core.Initialize.psm1 | 72 ++-
.../FFUUI.Core/FFUUI.Core.Shared.psm1 | 27 +-
FFUDevelopment/config/Sample_default.json | Bin 7558 -> 7634 bytes
docs/hyperv_settings.md | 28 +-
docs/parameters_reference.md | 17 +-
10 files changed, 923 insertions(+), 182 deletions(-)
diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1
index f6d69bb..12d72cf 100644
--- a/FFUDevelopment/BuildFFUVM.ps1
+++ b/FFUDevelopment/BuildFFUVM.ps1
@@ -111,6 +111,9 @@ Fixed size of the Windows partition in bytes. Required when AdditionalDataPartit
.PARAMETER RecoveryPartitionSize
Optional fixed size of the Recovery partition in bytes. Leave as 0 to calculate the Recovery partition size from winre.wim plus buffer space.
+.PARAMETER CreateRecoveryPartition
+When set to $false, skips creating the Windows Recovery partition. Default is $true.
+
.PARAMETER AdditionalDataPartitions
Optional data partitions to create after the Recovery partition. Each item supports Name, Label, DriveLetter, SizeBytes or SizeGB, FillRemaining, and FileSystem.
@@ -187,7 +190,7 @@ Path to a custom Office configuration XML file to use for installation.
When set to $true, will optimize the FFU file. Default is $true.
.PARAMETER OptimizeFFUPartitionNumber
-Optional partition number to pass to DISM /Optimize-FFU /PartitionNumber. Leave as 0 to optimize with DISM defaults.
+Optional partition number to pass to DISM /Optimize-FFU /PartitionNumber. Leave as 0 to optimize the FillRemaining partition, or Windows when no partition uses FillRemaining.
.PARAMETER OptionalFeatures
Provide a semicolon-separated list of Windows optional features you want to include in the FFU (e.g., netfx3;TFTP).
@@ -366,6 +369,7 @@ param(
[uint64]$Disksize = 50GB,
[uint64]$OSPartitionSize = 0,
[uint64]$RecoveryPartitionSize = 0,
+ [bool]$CreateRecoveryPartition = $true,
[object[]]$AdditionalDataPartitions = @(),
[int]$Processors = 4,
[bool]$EnableVMNetworking,
@@ -898,6 +902,7 @@ class VhdxCacheItem {
[string]$VhdxFileName = ""
[uint32]$LogicalSectorSizeBytes = ""
[uint64]$Disksize = ""
+ [bool]$CreateRecoveryPartition = $true
[string]$SystemPartitionDriveLetter = ""
[string]$WindowsPartitionDriveLetter = ""
[string]$RecoveryPartitionDriveLetter = ""
@@ -3089,14 +3094,17 @@ function Get-NormalizedPartitionDriveLetters {
[string]$SystemPartitionDriveLetter,
[string]$WindowsPartitionDriveLetter,
[string]$RecoveryPartitionDriveLetter,
+ [bool]$CreateRecoveryPartition = $true,
[object[]]$AdditionalDataPartitions = @(),
[switch]$ValidateAvailable
)
$requestedLetters = [ordered]@{
- SystemPartitionDriveLetter = $SystemPartitionDriveLetter
- WindowsPartitionDriveLetter = $WindowsPartitionDriveLetter
- RecoveryPartitionDriveLetter = $RecoveryPartitionDriveLetter
+ SystemPartitionDriveLetter = $SystemPartitionDriveLetter
+ WindowsPartitionDriveLetter = $WindowsPartitionDriveLetter
+ }
+ if ($CreateRecoveryPartition) {
+ $requestedLetters['RecoveryPartitionDriveLetter'] = $RecoveryPartitionDriveLetter
}
foreach ($dataPartition in @($AdditionalDataPartitions)) {
if ($null -eq $dataPartition) { continue }
@@ -3123,7 +3131,7 @@ function Get-NormalizedPartitionDriveLetters {
$duplicateLetters = @($normalizedLetters.Values | Group-Object | Where-Object { $_.Count -gt 1 })
if ($duplicateLetters.Count -gt 0) {
$duplicateLetterList = ($duplicateLetters | ForEach-Object { $_.Name }) -join ', '
- throw "System, Windows, and Recovery partition drive letters must be unique. Duplicate value(s): $duplicateLetterList."
+ throw "Build partition drive letters must be unique. Duplicate value(s): $duplicateLetterList."
}
if ($ValidateAvailable) {
@@ -3229,6 +3237,7 @@ function Get-PartitionLayoutSignature {
param(
[uint64]$OSPartitionSize,
[uint64]$RecoveryPartitionSize,
+ [bool]$CreateRecoveryPartition = $true,
[object[]]$DataPartitions = @()
)
@@ -3236,7 +3245,7 @@ function Get-PartitionLayoutSignature {
"$($_.Name)|$($_.Label)|$($_.DriveLetter)|$($_.FileSystem)|$($_.SizeBytes)|$($_.FillRemaining)"
})
- return "OS=$OSPartitionSize;Recovery=$RecoveryPartitionSize;Data=$($dataPartitionSignatures -join ';')"
+ return "OS=$OSPartitionSize;CreateRecovery=$CreateRecoveryPartition;Recovery=$RecoveryPartitionSize;Data=$($dataPartitionSignatures -join ';')"
}
function Get-PartitionDriveLetterCacheValue {
param(
@@ -3451,6 +3460,56 @@ function Get-WindowsPartitionFromDisk {
return $basicDataPartitions | Select-Object -First 1
}
+function Get-FFUOptimizePartitionNumber {
+ param(
+ [Parameter(Mandatory = $true)]
+ [ciminstance]$Disk,
+ [int]$RequestedPartitionNumber = 0,
+ [string]$WindowsPartitionDriveLetter,
+ [object[]]$AdditionalDataPartitions = @()
+ )
+
+ if ($RequestedPartitionNumber -gt 0) {
+ return $RequestedPartitionNumber
+ }
+
+ $fillRemainingDataPartition = $null
+ foreach ($dataPartition in @($AdditionalDataPartitions)) {
+ if ($null -eq $dataPartition) { continue }
+
+ $fillRemaining = $false
+ if ($dataPartition.PSObject.Properties.Name -contains 'FillRemaining') {
+ $fillRemaining = [System.Convert]::ToBoolean($dataPartition.FillRemaining)
+ }
+
+ if ($fillRemaining) {
+ $fillRemainingDataPartition = $dataPartition
+ break
+ }
+ }
+ if ($null -ne $fillRemainingDataPartition) {
+ $driveLetter = ([string]$fillRemainingDataPartition.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant()
+ if ([string]::IsNullOrWhiteSpace($driveLetter)) {
+ throw "FillRemaining data partition '$($fillRemainingDataPartition.Name)' does not have a drive letter for FFU optimization."
+ }
+
+ $dataPartition = $Disk | Get-Partition | Where-Object { ([string]$_.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant() -eq $driveLetter } | Select-Object -First 1
+ if ($null -eq $dataPartition) {
+ throw "Unable to resolve FillRemaining data partition '$($fillRemainingDataPartition.Name)' at drive ${driveLetter}: for FFU optimization."
+ }
+
+ WriteLog "Using FillRemaining data partition '$($fillRemainingDataPartition.Name)' (partition number $($dataPartition.PartitionNumber)) for FFU optimization."
+ return [int]$dataPartition.PartitionNumber
+ }
+
+ $windowsPartition = Get-WindowsPartitionFromDisk -Disk $Disk -DriveLetter $WindowsPartitionDriveLetter
+ if ($null -eq $windowsPartition) {
+ throw 'Unable to resolve Windows partition for FFU optimization.'
+ }
+
+ WriteLog "Using Windows partition (partition number $($windowsPartition.PartitionNumber)) for FFU optimization."
+ return [int]$windowsPartition.PartitionNumber
+}
#Add boot files
function Add-BootFiles {
param(
@@ -4400,6 +4459,10 @@ function New-FFUFileName {
function New-FFU {
$captureContext = Get-CaptureVhdContext -VhdxPath $VHDXPath
$captureDisk = $captureContext.Disk
+ $resolvedFFUOptimizePartitionNumber = 0
+ if ($Optimize -eq $true) {
+ $resolvedFFUOptimizePartitionNumber = Get-FFUOptimizePartitionNumber -Disk $captureDisk -RequestedPartitionNumber $OptimizeFFUPartitionNumber -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -AdditionalDataPartitions $normalizedAdditionalDataPartitions
+ }
$ffuCaptureNamingInfo = Get-FFUCaptureNamingInfo -ShortenedWindowsSKU $shortenedWindowsSKU -WindowsRelease $WindowsRelease -WindowsVersion $WindowsVersion -InstallationType $installationType -IsWindows10LtscClient:$isWindows10LtscClient
try {
@@ -4461,9 +4524,9 @@ function New-FFU {
Set-Progress -Percentage 85 -Message "Optimizing FFU..."
WriteLog 'Optimizing FFU - This will take a few minutes, please be patient'
#Need to use ADK version of DISM to address bug in DISM - perhaps Windows 11 24H2 will fix this
- $optimizePartitionArgument = if ($OptimizeFFUPartitionNumber -gt 0) { " /PartitionNumber:$OptimizeFFUPartitionNumber" } else { '' }
+ $optimizePartitionArgument = if ($resolvedFFUOptimizePartitionNumber -gt 0) { " /PartitionNumber:$resolvedFFUOptimizePartitionNumber" } else { '' }
if (-not [string]::IsNullOrWhiteSpace($optimizePartitionArgument)) {
- WriteLog "Optimizing FFU with DISM partition number $OptimizeFFUPartitionNumber."
+ WriteLog "Optimizing FFU with DISM partition number $resolvedFFUOptimizePartitionNumber."
}
Invoke-Process cmd "/c ""$DandIEnv"" && dism /optimize-ffu /imagefile:$FFUFile$optimizePartitionArgument" | Out-Null
#Invoke-Process cmd "/c dism /optimize-ffu /imagefile:$FFUFile" | Out-Null
@@ -6177,16 +6240,25 @@ Set-Progress -Percentage 2 -Message "Validating parameters..."
try {
$normalizedAdditionalDataPartitions = ConvertTo-NormalizedDataPartitions -DataPartitions $AdditionalDataPartitions
if ($normalizedAdditionalDataPartitions.Count -gt 0 -and $OSPartitionSize -le 0) {
- throw 'OSPartitionSize must be set when AdditionalDataPartitions are configured so space remains for Recovery and data partitions.'
+ $osPartitionSizeMessage = if ($CreateRecoveryPartition) {
+ 'OSPartitionSize must be set when AdditionalDataPartitions are configured so space remains for Recovery and data partitions.'
+ }
+ else {
+ 'OSPartitionSize must be set when AdditionalDataPartitions are configured so space remains for data partitions.'
+ }
+ throw $osPartitionSizeMessage
}
- $partitionLayoutSignature = Get-PartitionLayoutSignature -OSPartitionSize $OSPartitionSize -RecoveryPartitionSize $RecoveryPartitionSize -DataPartitions $normalizedAdditionalDataPartitions
+ $partitionLayoutSignature = Get-PartitionLayoutSignature -OSPartitionSize $OSPartitionSize -RecoveryPartitionSize $RecoveryPartitionSize -CreateRecoveryPartition $CreateRecoveryPartition -DataPartitions $normalizedAdditionalDataPartitions
- $partitionDriveLetters = Get-NormalizedPartitionDriveLetters -SystemPartitionDriveLetter $SystemPartitionDriveLetter -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -RecoveryPartitionDriveLetter $RecoveryPartitionDriveLetter -AdditionalDataPartitions $normalizedAdditionalDataPartitions -ValidateAvailable
+ $partitionDriveLetters = Get-NormalizedPartitionDriveLetters -SystemPartitionDriveLetter $SystemPartitionDriveLetter -WindowsPartitionDriveLetter $WindowsPartitionDriveLetter -RecoveryPartitionDriveLetter $RecoveryPartitionDriveLetter -CreateRecoveryPartition $CreateRecoveryPartition -AdditionalDataPartitions $normalizedAdditionalDataPartitions -ValidateAvailable
$SystemPartitionDriveLetter = $partitionDriveLetters.SystemPartitionDriveLetter
$WindowsPartitionDriveLetter = $partitionDriveLetters.WindowsPartitionDriveLetter
- $RecoveryPartitionDriveLetter = $partitionDriveLetters.RecoveryPartitionDriveLetter
+ if ($CreateRecoveryPartition) {
+ $RecoveryPartitionDriveLetter = $partitionDriveLetters.RecoveryPartitionDriveLetter
+ }
$dataPartitionDriveLetterLog = if ($normalizedAdditionalDataPartitions.Count -gt 0) { ', Data=' + (($normalizedAdditionalDataPartitions | ForEach-Object { "$($_.Name):$($_.DriveLetter)" }) -join ', ') } else { '' }
- WriteLog "Using build partition drive letters: System=$SystemPartitionDriveLetter, Windows=$WindowsPartitionDriveLetter, Recovery=$RecoveryPartitionDriveLetter$dataPartitionDriveLetterLog"
+ $recoveryPartitionDriveLetterLog = if ($CreateRecoveryPartition) { ", Recovery=$RecoveryPartitionDriveLetter" } else { ', Recovery=disabled' }
+ WriteLog "Using build partition drive letters: System=$SystemPartitionDriveLetter, Windows=$WindowsPartitionDriveLetter$recoveryPartitionDriveLetterLog$dataPartitionDriveLetterLog"
}
catch {
$partitionDriveLetterValidationError = "Build validation failed: $($_.Exception.Message)"
@@ -7486,15 +7558,19 @@ try {
[uint64]$cachedDisksize = 0
if (-not [uint64]::TryParse([string]$vhdxCacheItem.Disksize, [ref]$cachedDisksize)) { WriteLog "Disksize invalid in cached config ($($vhdxCacheItem.Disksize)), continuing"; continue }
if ($cachedDisksize -ne $Disksize) { WriteLog "Disksize mismatch (cached: $cachedDisksize, current: $Disksize), continuing"; continue }
+ if ($vhdxCacheItem.PSObject.Properties.Name -notcontains 'CreateRecoveryPartition') { WriteLog 'CreateRecoveryPartition missing in cached config, continuing'; continue }
+ if ([bool]$vhdxCacheItem.CreateRecoveryPartition -ne $CreateRecoveryPartition) { WriteLog "CreateRecoveryPartition mismatch (cached: $($vhdxCacheItem.CreateRecoveryPartition), current: $CreateRecoveryPartition), continuing"; continue }
if ($vhdxCacheItem.PSObject.Properties.Name -notcontains 'SystemPartitionDriveLetter') { WriteLog 'SystemPartitionDriveLetter missing in cached config, continuing'; continue }
if ($vhdxCacheItem.PSObject.Properties.Name -notcontains 'WindowsPartitionDriveLetter') { WriteLog 'WindowsPartitionDriveLetter missing in cached config, continuing'; continue }
- if ($vhdxCacheItem.PSObject.Properties.Name -notcontains 'RecoveryPartitionDriveLetter') { WriteLog 'RecoveryPartitionDriveLetter missing in cached config, continuing'; continue }
+ if ($CreateRecoveryPartition -and $vhdxCacheItem.PSObject.Properties.Name -notcontains 'RecoveryPartitionDriveLetter') { WriteLog 'RecoveryPartitionDriveLetter missing in cached config, continuing'; continue }
$cachedSystemPartitionDriveLetter = Get-PartitionDriveLetterCacheValue -DriveLetterValue $vhdxCacheItem.SystemPartitionDriveLetter
$cachedWindowsPartitionDriveLetter = Get-PartitionDriveLetterCacheValue -DriveLetterValue $vhdxCacheItem.WindowsPartitionDriveLetter
- $cachedRecoveryPartitionDriveLetter = Get-PartitionDriveLetterCacheValue -DriveLetterValue $vhdxCacheItem.RecoveryPartitionDriveLetter
if ($cachedSystemPartitionDriveLetter -ne $SystemPartitionDriveLetter) { WriteLog "SystemPartitionDriveLetter mismatch (cached: $($vhdxCacheItem.SystemPartitionDriveLetter), current: $SystemPartitionDriveLetter), continuing"; continue }
if ($cachedWindowsPartitionDriveLetter -ne $WindowsPartitionDriveLetter) { WriteLog "WindowsPartitionDriveLetter mismatch (cached: $($vhdxCacheItem.WindowsPartitionDriveLetter), current: $WindowsPartitionDriveLetter), continuing"; continue }
- if ($cachedRecoveryPartitionDriveLetter -ne $RecoveryPartitionDriveLetter) { WriteLog "RecoveryPartitionDriveLetter mismatch (cached: $($vhdxCacheItem.RecoveryPartitionDriveLetter), current: $RecoveryPartitionDriveLetter), continuing"; continue }
+ if ($CreateRecoveryPartition) {
+ $cachedRecoveryPartitionDriveLetter = Get-PartitionDriveLetterCacheValue -DriveLetterValue $vhdxCacheItem.RecoveryPartitionDriveLetter
+ if ($cachedRecoveryPartitionDriveLetter -ne $RecoveryPartitionDriveLetter) { WriteLog "RecoveryPartitionDriveLetter mismatch (cached: $($vhdxCacheItem.RecoveryPartitionDriveLetter), current: $RecoveryPartitionDriveLetter), continuing"; continue }
+ }
if ($vhdxCacheItem.PSObject.Properties.Name -notcontains 'PartitionLayoutSignature') { WriteLog 'PartitionLayoutSignature missing in cached config, continuing'; continue }
if ($vhdxCacheItem.PartitionLayoutSignature -ne $partitionLayoutSignature) { WriteLog 'PartitionLayoutSignature mismatch, continuing'; continue }
@@ -7823,7 +7899,12 @@ try {
$osPartitionDriveLetter = $osPartition[1].DriveLetter
$WindowsPartition = $osPartitionDriveLetter + ':\'
- $recoveryPartition = New-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition[1] -RecoveryPartitionSize $RecoveryPartitionSize -DriveLetter $RecoveryPartitionDriveLetter -OsPartitionUsesMaximumSize ($OSPartitionSize -le 0)
+ if ($CreateRecoveryPartition) {
+ $recoveryPartition = New-RecoveryPartition -VhdxDisk $vhdxDisk -OsPartition $osPartition[1] -RecoveryPartitionSize $RecoveryPartitionSize -DriveLetter $RecoveryPartitionDriveLetter -OsPartitionUsesMaximumSize ($OSPartitionSize -le 0)
+ }
+ else {
+ WriteLog 'CreateRecoveryPartition is false. Skipping Windows Recovery partition creation.'
+ }
foreach ($additionalDataPartition in $normalizedAdditionalDataPartitions) {
New-DataPartition -VhdxDisk $vhdxDisk -DataPartition $additionalDataPartition | Out-Null
@@ -8002,6 +8083,7 @@ try {
$cachedVHDXInfo.VhdxFileName = $("$VMName.vhdx")
$cachedVHDXInfo.LogicalSectorSizeBytes = $LogicalSectorSizeBytes
$cachedVHDXInfo.Disksize = $Disksize
+ $cachedVHDXInfo.CreateRecoveryPartition = $CreateRecoveryPartition
$cachedVHDXInfo.SystemPartitionDriveLetter = [string]$SystemPartitionDriveLetter
$cachedVHDXInfo.WindowsPartitionDriveLetter = [string]$WindowsPartitionDriveLetter
$cachedVHDXInfo.RecoveryPartitionDriveLetter = [string]$RecoveryPartitionDriveLetter
diff --git a/FFUDevelopment/BuildFFUVM_UI.ps1 b/FFUDevelopment/BuildFFUVM_UI.ps1
index 7487e39..69cfb3e 100644
--- a/FFUDevelopment/BuildFFUVM_UI.ps1
+++ b/FFUDevelopment/BuildFFUVM_UI.ps1
@@ -48,7 +48,8 @@ $script:uiState = [PSCustomObject]@{
pollTimer = $null;
currentBuildProcess = $null;
lastConfigFilePath = $null;
- loadedDeviceNamingMode = $null
+ loadedDeviceNamingMode = $null;
+ createRecoveryPartition = $true
};
Flags = @{
installAppsForcedByUpdates = $false;
@@ -426,6 +427,12 @@ $script:uiState.Controls.btnRun.Add_Click({
# Gather config on the UI thread before starting the job
$config = Get-UIConfig -State $script:uiState
+ if (-not (Test-DiskLayoutConfiguration -State $script:uiState -Config $config)) {
+ $btnRun.IsEnabled = $true
+ $script:uiState.Controls.txtStatus.Text = "Build canceled: disk layout validation failed."
+ return
+ }
+
# Validate Additional FFU selection if enabled
if ($config.BuildUSBDrive -and $config.CopyAdditionalFFUFiles -and (($null -eq $config.AdditionalFFUFiles) -or ($config.AdditionalFFUFiles.Count -eq 0))) {
[System.Windows.MessageBox]::Show("Please select at least one additional FFU file to copy, or uncheck 'Copy Additional FFU Files'.", "Selection Required", "OK", "Warning") | Out-Null
diff --git a/FFUDevelopment/BuildFFUVM_UI.xaml b/FFUDevelopment/BuildFFUVM_UI.xaml
index 1353a0f..efdc2d2 100644
--- a/FFUDevelopment/BuildFFUVM_UI.xaml
+++ b/FFUDevelopment/BuildFFUVM_UI.xaml
@@ -324,9 +324,6 @@
-
-
-
@@ -339,110 +336,35 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
@@ -453,6 +375,7 @@
+
@@ -478,24 +401,28 @@
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
index b968e82..a4f01ff 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Config.psm1
@@ -9,6 +9,8 @@ function Get-UIConfig {
[Parameter(Mandatory = $true)]
[psobject]$State
)
+ Sync-DiskLayoutRowsToControls -State $State
+
# Create hash to store configuration
$config = [ordered]@{
AllowExternalHardDiskMedia = $State.Controls.chkAllowExternalHardDiskMedia.IsChecked
@@ -51,6 +53,7 @@ function Get-UIConfig {
Disksize = [int64]$State.Controls.txtDiskSize.Text * 1GB
OSPartitionSize = ConvertTo-PartitionSizeBytesFromGBText -Text $State.Controls.txtOSPartitionSizeGB.Text -FieldName 'Windows Partition Size'
RecoveryPartitionSize = ConvertTo-PartitionSizeBytesFromGBText -Text $State.Controls.txtRecoveryPartitionSizeGB.Text -FieldName 'Recovery Partition Size'
+ CreateRecoveryPartition = [bool]$State.Data.createRecoveryPartition
AdditionalDataPartitions = @(Get-AdditionalDataPartitionConfigRows -State $State)
DownloadDrivers = $State.Controls.chkDownloadDrivers.IsChecked
DriversFolder = $State.Controls.txtDriversFolder.Text
@@ -337,6 +340,174 @@ function Get-PartitionSizeGBDisplay {
return $sizeGb.ToString('0.##', [System.Globalization.CultureInfo]::InvariantCulture)
}
+function Get-PartitionDriveLetterOptions {
+ return @('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z')
+}
+
+function New-DiskLayoutPartitionRow {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$PartitionType,
+ [Parameter(Mandatory = $true)]
+ [string]$Name,
+ [string]$Label,
+ [string]$DriveLetter,
+ [string]$SizeGB,
+ [int64]$SizeBytes = 0,
+ [bool]$FillRemaining,
+ [string]$FileSystem = 'NTFS',
+ [bool]$CanSelect,
+ [bool]$CanEditDriveLetter,
+ [bool]$CanEditSize,
+ [bool]$CanEditFillRemaining,
+ [string]$FillRemainingVisibility = 'Visible',
+ [bool]$CanRemove,
+ [bool]$CanReorder
+ )
+
+ if ([string]::IsNullOrWhiteSpace($Label)) {
+ $Label = $Name
+ }
+
+ return [PSCustomObject]@{
+ IsSelected = $false
+ CanSelect = $CanSelect
+ PartitionType = $PartitionType
+ Name = $Name
+ Label = $Label
+ DriveLetter = $DriveLetter
+ DriveLetterOptions = @(Get-PartitionDriveLetterOptions)
+ SizeGB = $SizeGB
+ SizeBytes = $SizeBytes
+ FillRemaining = $FillRemaining
+ FillRemainingVisibility = $FillRemainingVisibility
+ FileSystem = $FileSystem
+ CanEditDriveLetter = $CanEditDriveLetter
+ CanEditSize = $CanEditSize
+ CanEditFillRemaining = $CanEditFillRemaining
+ CanRemove = $CanRemove
+ CanReorder = $CanReorder
+ }
+}
+
+function Get-ComboBoxDriveLetterValue {
+ param(
+ [object]$ComboBox,
+ [string]$DefaultValue
+ )
+
+ $driveLetter = [string](Get-ComboBoxSelectedContent -ComboBox $ComboBox)
+ if ([string]::IsNullOrWhiteSpace($driveLetter)) {
+ $driveLetter = $DefaultValue
+ }
+
+ return $driveLetter.Trim().TrimEnd(':').ToUpperInvariant()
+}
+
+function Get-DiskLayoutPartitionRows {
+ param(
+ [Parameter(Mandatory = $true)]
+ [psobject]$State
+ )
+
+ if ($null -eq $State.Data.additionalDataPartitionsDataList) {
+ $State.Data.additionalDataPartitionsDataList = [System.Collections.Generic.List[PSCustomObject]]::new()
+ }
+ if ($null -eq $State.Data.createRecoveryPartition) {
+ $State.Data.createRecoveryPartition = $true
+ }
+
+ $hasDataPartitions = @($State.Data.additionalDataPartitionsDataList).Count -gt 0
+ $windowsSizeText = [string]$State.Controls.txtOSPartitionSizeGB.Text
+ $windowsCanFillRemaining = -not $hasDataPartitions
+ $windowsFillRemaining = $windowsCanFillRemaining -and [string]::IsNullOrWhiteSpace($windowsSizeText)
+ $windowsFillRemainingVisibility = if ($windowsCanFillRemaining) { 'Visible' } else { 'Hidden' }
+
+ $rows = [System.Collections.Generic.List[PSCustomObject]]::new()
+ $rows.Add((New-DiskLayoutPartitionRow -PartitionType 'System' -Name 'System' -DriveLetter (Get-ComboBoxDriveLetterValue -ComboBox $State.Controls.cmbSystemPartitionDriveLetter -DefaultValue 'S') -SizeGB '0.26' -SizeBytes 260MB -CanEditDriveLetter $true -CanEditSize $false -CanEditFillRemaining $false -FillRemainingVisibility 'Hidden' -CanRemove $false -CanReorder $false))
+ $rows.Add((New-DiskLayoutPartitionRow -PartitionType 'MSR' -Name 'MSR' -SizeGB '0.016' -SizeBytes 16MB -CanEditDriveLetter $false -CanEditSize $false -CanEditFillRemaining $false -FillRemainingVisibility 'Hidden' -CanRemove $false -CanReorder $false))
+ $rows.Add((New-DiskLayoutPartitionRow -PartitionType 'Windows' -Name 'Windows' -DriveLetter (Get-ComboBoxDriveLetterValue -ComboBox $State.Controls.cmbWindowsPartitionDriveLetter -DefaultValue 'W') -SizeGB $windowsSizeText -SizeBytes (ConvertTo-PartitionSizeBytesFromGBText -Text $windowsSizeText -FieldName 'Windows Partition Size') -FillRemaining $windowsFillRemaining -CanEditDriveLetter $true -CanEditSize $true -CanEditFillRemaining $windowsCanFillRemaining -FillRemainingVisibility $windowsFillRemainingVisibility -CanRemove $false -CanReorder $false))
+
+ if ([bool]$State.Data.createRecoveryPartition) {
+ $rows.Add((New-DiskLayoutPartitionRow -PartitionType 'Recovery' -Name 'Recovery' -DriveLetter (Get-ComboBoxDriveLetterValue -ComboBox $State.Controls.cmbRecoveryPartitionDriveLetter -DefaultValue 'R') -SizeGB ([string]$State.Controls.txtRecoveryPartitionSizeGB.Text) -SizeBytes (ConvertTo-PartitionSizeBytesFromGBText -Text $State.Controls.txtRecoveryPartitionSizeGB.Text -FieldName 'Recovery Partition Size') -CanSelect $true -CanEditDriveLetter $true -CanEditSize $true -CanEditFillRemaining $false -FillRemainingVisibility 'Hidden' -CanRemove $true -CanReorder $false))
+ }
+
+ foreach ($dataPartition in @($State.Data.additionalDataPartitionsDataList)) {
+ $rows.Add((New-DiskLayoutPartitionRow -PartitionType 'Data' -Name $dataPartition.Name -Label $dataPartition.Label -DriveLetter $dataPartition.DriveLetter -SizeGB $dataPartition.SizeGB -SizeBytes ([int64]$dataPartition.SizeBytes) -FillRemaining ([bool]$dataPartition.FillRemaining) -FileSystem $dataPartition.FileSystem -CanSelect $true -CanEditDriveLetter $true -CanEditSize $true -CanEditFillRemaining $true -CanRemove $true -CanReorder $true))
+ }
+
+ return $rows.ToArray()
+}
+
+function Sync-DiskLayoutRowsToControls {
+ param(
+ [Parameter(Mandatory = $true)]
+ [psobject]$State
+ )
+
+ if ($null -eq $State.Controls.lstDataPartitions) { return }
+ if ($null -eq $State.Data.createRecoveryPartition) {
+ $State.Data.createRecoveryPartition = $true
+ }
+
+ $layoutRows = @($State.Controls.lstDataPartitions.ItemsSource)
+ if ($layoutRows.Count -eq 0) { return }
+
+ $State.Data.createRecoveryPartition = [bool](@($layoutRows | Where-Object { $_.PartitionType -eq 'Recovery' }).Count -gt 0)
+ $updatedDataPartitions = [System.Collections.Generic.List[PSCustomObject]]::new()
+
+ foreach ($row in $layoutRows) {
+ switch ($row.PartitionType) {
+ 'System' {
+ $row.DriveLetter = ([string]$row.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant()
+ $State.Controls.cmbSystemPartitionDriveLetter.SelectedItem = ($State.Controls.cmbSystemPartitionDriveLetter.Items | Where-Object { $_.Content -eq $row.DriveLetter } | Select-Object -First 1)
+ }
+ 'Windows' {
+ $row.DriveLetter = ([string]$row.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant()
+ $State.Controls.cmbWindowsPartitionDriveLetter.SelectedItem = ($State.Controls.cmbWindowsPartitionDriveLetter.Items | Where-Object { $_.Content -eq $row.DriveLetter } | Select-Object -First 1)
+ if ($row.FillRemaining) {
+ $State.Controls.txtOSPartitionSizeGB.Clear()
+ }
+ else {
+ $State.Controls.txtOSPartitionSizeGB.Text = [string]$row.SizeGB
+ }
+ }
+ 'Recovery' {
+ $row.DriveLetter = ([string]$row.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant()
+ $State.Controls.cmbRecoveryPartitionDriveLetter.SelectedItem = ($State.Controls.cmbRecoveryPartitionDriveLetter.Items | Where-Object { $_.Content -eq $row.DriveLetter } | Select-Object -First 1)
+ $State.Controls.txtRecoveryPartitionSizeGB.Text = [string]$row.SizeGB
+ }
+ 'Data' {
+ $row.DriveLetter = ([string]$row.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant()
+ if ($row.FillRemaining) {
+ $row.SizeGB = ''
+ $row.SizeBytes = 0
+ }
+ else {
+ $row.SizeBytes = ConvertTo-PartitionSizeBytesFromGBText -Text $row.SizeGB -FieldName "Data Partition '$($row.Name)' Size"
+ }
+ $updatedDataPartitions.Add([PSCustomObject]@{
+ Name = $row.Name
+ Label = $row.Label
+ DriveLetter = $row.DriveLetter
+ SizeGB = $row.SizeGB
+ SizeBytes = [int64]$row.SizeBytes
+ FillRemaining = [bool]$row.FillRemaining
+ FileSystem = $row.FileSystem
+ })
+ }
+ }
+ }
+
+ if ($null -eq $State.Data.additionalDataPartitionsDataList) {
+ $State.Data.additionalDataPartitionsDataList = [System.Collections.Generic.List[PSCustomObject]]::new()
+ }
+ $State.Data.additionalDataPartitionsDataList.Clear()
+ foreach ($dataPartition in $updatedDataPartitions) {
+ $State.Data.additionalDataPartitionsDataList.Add($dataPartition)
+ }
+}
+
function Update-AdditionalDataPartitionsListView {
param(
[Parameter(Mandatory = $true)]
@@ -348,12 +519,361 @@ function Update-AdditionalDataPartitionsListView {
}
if ($null -ne $State.Controls.lstDataPartitions) {
- $State.Controls.lstDataPartitions.ItemsSource = $State.Data.additionalDataPartitionsDataList.ToArray()
+ $State.Controls.lstDataPartitions.ItemsSource = @(Get-DiskLayoutPartitionRows -State $State)
$State.Controls.lstDataPartitions.Items.Refresh()
Request-ListViewColumnAutoResize -ListView $State.Controls.lstDataPartitions
+ if ($null -ne $State.Controls.chkSelectAllDataPartitions) {
+ Update-SelectAllHeaderCheckBoxState -ListView $State.Controls.lstDataPartitions -HeaderCheckBox $State.Controls.chkSelectAllDataPartitions
+ }
+ Update-DiskLayoutActionButtonsState -State $State
+ Update-DiskLayoutCapacityStatus -State $State
}
}
+function Get-DiskLayoutSizeDisplay {
+ param(
+ [int64]$SizeBytes
+ )
+
+ $sizeGb = [decimal]$SizeBytes / 1GB
+ return $sizeGb.ToString('0.##', [System.Globalization.CultureInfo]::InvariantCulture)
+}
+
+function Get-DiskLayoutCapacityStatus {
+ param(
+ [Parameter(Mandatory = $true)]
+ [psobject]$State
+ )
+
+ try {
+ $diskSizeBytes = ConvertTo-PartitionSizeBytesFromGBText -Text $State.Controls.txtDiskSize.Text -FieldName 'Disk Size'
+ }
+ catch {
+ return [PSCustomObject]@{ Level = 'Red'; Message = $_.Exception.Message }
+ }
+
+ if ($diskSizeBytes -le 0) {
+ return [PSCustomObject]@{ Level = 'Red'; Message = 'Disk Size must be greater than 0 GB.' }
+ }
+
+ $partitionRows = @($State.Controls.lstDataPartitions.ItemsSource)
+ if ($partitionRows.Count -eq 0) {
+ $partitionRows = @(Get-DiskLayoutPartitionRows -State $State)
+ }
+
+ $fixedPartitionSizeBytes = [int64]0
+ $fillRemainingPartitionNames = [System.Collections.Generic.List[string]]::new()
+ $hasAutoSizedRecovery = $false
+ $dataPartitionCount = @($partitionRows | Where-Object { $_.PartitionType -eq 'Data' }).Count
+
+ foreach ($partitionRow in $partitionRows) {
+ if ($partitionRow.PartitionType -eq 'Recovery' -and -not [bool]$State.Data.createRecoveryPartition) { continue }
+
+ if ($partitionRow.FillRemaining) {
+ $fillRemainingPartitionNames.Add([string]$partitionRow.Name)
+ continue
+ }
+
+ if ($partitionRow.PartitionType -eq 'Windows' -and $dataPartitionCount -gt 0 -and [string]::IsNullOrWhiteSpace([string]$partitionRow.SizeGB)) {
+ return [PSCustomObject]@{ Level = 'Red'; Message = 'Set a fixed Windows partition size before adding data partitions.' }
+ }
+
+ if ($partitionRow.PartitionType -eq 'Recovery' -and [string]::IsNullOrWhiteSpace([string]$partitionRow.SizeGB)) {
+ $hasAutoSizedRecovery = $true
+ continue
+ }
+
+ if ($partitionRow.PartitionType -eq 'Data' -and [string]::IsNullOrWhiteSpace([string]$partitionRow.SizeGB)) {
+ return [PSCustomObject]@{ Level = 'Red'; Message = "Data partition '$($partitionRow.Name)' must have a size or Fill Remaining selected." }
+ }
+
+ if (-not [bool]$partitionRow.CanEditSize -and [int64]$partitionRow.SizeBytes -gt 0) {
+ $fixedPartitionSizeBytes += [int64]$partitionRow.SizeBytes
+ continue
+ }
+
+ try {
+ $partitionSizeBytes = ConvertTo-PartitionSizeBytesFromGBText -Text $partitionRow.SizeGB -FieldName "$($partitionRow.Name) Partition Size"
+ }
+ catch {
+ return [PSCustomObject]@{ Level = 'Red'; Message = $_.Exception.Message }
+ }
+
+ if ($partitionSizeBytes -gt 0) {
+ $fixedPartitionSizeBytes += $partitionSizeBytes
+ }
+ }
+
+ if ($fillRemainingPartitionNames.Count -gt 1) {
+ return [PSCustomObject]@{ Level = 'Red'; Message = 'Only one partition can fill remaining disk space.' }
+ }
+
+ if ($fixedPartitionSizeBytes -gt $diskSizeBytes) {
+ $overByBytes = $fixedPartitionSizeBytes - $diskSizeBytes
+ return [PSCustomObject]@{ Level = 'Red'; Message = "Partition sizes exceed Disk Size by $(Get-DiskLayoutSizeDisplay -SizeBytes $overByBytes) GB. Fixed total: $(Get-DiskLayoutSizeDisplay -SizeBytes $fixedPartitionSizeBytes) GB of $(Get-DiskLayoutSizeDisplay -SizeBytes $diskSizeBytes) GB." }
+ }
+
+ $remainingBytes = $diskSizeBytes - $fixedPartitionSizeBytes
+ $remainingPurpose = if ($fillRemainingPartitionNames.Count -gt 0 -and $hasAutoSizedRecovery) {
+ "remaining for $($fillRemainingPartitionNames -join ', ') and the auto-sized Recovery partition"
+ }
+ elseif ($fillRemainingPartitionNames.Count -gt 0) {
+ "remaining for $($fillRemainingPartitionNames -join ', ')"
+ }
+ elseif ($hasAutoSizedRecovery) {
+ 'remaining for the auto-sized Recovery partition and unallocated space'
+ }
+ else {
+ 'left unallocated'
+ }
+
+ return [PSCustomObject]@{ Level = 'Green'; Message = "Partition sizes fit within Disk Size. $(Get-DiskLayoutSizeDisplay -SizeBytes $remainingBytes) GB $remainingPurpose." }
+}
+
+function Update-DiskLayoutCapacityStatus {
+ param(
+ [Parameter(Mandatory = $true)]
+ [psobject]$State
+ )
+
+ if ($null -eq $State.Controls.ellipseDiskLayoutCapacityStatus -or $null -eq $State.Controls.txtDiskLayoutCapacityStatusValue) { return }
+ if ($State.Flags -is [System.Collections.IDictionary] -and $true -eq $State.Flags['updatingDiskLayoutCapacityStatus']) { return }
+
+ try {
+ if ($State.Flags -is [System.Collections.IDictionary]) {
+ $State.Flags['updatingDiskLayoutCapacityStatus'] = $true
+ }
+
+ $capacityStatus = Get-DiskLayoutCapacityStatus -State $State
+ $State.Controls.ellipseDiskLayoutCapacityStatus.Fill = switch ($capacityStatus.Level) {
+ 'Green' { [System.Windows.Media.Brushes]::LimeGreen }
+ 'Red' { [System.Windows.Media.Brushes]::IndianRed }
+ default { [System.Windows.Media.Brushes]::Gold }
+ }
+ $State.Controls.txtDiskLayoutCapacityStatusValue.Text = $capacityStatus.Message
+ }
+ finally {
+ if ($State.Flags -is [System.Collections.IDictionary]) {
+ $State.Flags['updatingDiskLayoutCapacityStatus'] = $false
+ }
+ }
+}
+
+function Update-DiskLayoutFillRemainingState {
+ param(
+ [Parameter(Mandatory = $true)]
+ [psobject]$State,
+ [Parameter(Mandatory = $true)]
+ [object]$PartitionRow
+ )
+
+ if ($PartitionRow.FillRemaining) {
+ $PartitionRow.SizeGB = ''
+ $PartitionRow.SizeBytes = 0
+ if ($PartitionRow.PartitionType -eq 'Windows') {
+ $State.Controls.txtOSPartitionSizeGB.Clear()
+ }
+ }
+
+ if ($null -ne $State.Controls.lstDataPartitions) {
+ $State.Controls.lstDataPartitions.Items.Refresh()
+ }
+ Update-DiskLayoutCapacityStatus -State $State
+}
+
+function Update-DiskLayoutActionButtonsState {
+ param(
+ [Parameter(Mandatory = $true)]
+ [psobject]$State
+ )
+
+ if ($null -eq $State.Controls.lstDataPartitions) { return }
+
+ $selectedItem = $State.Controls.lstDataPartitions.SelectedItem
+ $selectedDataRows = @($State.Controls.lstDataPartitions.Items | Where-Object { $_.PartitionType -eq 'Data' })
+ $selectedDataIndex = -1
+ for ($dataIndex = 0; $dataIndex -lt $selectedDataRows.Count; $dataIndex++) {
+ if ([object]::ReferenceEquals($selectedDataRows[$dataIndex], $selectedItem)) {
+ $selectedDataIndex = $dataIndex
+ break
+ }
+ }
+
+ $canMove = $selectedDataIndex -ge 0
+ if ($null -ne $State.Controls.btnMoveDataPartitionTop) { $State.Controls.btnMoveDataPartitionTop.IsEnabled = $canMove -and $selectedDataIndex -gt 0 }
+ if ($null -ne $State.Controls.btnMoveDataPartitionUp) { $State.Controls.btnMoveDataPartitionUp.IsEnabled = $canMove -and $selectedDataIndex -gt 0 }
+ if ($null -ne $State.Controls.btnMoveDataPartitionDown) { $State.Controls.btnMoveDataPartitionDown.IsEnabled = $canMove -and $selectedDataIndex -lt ($selectedDataRows.Count - 1) }
+ if ($null -ne $State.Controls.btnMoveDataPartitionBottom) { $State.Controls.btnMoveDataPartitionBottom.IsEnabled = $canMove -and $selectedDataIndex -lt ($selectedDataRows.Count - 1) }
+
+ if ($null -ne $State.Controls.btnRestoreRecoveryPartition) {
+ $State.Controls.btnRestoreRecoveryPartition.Visibility = if ([bool]$State.Data.createRecoveryPartition) { 'Collapsed' } else { 'Visible' }
+ $State.Controls.btnRestoreRecoveryPartition.IsEnabled = -not [bool]$State.Data.createRecoveryPartition
+ }
+}
+
+function Move-DataPartitionRow {
+ param(
+ [Parameter(Mandatory = $true)]
+ [psobject]$State,
+ [Parameter(Mandatory = $true)]
+ [ValidateSet('Top', 'Up', 'Down', 'Bottom')]
+ [string]$Direction
+ )
+
+ $selectedItem = $State.Controls.lstDataPartitions.SelectedItem
+ if ($null -eq $selectedItem -or $selectedItem.PartitionType -ne 'Data') { return }
+
+ Sync-DiskLayoutRowsToControls -State $State
+ $dataRows = @($State.Controls.lstDataPartitions.Items | Where-Object { $_.PartitionType -eq 'Data' })
+ $currentIndex = -1
+ for ($dataIndex = 0; $dataIndex -lt $dataRows.Count; $dataIndex++) {
+ if ([object]::ReferenceEquals($dataRows[$dataIndex], $selectedItem)) {
+ $currentIndex = $dataIndex
+ break
+ }
+ }
+
+ if ($currentIndex -lt 0) { return }
+
+ $targetIndex = switch ($Direction) {
+ 'Top' { 0 }
+ 'Up' { [Math]::Max(0, $currentIndex - 1) }
+ 'Down' { [Math]::Min($State.Data.additionalDataPartitionsDataList.Count - 1, $currentIndex + 1) }
+ 'Bottom' { $State.Data.additionalDataPartitionsDataList.Count - 1 }
+ }
+
+ if ($targetIndex -eq $currentIndex) { return }
+
+ $movingItem = $State.Data.additionalDataPartitionsDataList[$currentIndex]
+ $State.Data.additionalDataPartitionsDataList.RemoveAt($currentIndex)
+ $State.Data.additionalDataPartitionsDataList.Insert($targetIndex, $movingItem)
+ Update-AdditionalDataPartitionsListView -State $State
+
+ $updatedDataRows = @($State.Controls.lstDataPartitions.Items | Where-Object { $_.PartitionType -eq 'Data' })
+ if ($targetIndex -ge 0 -and $targetIndex -lt $updatedDataRows.Count) {
+ $State.Controls.lstDataPartitions.SelectedItem = $updatedDataRows[$targetIndex]
+ }
+
+ Update-DiskLayoutActionButtonsState -State $State
+}
+
+function Clear-AdditionalDataPartitions {
+ param(
+ [Parameter(Mandatory = $true)]
+ [psobject]$State
+ )
+
+ $result = [System.Windows.MessageBox]::Show("Are you sure you want to clear all additional data partitions?", "Clear Data Partitions", [System.Windows.MessageBoxButton]::YesNo, [System.Windows.MessageBoxImage]::Question)
+ if ($result -ne [System.Windows.MessageBoxResult]::Yes) { return }
+
+ if ($null -eq $State.Data.additionalDataPartitionsDataList) {
+ $State.Data.additionalDataPartitionsDataList = [System.Collections.Generic.List[PSCustomObject]]::new()
+ }
+ $State.Data.additionalDataPartitionsDataList.Clear()
+ Clear-AdditionalDataPartitionForm -State $State
+ Update-AdditionalDataPartitionsListView -State $State
+
+ if ($null -ne $State.Controls.txtStatus) {
+ $State.Controls.txtStatus.Text = "Additional data partitions list cleared."
+ }
+}
+
+function Restore-RecoveryPartition {
+ param(
+ [Parameter(Mandatory = $true)]
+ [psobject]$State
+ )
+
+ $State.Data.createRecoveryPartition = $true
+ Update-AdditionalDataPartitionsListView -State $State
+}
+
+function Test-DiskLayoutConfiguration {
+ param(
+ [Parameter(Mandatory = $true)]
+ [psobject]$State,
+ [Parameter(Mandatory = $true)]
+ [System.Collections.IDictionary]$Config
+ )
+
+ $errors = [System.Collections.Generic.List[string]]::new()
+ $driveLetterEntries = [System.Collections.Generic.List[PSCustomObject]]::new()
+
+ $partitionRows = @($State.Controls.lstDataPartitions.ItemsSource)
+ foreach ($partitionRow in $partitionRows) {
+ if ($partitionRow.PartitionType -eq 'MSR') { continue }
+ if ($partitionRow.PartitionType -eq 'Recovery' -and -not [bool]$Config.CreateRecoveryPartition) { continue }
+
+ $driveLetter = ([string]$partitionRow.DriveLetter).Trim().TrimEnd(':').ToUpperInvariant()
+ if ([string]::IsNullOrWhiteSpace($driveLetter) -or $driveLetter -notmatch '^[A-Z]$') {
+ $errors.Add("$($partitionRow.Name) must use a single drive letter from A to Z.")
+ continue
+ }
+
+ $driveLetterEntries.Add([PSCustomObject]@{
+ Name = $partitionRow.Name
+ DriveLetter = $driveLetter
+ })
+ }
+
+ $duplicateDriveLetters = @($driveLetterEntries | Group-Object -Property DriveLetter | Where-Object { $_.Count -gt 1 })
+ foreach ($duplicateDriveLetter in $duplicateDriveLetters) {
+ $partitionNames = ($duplicateDriveLetter.Group | ForEach-Object { $_.Name }) -join ', '
+ $errors.Add("Drive letter $($duplicateDriveLetter.Name) is used by multiple partitions: $partitionNames.")
+ }
+
+ $dataPartitions = @($Config.AdditionalDataPartitions)
+ if ($dataPartitions.Count -gt 0 -and $Config.OSPartitionSize -le 0) {
+ $errors.Add('Windows must use a fixed size when data partitions are configured.')
+ }
+
+ $fillRemainingCount = 0
+ if ($Config.OSPartitionSize -le 0) {
+ $fillRemainingCount++
+ }
+ $fillRemainingCount += @($dataPartitions | Where-Object { $_.FillRemaining }).Count
+ if ($fillRemainingCount -gt 1) {
+ $errors.Add('Only one partition can fill remaining disk space.')
+ }
+
+ $duplicateDataNames = @($dataPartitions | Group-Object -Property Name | Where-Object { $_.Count -gt 1 })
+ foreach ($duplicateDataName in $duplicateDataNames) {
+ $errors.Add("Data partition name '$($duplicateDataName.Name)' is used more than once.")
+ }
+
+ [uint64]$fixedPartitionSizeBytes = 260MB + 16MB
+ if ($Config.OSPartitionSize -gt 0) {
+ $fixedPartitionSizeBytes += [uint64]$Config.OSPartitionSize
+ }
+ if ([bool]$Config.CreateRecoveryPartition -and $Config.RecoveryPartitionSize -gt 0) {
+ $fixedPartitionSizeBytes += [uint64]$Config.RecoveryPartitionSize
+ }
+ foreach ($dataPartition in $dataPartitions) {
+ if ($dataPartition.FillRemaining) { continue }
+ if ([uint64]$dataPartition.SizeBytes -le 0) {
+ $errors.Add("Data partition '$($dataPartition.Name)' must have a size or Fill Remaining selected.")
+ continue
+ }
+ $fixedPartitionSizeBytes += [uint64]$dataPartition.SizeBytes
+ }
+
+ if ([uint64]$Config.Disksize -le 0) {
+ $errors.Add('Disk Size must be greater than 0 GB.')
+ }
+ elseif ($fixedPartitionSizeBytes -gt [uint64]$Config.Disksize) {
+ $fixedPartitionSizeGB = [Math]::Round(($fixedPartitionSizeBytes / 1GB), 2)
+ $diskSizeGB = [Math]::Round(([uint64]$Config.Disksize / 1GB), 2)
+ $errors.Add("Fixed partition sizes total $fixedPartitionSizeGB GB, which exceeds the configured disk size of $diskSizeGB GB.")
+ }
+
+ if ($errors.Count -gt 0) {
+ [System.Windows.MessageBox]::Show(($errors -join [System.Environment]::NewLine), "Disk Layout Validation", "OK", "Warning") | Out-Null
+ return $false
+ }
+
+ return $true
+}
+
function Clear-AdditionalDataPartitionForm {
param(
[Parameter(Mandatory = $true)]
@@ -372,6 +892,14 @@ function Add-AdditionalDataPartition {
[psobject]$State
)
+ try {
+ Sync-DiskLayoutRowsToControls -State $State
+ }
+ catch {
+ [System.Windows.MessageBox]::Show($_.Exception.Message, "Disk Layout", "OK", "Warning") | Out-Null
+ return $false
+ }
+
if ($null -eq $State.Data.additionalDataPartitionsDataList) {
$State.Data.additionalDataPartitionsDataList = [System.Collections.Generic.List[PSCustomObject]]::new()
}
@@ -395,8 +923,15 @@ function Add-AdditionalDataPartition {
$reservedDriveLetters = @(
Get-ComboBoxSelectedContent -ComboBox $State.Controls.cmbSystemPartitionDriveLetter
Get-ComboBoxSelectedContent -ComboBox $State.Controls.cmbWindowsPartitionDriveLetter
- Get-ComboBoxSelectedContent -ComboBox $State.Controls.cmbRecoveryPartitionDriveLetter
) | ForEach-Object { ([string]$_).Trim().TrimEnd(':').ToUpperInvariant() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
+ if ([bool]$State.Data.createRecoveryPartition) {
+ $reservedDriveLetters += ([string](Get-ComboBoxSelectedContent -ComboBox $State.Controls.cmbRecoveryPartitionDriveLetter)).Trim().TrimEnd(':').ToUpperInvariant()
+ }
+
+ if ([string]::IsNullOrWhiteSpace([string]$State.Controls.txtOSPartitionSizeGB.Text)) {
+ [System.Windows.MessageBox]::Show("Set a fixed Windows partition size before adding data partitions. Windows can fill remaining space only when no data partitions are configured.", "Windows Partition Size Required", "OK", "Warning") | Out-Null
+ return $false
+ }
if ($reservedDriveLetters -contains $driveLetter) {
[System.Windows.MessageBox]::Show("Drive letter $driveLetter is already used by a required build partition.", "Duplicate Drive Letter", "OK", "Warning") | Out-Null
@@ -474,14 +1009,22 @@ function Remove-SelectedDataPartition {
[psobject]$State
)
- $itemsToRemove = @($State.Controls.lstDataPartitions.SelectedItems)
+ $itemsToRemove = @($State.Controls.lstDataPartitions.Items | Where-Object { $_.IsSelected -and $_.CanRemove })
if ($itemsToRemove.Count -eq 0) {
- [System.Windows.MessageBox]::Show("Please select one or more data partitions to remove.", "Selection Required", "OK", "Warning") | Out-Null
+ [System.Windows.MessageBox]::Show("Select one or more removable partitions to remove.", "Selection Required", "OK", "Warning") | Out-Null
return
}
foreach ($itemToRemove in $itemsToRemove) {
- $State.Data.additionalDataPartitionsDataList.Remove($itemToRemove) | Out-Null
+ if ($itemToRemove.PartitionType -eq 'Recovery') {
+ $State.Data.createRecoveryPartition = $false
+ }
+ elseif ($itemToRemove.PartitionType -eq 'Data') {
+ $dataItem = @($State.Data.additionalDataPartitionsDataList | Where-Object { $_.Name -eq $itemToRemove.Name -and $_.DriveLetter -eq $itemToRemove.DriveLetter } | Select-Object -First 1)
+ if ($dataItem.Count -gt 0) {
+ $State.Data.additionalDataPartitionsDataList.Remove($dataItem[0]) | Out-Null
+ }
+ }
}
Update-AdditionalDataPartitionsListView -State $State
@@ -497,6 +1040,8 @@ function Get-AdditionalDataPartitionConfigRows {
return @()
}
+ Sync-DiskLayoutRowsToControls -State $State
+
return @($State.Data.additionalDataPartitionsDataList | ForEach-Object {
[PSCustomObject]@{
Name = $_.Name
@@ -836,10 +1381,16 @@ function Update-UIFromConfig {
Set-UIValue -ControlName 'txtVMNamePrefix' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'FFUPrefix' -State $State
Set-UIValue -ControlName 'txtOSPartitionSizeGB' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'OSPartitionSize' -TransformValue { param($val) Get-PartitionSizeGBDisplay -SizeBytes $val } -State $State
Set-UIValue -ControlName 'txtRecoveryPartitionSizeGB' -PropertyName 'Text' -ConfigObject $ConfigContent -ConfigKey 'RecoveryPartitionSize' -TransformValue { param($val) Get-PartitionSizeGBDisplay -SizeBytes $val } -State $State
- Import-AdditionalDataPartitionsFromConfig -State $State -ConfigContent $ConfigContent
+ if ($ConfigContent.PSObject.Properties.Name -contains 'CreateRecoveryPartition') {
+ $State.Data.createRecoveryPartition = [System.Convert]::ToBoolean($ConfigContent.CreateRecoveryPartition)
+ }
+ else {
+ $State.Data.createRecoveryPartition = $true
+ }
Set-UIValue -ControlName 'cmbSystemPartitionDriveLetter' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'SystemPartitionDriveLetter' -TransformValue { param($val) ([string]$val).Trim().TrimEnd(':').ToUpperInvariant() } -State $State
Set-UIValue -ControlName 'cmbWindowsPartitionDriveLetter' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'WindowsPartitionDriveLetter' -TransformValue { param($val) ([string]$val).Trim().TrimEnd(':').ToUpperInvariant() } -State $State
Set-UIValue -ControlName 'cmbRecoveryPartitionDriveLetter' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'RecoveryPartitionDriveLetter' -TransformValue { param($val) ([string]$val).Trim().TrimEnd(':').ToUpperInvariant() } -State $State
+ Import-AdditionalDataPartitionsFromConfig -State $State -ConfigContent $ConfigContent
Set-UIValue -ControlName 'cmbLogicalSectorSize' -PropertyName 'SelectedItem' -ConfigObject $ConfigContent -ConfigKey 'LogicalSectorSizeBytes' -TransformValue { param($val) $val.ToString() } -State $State
$State.Controls.spVMNetworkingSettings.IsEnabled = $true -eq $State.Controls.chkEnableVMNetworking.IsChecked
if (-not ($true -eq $State.Controls.chkEnableVMNetworking.IsChecked)) {
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1
index 0e462e7..20c122f 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1
@@ -995,6 +995,19 @@ function Register-EventHandlers {
Update-VMNetworkingControls -State $localState
})
+ $diskLayoutTextChangedHandler = {
+ param($eventSource, $textChangedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ Update-DiskLayoutCapacityStatus -State $localState
+ }
+
+ foreach ($diskLayoutTextBox in @($State.Controls.txtDiskSize, $State.Controls.txtOSPartitionSizeGB, $State.Controls.txtRecoveryPartitionSizeGB)) {
+ if ($null -ne $diskLayoutTextBox) {
+ $diskLayoutTextBox.Add_TextChanged($diskLayoutTextChangedHandler)
+ }
+ }
+
$State.Controls.chkDataPartitionFillRemaining.Add_Checked({
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
@@ -1028,19 +1041,98 @@ function Register-EventHandlers {
param($eventSource, $routedEventArgs)
$window = [System.Windows.Window]::GetWindow($eventSource)
$localState = $window.Tag
-
- Clear-ListViewContent -State $localState `
- -ListViewControl $localState.Controls.lstDataPartitions `
- -BackingDataList $localState.Data.additionalDataPartitionsDataList `
- -ConfirmationTitle "Clear Data Partitions" `
- -ConfirmationMessage "Are you sure you want to clear all additional data partitions?" `
- -StatusMessage "Additional data partitions list cleared." `
- -TextBoxesToClear @($localState.Controls.txtDataPartitionName, $localState.Controls.txtDataPartitionSizeGB)
-
- $localState.Controls.chkDataPartitionFillRemaining.IsChecked = $false
- $localState.Controls.txtDataPartitionSizeGB.IsEnabled = $true
+ Clear-AdditionalDataPartitions -State $localState
})
+ $State.Controls.btnRestoreRecoveryPartition.Add_Click({
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ Restore-RecoveryPartition -State $localState
+ })
+
+ $State.Controls.btnMoveDataPartitionTop.Add_Click({
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ Move-DataPartitionRow -State $localState -Direction Top
+ })
+
+ $State.Controls.btnMoveDataPartitionUp.Add_Click({
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ Move-DataPartitionRow -State $localState -Direction Up
+ })
+
+ $State.Controls.btnMoveDataPartitionDown.Add_Click({
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ Move-DataPartitionRow -State $localState -Direction Down
+ })
+
+ $State.Controls.btnMoveDataPartitionBottom.Add_Click({
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ Move-DataPartitionRow -State $localState -Direction Bottom
+ })
+
+ $State.Controls.lstDataPartitions.Add_PreviewKeyDown({
+ param($eventSource, $keyEvent)
+ if ($keyEvent.Key -eq 'Space') {
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ Invoke-ListViewItemToggle -ListView $eventSource -State $localState -HeaderCheckBoxKeyName 'chkSelectAllDataPartitions'
+ Update-DiskLayoutActionButtonsState -State $localState
+ $keyEvent.Handled = $true
+ }
+ })
+
+ $State.Controls.lstDataPartitions.Add_SelectionChanged({
+ param($eventSource, $selChangeEvent)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ Update-DiskLayoutActionButtonsState -State $localState
+ })
+
+ $State.Controls.lstDataPartitions.AddHandler(
+ [System.Windows.Controls.Primitives.ButtonBase]::ClickEvent,
+ [System.Windows.RoutedEventHandler] {
+ param($eventSource, $routedEventArgs)
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+
+ $originalSource = $routedEventArgs.OriginalSource
+ if ($originalSource -is [System.Windows.Controls.Primitives.ToggleButton] -and -not ($originalSource -is [System.Windows.Controls.CheckBox])) { return }
+
+ $partitionRow = $originalSource.DataContext
+ if ($originalSource -is [System.Windows.Controls.CheckBox] -and [string]$originalSource.Tag -eq 'DiskLayoutFillRemaining' -and $null -ne $partitionRow -and $null -ne $partitionRow.PSObject.Properties['PartitionType']) {
+ Update-DiskLayoutFillRemainingState -State $localState -PartitionRow $partitionRow
+ }
+ else {
+ Update-DiskLayoutCapacityStatus -State $localState
+ }
+ Update-DiskLayoutActionButtonsState -State $localState
+ })
+
+ $State.Controls.lstDataPartitions.AddHandler(
+ [System.Windows.UIElement]::LostKeyboardFocusEvent,
+ [System.Windows.Input.KeyboardFocusChangedEventHandler] {
+ param($eventSource, $focusChangedEventArgs)
+ $textBox = $focusChangedEventArgs.OriginalSource
+ if ($null -eq $textBox -or -not ($textBox -is [System.Windows.Controls.TextBox])) { return }
+
+ $partitionRow = $textBox.DataContext
+ if ($null -eq $partitionRow -or $null -eq $partitionRow.PSObject.Properties['PartitionType']) { return }
+
+ $window = [System.Windows.Window]::GetWindow($eventSource)
+ $localState = $window.Tag
+ Update-DiskLayoutCapacityStatus -State $localState
+ },
+ $true)
+
# Persist custom VM switch name when user edits it while 'Other' is selected
$State.Controls.txtCustomVMSwitchName.Add_LostFocus({
param($eventSource, $routedEventArgs)
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
index 26c70c2..3b7d0e3 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Initialize.psm1
@@ -260,6 +260,8 @@ function Initialize-UIControls {
$State.Controls.cmbRecoveryPartitionDriveLetter = $window.FindName('cmbRecoveryPartitionDriveLetter')
$State.Controls.txtOSPartitionSizeGB = $window.FindName('txtOSPartitionSizeGB')
$State.Controls.txtRecoveryPartitionSizeGB = $window.FindName('txtRecoveryPartitionSizeGB')
+ $State.Controls.ellipseDiskLayoutCapacityStatus = $window.FindName('ellipseDiskLayoutCapacityStatus')
+ $State.Controls.txtDiskLayoutCapacityStatusValue = $window.FindName('txtDiskLayoutCapacityStatusValue')
$State.Controls.txtDataPartitionName = $window.FindName('txtDataPartitionName')
$State.Controls.cmbDataPartitionDriveLetter = $window.FindName('cmbDataPartitionDriveLetter')
$State.Controls.txtDataPartitionSizeGB = $window.FindName('txtDataPartitionSizeGB')
@@ -267,6 +269,11 @@ function Initialize-UIControls {
$State.Controls.btnAddDataPartition = $window.FindName('btnAddDataPartition')
$State.Controls.btnRemoveSelectedDataPartitions = $window.FindName('btnRemoveSelectedDataPartitions')
$State.Controls.btnClearDataPartitions = $window.FindName('btnClearDataPartitions')
+ $State.Controls.btnRestoreRecoveryPartition = $window.FindName('btnRestoreRecoveryPartition')
+ $State.Controls.btnMoveDataPartitionTop = $window.FindName('btnMoveDataPartitionTop')
+ $State.Controls.btnMoveDataPartitionUp = $window.FindName('btnMoveDataPartitionUp')
+ $State.Controls.btnMoveDataPartitionDown = $window.FindName('btnMoveDataPartitionDown')
+ $State.Controls.btnMoveDataPartitionBottom = $window.FindName('btnMoveDataPartitionBottom')
$State.Controls.lstDataPartitions = $window.FindName('lstDataPartitions')
$State.Controls.cmbLogicalSectorSize = $window.FindName('cmbLogicalSectorSize')
$State.Controls.txtProductKey = $window.FindName('txtProductKey')
@@ -461,6 +468,8 @@ function Initialize-UIDefaults {
$State.Controls.cmbSystemPartitionDriveLetter.SelectedItem = ($State.Controls.cmbSystemPartitionDriveLetter.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.SystemPartitionDriveLetter })
$State.Controls.cmbWindowsPartitionDriveLetter.SelectedItem = ($State.Controls.cmbWindowsPartitionDriveLetter.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.WindowsPartitionDriveLetter })
$State.Controls.cmbRecoveryPartitionDriveLetter.SelectedItem = ($State.Controls.cmbRecoveryPartitionDriveLetter.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.RecoveryPartitionDriveLetter })
+ $State.Controls.txtOSPartitionSizeGB.Clear()
+ $State.Data.createRecoveryPartition = $true
$State.Controls.cmbLogicalSectorSize.SelectedItem = ($State.Controls.cmbLogicalSectorSize.Items | Where-Object { $_.Content -eq $State.Defaults.generalDefaults.LogicalSectorSize.ToString() })
# Populate Windows Release, Version, and SKU comboboxes
@@ -823,13 +832,70 @@ function Initialize-DynamicUIElements {
# Keep BYO application columns sized to the current visible content.
Enable-ListViewColumnAutoResize -ListView $State.Controls.lstApplications -FixedColumnIndexes @(0)
- # Additional Data Partitions ListView setup
- $State.Controls.lstDataPartitions.ItemsSource = $State.Data.additionalDataPartitionsDataList.ToArray()
+ # Disk Layout ListView setup
+ $diskLayoutGridView = New-Object System.Windows.Controls.GridView
+ $State.Controls.lstDataPartitions.View = $diskLayoutGridView
+
$itemStyleDataPartitions = New-Object System.Windows.Style([System.Windows.Controls.ListViewItem])
if ($null -ne $listViewItemBaseStyle) { $itemStyleDataPartitions.BasedOn = $listViewItemBaseStyle }
$itemStyleDataPartitions.Setters.Add((New-Object System.Windows.Setter([System.Windows.Controls.ListViewItem]::HorizontalContentAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)))
$State.Controls.lstDataPartitions.ItemContainerStyle = $itemStyleDataPartitions
- Request-ListViewColumnAutoResize -ListView $State.Controls.lstDataPartitions
+
+ Add-SelectableGridViewColumn -ListView $State.Controls.lstDataPartitions -State $State -HeaderCheckBoxKeyName "chkSelectAllDataPartitions" -ColumnWidth 60 -IsSelectablePropertyName "CanSelect" -HeaderToolTip "Select all removable partitions in the disk layout." -ItemToolTip "Select this partition for removal. System, MSR, and Windows partitions cannot be removed."
+ Add-SortableColumn -gridView $diskLayoutGridView -header "Name" -binding "Name" -width 180 -headerHorizontalAlignment Left
+
+ $driveLetterColumn = New-Object System.Windows.Controls.GridViewColumn
+ $driveLetterColumn.Header = "Drive Letter"
+ $driveLetterColumn.Width = 120
+ $driveLetterTemplate = New-Object System.Windows.DataTemplate
+ $driveLetterFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.ComboBox])
+ $driveLetterFactory.SetBinding([System.Windows.Controls.ItemsControl]::ItemsSourceProperty, (New-Object System.Windows.Data.Binding("DriveLetterOptions")))
+ $driveLetterBinding = New-Object System.Windows.Data.Binding("DriveLetter")
+ $driveLetterBinding.Mode = [System.Windows.Data.BindingMode]::TwoWay
+ $driveLetterBinding.UpdateSourceTrigger = [System.Windows.Data.UpdateSourceTrigger]::PropertyChanged
+ $driveLetterFactory.SetBinding([System.Windows.Controls.Primitives.Selector]::SelectedItemProperty, $driveLetterBinding)
+ $driveLetterFactory.SetBinding([System.Windows.Controls.Control]::IsEnabledProperty, (New-Object System.Windows.Data.Binding("CanEditDriveLetter")))
+ $driveLetterTemplate.VisualTree = $driveLetterFactory
+ $driveLetterColumn.CellTemplate = $driveLetterTemplate
+ $diskLayoutGridView.Columns.Add($driveLetterColumn)
+
+ $sizeColumn = New-Object System.Windows.Controls.GridViewColumn
+ $sizeColumn.Header = "Size (GB)"
+ $sizeColumn.Width = 120
+ $sizeTemplate = New-Object System.Windows.DataTemplate
+ $sizeFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.TextBox])
+ $sizeBinding = New-Object System.Windows.Data.Binding("SizeGB")
+ $sizeBinding.Mode = [System.Windows.Data.BindingMode]::TwoWay
+ $sizeBinding.UpdateSourceTrigger = [System.Windows.Data.UpdateSourceTrigger]::PropertyChanged
+ $sizeFactory.SetBinding([System.Windows.Controls.TextBox]::TextProperty, $sizeBinding)
+ $sizeFactory.SetBinding([System.Windows.Controls.Control]::IsEnabledProperty, (New-Object System.Windows.Data.Binding("CanEditSize")))
+ $sizeTemplate.VisualTree = $sizeFactory
+ $sizeColumn.CellTemplate = $sizeTemplate
+ $diskLayoutGridView.Columns.Add($sizeColumn)
+
+ $fillRemainingColumn = New-Object System.Windows.Controls.GridViewColumn
+ $fillRemainingColumn.Header = "Fill Remaining"
+ $fillRemainingColumn.Width = 140
+ $fillRemainingTemplate = New-Object System.Windows.DataTemplate
+ $fillRemainingGridFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.Grid])
+ $fillRemainingGridFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Stretch)
+ $fillRemainingFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.CheckBox])
+ $fillRemainingFactory.SetValue([System.Windows.FrameworkElement]::TagProperty, 'DiskLayoutFillRemaining')
+ $fillRemainingFactory.SetValue([System.Windows.Controls.Control]::ToolTipProperty, 'Use all remaining VHDX space for this partition. Only one partition can fill remaining space.')
+ $fillRemainingFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Center)
+ $fillRemainingFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
+ $fillRemainingBinding = New-Object System.Windows.Data.Binding("FillRemaining")
+ $fillRemainingBinding.Mode = [System.Windows.Data.BindingMode]::TwoWay
+ $fillRemainingBinding.UpdateSourceTrigger = [System.Windows.Data.UpdateSourceTrigger]::PropertyChanged
+ $fillRemainingFactory.SetBinding([System.Windows.Controls.Primitives.ToggleButton]::IsCheckedProperty, $fillRemainingBinding)
+ $fillRemainingFactory.SetBinding([System.Windows.Controls.Control]::IsEnabledProperty, (New-Object System.Windows.Data.Binding("CanEditFillRemaining")))
+ $fillRemainingFactory.SetBinding([System.Windows.UIElement]::VisibilityProperty, (New-Object System.Windows.Data.Binding("FillRemainingVisibility")))
+ $fillRemainingGridFactory.AppendChild($fillRemainingFactory)
+ $fillRemainingTemplate.VisualTree = $fillRemainingGridFactory
+ $fillRemainingColumn.CellTemplate = $fillRemainingTemplate
+ $diskLayoutGridView.Columns.Add($fillRemainingColumn)
+
+ Update-AdditionalDataPartitionsListView -State $State
# Apps Script Variables ListView setup
# Bind ItemsSource to the data list
diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1
index 3aef928..406176d 100644
--- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1
+++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1
@@ -332,6 +332,9 @@ function Add-SelectableGridViewColumn {
[Parameter(Mandatory)]
[double]$ColumnWidth,
[string]$IsSelectedPropertyName = "IsSelected",
+ [string]$IsSelectablePropertyName,
+ [string]$HeaderToolTip = 'Select or clear all selectable rows.',
+ [string]$ItemToolTip = 'Select this row.',
[switch]$HeaderSelectionAffectsVisibleItemsOnly
)
@@ -350,10 +353,12 @@ function Add-SelectableGridViewColumn {
# Store header metadata, including whether select-all should only affect visible rows.
$headerTagObject = [PSCustomObject]@{
PropertyName = $IsSelectedPropertyName
+ SelectablePropertyName = $IsSelectablePropertyName
ListViewControl = $ListView
HeaderSelectionAffectsVisibleItemsOnly = [bool]$HeaderSelectionAffectsVisibleItemsOnly
}
$headerCheckBox.Tag = $headerTagObject
+ $headerCheckBox.ToolTip = $HeaderToolTip
$headerCheckBox.Add_Checked({
param($senderCheckBoxLocal, $eventArgsCheckedLocal)
@@ -379,7 +384,14 @@ function Add-SelectableGridViewColumn {
}
if ($collectionToUpdate.Count -gt 0) {
- foreach ($item in $collectionToUpdate) { $item.$($localPropertyName) = $true }
+ foreach ($item in $collectionToUpdate) {
+ $selectablePropertyName = $tagData.SelectablePropertyName
+ if (-not [string]::IsNullOrWhiteSpace($selectablePropertyName) -and $null -ne $item.PSObject.Properties[$selectablePropertyName] -and -not [bool]$item.$selectablePropertyName) {
+ $item.$($localPropertyName) = $false
+ continue
+ }
+ $item.$($localPropertyName) = $true
+ }
$actualListView.Items.Refresh()
}
})
@@ -451,8 +463,12 @@ function Add-SelectableGridViewColumn {
$checkBoxFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.CheckBox])
$checkBoxFactory.SetBinding([System.Windows.Controls.CheckBox]::IsCheckedProperty, (New-Object System.Windows.Data.Binding($IsSelectedPropertyName)))
+ if (-not [string]::IsNullOrWhiteSpace($IsSelectablePropertyName)) {
+ $checkBoxFactory.SetBinding([System.Windows.Controls.CheckBox]::IsEnabledProperty, (New-Object System.Windows.Data.Binding($IsSelectablePropertyName)))
+ }
$checkBoxFactory.SetValue([System.Windows.FrameworkElement]::HorizontalAlignmentProperty, [System.Windows.HorizontalAlignment]::Center)
$checkBoxFactory.SetValue([System.Windows.FrameworkElement]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
+ $checkBoxFactory.SetValue([System.Windows.Controls.Control]::ToolTipProperty, $ItemToolTip)
# MODIFICATION: Store the actual ListView object in the item checkbox's Tag
$tagObject = [PSCustomObject]@{
@@ -731,6 +747,14 @@ function Update-SelectAllHeaderCheckBoxState {
$collectionToInspect = @($ListView.Items)
}
+ $selectablePropertyName = $null
+ if ($null -ne $HeaderCheckBox.Tag -and $null -ne $HeaderCheckBox.Tag.PSObject.Properties['SelectablePropertyName']) {
+ $selectablePropertyName = [string]$HeaderCheckBox.Tag.SelectablePropertyName
+ }
+ if (-not [string]::IsNullOrWhiteSpace($selectablePropertyName)) {
+ $collectionToInspect = @($collectionToInspect | Where-Object { $null -eq $_.PSObject.Properties[$selectablePropertyName] -or [bool]$_.$selectablePropertyName })
+ }
+
# If no items are available in the selected scope, force unchecked.
if ($collectionToInspect.Count -eq 0) {
$HeaderCheckBox.IsChecked = $false
@@ -771,6 +795,7 @@ function Invoke-ListViewItemToggle {
$selectedItem = $ListView.SelectedItem
if ($null -eq $selectedItem) { return }
+ if ($null -ne $selectedItem.PSObject.Properties['CanSelect'] -and -not [bool]$selectedItem.CanSelect) { return }
# Store the current index to restore focus later
$currentIndex = $ListView.SelectedIndex
diff --git a/FFUDevelopment/config/Sample_default.json b/FFUDevelopment/config/Sample_default.json
index 1da3cf730a153c30ec5c9e31ebc37a886a1aa531..59fed4c05253338fe5f3cbb42de7e118f7db3c94 100644
GIT binary patch
delta 25
hcmZp(zGOWiW^w`t&*UAVN|S>G