From d26d5a171ef01bc44b18e48e55612da3799659f2 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 10 May 2026 14:53:03 +0200 Subject: [PATCH] Flags-column funnel always opens popup; header on top Two related fixes for the flags-filter UX. Assisted-by: Claude:claude-opus-4-7:Claude Code --- ILSpy/Controls/FlagsFilterPopup.cs | 44 ++++++++++++++----------- ILSpy/Metadata/MetadataColumnBuilder.cs | 41 +++++++++++++---------- 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/ILSpy/Controls/FlagsFilterPopup.cs b/ILSpy/Controls/FlagsFilterPopup.cs index f93b41a8a..f0abdcc34 100644 --- a/ILSpy/Controls/FlagsFilterPopup.cs +++ b/ILSpy/Controls/FlagsFilterPopup.cs @@ -53,6 +53,31 @@ namespace ILSpy.Views.Filters ApplyAccentPalette(Styles); var stack = new StackPanel { Orientation = Orientation.Vertical, Spacing = 4 }; + + // Header row at the top: hint on the left, Clear button on the right. Same + // content the popup used to put at the bottom — moved to the top so the + // "how to use this" text and the explicit clear affordance are both visible + // without scrolling past the chip groups. + var clearButton = new Button { + Content = "Clear", + MinHeight = 0, + Padding = new Thickness(6, 1), + VerticalAlignment = VerticalAlignment.Top, + }; + clearButton.Click += (_, _) => Clear(); + var chipHint = new TextBlock { + Text = "Click a chip to select only that value · Shift+Click to add to / remove from the selection · Click to clear", + FontStyle = FontStyle.Italic, + Foreground = Brushes.Gray, + TextWrapping = TextWrapping.Wrap, + VerticalAlignment = VerticalAlignment.Center, + }; + var headerRow = new DockPanel { LastChildFill = true, Margin = new Thickness(0, 0, 0, 4) }; + DockPanel.SetDock(clearButton, global::Avalonia.Controls.Dock.Right); + headerRow.Children.Add(clearButton); + headerRow.Children.Add(chipHint); + stack.Children.Add(headerRow); + foreach (var g in state.Schema.MutexGroups) { var chips = new MutexChipGroup(state, g); @@ -65,17 +90,6 @@ namespace ILSpy.Views.Filters stack.Children.Add(independentGroup); } - // Hint text in the same style as the live filter summary below — just a quiet - // italic line explaining the chip click semantics so users don't have to guess. - var chipHint = new TextBlock { - Text = "Click a chip to select only that value · Shift+Click to add to / remove from the selection · Click to clear", - FontStyle = FontStyle.Italic, - Foreground = Brushes.Gray, - TextWrapping = TextWrapping.Wrap, - Margin = new Thickness(0, 6, 0, 0), - }; - stack.Children.Add(chipHint); - summary = new TextBlock { FontStyle = FontStyle.Italic, Foreground = Brushes.Gray, @@ -84,14 +98,6 @@ namespace ILSpy.Views.Filters }; stack.Children.Add(summary); - var clearButton = new Button { - Content = "Clear", - HorizontalAlignment = HorizontalAlignment.Right, - Margin = new Thickness(0, 4, 0, 0), - }; - clearButton.Click += (_, _) => Clear(); - stack.Children.Add(clearButton); - Content = new Border { Padding = new Thickness(8), MinWidth = 220, diff --git a/ILSpy/Metadata/MetadataColumnBuilder.cs b/ILSpy/Metadata/MetadataColumnBuilder.cs index 5d2ef6fd0..cf1bf1137 100644 --- a/ILSpy/Metadata/MetadataColumnBuilder.cs +++ b/ILSpy/Metadata/MetadataColumnBuilder.cs @@ -197,23 +197,25 @@ namespace ILSpy.Metadata headerRow.Children.Add(headerContent); filterIconHost.PointerPressed += (_, e) => { - bool isActive = !string.IsNullOrWhiteSpace(filter.Text) - || (filter.FlagsState != null && !filter.FlagsState.IsEmpty); - if (isActive) - { - // Icon is an X right now — click clears the filter. Both branches raise - // the appropriate PropertyChanged so Update() swaps the icon back to the - // funnel and the row is re-evaluated. - filter.Text = string.Empty; - filter.FlagsState?.Clear(); - } - else if (popup != null) + if (popup != null) { + // Flag columns: the funnel always opens the popup. Modifying the filter + // requires the chip UI inside the popup, and clearing is owned by the + // popup's own Clear button — calling FlagsState.Clear() from out here + // would leave the chip IsChecked state stale because we'd skip the + // SyncFromState() pass that FlagsFilterPopup.Clear() does. popup.IsOpen = true; } - else if (textBox != null) + else { - textBox.Focus(); + // Text columns: the icon doubles as a clear button when a filter is active + // (icon visually morphs into an X via Update()). Click while active wipes + // the TextBox; click while inactive focuses it. + bool isActive = !string.IsNullOrWhiteSpace(filter.Text); + if (isActive) + filter.Text = string.Empty; + else if (textBox != null) + textBox.Focus(); } e.Handled = true; }; @@ -242,10 +244,13 @@ namespace ILSpy.Metadata } // Flag columns leave label IsVisible alone — it stays on for sort clicks while // the funnel icon owns the popup-open gesture. - // Active state swaps the funnel out for an X — icon becomes the "clear filter" - // affordance. Funnel uses Fill (closed shape); X is two crossing strokes, so - // it has no Fill and instead paints with Stroke. Toggle both modes in lockstep. - if (active) + + // Icon affordance: the X form indicates "click to clear". That only matches + // the click behaviour on text columns (where clicking when active does clear); + // for flag columns the click always opens the popup, so we keep the funnel + // shape and just tint it SteelBlue to signal the filter is active. + bool useXForm = active && popup is null; + if (useXForm) { filterIcon.Data = xGeometry; filterIcon.Fill = null; @@ -255,7 +260,7 @@ namespace ILSpy.Metadata else { filterIcon.Data = funnelGeometry; - filterIcon.Fill = Brushes.Gray; + filterIcon.Fill = active ? Brushes.SteelBlue : Brushes.Gray; filterIcon.Stroke = null; } }