From 32ab9f0ab02f8e609764b68ff590360ee496718f Mon Sep 17 00:00:00 2001 From: MyDrift Date: Sun, 9 Aug 2026 20:13:21 +0200 Subject: [PATCH] Turn the Install category filters into toggle chips (#4926) * Turn the Install category filters into toggle chips - Replace the filter buttons with toggles that show which ones are active - Add ctrl click to select more than one category - Keep the search box and the category filter independent of each other - Expand matching categories while a filter is active * Fix the filter interactions the category chips missed - Pass the selected categories to the lazy rendering batches, the old call used a parameter that no longer exists and threw while typing - Keep the category filter when switching tabs, the chips stayed checked while the filter itself was dropped - Put a category back to collapsed once the filter that expanded it is gone * Document the Install category filters - Add a guide section for the chips, ctrl click and clearing the filter - Note that search and the category chips apply together * Correct the category filter guide wording - Clearing by clicking the chip only applies when it is the only one selected - Only categories with matches expand, and only auto expanded ones re-collapse --- docs/src/content/docs/guides/application.mdx | 8 +- .../private/Find-AppsByNameOrDescription.ps1 | 66 +++++++++------- .../Initialize-InstallCategoryAppList.ps1 | 5 ++ .../private/Invoke-WinUtilAppCategoryChip.ps1 | 20 +++++ .../private/Set-WinUtilAppCategoryFilter.ps1 | 45 +++++++++-- .../Start-WinUtilInstallAppRendering.ps1 | 10 ++- .../private/Update-WinUtilAppCategoryChip.ps1 | 19 +++++ functions/public/Invoke-WPFTab.ps1 | 5 +- pester/install-rendering.Tests.ps1 | 4 +- pester/search-filter.Tests.ps1 | 76 ++++++++++++++++++- pester/xaml.Tests.ps1 | 8 +- scripts/main.ps1 | 54 +++++++------ xaml/inputXML.xaml | 71 +++++++++++++---- 13 files changed, 311 insertions(+), 80 deletions(-) create mode 100644 functions/private/Invoke-WinUtilAppCategoryChip.ps1 create mode 100644 functions/private/Update-WinUtilAppCategoryChip.ps1 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 ![Collapse or expand all categories](../../../assets/screenshots/install-pics/Collapse&ExpandCat.png) + +* 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 @@ - + - -