From 60b8b796a3eeea82bbca7ef2089121cdbe0125be Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 10 May 2026 08:11:54 +0200 Subject: [PATCH] Drop dropdown chevron from flags column headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flag columns no longer carry a separate "▾" trigger button. The funnel icon already owns the popup-open gesture (clicking it opens the popup, click-while- active clears the filter), so the second affordance was redundant — and forced the same hover-swap dance text columns need to keep the column-name visible. With the chevron gone, the label stays visible at all times for flag columns; the popup is parked invisibly in the visual tree with PlacementTarget pointing at the funnel icon. Text columns keep the existing label/TextBox swap because they need somewhere to type. Assisted-by: Claude:claude-opus-4-7:Claude Code --- ILSpy/Metadata/MetadataColumnBuilder.cs | 80 ++++++++++++++----------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/ILSpy/Metadata/MetadataColumnBuilder.cs b/ILSpy/Metadata/MetadataColumnBuilder.cs index b39b9d18f..5d2ef6fd0 100644 --- a/ILSpy/Metadata/MetadataColumnBuilder.cs +++ b/ILSpy/Metadata/MetadataColumnBuilder.cs @@ -168,27 +168,33 @@ namespace ILSpy.Metadata && Attribute.IsDefined(propertyType, typeof(FlagsAttribute)); Popup? popup = null; - Control inputRow; + TextBox? textBox = null; + Control headerContent; if (isFlagsColumn) { - (inputRow, popup) = BuildFlagsDropdownTrigger(filter, propertyType); + // Flags columns: label stays visible always — the funnel icon alone is the + // popup trigger, no separate dropdown chevron needed since the popup carries + // the entire filter UI. PlacementTarget is the funnel itself so the popup + // drops below the icon's hit area. + popup = BuildFlagsPopup(filter, propertyType, filterIconHost); + headerContent = label; } else { - inputRow = BuildFilterTextBox(filter); + // Text columns: hover/focus reveal the TextBox in the same slot as the label. + // The label hides while the input is shown so the column header stays one row. + textBox = BuildFilterTextBox(filter); + textBox.IsVisible = false; + var swap = new Panel { HorizontalAlignment = HorizontalAlignment.Stretch }; + swap.Children.Add(label); + swap.Children.Add(textBox); + headerContent = swap; } - inputRow.IsVisible = false; - - // Both occupy the same slot; only one is visible at a time. Children inside a - // Panel overlap by default, so this is the cheapest way to swap. - var swap = new Panel { HorizontalAlignment = HorizontalAlignment.Stretch }; - swap.Children.Add(label); - swap.Children.Add(inputRow); var headerRow = new DockPanel { LastChildFill = true }; DockPanel.SetDock(filterIconHost, global::Avalonia.Controls.Dock.Right); headerRow.Children.Add(filterIconHost); - headerRow.Children.Add(swap); + headerRow.Children.Add(headerContent); filterIconHost.PointerPressed += (_, e) => { bool isActive = !string.IsNullOrWhiteSpace(filter.Text) @@ -205,9 +211,9 @@ namespace ILSpy.Metadata { popup.IsOpen = true; } - else if (inputRow is TextBox tb) + else if (textBox != null) { - tb.Focus(); + textBox.Focus(); } e.Handled = true; }; @@ -228,8 +234,14 @@ namespace ILSpy.Metadata bool active = !string.IsNullOrWhiteSpace(filter.Text) || (filter.FlagsState != null && !filter.FlagsState.IsEmpty); bool showInput = hovered || active || popupOpen || focusInside; - inputRow.IsVisible = showInput; - label.IsVisible = !showInput; + if (textBox != null) + { + // Text columns swap label / TextBox in the same slot. + textBox.IsVisible = showInput; + label.IsVisible = !showInput; + } + // 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. @@ -249,12 +261,17 @@ namespace ILSpy.Metadata } root.PointerEntered += (_, _) => { hovered = true; Update(); }; root.PointerExited += (_, _) => { hovered = false; Update(); }; - // Focus tracking: clicking the funnel calls inputRow.Focus(), and the user then - // reaches for the (now visible) TextBox. Without keeping the input visible while - // it has focus, the pointer leaving the header on the way to the TextBox would - // fire PointerExited and hide it again — making the TextBox unclickable. - inputRow.GotFocus += (_, _) => { focusInside = true; Update(); }; - inputRow.LostFocus += (_, _) => { focusInside = false; Update(); }; + if (textBox != null) + { + // Focus tracking: clicking the funnel calls textBox.Focus(), and the user + // then reaches for the (now visible) TextBox. Without keeping the input + // visible while it has focus, the pointer leaving the header on the way to + // the TextBox would fire PointerExited and hide it again — making the + // TextBox unclickable. Flag columns don't have a TextBox so this hook is + // a no-op for them. + textBox.GotFocus += (_, _) => { focusInside = true; Update(); }; + textBox.LostFocus += (_, _) => { focusInside = false; Update(); }; + } filter.PropertyChanged += (_, _) => Update(); if (popup != null) { @@ -291,23 +308,16 @@ namespace ILSpy.Metadata return box; } - static (Control Trigger, Popup Popup) BuildFlagsDropdownTrigger(ColumnFilter filter, Type enumType) + static Popup BuildFlagsPopup(ColumnFilter filter, Type enumType, Control placementTarget) { - // Schema-driven dropdown: FlagsFilterPopup distinguishes mutex sub-ranges + // Schema-driven popup: FlagsFilterPopup distinguishes mutex sub-ranges // (multi-select chips) from independent flags (tri-state pills) and drives - // ColumnFilter.FlagsState. No accompanying TextBox — the popup is the entire - // filter UI for [Flags] columns. + // ColumnFilter.FlagsState. The funnel icon owns the open gesture, so this + // helper only assembles the Popup itself — no separate trigger button needed. var schema = ILSpy.Metadata.Filters.FlagsSchemaInferer.For(enumType); filter.FlagsState ??= new ILSpy.Metadata.Filters.FilterState(schema); var popupContent = new ILSpy.Views.Filters.FlagsFilterPopup(filter.FlagsState); - var openButton = new Button { - Content = "▾", - Padding = new Thickness(4, 0), - MinHeight = 0, - HorizontalAlignment = HorizontalAlignment.Right, - VerticalAlignment = VerticalAlignment.Center, - }; var popupRoot = new Border { BorderBrush = Brushes.Gray, BorderThickness = new Thickness(1), @@ -329,14 +339,12 @@ namespace ILSpy.Metadata popupRoot.AddHandler(global::Avalonia.Input.InputElement.PointerWheelChangedEvent, (_, e) => e.Handled = true, handledEventsToo: true); - var popup = new Popup { - PlacementTarget = openButton, + return new Popup { + PlacementTarget = placementTarget, Placement = PlacementMode.BottomEdgeAlignedLeft, IsLightDismissEnabled = true, Child = popupRoot, }; - openButton.Click += (_, _) => popup.IsOpen = true; - return (openButton, popup); } static DataGridTextColumn BuildTextColumn(PropertyInfo prop, ColumnInfoAttribute? info)