From b8b81edcdb5d5de8b3ac08a45a23e504151b599d Mon Sep 17 00:00:00 2001 From: Malin Fossum Date: Wed, 19 Aug 2026 23:26:11 +0200 Subject: [PATCH] Show preset JSON key in app, tweak, and feature tooltips (#4995) * Add Get-WinUtilEntryToolTip helper for preset-key tooltips * Show preset JSON key in app and tweak tooltips * Match apps search against preset key for consistency with tweaks * Limit preset-key tooltips to preset-representable controls Comboboxes and package-manager radio buttons cannot appear in a preset file. Update-WinUtilSelections routes a flat list of keys by the WPFInstall/WPFTweaks/WPFToggle/WPFFeature/WPFAppx prefixes, so a preset can carry neither a combobox selected value nor a radio group choice; WingetRadioButton and ChocoRadioButton do not even carry a WPF prefix. Advertising those control names as preset keys invited users to add keys that fall through the switch and abort the whole import. Revert the combobox label and radio button tooltips to the plain description, and add tests that fail if the key is reattached to a control the preset importer cannot accept. * fix: address preset key review feedback * docs: clarify preset key search scope --------- Co-authored-by: Chris Titus --- docs/src/content/docs/guides/automation.mdx | 2 + .../private/Find-AppsByNameOrDescription.ps1 | 9 +- functions/private/Get-WinUtilEntryToolTip.ps1 | 25 ++++ .../private/Initialize-InstallAppEntry.ps1 | 2 +- functions/public/Invoke-WPFUIElements.ps1 | 4 +- pester/search-filter.Tests.ps1 | 13 ++ pester/tooltip-key.Tests.ps1 | 118 ++++++++++++++++++ 7 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 functions/private/Get-WinUtilEntryToolTip.ps1 create mode 100644 pester/tooltip-key.Tests.ps1 diff --git a/docs/src/content/docs/guides/automation.mdx b/docs/src/content/docs/guides/automation.mdx index 4dc87775..5612066d 100644 --- a/docs/src/content/docs/guides/automation.mdx +++ b/docs/src/content/docs/guides/automation.mdx @@ -22,6 +22,8 @@ Example: To view exactly what each preset does, see: https://github.com/ChrisTitusTech/winutil/blob/main/config/preset.json +To find a key for a custom configuration, hover over a supported application, tweak, feature, or AppX entry in WinUtil. Its tooltip shows `Preset key: `. Application keys are searchable on the Install tab, while tweak and AppX keys are searchable on their respective tabs. Feature keys are available from their tooltips because the Config/Features tab does not have search. Controls that cannot be applied from a preset, such as toggle switches, drop-downs, and package-manager choices, intentionally do not advertise a preset key. + To create your own config file: 1. Open WinUtil. diff --git a/functions/private/Find-AppsByNameOrDescription.ps1 b/functions/private/Find-AppsByNameOrDescription.ps1 index 44f4003c..c94fc451 100644 --- a/functions/private/Find-AppsByNameOrDescription.ps1 +++ b/functions/private/Find-AppsByNameOrDescription.ps1 @@ -5,9 +5,9 @@ function Find-AppsByNameOrDescription { .DESCRIPTION Search text and categories are independent filters that both have to pass. An entry is - shown when its name or description matches the search text, and when its category is in - the selected set. An empty search matches everything, and an empty category set matches - every category. + shown when its name, description, or application preset key matches the search text, and + when its category is in the selected set. An empty search matches everything, and an empty + category set matches every category. While either filter is active the matching categories are expanded, since a collapsed category would otherwise hide the very results that were asked for. With no filter at @@ -115,7 +115,8 @@ function Find-AppsByNameOrDescription { $categoryMatch = -not $hasCategories -or $activeCategories -contains $appEntry.Category $textMatch = -not $hasSearch -or $appEntry.Content -like "*$escapedSearchString*" -or - $appEntry.Description -like "*$escapedSearchString*" + $appEntry.Description -like "*$escapedSearchString*" -or + $appTag -like "*$escapedSearchString*" if ($categoryMatch -and $textMatch) { $appControl.Visibility = [Windows.Visibility]::Visible diff --git a/functions/private/Get-WinUtilEntryToolTip.ps1 b/functions/private/Get-WinUtilEntryToolTip.ps1 new file mode 100644 index 00000000..6cde230d --- /dev/null +++ b/functions/private/Get-WinUtilEntryToolTip.ps1 @@ -0,0 +1,25 @@ +function Get-WinUtilEntryToolTip { + <# + .SYNOPSIS + Builds the tooltip string for an app/tweak/feature entry: its description plus its preset JSON key + + .PARAMETER Description + The entry's description from the config JSON. May be null or empty. + + .PARAMETER Key + The entry's JSON key as used in preset files (e.g. WPFInstallbrave, WPFTweaksTele). + #> + param( + [Parameter(Mandatory = $false)] + [string]$Description, + + [Parameter(Mandatory = $true)] + [string]$Key + ) + + if ([string]::IsNullOrWhiteSpace($Description)) { + return "Preset key: $Key" + } + + return "$Description`n`nPreset key: $Key" +} diff --git a/functions/private/Initialize-InstallAppEntry.ps1 b/functions/private/Initialize-InstallAppEntry.ps1 index 8318f12b..8edb039c 100644 --- a/functions/private/Initialize-InstallAppEntry.ps1 +++ b/functions/private/Initialize-InstallAppEntry.ps1 @@ -19,7 +19,7 @@ function Initialize-InstallAppEntry { $border = New-Object Windows.Controls.Border $border.Style = $sync.Form.Resources.AppEntryBorderStyle $border.Tag = $appKey - $border.ToolTip = $app.description + $border.ToolTip = Get-WinUtilEntryToolTip -Description $app.description -Key $appKey $border.Add_MouseLeftButtonUp({ # Resolve through $sync because the border's child is a layout Grid for FOSS entries $childCheckbox = $sync.$($this.Tag) diff --git a/functions/public/Invoke-WPFUIElements.ps1 b/functions/public/Invoke-WPFUIElements.ps1 index eb50fae0..01559fdf 100644 --- a/functions/public/Invoke-WPFUIElements.ps1 +++ b/functions/public/Invoke-WPFUIElements.ps1 @@ -224,7 +224,7 @@ function Invoke-WPFUIElements { $toggleButton = New-Object Windows.Controls.Primitives.ToggleButton $toggleButton.Name = $entryInfo.Name $toggleButton.Content = $entryInfo.Content[1] - $toggleButton.ToolTip = $entryInfo.Description + $toggleButton.ToolTip = Get-WinUtilEntryToolTip -Description $entryInfo.Description -Key $entryInfo.Name $toggleButton.HorizontalAlignment = "Left" $toggleButton.Style = $ToggleButtonStyle [System.Windows.Automation.AutomationProperties]::SetName($toggleButton, $entryInfo.Content[0]) @@ -490,7 +490,7 @@ function Invoke-WPFUIElements { $checkBox.Name = $entryInfo.Name $checkBox.Content = $entryInfo.Content $checkBox.SetResourceReference([Windows.Controls.Control]::FontSizeProperty, "FontSize") - $checkBox.ToolTip = $entryInfo.Description + $checkBox.ToolTip = Get-WinUtilEntryToolTip -Description $entryInfo.Description -Key $entryInfo.Name $checkBox.SetResourceReference([Windows.Controls.Control]::MarginProperty, "CheckBoxMargin") $checkBox.UseLayoutRounding = $true [System.Windows.Automation.AutomationProperties]::SetName($checkBox, $entryInfo.Content) diff --git a/pester/search-filter.Tests.ps1 b/pester/search-filter.Tests.ps1 index 74211424..4c7bb6b1 100644 --- a/pester/search-filter.Tests.ps1 +++ b/pester/search-filter.Tests.ps1 @@ -360,6 +360,19 @@ Describe "Find-AppsByNameOrDescription" { $editorItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed) } + It "matches apps by preset key" { + $browserItem = New-WinUtilAppSearchItem -Tag "WPFInstallBrowser" + $mediaItem = New-WinUtilAppSearchItem -Tag "WPFInstallMedia" + $category = New-WinUtilAppCategory -Label "- Browsers" -Items @($browserItem, $mediaItem) + New-WinUtilAppSearchContext -Categories @($category) + + Find-AppsByNameOrDescription -SearchString "WPFInstallBrowser" + + $browserItem.Visibility | Should -Be ([Windows.Visibility]::Visible) + $mediaItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed) + $category.Visibility | Should -Be ([Windows.Visibility]::Visible) + } + It "treats wildcard characters as literal app search text" { $literalItem = New-WinUtilAppSearchItem -Tag "WPFInstallLiteral" $mediaItem = New-WinUtilAppSearchItem -Tag "WPFInstallMedia" diff --git a/pester/tooltip-key.Tests.ps1 b/pester/tooltip-key.Tests.ps1 new file mode 100644 index 00000000..595839fd --- /dev/null +++ b/pester/tooltip-key.Tests.ps1 @@ -0,0 +1,118 @@ +#=========================================================================== +# Tests - Entry ToolTip Helper +#=========================================================================== + +BeforeAll { + $script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path + . (Join-Path $script:repoRoot "functions\private\Get-WinUtilEntryToolTip.ps1") + . (Join-Path $script:repoRoot "functions\private\Update-WinUtilSelections.ps1") + + $applications = Get-Content (Join-Path $script:repoRoot "config\applications.json") -Raw | ConvertFrom-Json + $applicationsHashtable = @{} + foreach ($property in $applications.PSObject.Properties) { + $applicationsHashtable["WPFInstall$($property.Name)"] = $property.Value + } + $appx = Get-Content (Join-Path $script:repoRoot "config\appx.json") -Raw | ConvertFrom-Json + $appxHashtable = @{} + foreach ($property in $appx.PSObject.Properties) { + $appxHashtable[$property.Name] = $property.Value + } + $script:selectionConfigs = @{ + applicationsHashtable = $applicationsHashtable + tweaks = Get-Content (Join-Path $script:repoRoot "config\tweaks.json") -Raw | ConvertFrom-Json + feature = Get-Content (Join-Path $script:repoRoot "config\feature.json") -Raw | ConvertFrom-Json + appxHashtable = $appxHashtable + } + + # Map each control type the renderer handles to whether its branch adds the preset key. + # Read from the source AST so re-adding the helper to an unsupported branch fails the test. + $script:rendererClauses = @{} + $rendererPath = Join-Path $script:repoRoot "functions\public\Invoke-WPFUIElements.ps1" + $rendererAst = [System.Management.Automation.Language.Parser]::ParseFile($rendererPath, [ref]$null, [ref]$null) + $typeSwitch = $rendererAst.FindAll({ + $args[0] -is [System.Management.Automation.Language.SwitchStatementAst] + }, $true) | Where-Object { $_.Clauses.Item1.Extent.Text -contains '"Combobox"' } + + foreach ($clause in $typeSwitch.Clauses) { + $clauseName = $clause.Item1.Extent.Text.Trim('"') + $script:rendererClauses[$clauseName] = [bool]($clause.Item2.Extent.Text -match 'Get-WinUtilEntryToolTip') + } + $script:rendererClauses["default"] = [bool]($typeSwitch.Default.Extent.Text -match 'Get-WinUtilEntryToolTip') + + $appRendererPath = Join-Path $script:repoRoot "functions\private\Initialize-InstallAppEntry.ps1" + $script:appRenderer = Get-Content $appRendererPath -Raw + + # Every entry the renderer draws, with the branch that draws it + $script:renderedEntries = foreach ($configName in "appnavigation", "tweaks", "feature", "appx") { + $config = Get-Content (Join-Path $script:repoRoot "config\$configName.json") -Raw | ConvertFrom-Json + foreach ($property in $config.PSObject.Properties) { + $clauseName = "default" + if ($property.Value.Type -and $script:rendererClauses.ContainsKey($property.Value.Type)) { + $clauseName = $property.Value.Type + } + [pscustomobject]@{ + Config = $configName + Key = $property.Name + Clause = $clauseName + } + } + } +} + +Describe "Get-WinUtilEntryToolTip" { + It "appends the preset key after the description" { + Get-WinUtilEntryToolTip -Description "Fast private browser" -Key "WPFInstallBrave" | + Should -Be "Fast private browser`n`nPreset key: WPFInstallBrave" + } + + It "returns only the key line when description is null" { + Get-WinUtilEntryToolTip -Description $null -Key "WPFTweaksTele" | + Should -Be "Preset key: WPFTweaksTele" + } + + It "returns only the key line when description is whitespace" { + Get-WinUtilEntryToolTip -Description " " -Key "WPFTweaksTele" | + Should -Be "Preset key: WPFTweaksTele" + } + + It "returns a plain string, not a UI object" { + (Get-WinUtilEntryToolTip -Description "x" -Key "WPFTweaksTele").GetType().Name | + Should -Be "String" + } +} + +Describe "Preset key tooltips" { + It "leaves unsupported controls unlabelled" { + # Comboboxes and radio buttons are not representable in the flat preset format. + # Toggle keys can be imported, but preset execution does not apply their state. + $script:rendererClauses["Toggle"] | Should -BeFalse + $script:rendererClauses["Combobox"] | Should -BeFalse + $script:rendererClauses["RadioButton"] | Should -BeFalse + } + + It "labels entries created by the application renderer" { + $script:appRenderer | Should -Match '\$border\.ToolTip\s*=\s*Get-WinUtilEntryToolTip\s+-Description\s+\$app\.description\s+-Key\s+\$appKey' + } + + It "labels every entry with a key the preset importer accepts" { + $labelled = @($script:renderedEntries | Where-Object { $script:rendererClauses[$_.Clause] }) + $labelled.Count | Should -BeGreaterThan 0 + + foreach ($entry in $labelled) { + $sync = @{ + selectedApps = [System.Collections.Generic.List[string]]::new() + selectedTweaks = [System.Collections.Generic.List[string]]::new() + selectedToggles = [System.Collections.Generic.List[string]]::new() + selectedFeatures = [System.Collections.Generic.List[string]]::new() + selectedAppx = [System.Collections.Generic.List[string]]::new() + configs = $script:selectionConfigs + } + + { Update-WinUtilSelections -flatJson @($entry.Key) } | + Should -Not -Throw -Because "$($entry.Config).json key '$($entry.Key)' is shown as a preset key" + + $imported = @($sync.Values | ForEach-Object { $_ }) + $imported | Should -Contain $entry.Key -Because "importing '$($entry.Key)' must restore that selection" + } + } +}