From 51cb3811dd1ee1510af34e820fd190896f8841a0 Mon Sep 17 00:00:00 2001 From: FourSeven Date: Thu, 16 Jul 2026 01:54:22 +0000 Subject: [PATCH] Add category search chips to Install tab for improved filtering (#4826) * Add Quick Category Search Chips to Install tab Add category shortcut chips on the Install tab that insert the category name into the existing search box. Append each app's category as a description tag so the existing description search filters by category. Reuses the standard button style and existing search; no search, install, or preset logic is changed. * Fix install category chip filtering * Fix category filter refresh behavior --------- Co-authored-by: Chris Titus --- .../private/Find-AppsByNameOrDescription.ps1 | 17 +++++++++---- .../private/Set-WinUtilAppCategoryFilter.ps1 | 17 +++++++++++++ .../Start-WinUtilInstallAppRendering.ps1 | 2 +- pester/install-rendering.Tests.ps1 | 3 ++- pester/search-filter.Tests.ps1 | 22 +++++++++++++++++ pester/xaml.Tests.ps1 | 11 +++++++++ scripts/main.ps1 | 24 ++++++++++++++++++- xaml/inputXML.xaml | 20 +++++++++++++++- 8 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 functions/private/Set-WinUtilAppCategoryFilter.ps1 diff --git a/functions/private/Find-AppsByNameOrDescription.ps1 b/functions/private/Find-AppsByNameOrDescription.ps1 index ec405789..96037237 100644 --- a/functions/private/Find-AppsByNameOrDescription.ps1 +++ b/functions/private/Find-AppsByNameOrDescription.ps1 @@ -10,6 +10,9 @@ function Find-AppsByNameOrDescription { .PARAMETER SearchString The string to be searched for. Wildcards are treated as literal characters. + .PARAMETER Category + When provided, only applications in this exact category are shown. + .NOTES - Uses module-scope $sync (no parameter needed; inherits from caller's scope) - Performs literal matching (no wildcard expansion) @@ -18,7 +21,10 @@ function Find-AppsByNameOrDescription { #> param( [Parameter(Mandatory = $false)] - [string]$SearchString = "" + [string]$SearchString = "", + + [Parameter(Mandatory = $false)] + [string]$Category = "" ) # Validate that $sync exists and has required structure @@ -39,7 +45,7 @@ function Find-AppsByNameOrDescription { try { # Reset the visibility if the search string is empty or the search is cleared - if ([string]::IsNullOrWhiteSpace($SearchString)) { + if ([string]::IsNullOrWhiteSpace($SearchString) -and [string]::IsNullOrWhiteSpace($Category)) { $sync.ItemsControl.Items | ForEach-Object { # Each item is a StackPanel container $_.Visibility = [Windows.Visibility]::Visible @@ -94,10 +100,11 @@ function Find-AppsByNameOrDescription { # Check if app matches search criteria if ($null -ne $appEntry) { - $contentMatch = $appEntry.Content -like "*$escapedSearchString*" - $descriptionMatch = $appEntry.Description -like "*$escapedSearchString*" + $categoryMatch = -not [string]::IsNullOrWhiteSpace($Category) -and $appEntry.Category -eq $Category + $contentMatch = [string]::IsNullOrWhiteSpace($Category) -and $appEntry.Content -like "*$escapedSearchString*" + $descriptionMatch = [string]::IsNullOrWhiteSpace($Category) -and $appEntry.Description -like "*$escapedSearchString*" - if ($contentMatch -or $descriptionMatch) { + if ($categoryMatch -or $contentMatch -or $descriptionMatch) { # Show the App and mark that this category has a match $appControl.Visibility = [Windows.Visibility]::Visible $categoryHasMatch = $true diff --git a/functions/private/Set-WinUtilAppCategoryFilter.ps1 b/functions/private/Set-WinUtilAppCategoryFilter.ps1 new file mode 100644 index 00000000..dbc0e555 --- /dev/null +++ b/functions/private/Set-WinUtilAppCategoryFilter.ps1 @@ -0,0 +1,17 @@ +function Set-WinUtilAppCategoryFilter { + <# + .SYNOPSIS + Applies an exact application category filter from an Install tab search chip. + + .PARAMETER Category + The application category to show. An empty value clears the filter. + #> + param( + [Parameter(Mandatory = $false)] + [string]$Category = "" + ) + + $sync.SearchBar.Tag = $Category + $sync.SearchBar.Text = $Category + Find-AppsByNameOrDescription -SearchString $Category -Category $Category +} diff --git a/functions/private/Start-WinUtilInstallAppRendering.ps1 b/functions/private/Start-WinUtilInstallAppRendering.ps1 index 2eed3844..f3bf9da1 100644 --- a/functions/private/Start-WinUtilInstallAppRendering.ps1 +++ b/functions/private/Start-WinUtilInstallAppRendering.ps1 @@ -9,7 +9,7 @@ function Invoke-WinUtilInstallAppRenderBatch { } if ($sync.currentTab -eq "Install" -and $sync.SearchBar -and -not [string]::IsNullOrWhiteSpace($sync.SearchBar.Text)) { - Find-AppsByNameOrDescription -SearchString $sync.SearchBar.Text + Find-AppsByNameOrDescription -SearchString $sync.SearchBar.Text -Category $sync.SearchBar.Tag } } diff --git a/pester/install-rendering.Tests.ps1 b/pester/install-rendering.Tests.ps1 index 9bb4d366..5c77f6a9 100644 --- a/pester/install-rendering.Tests.ps1 +++ b/pester/install-rendering.Tests.ps1 @@ -21,6 +21,7 @@ Describe "Install app rendering startup contract" { $renderScript | Should -Match 'Dispatcher\.BeginInvoke' $renderScript | Should -Match 'Invoke-WinUtilInstallAppRenderNextBatch' $renderScript | Should -Match 'Initialize-InstallAppEntry' + $renderScript | Should -Match 'Find-AppsByNameOrDescription -SearchString \$sync\.SearchBar\.Text -Category \$sync\.SearchBar\.Tag' $renderScript | Should -Match '\$sync\.InstallAppEntriesRendered = \$true' } @@ -59,7 +60,7 @@ Describe "Install app rendering startup contract" { } function global:Find-AppsByNameOrDescription { - param($SearchString) + param($SearchString, $Category) throw "Search should not run for an empty search box in this test." } diff --git a/pester/search-filter.Tests.ps1 b/pester/search-filter.Tests.ps1 index 8231f5d1..254ce8fd 100644 --- a/pester/search-filter.Tests.ps1 +++ b/pester/search-filter.Tests.ps1 @@ -172,18 +172,27 @@ namespace Windows.Controls WPFInstallBrowser = [pscustomobject]@{ Content = "Firefox" Description = "Fast private browser" + Category = "Browsers" } WPFInstallMedia = [pscustomobject]@{ Content = "VLC" Description = "Media player" + Category = "Multimedia Tools" } WPFInstallLiteral = [pscustomobject]@{ Content = "Tool [abc]" Description = "Literal wildcard sample" + Category = "Utilities" } WPFInstallEditor = [pscustomobject]@{ Content = "Code Editor" Description = "Text editing" + Category = "Development" + } + WPFInstallPowerToys = [pscustomobject]@{ + Content = "PowerToys" + Description = "A collection of system utilities" + Category = "Microsoft Tools" } } } @@ -363,6 +372,19 @@ Describe "Find-AppsByNameOrDescription" { $mediaItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed) $category.Visibility | Should -Be ([Windows.Visibility]::Visible) } + + It "filters category chips by exact application category" { + $utilityItem = New-WinUtilAppSearchItem -Tag "WPFInstallLiteral" + $powerToysItem = New-WinUtilAppSearchItem -Tag "WPFInstallPowerToys" + $category = New-WinUtilAppCategory -Label "- Tools" -Items @($utilityItem, $powerToysItem) + New-WinUtilAppSearchContext -Categories @($category) + + Find-AppsByNameOrDescription -SearchString "Utilities" -Category "Utilities" + + $utilityItem.Visibility | Should -Be ([Windows.Visibility]::Visible) + $powerToysItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed) + $category.Visibility | Should -Be ([Windows.Visibility]::Visible) + } } Describe "Find-TweaksByNameOrDescription" { diff --git a/pester/xaml.Tests.ps1 b/pester/xaml.Tests.ps1 index 5b07c562..c6801bf9 100644 --- a/pester/xaml.Tests.ps1 +++ b/pester/xaml.Tests.ps1 @@ -137,6 +137,17 @@ Describe "XAML document" { "SearchBar", "SearchBarIcon", "SearchBarClearButton", + "WPFSearchChips", + "WPFSearchChipAll", + "WPFSearchChipBrowsers", + "WPFSearchChipCommunications", + "WPFSearchChipDevelopment", + "WPFSearchChipGames", + "WPFSearchChipMicrosoftTools", + "WPFSearchChipMultimediaTools", + "WPFSearchChipProTools", + "WPFSearchChipSelfhostedTools", + "WPFSearchChipUtilities", "appscategory", "appspanel", "tweakspanel", diff --git a/scripts/main.ps1 b/scripts/main.ps1 index 90ba5a73..121bd9ff 100644 --- a/scripts/main.ps1 +++ b/scripts/main.ps1 @@ -341,7 +341,7 @@ $searchBarTimer.add_Tick({ $searchBarTimer.Stop() switch ($sync.currentTab) { "Install" { - Find-AppsByNameOrDescription -SearchString $sync.SearchBar.Text + Find-AppsByNameOrDescription -SearchString $sync.SearchBar.Text -Category $sync.SearchBar.Tag } "Tweaks" { Find-TweaksByNameOrDescription -SearchString $sync.SearchBar.Text @@ -352,6 +352,10 @@ $searchBarTimer.add_Tick({ } }) $sync["SearchBar"].Add_TextChanged({ + if ($sync.SearchBar.Tag -ne $sync.SearchBar.Text) { + $sync.SearchBar.Tag = $null + } + if ($sync.SearchBar.Text -ne "") { $sync.SearchBarClearButton.Visibility = "Visible" $sync.SearchBarIcon.Visibility = "Collapsed" @@ -359,12 +363,30 @@ $sync["SearchBar"].Add_TextChanged({ $sync.SearchBarClearButton.Visibility = "Collapsed" $sync.SearchBarIcon.Visibility = "Visible" } + + # Category chip handlers apply their filter immediately. + if ($sync.SearchBar.Tag -eq $sync.SearchBar.Text) { + return + } + if ($searchBarTimer.IsEnabled) { $searchBarTimer.Stop() } $searchBarTimer.Start() }) +# Quick Category Search Chips +$sync["WPFSearchChipAll"].Add_Click({ Set-WinUtilAppCategoryFilter }) +$sync["WPFSearchChipBrowsers"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Browsers" }) +$sync["WPFSearchChipCommunications"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Communications" }) +$sync["WPFSearchChipDevelopment"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Development" }) +$sync["WPFSearchChipGames"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Games" }) +$sync["WPFSearchChipMicrosoftTools"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Microsoft Tools" }) +$sync["WPFSearchChipMultimediaTools"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Multimedia Tools" }) +$sync["WPFSearchChipProTools"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Pro Tools" }) +$sync["WPFSearchChipSelfhostedTools"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Selfhosted Tools" }) +$sync["WPFSearchChipUtilities"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Utilities" }) + $sync["Form"].Add_Loaded({ param($e) $null = $e diff --git a/xaml/inputXML.xaml b/xaml/inputXML.xaml index 50a2326e..127e8e41 100644 --- a/xaml/inputXML.xaml +++ b/xaml/inputXML.xaml @@ -1275,8 +1275,26 @@ + + + + - + + +