Skip to content

Commit 26949c4

Browse files
committed
combbox popup align
1 parent 1cb363b commit 26949c4

File tree

2 files changed

+227
-12
lines changed

2 files changed

+227
-12
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
using System;
2+
using System.Reflection;
3+
using System.Windows;
4+
using System.Windows.Controls;
5+
using System.Windows.Controls.Primitives;
6+
using System.Windows.Input;
7+
using iNKORE.UI.WPF.Converters;
8+
9+
namespace iNKORE.UI.WPF.Modern.Controls.Helpers
10+
{
11+
public sealed class WinUIComboBoxBehaviorHelper
12+
{
13+
private const string c_popupBorderName = "PopupBorder";
14+
15+
private const string c_editableTextName = "PART_EditableTextBox";
16+
17+
//private const string c_editableTextBorderName = "BorderElement";
18+
private const string c_backgroundName = "Background";
19+
20+
private const string c_highlightBackgroundName = "HighlightBackground";
21+
22+
//private const string c_controlCornerRadiusKey = "ControlCornerRadius";
23+
private const string c_overlayCornerRadiusKey = "OverlayCornerRadius";
24+
25+
/// <summary>
26+
/// Adds WinUI behaviors
27+
/// 1. align selected container in popup to combobox.
28+
/// 2. in case of no selection, first in popup is highlighted (done)
29+
/// 3. mouse hovering shouldn't trigger focus ?! (done)
30+
/// 4. persist selected item only when drop down closes?? (not done)
31+
/// 5. KeepInteriorCornersSquare (already done)
32+
/// </summary>
33+
public static readonly DependencyProperty IsEnabledProperty =
34+
DependencyProperty.RegisterAttached(
35+
"IsEnabled",
36+
typeof(bool),
37+
typeof(WinUIComboBoxBehaviorHelper),
38+
new PropertyMetadata(false, OnIsEnabledChanged));
39+
40+
public static bool GetIsEnabled(ComboBox comboBox)
41+
{
42+
return (bool)comboBox.GetValue(IsEnabledProperty);
43+
}
44+
45+
public static void SetIsEnabled(ComboBox comboBox, bool value)
46+
{
47+
comboBox.SetValue(IsEnabledProperty, value);
48+
}
49+
50+
private static void OnIsEnabledChanged(DependencyObject sender,
51+
DependencyPropertyChangedEventArgs args)
52+
{
53+
if (sender is ComboBox comboBox)
54+
{
55+
bool shouldMonitorDropDownState = (bool)args.NewValue;
56+
if (shouldMonitorDropDownState)
57+
{
58+
comboBox.DropDownOpened += OnDropDownOpened;
59+
comboBox.DropDownClosed += OnDropDownClosed;
60+
}
61+
else
62+
{
63+
comboBox.DropDownOpened -= OnDropDownOpened;
64+
comboBox.DropDownClosed -= OnDropDownClosed;
65+
}
66+
}
67+
}
68+
69+
private static void OnOverrideMouseEnter(object sender, MouseEventArgs e)
70+
{
71+
e.Handled = true;
72+
}
73+
74+
private static void OnDropDownClosed(object sender, object args)
75+
{
76+
var comboBox = (ComboBox)sender;
77+
UpdateCornerRadius(comboBox, null, /*IsDropDownOpen=*/false, false);
78+
}
79+
80+
private static void OnDropDownOpened(object sender, object args)
81+
{
82+
var comboBox = (ComboBox)sender;
83+
comboBox.Dispatcher.BeginInvoke(() =>
84+
{
85+
var popup = GetTemplateChild<Popup>("PART_Popup", comboBox);
86+
var isOpenDown = IsPopupOpenDown(comboBox, popup.VerticalOffset);
87+
88+
AlignSelectedContainer(comboBox, popup, isOpenDown);
89+
UpdateCornerRadius(comboBox, popup, true, isOpenDown);
90+
});
91+
}
92+
93+
private static void AlignSelectedContainer(ComboBox comboBox, Popup popup, bool isOpenDown)
94+
{
95+
if (!isOpenDown ||
96+
GetToAlignContainer(comboBox) is not { } itemContainer ||
97+
itemContainer.TranslatePoint(new Point(0, -itemContainer.ActualHeight + comboBox.Padding.Top),
98+
comboBox) is not { Y: not 0 } itemTop)
99+
{
100+
return;
101+
}
102+
103+
popup.VerticalOffset -= itemTop.Y;
104+
105+
if (itemContainer.ActualHeight - comboBox.ActualHeight > 0)
106+
{
107+
popup.VerticalOffset -= comboBox.ActualHeight;
108+
}
109+
}
110+
111+
private static FrameworkElement GetToAlignContainer(ComboBox comboBox)
112+
{
113+
DependencyObject container;
114+
if (comboBox.SelectedItem is null)
115+
{
116+
container = comboBox.ItemContainerGenerator.ContainerFromIndex(
117+
(int)Math.Ceiling(comboBox.Items.Count / 2.0));
118+
119+
if (comboBox.ItemContainerGenerator.ContainerFromIndex(0) is ComboBoxItem item)
120+
{
121+
var highlightedInfoProperty = typeof(ComboBox).GetProperty("HighlightedInfo",
122+
BindingFlags.Instance | BindingFlags.NonPublic);
123+
124+
var setter = highlightedInfoProperty.SetMethod;
125+
126+
var itemInfo = typeof(ComboBox)
127+
.GetMethod("ItemInfoFromContainer", BindingFlags.Instance | BindingFlags.NonPublic)?
128+
.Invoke(comboBox, [item]);
129+
130+
setter?.Invoke(comboBox, [itemInfo]);
131+
}
132+
}
133+
else
134+
{
135+
container = comboBox.ItemContainerGenerator.ContainerFromItem(comboBox.SelectedItem);
136+
}
137+
138+
return container as FrameworkElement;
139+
}
140+
141+
private static void UpdateCornerRadius(ComboBox comboBox, Popup? popup, bool isDropDownOpen, bool isOpenDown)
142+
{
143+
var textBoxRadius = ControlHelper.GetCornerRadius(comboBox);
144+
var popupRadius = (CornerRadius)ResourceLookup(comboBox, c_overlayCornerRadiusKey);
145+
146+
if (isDropDownOpen)
147+
{
148+
if (popup?.VerticalOffset is 0)
149+
{
150+
popupRadius = GetFilteredPopupRadius(popupRadius, isOpenDown);
151+
}
152+
153+
var textBoxRadiusFilter = isOpenDown ? CornerRadiusFilterKind.Top : CornerRadiusFilterKind.Bottom;
154+
textBoxRadius = CornerRadiusFilterConverter.Convert(textBoxRadius, textBoxRadiusFilter);
155+
}
156+
157+
if (GetTemplateChild<Border>(c_popupBorderName, comboBox) is { } popupBorder)
158+
{
159+
popupBorder.CornerRadius = popupRadius;
160+
}
161+
162+
if (comboBox.IsEditable)
163+
{
164+
if (GetTemplateChild<TextBox>(c_editableTextName, comboBox) is { } textBox)
165+
{
166+
ControlHelper.SetCornerRadius(textBox, textBoxRadius);
167+
}
168+
}
169+
else
170+
{
171+
if (GetTemplateChild<Border>(c_backgroundName, comboBox) is { } background)
172+
{
173+
background.CornerRadius = textBoxRadius;
174+
}
175+
176+
if (GetTemplateChild<Border>(c_highlightBackgroundName, comboBox) is { } highlightBackground)
177+
{
178+
highlightBackground.CornerRadius = textBoxRadius;
179+
}
180+
}
181+
}
182+
183+
private static CornerRadius GetFilteredPopupRadius(CornerRadius popupRadius, bool isOpenDown)
184+
{
185+
var popupRadiusFilter = isOpenDown ? CornerRadiusFilterKind.Bottom : CornerRadiusFilterKind.Top;
186+
return CornerRadiusFilterConverter.Convert(popupRadius, popupRadiusFilter);
187+
}
188+
189+
private static bool IsPopupOpenDown(ComboBox comboBox, double popupVerticalOffset)
190+
{
191+
double verticalOffset = popupVerticalOffset;
192+
if (GetTemplateChild<Border>(c_popupBorderName, comboBox) is { } popupBorder)
193+
{
194+
if (GetTemplateChild<TextBox>(c_editableTextName, comboBox) is { } textBox)
195+
{
196+
var popupTop = popupBorder.TranslatePoint(new Point(0, 0), textBox);
197+
verticalOffset = popupTop.Y;
198+
}
199+
}
200+
201+
return verticalOffset > popupVerticalOffset;
202+
}
203+
204+
private static object ResourceLookup(Control control, object key)
205+
{
206+
return control.TryFindResource(key);
207+
}
208+
209+
private static T GetTemplateChild<T>(string childName, Control control) where T : DependencyObject
210+
{
211+
return control.Template?.FindName(childName, control) as T;
212+
}
213+
}
214+
}

