diff --git a/docs/src/content/docs/guides/application.mdx b/docs/src/content/docs/guides/application.mdx
index faa4d789..cd6497a2 100644
--- a/docs/src/content/docs/guides/application.mdx
+++ b/docs/src/content/docs/guides/application.mdx
@@ -42,6 +42,12 @@ Use the Applications tab to install, upgrade, uninstall, and review supported ap

+
+* Click a category chip at the top of the tab to show only that category. The chip stays highlighted while its filter is active.
+* Hold `Ctrl` and click to add more categories to the filter, or to remove one again.
+* Click `All`, or click the highlighted category again while it is the only one selected, to clear the filter.
+* Categories with matching results open while a filter is active. Ones that filtering opened for you go back to collapsed when you clear it, ones you opened yourself stay open.
+
* The `Selected Apps` counter in the sidebar shows how many applications are currently selected.
* Use it to keep track of your selection as you browse categories.
@@ -57,7 +63,7 @@ Use the Applications tab to install, upgrade, uninstall, and review supported ap
:::tip
-If you have trouble finding an application, press `Ctrl + F` and search for its name. The list filters as you type.
+If you have trouble finding an application, press `Ctrl + F` and search for its name. The list filters as you type. The search and the category chips work together, so you can search inside the categories you picked.
:::
:::note
diff --git a/functions/private/Find-AppsByNameOrDescription.ps1 b/functions/private/Find-AppsByNameOrDescription.ps1
index 96037237..44f4003c 100644
--- a/functions/private/Find-AppsByNameOrDescription.ps1
+++ b/functions/private/Find-AppsByNameOrDescription.ps1
@@ -1,21 +1,26 @@
function Find-AppsByNameOrDescription {
<#
.SYNOPSIS
- Searches through the Apps on the Install Tab and hides all entries that do not match the string
+ Filters the Install tab entries by search text and by category
.DESCRIPTION
- Filters application entries by name or description using literal string matching.
- Respects collapsed category state and handles null $sync gracefully.
+ 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.
+
+ 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
+ all the collapsed state the user set is restored.
.PARAMETER SearchString
- The string to be searched for. Wildcards are treated as literal characters.
+ The string to search for. Wildcards are treated as literal characters.
- .PARAMETER Category
- When provided, only applications in this exact category are shown.
+ .PARAMETER Categories
+ The categories to show. An empty or missing array shows all of them.
.NOTES
- Uses module-scope $sync (no parameter needed; inherits from caller's scope)
- - Performs literal matching (no wildcard expansion)
- Safely handles missing hashtable keys and null UI elements
- Protected by try/catch to prevent UI thread crashes
#>
@@ -24,7 +29,7 @@ function Find-AppsByNameOrDescription {
[string]$SearchString = "",
[Parameter(Mandatory = $false)]
- [string]$Category = ""
+ [string[]]$Categories = @()
)
# Validate that $sync exists and has required structure
@@ -43,21 +48,34 @@ function Find-AppsByNameOrDescription {
return
}
+ # Categories that filtering expanded on the user's behalf, so clearing the filter can undo it
+ if ($null -eq $sync.AppCategoryAutoExpanded) {
+ $sync.AppCategoryAutoExpanded = @{}
+ }
+
try {
- # Reset the visibility if the search string is empty or the search is cleared
- if ([string]::IsNullOrWhiteSpace($SearchString) -and [string]::IsNullOrWhiteSpace($Category)) {
+ $activeCategories = @($Categories | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
+ $hasSearch = -not [string]::IsNullOrWhiteSpace($SearchString)
+ $hasCategories = $activeCategories.Count -gt 0
+
+ # Nothing is filtered, so put every entry back and leave the collapsed categories collapsed
+ if (-not $hasSearch -and -not $hasCategories) {
$sync.ItemsControl.Items | ForEach-Object {
- # Each item is a StackPanel container
$_.Visibility = [Windows.Visibility]::Visible
if ($_.Children.Count -ge 2) {
$categoryLabel = $_.Children[0]
$wrapPanel = $_.Children[1]
- # Keep category label visible
$categoryLabel.Visibility = [Windows.Visibility]::Visible
- # Respect the collapsed state of categories (indicated by + prefix)
+ # A category that filtering expanded goes back to how the user left it
+ $categoryName = $categoryLabel.Content -replace '^[+-] ', ''
+ if ($sync.AppCategoryAutoExpanded.ContainsKey($categoryName)) {
+ $categoryLabel.Content = $categoryLabel.Content -replace "^- ", "+ "
+ $sync.AppCategoryAutoExpanded.Remove($categoryName)
+ }
+
if ($categoryLabel.Content -like "+*") {
$wrapPanel.Visibility = [Windows.Visibility]::Collapsed
}
@@ -65,7 +83,6 @@ function Find-AppsByNameOrDescription {
$wrapPanel.Visibility = [Windows.Visibility]::Visible
}
- # Show all apps within the category
$wrapPanel.Children | ForEach-Object {
$_.Visibility = [Windows.Visibility]::Visible
}
@@ -77,7 +94,6 @@ function Find-AppsByNameOrDescription {
# Escape wildcard characters for literal matching
$escapedSearchString = [System.Management.Automation.WildcardPattern]::Escape($SearchString)
- # Perform search
$sync.ItemsControl.Items | ForEach-Object {
# Each item is a StackPanel container with Children[0] = label, Children[1] = WrapPanel
if ($_.Children.Count -ge 2) {
@@ -85,12 +101,9 @@ function Find-AppsByNameOrDescription {
$wrapPanel = $_.Children[1]
$categoryHasMatch = $false
- # Keep category label visible
$categoryLabel.Visibility = [Windows.Visibility]::Visible
- # Search through apps in this category
foreach ($appControl in $wrapPanel.Children) {
- # Safely retrieve app entry from hashtable
$appTag = $appControl.Tag
$appEntry = $null
@@ -98,14 +111,13 @@ function Find-AppsByNameOrDescription {
$appEntry = $sync.configs.applicationsHashtable[$appTag]
}
- # Check if app matches search criteria
if ($null -ne $appEntry) {
- $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*"
+ $categoryMatch = -not $hasCategories -or $activeCategories -contains $appEntry.Category
+ $textMatch = -not $hasSearch -or
+ $appEntry.Content -like "*$escapedSearchString*" -or
+ $appEntry.Description -like "*$escapedSearchString*"
- if ($categoryMatch -or $contentMatch -or $descriptionMatch) {
- # Show the App and mark that this category has a match
+ if ($categoryMatch -and $textMatch) {
$appControl.Visibility = [Windows.Visibility]::Visible
$categoryHasMatch = $true
}
@@ -119,17 +131,17 @@ function Find-AppsByNameOrDescription {
}
}
- # If category has matches, show the WrapPanel and update the category label to expanded state
if ($categoryHasMatch) {
$wrapPanel.Visibility = [Windows.Visibility]::Visible
$_.Visibility = [Windows.Visibility]::Visible
- # Update category label to show expanded state (-)
+ # Expand it, otherwise the matches stay hidden behind a collapsed header.
+ # Remember that it was collapsed so clearing the filter can put it back.
if ($categoryLabel.Content -like "+*") {
$categoryLabel.Content = $categoryLabel.Content -replace "^\+ ", "- "
+ $sync.AppCategoryAutoExpanded[($categoryLabel.Content -replace '^- ', '')] = $true
}
}
else {
- # Hide the entire category container if no matches
$_.Visibility = [Windows.Visibility]::Collapsed
}
}
diff --git a/functions/private/Initialize-InstallCategoryAppList.ps1 b/functions/private/Initialize-InstallCategoryAppList.ps1
index c2e198af..66f45222 100644
--- a/functions/private/Initialize-InstallCategoryAppList.ps1
+++ b/functions/private/Initialize-InstallCategoryAppList.ps1
@@ -62,6 +62,11 @@ function Initialize-InstallCategoryAppList {
# The WrapPanel is the second child
$wrapPanel = $categoryContainer.Children[1]
+ # An explicit click wins over anything filtering expanded automatically
+ if ($sync.AppCategoryAutoExpanded) {
+ $sync.AppCategoryAutoExpanded.Remove(($categoryToggle.Content -replace '^[+-] ', ''))
+ }
+
# Toggle visibility
if ($wrapPanel.Visibility -eq [Windows.Visibility]::Visible) {
$wrapPanel.Visibility = [Windows.Visibility]::Collapsed
diff --git a/functions/private/Invoke-WinUtilAppCategoryChip.ps1 b/functions/private/Invoke-WinUtilAppCategoryChip.ps1
new file mode 100644
index 00000000..e08a5d65
--- /dev/null
+++ b/functions/private/Invoke-WinUtilAppCategoryChip.ps1
@@ -0,0 +1,20 @@
+function Invoke-WinUtilAppCategoryChip {
+ <#
+ .SYNOPSIS
+ Handles a click on an Install tab category chip
+
+ .DESCRIPTION
+ The chip carries its category in Tag, so every chip shares this handler. Holding ctrl
+ adds the category to the current selection instead of replacing it.
+
+ .PARAMETER Chip
+ The chip that was clicked
+ #>
+ param(
+ [Parameter(Mandatory)]
+ $Chip
+ )
+
+ $ctrlDown = [bool]([System.Windows.Input.Keyboard]::Modifiers -band [System.Windows.Input.ModifierKeys]::Control)
+ Set-WinUtilAppCategoryFilter -Category $Chip.Tag -Additive:$ctrlDown
+}
diff --git a/functions/private/Set-WinUtilAppCategoryFilter.ps1 b/functions/private/Set-WinUtilAppCategoryFilter.ps1
index dbc0e555..b01475c9 100644
--- a/functions/private/Set-WinUtilAppCategoryFilter.ps1
+++ b/functions/private/Set-WinUtilAppCategoryFilter.ps1
@@ -1,17 +1,50 @@
function Set-WinUtilAppCategoryFilter {
<#
.SYNOPSIS
- Applies an exact application category filter from an Install tab search chip.
+ Applies the Install tab category filter and syncs the chip states to it
+
+ .DESCRIPTION
+ The selection lives in $sync.SelectedAppCategories. An empty selection means every
+ category is shown, which is what the All chip represents. The category filter and the
+ search box are independent: this only touches categories, and the current search text
+ is reapplied on top.
.PARAMETER Category
- The application category to show. An empty value clears the filter.
+ The category to act on. An empty value clears the filter back to All.
+
+ .PARAMETER Additive
+ Toggles this category in or out of the current selection instead of replacing it.
+ Bound to ctrl click.
#>
param(
[Parameter(Mandatory = $false)]
- [string]$Category = ""
+ [string]$Category = "",
+
+ [Parameter(Mandatory = $false)]
+ [switch]$Additive
)
- $sync.SearchBar.Tag = $Category
- $sync.SearchBar.Text = $Category
- Find-AppsByNameOrDescription -SearchString $Category -Category $Category
+ if ($null -eq $sync.SelectedAppCategories) {
+ $sync.SelectedAppCategories = [System.Collections.Generic.List[string]]::new()
+ }
+ $selected = $sync.SelectedAppCategories
+
+ if ([string]::IsNullOrWhiteSpace($Category)) {
+ $selected.Clear()
+ } elseif ($Additive) {
+ if ($selected.Contains($Category)) {
+ [void]$selected.Remove($Category)
+ } else {
+ $selected.Add($Category)
+ }
+ } elseif ($selected.Count -eq 1 -and $selected.Contains($Category)) {
+ # Clicking the only active category again clears the filter
+ $selected.Clear()
+ } else {
+ $selected.Clear()
+ $selected.Add($Category)
+ }
+
+ Update-WinUtilAppCategoryChip
+ Find-AppsByNameOrDescription -SearchString $sync.SearchBar.Text -Categories $selected.ToArray()
}
diff --git a/functions/private/Start-WinUtilInstallAppRendering.ps1 b/functions/private/Start-WinUtilInstallAppRendering.ps1
index f3bf9da1..1290de0e 100644
--- a/functions/private/Start-WinUtilInstallAppRendering.ps1
+++ b/functions/private/Start-WinUtilInstallAppRendering.ps1
@@ -8,8 +8,14 @@ function Invoke-WinUtilInstallAppRenderBatch {
$sync.$appKey = Initialize-InstallAppEntry -TargetElement $CategoryBatch.TargetElement -AppKey $appKey
}
- if ($sync.currentTab -eq "Install" -and $sync.SearchBar -and -not [string]::IsNullOrWhiteSpace($sync.SearchBar.Text)) {
- Find-AppsByNameOrDescription -SearchString $sync.SearchBar.Text -Category $sync.SearchBar.Tag
+ # Entries render in batches, so a filter that is already active has to be applied to each new
+ # batch. Categories count as an active filter just like search text does.
+ if ($sync.currentTab -eq "Install" -and $sync.SearchBar) {
+ $selectedCategories = if ($sync.SelectedAppCategories) { $sync.SelectedAppCategories.ToArray() } else { @() }
+
+ if (-not [string]::IsNullOrWhiteSpace($sync.SearchBar.Text) -or $selectedCategories.Count -gt 0) {
+ Find-AppsByNameOrDescription -SearchString $sync.SearchBar.Text -Categories $selectedCategories
+ }
}
}
diff --git a/functions/private/Update-WinUtilAppCategoryChip.ps1 b/functions/private/Update-WinUtilAppCategoryChip.ps1
new file mode 100644
index 00000000..8248f8d4
--- /dev/null
+++ b/functions/private/Update-WinUtilAppCategoryChip.ps1
@@ -0,0 +1,19 @@
+function Update-WinUtilAppCategoryChip {
+ <#
+ .SYNOPSIS
+ Pushes the current category selection onto the Install tab filter chips
+
+ .DESCRIPTION
+ The chips are toggle buttons, so their checked state has to follow the selection
+ rather than whatever the last click did to them. The All chip is checked when no
+ category is selected.
+ #>
+ $selected = $sync.SelectedAppCategories
+ if ($null -eq $selected) { return }
+
+ foreach ($chip in $sync.AppCategoryChips) {
+ $control = $sync[$chip.Name]
+ if ($null -eq $control) { continue }
+ $control.IsChecked = if ($chip.Category) { $selected.Contains($chip.Category) } else { $selected.Count -eq 0 }
+ }
+}
diff --git a/functions/public/Invoke-WPFTab.ps1 b/functions/public/Invoke-WPFTab.ps1
index 43c7a06a..d0bac4f6 100644
--- a/functions/public/Invoke-WPFTab.ps1
+++ b/functions/public/Invoke-WPFTab.ps1
@@ -32,8 +32,9 @@ function Invoke-WPFTab {
# Always reset the filter for the current tab
if ($sync.currentTab -eq "Install") {
- # Reset Install tab filter
- Find-AppsByNameOrDescription -SearchString ""
+ # Reset the search text, but keep the categories the chips are still showing as selected
+ $selectedCategories = if ($sync.SelectedAppCategories) { $sync.SelectedAppCategories.ToArray() } else { @() }
+ Find-AppsByNameOrDescription -SearchString "" -Categories $selectedCategories
} elseif ($sync.currentTab -eq "Tweaks") {
# Reset Tweaks tab filter
Find-TweaksByNameOrDescription -SearchString ""
diff --git a/pester/install-rendering.Tests.ps1 b/pester/install-rendering.Tests.ps1
index 5c77f6a9..33398243 100644
--- a/pester/install-rendering.Tests.ps1
+++ b/pester/install-rendering.Tests.ps1
@@ -21,7 +21,9 @@ 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 'Find-AppsByNameOrDescription -SearchString \$sync\.SearchBar\.Text -Categories \$selectedCategories'
+ # A batch has to be filtered when either filter is on, not only when there is search text
+ $renderScript | Should -Match '\$selectedCategories\.Count -gt 0'
$renderScript | Should -Match '\$sync\.InstallAppEntriesRendered = \$true'
}
diff --git a/pester/search-filter.Tests.ps1 b/pester/search-filter.Tests.ps1
index 254ce8fd..74211424 100644
--- a/pester/search-filter.Tests.ps1
+++ b/pester/search-filter.Tests.ps1
@@ -379,12 +379,86 @@ Describe "Find-AppsByNameOrDescription" {
$category = New-WinUtilAppCategory -Label "- Tools" -Items @($utilityItem, $powerToysItem)
New-WinUtilAppSearchContext -Categories @($category)
- Find-AppsByNameOrDescription -SearchString "Utilities" -Category "Utilities"
+ Find-AppsByNameOrDescription -Categories @("Utilities")
$utilityItem.Visibility | Should -Be ([Windows.Visibility]::Visible)
$powerToysItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
$category.Visibility | Should -Be ([Windows.Visibility]::Visible)
}
+
+ It "shows apps from every selected category when several chips are active" {
+ $utilityItem = New-WinUtilAppSearchItem -Tag "WPFInstallLiteral"
+ $powerToysItem = New-WinUtilAppSearchItem -Tag "WPFInstallPowerToys"
+ $browserItem = New-WinUtilAppSearchItem -Tag "WPFInstallBrowser"
+ $category = New-WinUtilAppCategory -Label "- Tools" -Items @($utilityItem, $powerToysItem, $browserItem)
+ New-WinUtilAppSearchContext -Categories @($category)
+
+ Find-AppsByNameOrDescription -Categories @("Utilities", "Microsoft Tools")
+
+ $utilityItem.Visibility | Should -Be ([Windows.Visibility]::Visible)
+ $powerToysItem.Visibility | Should -Be ([Windows.Visibility]::Visible)
+ $browserItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
+ }
+
+ It "applies the search text and the category filter together" {
+ $powerToysItem = New-WinUtilAppSearchItem -Tag "WPFInstallPowerToys"
+ $literalItem = New-WinUtilAppSearchItem -Tag "WPFInstallLiteral"
+ $category = New-WinUtilAppCategory -Label "- Tools" -Items @($powerToysItem, $literalItem)
+ New-WinUtilAppSearchContext -Categories @($category)
+
+ Find-AppsByNameOrDescription -SearchString "PowerToys" -Categories @("Microsoft Tools")
+
+ $powerToysItem.Visibility | Should -Be ([Windows.Visibility]::Visible)
+ $literalItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
+ }
+
+ It "hides a category when the search text matches nothing inside the selected categories" {
+ $powerToysItem = New-WinUtilAppSearchItem -Tag "WPFInstallPowerToys"
+ $category = New-WinUtilAppCategory -Label "- Tools" -Items @($powerToysItem)
+ New-WinUtilAppSearchContext -Categories @($category)
+
+ Find-AppsByNameOrDescription -SearchString "Firefox" -Categories @("Microsoft Tools")
+
+ $powerToysItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
+ $category.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
+ }
+
+ It "expands a collapsed category that has matches for the selected filter" {
+ $powerToysItem = New-WinUtilAppSearchItem -Tag "WPFInstallPowerToys"
+ $category = New-WinUtilAppCategory -Label "+ Tools" -Items @($powerToysItem)
+ New-WinUtilAppSearchContext -Categories @($category)
+
+ Find-AppsByNameOrDescription -Categories @("Microsoft Tools")
+
+ $category.Children[0].Content | Should -Be "- Tools"
+ $category.Children[1].Visibility | Should -Be ([Windows.Visibility]::Visible)
+ }
+
+ It "re-collapses a category it expanded once the filter is cleared" {
+ $powerToysItem = New-WinUtilAppSearchItem -Tag "WPFInstallPowerToys"
+ $category = New-WinUtilAppCategory -Label "+ Tools" -Items @($powerToysItem)
+ New-WinUtilAppSearchContext -Categories @($category)
+
+ Find-AppsByNameOrDescription -Categories @("Microsoft Tools")
+ $category.Children[0].Content | Should -Be "- Tools"
+
+ Find-AppsByNameOrDescription -SearchString ""
+
+ $category.Children[0].Content | Should -Be "+ Tools"
+ $category.Children[1].Visibility | Should -Be ([Windows.Visibility]::Collapsed)
+ }
+
+ It "leaves a category the user had expanded alone when the filter is cleared" {
+ $powerToysItem = New-WinUtilAppSearchItem -Tag "WPFInstallPowerToys"
+ $category = New-WinUtilAppCategory -Label "- Tools" -Items @($powerToysItem)
+ New-WinUtilAppSearchContext -Categories @($category)
+
+ Find-AppsByNameOrDescription -Categories @("Microsoft Tools")
+ Find-AppsByNameOrDescription -SearchString ""
+
+ $category.Children[0].Content | Should -Be "- Tools"
+ $category.Children[1].Visibility | Should -Be ([Windows.Visibility]::Visible)
+ }
}
Describe "Find-TweaksByNameOrDescription" {
diff --git a/pester/xaml.Tests.ps1 b/pester/xaml.Tests.ps1
index 010ff099..5f9ce2fb 100644
--- a/pester/xaml.Tests.ps1
+++ b/pester/xaml.Tests.ps1
@@ -182,7 +182,8 @@ Describe "XAML document" {
It "wires the Document search chip to an existing Document category" {
$mainScript = Get-Content -Path $script:mainScriptPath -Raw
- $mainScript | Should -Match '\$sync\["WPFSearchChipDocument"\]\.Add_Click\(\{ Set-WinUtilAppCategoryFilter -Category "Document" \}\)'
+ $mainScript | Should -Match '@\{ Name = "WPFSearchChipDocument";\s+Category = "Document" \}'
+ $mainScript | Should -Match '\$sync\["WPFSearchChipDocument"\]\.Add_Click\(\{ Invoke-WinUtilAppCategoryChip -Chip \$this \}\)'
$applications = Get-WinUtilConfigObject -Name "applications"
$categories = @($applications.PSObject.Properties | ForEach-Object { $_.Value.category } | Sort-Object -Unique)
@@ -468,7 +469,10 @@ Describe "XAML and sync wiring" {
"Win11ISOProcessRunning",
"Win11ISOWorkDir",
"Win11ISOContentsDir",
- "Win11ISOUSBDisks"
+ "Win11ISOUSBDisks",
+ "AppCategoryChips",
+ "SelectedAppCategories",
+ "AppCategoryAutoExpanded"
)
$allowedNames = @($xamlNames + $generatedNames + $dynamicStateNames) | Sort-Object -Unique
$bracketReferences = @(
diff --git a/scripts/main.ps1 b/scripts/main.ps1
index 4d050052..a3770019 100644
--- a/scripts/main.ps1
+++ b/scripts/main.ps1
@@ -329,7 +329,7 @@ $searchBarTimer.add_Tick({
$searchBarTimer.Stop()
switch ($sync.currentTab) {
"Install" {
- Find-AppsByNameOrDescription -SearchString $sync.SearchBar.Text -Category $sync.SearchBar.Tag
+ Find-AppsByNameOrDescription -SearchString $sync.SearchBar.Text -Categories $sync.SelectedAppCategories.ToArray()
}
"Tweaks" {
Find-TweaksByNameOrDescription -SearchString $sync.SearchBar.Text
@@ -340,10 +340,6 @@ $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"
@@ -352,29 +348,43 @@ $sync["SearchBar"].Add_TextChanged({
$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["WPFSearchChipDocument"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Document" })
-$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" })
+# Category filter chips. The chip carries its category in Tag, so one handler covers all of them.
+$sync.AppCategoryChips = @(
+ @{ Name = "WPFSearchChipAll"; Category = "" }
+ @{ Name = "WPFSearchChipBrowsers"; Category = "Browsers" }
+ @{ Name = "WPFSearchChipCommunications"; Category = "Communications" }
+ @{ Name = "WPFSearchChipDevelopment"; Category = "Development" }
+ @{ Name = "WPFSearchChipDocument"; Category = "Document" }
+ @{ Name = "WPFSearchChipGames"; Category = "Games" }
+ @{ Name = "WPFSearchChipMicrosoftTools"; Category = "Microsoft Tools" }
+ @{ Name = "WPFSearchChipMultimediaTools"; Category = "Multimedia Tools" }
+ @{ Name = "WPFSearchChipProTools"; Category = "Pro Tools" }
+ @{ Name = "WPFSearchChipSelfhostedTools"; Category = "Selfhosted Tools" }
+ @{ Name = "WPFSearchChipUtilities"; Category = "Utilities" }
+)
+$sync.SelectedAppCategories = [System.Collections.Generic.List[string]]::new()
+
+foreach ($appCategoryChip in $sync.AppCategoryChips) {
+ $sync[$appCategoryChip.Name].Tag = $appCategoryChip.Category
+}
+
+$sync["WPFSearchChipAll"].Add_Click({ Invoke-WinUtilAppCategoryChip -Chip $this })
+$sync["WPFSearchChipBrowsers"].Add_Click({ Invoke-WinUtilAppCategoryChip -Chip $this })
+$sync["WPFSearchChipCommunications"].Add_Click({ Invoke-WinUtilAppCategoryChip -Chip $this })
+$sync["WPFSearchChipDevelopment"].Add_Click({ Invoke-WinUtilAppCategoryChip -Chip $this })
+$sync["WPFSearchChipDocument"].Add_Click({ Invoke-WinUtilAppCategoryChip -Chip $this })
+$sync["WPFSearchChipGames"].Add_Click({ Invoke-WinUtilAppCategoryChip -Chip $this })
+$sync["WPFSearchChipMicrosoftTools"].Add_Click({ Invoke-WinUtilAppCategoryChip -Chip $this })
+$sync["WPFSearchChipMultimediaTools"].Add_Click({ Invoke-WinUtilAppCategoryChip -Chip $this })
+$sync["WPFSearchChipProTools"].Add_Click({ Invoke-WinUtilAppCategoryChip -Chip $this })
+$sync["WPFSearchChipSelfhostedTools"].Add_Click({ Invoke-WinUtilAppCategoryChip -Chip $this })
+$sync["WPFSearchChipUtilities"].Add_Click({ Invoke-WinUtilAppCategoryChip -Chip $this })
$sync["Form"].Add_Loaded({
param($e)
diff --git a/xaml/inputXML.xaml b/xaml/inputXML.xaml
index a3667c56..b3b58d2d 100644
--- a/xaml/inputXML.xaml
+++ b/xaml/inputXML.xaml
@@ -997,6 +997,44 @@
+
+
@@ -1313,26 +1351,27 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+ Margin="10,0,10,0"
+ ToolTip="Filter by category. Ctrl click to select more than one."/>
+
+
+
+
+
+
+
+
+
+
+