Rework the Win11 Creator tab into a three step wizard (#4925)

* Rework the Win11 Creator tab into a three step wizard

- Turn the stacked sections into pages you can navigate between
- Move the edition picker and driver option to the modify step
- Show a working page while mounting and modifying run
- Show what the run changes and a finished panel with the output path
- Put the status log in its own card next to the steps
- Rename Clean & Reset to Start Over and move it below the actions

* Follow the existing colours on the Win11 Creator tab

- Take button text and icon colour from the button they sit in
- Stop the implicit TextBlock style painting a label background on buttons
- Colour step headers from the tab item state
- Use the label accent for headings and field labels like the other tabs

* Cut repeated markup on the Win11 Creator tab

- Render button icon and label from a ContentTemplate, glyph goes in Tag
- Render the step header from a HeaderTemplate the same way
- Move the page scroll wrapper into the tab control template
- Derive the text styles from one another instead of repeating setters

* fix: tidy the dropdown and stop the spinner stuttering

- combo box items painted their own background while the popup painted
  another, so the list read as loose labels on a mismatched sheet; items are
  transparent now and the popup carries the colour
- stretch the item presenter so a highlight spans the popup instead of just
  the text, and bind the popup width to the closed box
- cache the working page's spinner: it animates a rotation on a TextBlock,
  which re-rasterises the glyph every frame on the interface thread
- run that animation only while the page is visible; it was started on Loaded
  and never stopped, so it kept spinning after the step was done

* Stop the Win11 Creator status log rendering justified

The shared TextBox style sets HorizontalContentAlignment to Stretch, which
WPF reads as TextAlignment.Justify. Every other box in WinUtil is single
line, so the status log is the only place it shows, and there it stretched
each wrapped line to the full width with gaps between the words.

Left-aligns it, and while the log is being read as a log rather than as
prose: monospace from a new Win11LogFontFamily theme key so the timestamps
form a column and a wrapped line is visibly not a new entry, and no effect,
since the shared style's drop shadow has neither border nor background to
sit under on this control and smeared the glyphs instead.

* Show the working page for export, USB write and Start Over

Only mount and modify moved to the working page. Saving an ISO, writing a
USB drive and Start Over all ran with the finished output page still on
screen, so the three longest operations in the tab were the ones that looked
like nothing was happening.

All three now open the working page for their duration and return to the
page they belong on: the output step for the two that produce media, the ISO
picker for Start Over, which has just deleted the working directory. Failures
leave the working page before their message box rather than putting a modal
over a spinner, and a finally guard covers cancellation, which skips catch.

Start Over spins the icon backwards. Its work undoes rather than produces,
and reading that off the animation is quicker than reading the label.

* cut comments

Leaves the four that answer a "why is it written this way" a reader cannot
get from the code: the shared TextBox style's justify and drop shadow, the
spinner tag being set before the page is shown, going back to Select rather
than Modify after a failed modification, and the finally guards existing for
cancellation rather than for failure.

* cut changes that only reword

Reverts three edits that changed wording without changing meaning, and drops
the comment on the failed-modification branch.

* cut comments that narrate the code

* address coderabbit review

Hyphenate three-step, and let the chevrons grow past their minimum instead of
pinning a size the glyph could outgrow.

* dismount a replaced ISO and name the USB disk when done

Picking a second ISO after verifying one left the first attached: the mount job
clears Win11ISOImagePath, which is the only handle cleanup has to it. Dismount
it in the job, off the interface thread, before that reset.

The done panel now names the disk it wrote, the way the ISO branch names its file.

* keep the wizard inside the card at the minimum window width

The step column and the chevron grid's middle column were both pinned at 620,
while the left card is about 485px at the 800px minimum. The pages do not
scroll sideways, so controls past the card edge were unreachable.

Both are now capped rather than fixed, and the removed WPFWin11ISOArchLabel was
never assigned by anything.

* stop a replacement mount when the old ISO will not dismount

The warn-and-continue path cleared Win11ISOImagePath anyway, which strands the
old mount for the rest of the session - the leak the dismount was added to
close. It now reports and throws instead, from inside the try so the finally
still re-enables the browse and mount buttons.

* cut comments that narrate the code

* drop the OneDrive sync claim from the modify step list

The post-install script sets DisableFileSyncNGSC=1, but it is prepended to
FirstLogon.ps1, whose own body then sets the same value back to 0. The policy
never survives, so the bullet was promising something the run does not do.
Replaced with search box suggestions, which it does do.
This commit is contained in:
MyDrift
2026-09-19 16:50:51 -05:00
committed by GitHub
parent 9ac45d6c31
commit f9139c75ff
10 changed files with 762 additions and 350 deletions
@@ -29,6 +29,8 @@ function Invoke-WinUtilFontScaling {
"FontSize",
"ButtonFontSize",
"HeaderFontSize",
"Win11StepTitleFontSize",
"Win11StepHeroFontSize",
"TabButtonFontSize",
"ConfigTabButtonFontSize",
"IconFontSize",
+110 -38
View File
@@ -64,6 +64,45 @@ function Write-WinUtilISOLog {
}
}
function Set-WinUtilISOStep {
<#
.SYNOPSIS
Selects a page of the Win11 Creator wizard and sets which pages can be navigated back to
.PARAMETER Step
Select, Modify, Working or Output
.PARAMETER Label
Headline shown on the working page while a long operation runs
.PARAMETER Reverse
Spins the working page icon backwards
#>
param(
[Parameter(Mandatory)]
[ValidateSet("Select", "Modify", "Working", "Output")]
[string]$Step,
[string]$Label,
[switch]$Reverse
)
Invoke-WPFUIThread -Parameters @{ Step = $Step; Label = $Label; Reverse = [bool]$Reverse } -ScriptBlock {
param($Step, $Label, $Reverse)
if ($Label) { $sync["WPFWin11ISOWorkingLabel"].Text = $Label }
$sync["WPFWin11ISOWorkingSpinner"].Tag = if ($Reverse) { "Reverse" } else { "Forward" }
$sync["WPFWin11ISOSelectSection"].IsEnabled = $Step -in @("Select", "Modify")
$sync["WPFWin11ISOModifySection"].IsEnabled = $Step -eq "Modify"
$sync["WPFWin11ISOOutputSection"].IsEnabled = $Step -eq "Output"
$sync["WPFWin11ISO$($Step)Section"].IsSelected = $true
}
}
function Get-WinUtilEditionIdFromName {
<#
.SYNOPSIS
@@ -108,10 +147,9 @@ function Invoke-WinUtilISOBrowse {
$sync["WPFWin11ISOPath"].Text = $isoPath
$sync["WPFWin11ISOFileInfo"].Text = "File size: $fileSizeGB GB"
$sync["WPFWin11ISOFileInfo"].Visibility = "Visible"
$sync["WPFWin11ISOMountSection"].Visibility = "Visible"
$sync["WPFWin11ISOVerifyResultPanel"].Visibility = "Collapsed"
$sync["WPFWin11ISOModifySection"].Visibility = "Collapsed"
$sync["WPFWin11ISOOutputSection"].Visibility = "Collapsed"
Set-WinUtilISOStep -Step "Select"
Write-WinUtilISOLog "ISO selected: $isoPath ($fileSizeGB GB)"
}
@@ -134,17 +172,32 @@ function Invoke-WinUtilISOMountAndVerify {
$sync["WPFWin11ISOMountButton"].IsEnabled = $false
$sync["WPFWin11ISOModifyButton"].IsEnabled = $false
$sync["WPFWin11ISOVerifyResultPanel"].Visibility = "Collapsed"
$sync["WPFWin11ISOModifySection"].Visibility = "Collapsed"
}
Set-WinUtilISOStep -Step "Working" -Label "Mounting and verifying the ISO"
$verified = $false
$mountedByThisRun = $false
$sync["Win11ISOImageInfo"] = $null
$sync["Win11ISODriveLetter"] = $null
$sync["Win11ISOWimPath"] = $null
$sync["Win11ISOImagePath"] = $null
try {
$previous = $sync["Win11ISOImagePath"]
if ($previous -and $previous -ne $isoPath -and (Get-DiskImage -ImagePath $previous -ErrorAction SilentlyContinue).Attached) {
try {
Dismount-DiskImage -ImagePath $previous -ErrorAction Stop
Write-WinUtilISOLog "Dismounted the previously verified ISO: $previous"
} catch {
Write-WinUtilISOLog -Level "ERROR" -Message "Could not dismount the previously verified ISO ${previous}: $_"
Show-WinUtilMessage -Message "The previously verified ISO is still mounted and could not be dismounted:`n`n$previous`n`nDismount it yourself, then select an ISO again." -Title "Previous ISO Still Mounted" -Button "OK" -Icon "Error" | Out-Null
$stillMounted = [System.InvalidOperationException]::new("Could not dismount the previously verified ISO $previous.")
$stillMounted.Data["WinUtilErrorReported"] = $true
throw $stillMounted
}
}
$sync["Win11ISOImageInfo"] = $null
$sync["Win11ISODriveLetter"] = $null
$sync["Win11ISOWimPath"] = $null
$sync["Win11ISOImagePath"] = $null
Write-WinUtilISOLog "Mounting ISO: $isoPath"
Step-WinUtilJob -Status "Mounting ISO..." -Percent 10
@@ -207,7 +260,8 @@ function Invoke-WinUtilISOMountAndVerify {
} -ScriptBlock {
param($DriveLetter, $ImageFileName, $imageInfo)
$sync["WPFWin11ISOMountDriveLetter"].Text = "Mounted at: $DriveLetter | Image file: $ImageFileName"
$sync["WPFWin11ISOMountDriveLetter"].Text = $DriveLetter
$sync["WPFWin11ISOImageFile"].Text = $ImageFileName
$sync["WPFWin11ISOEditionComboBox"].Items.Clear()
foreach ($img in $imageInfo) {
[void]$sync["WPFWin11ISOEditionComboBox"].Items.Add("$($img.ImageIndex): $($img.ImageName)")
@@ -222,7 +276,7 @@ function Invoke-WinUtilISOMountAndVerify {
$sync["WPFWin11ISOEditionComboBox"].SelectedIndex = if ($proIndex -ge 0) { $proIndex } else { 0 }
}
$sync["WPFWin11ISOVerifyResultPanel"].Visibility = "Visible"
$sync["WPFWin11ISOModifySection"].Visibility = "Visible"
Set-WinUtilISOStep -Step "Modify"
}
$verified = $true
@@ -246,6 +300,10 @@ function Invoke-WinUtilISOMountAndVerify {
$sync["WPFWin11ISOBrowseButton"].IsEnabled = $true
$sync["WPFWin11ISOMountButton"].IsEnabled = $true
$sync["WPFWin11ISOModifyButton"].IsEnabled = [bool]$Verified
if ($sync["WPFWin11ISOWorkingSection"].IsSelected) {
Set-WinUtilISOStep -Step "Select"
}
}
}
}
@@ -257,7 +315,7 @@ function Invoke-WinUtilISOModify {
$wimPath = $sync["Win11ISOWimPath"]
if (-not $isoPath) {
Show-WinUtilMessage -Message "No verified ISO found. Please complete Steps 1 and 2 first." -Title "Not Ready" -Button "OK" -Icon "Warning" | Out-Null
Show-WinUtilMessage -Message "No verified ISO found. Please select and verify an ISO first." -Title "Not Ready" -Button "OK" -Icon "Warning" | Out-Null
return
}
@@ -297,10 +355,8 @@ function Invoke-WinUtilISOModify {
Invoke-WPFUIThread -ScriptBlock {
$sync["WPFWin11ISOModifyButton"].IsEnabled = $false
$sync["WPFWin11ISOSelectSection"].Visibility = "Collapsed"
$sync["WPFWin11ISOMountSection"].Visibility = "Collapsed"
$sync["WPFWin11ISOModifySection"].Visibility = "Collapsed"
}
Set-WinUtilISOStep -Step "Working" -Label "Modifying install.wim"
$modified = $false
try {
@@ -362,11 +418,9 @@ function Invoke-WinUtilISOModify {
$sync["Win11ISOContentsDir"] = $isoContents
Step-WinUtilJob -Status "Modification complete" -Percent 100
Write-WinUtilISOLog "install.wim modification complete. Choose an output option in Step 4."
Write-WinUtilISOLog "install.wim modification complete. Choose an output option in the last step."
Invoke-WPFUIThread -ScriptBlock {
$sync["WPFWin11ISOOutputSection"].Visibility = "Visible"
}
Set-WinUtilISOStep -Step "Output"
$modified = $true
} catch {
Write-WinUtilISOLog -Level "ERROR" -Message "Modification failed: $_"
@@ -404,10 +458,9 @@ function Invoke-WinUtilISOModify {
param($Modified)
$sync["WPFWin11ISOModifyButton"].IsEnabled = [bool]$Modified
if ($sync["WPFWin11ISOOutputSection"].Visibility -ne "Visible") {
$sync["WPFWin11ISOSelectSection"].Visibility = "Visible"
$sync["WPFWin11ISOMountSection"].Visibility = "Visible"
$sync["WPFWin11ISOModifySection"].Visibility = if ($Modified) { "Visible" } else { "Collapsed" }
if ($sync["WPFWin11ISOWorkingSection"].IsSelected) {
Set-WinUtilISOStep -Step "Select"
}
}
}
@@ -443,24 +496,21 @@ function Invoke-WinUtilISOCheckExistingWork {
$sync["Win11ISOWorkDir"] = $existingWorkDir.FullName
$sync["Win11ISOContentsDir"] = $isoContents
$sync["WPFWin11ISOSelectSection"].Visibility = "Collapsed"
$sync["WPFWin11ISOMountSection"].Visibility = "Collapsed"
$sync["WPFWin11ISOModifySection"].Visibility = "Collapsed"
$sync["WPFWin11ISOOutputSection"].Visibility = "Visible"
Set-WinUtilISOStep -Step "Output"
$modified = $existingWorkDir.LastWriteTime.ToString("yyyy-MM-dd HH:mm")
Write-WinUtilISOLog "Existing working directory found: $($existingWorkDir.FullName)"
Write-WinUtilISOLog "Last modified: $modified - Skipping Steps 1-3 and resuming at Step 4."
Write-WinUtilISOLog "Click 'Clean & Reset' if you want to start over with a new ISO."
Write-WinUtilISOLog "Last modified: $modified - Skipping the earlier steps and resuming at the output step."
Write-WinUtilISOLog "Click 'Start Over' if you want to start over with a new ISO."
Show-WinUtilMessage -Message "A previous WinUtil ISO working directory was found:`n`n$($existingWorkDir.FullName)`n`n(Last modified: $modified)`n`nStep 4 (output options) has been restored so you can save the already-modified image.`n`nClick 'Clean & Reset' in Step 4 if you want to start over." -Title "Existing Work Found" -Button "OK" -Icon "Info" | Out-Null
Show-WinUtilMessage -Message "A previous WinUtil ISO working directory was found:`n`n$($existingWorkDir.FullName)`n`n(Last modified: $modified)`n`nThe output step has been restored so you can save the already-modified image.`n`nClick 'Start Over' there if you want to start over." -Title "Existing Work Found" -Button "OK" -Icon "Info" | Out-Null
}
function Invoke-WinUtilISOCleanAndReset {
$workDir = $sync["Win11ISOWorkDir"]
if ($workDir -and (Test-Path $workDir)) {
$confirm = Show-WinUtilMessage -Message "This will delete the temporary working directory:`n`n$workDir`n`nAnd reset the interface back to the start.`n`nContinue?" -Title "Clean & Reset" -Button "YesNo" -Icon "Warning"
$confirm = Show-WinUtilMessage -Message "This will delete the temporary working directory:`n`n$workDir`n`nAnd reset the interface back to the start.`n`nContinue?" -Title "Start Over" -Button "YesNo" -Icon "Warning"
if ($confirm -ne "Yes") { return }
}
@@ -470,6 +520,7 @@ function Invoke-WinUtilISOCleanAndReset {
param($workDir)
Invoke-WPFUIThread -ScriptBlock { $sync["WPFWin11ISOCleanResetButton"].IsEnabled = $false }
Set-WinUtilISOStep -Step "Working" -Label "Starting over" -Reverse
try {
if ($workDir) {
@@ -544,19 +595,23 @@ function Invoke-WinUtilISOCleanAndReset {
Invoke-WPFUIThread -ScriptBlock {
$sync["WPFWin11ISOPath"].Text = "No ISO selected..."
$sync["WPFWin11ISOFileInfo"].Visibility = "Collapsed"
$sync["WPFWin11ISOFileInfo"].Visibility = "Hidden"
$sync["WPFWin11ISOVerifyResultPanel"].Visibility = "Collapsed"
$sync["WPFWin11ISOOptionUSB"].Visibility = "Collapsed"
$sync["WPFWin11ISOOutputSection"].Visibility = "Collapsed"
$sync["WPFWin11ISOModifySection"].Visibility = "Collapsed"
$sync["WPFWin11ISOMountSection"].Visibility = "Collapsed"
$sync["WPFWin11ISOSelectSection"].Visibility = "Visible"
$sync["WPFWin11ISODonePanel"].Visibility = "Collapsed"
$sync["WPFWin11ISOModifyButton"].IsEnabled = $true
$sync["WPFWin11ISOStatusLog"].Text = "Ready. Please select a Windows 11 ISO to begin."
Set-WinUtilISOStep -Step "Select"
}
Step-WinUtilJob -Hide
} finally {
Invoke-WPFUIThread -ScriptBlock { $sync["WPFWin11ISOCleanResetButton"].IsEnabled = $true }
Invoke-WPFUIThread -ScriptBlock {
$sync["WPFWin11ISOCleanResetButton"].IsEnabled = $true
if ($sync["WPFWin11ISOWorkingSection"].IsSelected) {
Set-WinUtilISOStep -Step "Select"
}
}
}
}
}
@@ -565,7 +620,7 @@ function Invoke-WinUtilISOExport {
$contentsDir = $sync["Win11ISOContentsDir"]
if (-not $contentsDir -or -not (Test-Path $contentsDir)) {
Show-WinUtilMessage -Message "No modified ISO content found. Please complete Steps 1-3 first." -Title "Not Ready" -Button "OK" -Icon "Warning" | Out-Null
Show-WinUtilMessage -Message "No modified ISO content found. Please run the modification step first." -Title "Not Ready" -Button "OK" -Icon "Warning" | Out-Null
return
}
@@ -586,10 +641,12 @@ function Invoke-WinUtilISOExport {
param($contentsDir, $outputISO)
Invoke-WPFUIThread -ScriptBlock { $sync["WPFWin11ISOChooseISOButton"].IsEnabled = $false }
Set-WinUtilISOStep -Step "Working" -Label "Building the ISO file"
try {
$oscdimg = Get-WinUtilOscdimgPath
if (-not $oscdimg) {
Set-WinUtilISOStep -Step "Output"
Show-WinUtilMessage -Message "oscdimg.exe could not be found or installed automatically.`n`nPlease install it manually:`n winget install -e --id Microsoft.OSCDIMG`n`nOr install the Windows ADK from:`nhttps://learn.microsoft.com/windows-hardware/get-started/adk-install" -Title "oscdimg Not Found" -Button "OK" -Icon "Warning" | Out-Null
throw "oscdimg.exe could not be found or installed automatically."
}
@@ -648,14 +705,29 @@ function Invoke-WinUtilISOExport {
Step-WinUtilJob -Status "ISO exported" -Percent 100
Write-WinUtilISOLog "ISO exported successfully: $outputISO"
Invoke-WPFUIThread -Parameters @{ OutputISO = $outputISO } -ScriptBlock {
param($OutputISO)
$sync["WPFWin11ISODoneLabel"].Text = "ISO saved to $OutputISO"
$sync["WPFWin11ISODonePanel"].Visibility = "Visible"
}
Set-WinUtilISOStep -Step "Output"
Show-WinUtilMessage -Message "ISO exported successfully!`n`n$outputISO" -Title "Export Complete" -Button "OK" -Icon "Info" | Out-Null
} catch {
Write-WinUtilISOLog -Level "ERROR" -Message "ISO export failed: $_"
$_.Exception.Data["WinUtilErrorReported"] = $true
Set-WinUtilISOStep -Step "Output"
Show-WinUtilMessage -Message "ISO export failed:`n`n$_" -Title "Error" -Button "OK" -Icon "Error" | Out-Null
throw
} finally {
Invoke-WPFUIThread -ScriptBlock { $sync["WPFWin11ISOChooseISOButton"].IsEnabled = $true }
Invoke-WPFUIThread -ScriptBlock {
$sync["WPFWin11ISOChooseISOButton"].IsEnabled = $true
# Cancellation skips catch, so the working page can still be up here
if ($sync["WPFWin11ISOWorkingSection"].IsSelected) {
Set-WinUtilISOStep -Step "Output"
}
}
}
}
}
+18 -2
View File
@@ -39,7 +39,7 @@ function Invoke-WinUtilISOWriteUSB {
$usbDisks = $sync["Win11ISOUSBDisks"]
if (-not $contentsDir -or -not (Test-Path $contentsDir)) {
Show-WinUtilMessage -Message "No modified ISO content found. Please complete Steps 1-3 first." -Title "Not Ready" -Button "OK" -Icon "Warning" | Out-Null
Show-WinUtilMessage -Message "No modified ISO content found. Please run the modification step first." -Title "Not Ready" -Button "OK" -Icon "Warning" | Out-Null
return
}
@@ -87,6 +87,7 @@ function Invoke-WinUtilISOWriteUSB {
param($DiskNumber, $contentsDir)
Invoke-WPFUIThread -ScriptBlock { $sync["WPFWin11ISOWriteUSBButton"].IsEnabled = $false }
Set-WinUtilISOStep -Step "Working" -Label "Writing the USB drive"
try {
Write-WinUtilISOLog "Starting USB write to Disk $DiskNumber..."
@@ -233,14 +234,29 @@ function Invoke-WinUtilISOWriteUSB {
Step-WinUtilJob -Status "USB write complete" -Percent 100
Write-WinUtilISOLog "USB drive is ready for use."
Invoke-WPFUIThread -Parameters @{ DiskNumber = $DiskNumber } -ScriptBlock {
param($DiskNumber)
$sync["WPFWin11ISODoneLabel"].Text = "Disk $DiskNumber is ready to boot from."
$sync["WPFWin11ISODonePanel"].Visibility = "Visible"
}
Set-WinUtilISOStep -Step "Output"
Show-WinUtilMessage -Message "USB drive created successfully!`n`nYou can now boot from this drive to install Windows 11." -Title "USB Ready" -Button "OK" -Icon "Info" | Out-Null
} catch {
Write-WinUtilISOLog -Level "ERROR" -Message "USB write failed: $_"
$_.Exception.Data["WinUtilErrorReported"] = $true
Set-WinUtilISOStep -Step "Output"
Show-WinUtilMessage -Message "USB write failed:`n`n$_" -Title "USB Write Error" -Button "OK" -Icon "Error" | Out-Null
throw
} finally {
Invoke-WPFUIThread -ScriptBlock { $sync["WPFWin11ISOWriteUSBButton"].IsEnabled = $true }
Invoke-WPFUIThread -ScriptBlock {
$sync["WPFWin11ISOWriteUSBButton"].IsEnabled = $true
# Cancellation skips catch, so the working page can still be up here
if ($sync["WPFWin11ISOWorkingSection"].IsSelected) {
Set-WinUtilISOStep -Step "Output"
}
}
}
}
}
@@ -512,6 +512,14 @@ Version : <a href="https://github.com/ChrisTitusTech/winutil/releases/tag/$($sy
Invoke-WinUtilISOCleanAndReset
})
$sync["WPFWin11ISOBackButton"].Add_Click({
$sync["WPFWin11ISOSelectSection"].IsSelected = $true
})
$sync["WPFWin11ISOForwardButton"].Add_Click({
$sync["WPFWin11ISOModifySection"].IsSelected = $true
})
$buildClock.Stop()
Write-WinUtilLog -Level "DEBUG" -Component "UI" -Message "Interface built in $($buildClock.ElapsedMilliseconds) ms, showing the window."
Write-WinUtilTimingSummary -Scope "UI" -TotalMilliseconds $buildClock.ElapsedMilliseconds