source/iNKORE.UI.WPF.Modern/Themes/Controls/ComboBox.xaml

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
55
xmlns:local="clr-namespace:iNKORE.UI.WPF.Modern.Controls"
66
xmlns:primitives="clr-namespace:iNKORE.UI.WPF.Modern.Controls.Primitives"
7-
xmlns:converters="clr-namespace:iNKORE.UI.WPF.Modern.Common.Converters"
8-
xmlns:sys="clr-namespace:System;assembly=mscorlib"
97
xmlns:chelper="clr-namespace:iNKORE.UI.WPF.Modern.Controls.Helpers"
108
xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf"
119
xmlns:common="clr-namespace:iNKORE.UI.WPF.Modern.Common">
@@ -53,7 +51,6 @@
5351
<Setter Property="KeyboardNavigation.TabNavigation" Value="Local" />
5452
<Setter Property="Padding" Value="{DynamicResource ComboBoxItemThemePadding}" />
5553
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
56-
<Setter Property="VerticalContentAlignment" Value="Center" />
5754
<Setter Property="FocusVisualStyle" Value="{DynamicResource {x:Static SystemParameters.FocusVisualStyleKey}}" />
5855
<Setter Property="chelper:FocusVisualHelper.FocusVisualMargin" Value="-3" />
5956
<Setter Property="chelper:FocusVisualHelper.UseSystemFocusVisuals" Value="True" />
@@ -146,7 +143,7 @@
146143
</MultiTrigger.Conditions>
147144
<Setter TargetName="LayoutRoot" Property="Background" Value="{DynamicResource ComboBoxItemBackgroundPointerOver}" />
148145
<Setter TargetName="LayoutRoot" Property="BorderBrush" Value="{DynamicResource ComboBoxItemBorderBrushPointerOver}" />
149-
<Setter Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundPointerOver}" />
146+
<Setter TargetName="ContentPresenter" Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundPointerOver}" />
150147
</MultiTrigger>
151148
<MultiTrigger>
152149
<MultiTrigger.Conditions>
@@ -155,7 +152,7 @@
155152
</MultiTrigger.Conditions>
156153
<Setter TargetName="LayoutRoot" Property="Background" Value="{DynamicResource ComboBoxItemBackgroundPressed}" />
157154
<Setter TargetName="LayoutRoot" Property="BorderBrush" Value="{DynamicResource ComboBoxItemBorderBrushPressed}" />
158-
<Setter Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundPressed}" />
155+
<Setter TargetName="ContentPresenter" Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundPressed}" />
159156
</MultiTrigger>
160157
<MultiTrigger>
161158
<MultiTrigger.Conditions>
@@ -164,14 +161,14 @@
164161
</MultiTrigger.Conditions>
165162
<Setter TargetName="LayoutRoot" Property="Background" Value="{DynamicResource ComboBoxItemBackgroundDisabled}" />
166163
<Setter TargetName="LayoutRoot" Property="BorderBrush" Value="{DynamicResource ComboBoxItemBorderBrushDisabled}" />
167-
<Setter Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundDisabled}" />
164+
<Setter TargetName="ContentPresenter" Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundDisabled}" />
168165
</MultiTrigger>
169166
<!-- Selected -->
170167
<Trigger Property="IsSelected" Value="True">
171168
<Setter TargetName="Pill" Property="Opacity" Value="1" />
172169
<Setter TargetName="LayoutRoot" Property="Background" Value="{DynamicResource ComboBoxItemBackgroundSelected}" />
173170
<Setter TargetName="LayoutRoot" Property="BorderBrush" Value="{DynamicResource ComboBoxItemBorderBrushSelected}" />
174-
<Setter Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundSelected}" />
171+
<Setter TargetName="ContentPresenter" Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundSelected}" />
175172
</Trigger>
176173
<!-- SelectedFocused -->
177174
<MultiTrigger>
@@ -189,39 +186,43 @@
189186
<Condition Property="IsSelected" Value="True" />
190187
<Condition Property="IsFocused" Value="False" />
191188
</MultiTrigger.Conditions>
189+
<Setter TargetName="Pill" Property="Opacity" Value="1" />
192190
<Setter TargetName="LayoutRoot" Property="Background" Value="{DynamicResource ComboBoxItemBackgroundSelectedUnfocused}" />
193191
<Setter TargetName="LayoutRoot" Property="BorderBrush" Value="{DynamicResource ComboBoxItemBorderBrushSelectedUnfocused}" />
194-
<Setter Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundSelectedUnfocused}" />
192+
<Setter TargetName="ContentPresenter" Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundSelectedUnfocused}" />
195193
</MultiTrigger>
196194
<!-- SelectedDisabled -->
197195
<MultiTrigger>
198196
<MultiTrigger.Conditions>
199197
<Condition Property="IsSelected" Value="True" />
200198
<Condition Property="IsEnabled" Value="False" />
201199
</MultiTrigger.Conditions>
200+
<Setter TargetName="Pill" Property="Opacity" Value="1" />
202201
<Setter TargetName="LayoutRoot" Property="Background" Value="{DynamicResource ComboBoxItemBackgroundSelectedDisabled}" />
203202
<Setter TargetName="LayoutRoot" Property="BorderBrush" Value="{DynamicResource ComboBoxItemBorderBrushSelectedDisabled}" />
204-
<Setter Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundSelectedDisabled}" />
203+
<Setter TargetName="ContentPresenter" Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundSelectedDisabled}" />
205204
</MultiTrigger>
206205
<!-- SelectedPointerOver -->
207206
<MultiTrigger>
208207
<MultiTrigger.Conditions>
209208
<Condition Property="IsSelected" Value="True" />
210209
<Condition Property="IsMouseOver" Value="True" />
211210
</MultiTrigger.Conditions>
211+
<Setter TargetName="Pill" Property="Opacity" Value="1" />
212212
<Setter TargetName="LayoutRoot" Property="Background" Value="{DynamicResource ComboBoxItemBackgroundSelectedPointerOver}" />
213213
<Setter TargetName="LayoutRoot" Property="BorderBrush" Value="{DynamicResource ComboBoxItemBorderBrushSelectedPointerOver}" />
214-
<Setter Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundSelectedPointerOver}" />
214+
<Setter TargetName="ContentPresenter" Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundSelectedPointerOver}" />
215215
</MultiTrigger>
216216
<!-- SelectedPressed -->
217217
<MultiTrigger>
218218
<MultiTrigger.Conditions>
219219
<Condition Property="IsSelected" Value="True" />
220220
<Condition SourceName="LayoutRoot" Property="chelper:PressHelper.IsPressed" Value="True" />
221221
</MultiTrigger.Conditions>
222+
<Setter TargetName="Pill" Property="Opacity" Value="1" />
222223
<Setter TargetName="LayoutRoot" Property="Background" Value="{DynamicResource ComboBoxItemBackgroundSelectedPressed}" />
223224
<Setter TargetName="LayoutRoot" Property="BorderBrush" Value="{DynamicResource ComboBoxItemBorderBrushSelectedPressed}" />
224-
<Setter Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundSelectedPressed}" />
225+
<Setter TargetName="ContentPresenter" Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundSelectedPressed}" />
225226
</MultiTrigger>
226227
</ControlTemplate.Triggers>
227228
</ControlTemplate>
@@ -333,7 +334,7 @@
333334
<Setter Property="chelper:ControlHelper.PlaceholderForeground" Value="{DynamicResource ComboBoxPlaceHolderForeground}" />
334335
<Setter Property="chelper:FocusVisualHelper.UseSystemFocusVisuals" Value="{DynamicResource IsApplicationFocusVisualKindReveal}" />
335336
<Setter Property="FocusVisualStyle" Value="{DynamicResource {x:Static SystemParameters.FocusVisualStyleKey}}" />
336-
<Setter Property="chelper:ComboBoxHelper.KeepInteriorCornersSquare" Value="True" />
337+
<Setter Property="chelper:WinUIComboBoxBehaviorHelper.IsEnabled" Value="True" />
337338
<Setter Property="chelper:ControlHelper.CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
338339
<Setter Property="Validation.ErrorTemplate" Value="{DynamicResource TextControlValidationErrorTemplate}" />
339340
<Setter Property="Template">

0 commit comments

Comments
 (0)