Skip to content

Commit 4279bb7

Browse files
[SmartHint] Improvement for multi-line TextBox hint positioning (#3681)
* Extend SmartHint demo page to reproduce multi-line issue Signed-off-by: Nicolai Henriksen <[email protected]> * Expose TextBox.LineCount as an attached property TextBox.LineCount is not a DP on the TextBox control. So we add a behavior that can be attached which will listen for text changes and update the AP accordingly. This allows us to bind to TextFieldAssist.LineCount where needed. Signed-off-by: Nicolai Henriksen <[email protected]> * Add AutoSuggestBox to SmartHint demo page Signed-off-by: Nicolai Henriksen <[email protected]> * Modify initial vert offset converter to use lineCount in calculations Signed-off-by: Nicolai Henriksen <[email protected]> * Apply LineCount AP in relevant styles Signed-off-by: Nicolai Henriksen <[email protected]> * Change default RichTextBox.VerticalContentAlignment for best fit with floating hint Signed-off-by: Nicolai Henriksen <[email protected]> * Align AcceptsReturn=True and TextWrapping=Wrap behaviors * Remove left-over debug code --------- Signed-off-by: Nicolai Henriksen <[email protected]>
1 parent a1f2fd8 commit 4279bb7

9 files changed

+361
-15
lines changed

src/MainDemo.Wpf/Domain/SmartHintViewModel.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ internal class SmartHintViewModel : ViewModelBase
4444
private ScrollBarVisibility _selectedHorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
4545
private Thickness _outlineStyleBorderThickness = new(1);
4646
private Thickness _outlineStyleActiveBorderThickness = new(2);
47+
private TextWrapping _textBoxTextWrapping = TextWrapping.Wrap;
48+
private double _selectedMaxWidth = 200;
4749

4850
public IEnumerable<FloatingHintHorizontalAlignment> HorizontalAlignmentOptions { get; } = Enum.GetValues(typeof(FloatingHintHorizontalAlignment)).OfType<FloatingHintHorizontalAlignment>();
4951
public IEnumerable<double> FloatingScaleOptions { get; } = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0];
@@ -59,6 +61,9 @@ internal class SmartHintViewModel : ViewModelBase
5961
public IEnumerable<PrefixSuffixHintBehavior> PrefixSuffixHintBehaviorOptions { get; } = Enum.GetValues(typeof(PrefixSuffixHintBehavior)).OfType<PrefixSuffixHintBehavior>();
6062
public IEnumerable<ScrollBarVisibility> ScrollBarVisibilityOptions { get; } = Enum.GetValues(typeof(ScrollBarVisibility)).OfType<ScrollBarVisibility>();
6163
public IEnumerable<Thickness> CustomOutlineStyleBorderThicknessOptions { get; } = [new Thickness(1), new Thickness(2), new Thickness(3), new Thickness(4), new Thickness(5), new Thickness(6) ];
64+
public IEnumerable<TextWrapping> TextWrappingOptions { get; } = Enum.GetValues(typeof(TextWrapping)).OfType<TextWrapping>();
65+
public IEnumerable<double> MaxWidthOptions { get; } = [double.NaN, 200];
66+
public IEnumerable<string> AutoSuggestBoxSuggestions { get; } = ["alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel", "india", "juliette", "kilo", "lima"];
6267

6368
public bool FloatHint
6469
{
@@ -281,4 +286,16 @@ public Thickness OutlineStyleActiveBorderThickness
281286
get => _outlineStyleActiveBorderThickness;
282287
set => SetProperty(ref _outlineStyleActiveBorderThickness, value);
283288
}
289+
290+
public TextWrapping TextBoxTextWrapping
291+
{
292+
get => _textBoxTextWrapping;
293+
set => SetProperty(ref _textBoxTextWrapping, value);
294+
}
295+
296+
public double SelectedMaxWidth
297+
{
298+
get => _selectedMaxWidth;
299+
set => SetProperty(ref _selectedMaxWidth, value);
300+
}
284301
}

src/MainDemo.Wpf/SmartHint.xaml

Lines changed: 226 additions & 4 deletions
Large diffs are not rendered by default.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Microsoft.Xaml.Behaviors;
2+
3+
namespace MaterialDesignThemes.Wpf.Behaviors;
4+
5+
/// <summary>
6+
/// Internal behavior exposing the <see cref="TextBox.LineCount"/> (non-DP) as an attached property which we can bind to
7+
/// </summary>
8+
internal class TextBoxLineCountBehavior : Behavior<TextBox>
9+
{
10+
private void AssociatedObjectOnTextChanged(object sender, TextChangedEventArgs e)
11+
{
12+
AssociatedObject.SetCurrentValue(TextFieldAssist.LineCountProperty, AssociatedObject.LineCount);
13+
AssociatedObject.SetCurrentValue(TextFieldAssist.IsMultiLineProperty, AssociatedObject.LineCount > 1);
14+
}
15+
16+
protected override void OnAttached()
17+
{
18+
base.OnAttached();
19+
AssociatedObject.TextChanged += AssociatedObjectOnTextChanged;
20+
}
21+
22+
protected override void OnDetaching()
23+
{
24+
if (AssociatedObject != null)
25+
{
26+
AssociatedObject.TextChanged -= AssociatedObjectOnTextChanged;
27+
}
28+
base.OnDetaching();
29+
}
30+
}

src/MaterialDesignThemes.Wpf/Converters/FloatingHintInitialVerticalOffsetConverter.cs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,36 @@
33

44
namespace MaterialDesignThemes.Wpf.Converters;
55

6+
/// <summary>
7+
/// This converter is used to apply an initial vertical offset (downwards) of the floating hint in the case where the
8+
/// <see cref="SmartHint.FloatingTarget"/> is taller than the <see cref="SmartHint"/> itself. This is typically the case
9+
/// if a fixed (large) height is applied to the host control (e.g. <see cref="TextBox"/> or similar). In these cases the
10+
/// hint should not float directly on top of the <see cref="SmartHint.FloatingTarget"/>, but rather be pushed down to sit
11+
/// on top of the text inside the <see cref="SmartHint.FloatingTarget"/>.
12+
///
13+
/// There is an edge case that need to be dealt with, which is when the host element allows for text to wrap (i.e. in
14+
/// <see cref="TextBox"/> based templates). In this case, we need to take the number of text rows/line count into account
15+
/// in the calculation.
16+
/// </summary>
617
public class FloatingHintInitialVerticalOffsetConverter : IMultiValueConverter
718
{
819
public object? Convert(object?[]? values, Type targetType, object? parameter, CultureInfo culture)
920
{
10-
if (values is [double contentHostHeight, double hintHeight])
21+
if (values is [double contentHostHeight, double hintHeight, int lineCount])
1122
{
12-
return (contentHostHeight - hintHeight) / 2;
23+
double offsetMultiplier = 0;
24+
if (lineCount > 1)
25+
{
26+
// Edge case where there are multiple rows of text so we need to calculate how far the hint should be pushed down.
27+
// If there are 2 rows, we need to reduce the offset by 0.5*height, 3 rows should reduce by 1*height, 4 rows should reduce by 1.5*height, etc.
28+
offsetMultiplier = lineCount / 2.0 - 0.5;
29+
}
30+
// Set an initial offset in order to push the hint down to where the actual text is displayed.
31+
// The value is clamped to be >= 0 which is needed for TextBoxes where a vertical scrollbar is needed (i.e. more lines
32+
// that are actually visible on screen) to avoid moving the hint further away than the actual viewport.
33+
return Math.Max(0, (contentHostHeight - hintHeight) / 2 - (offsetMultiplier * hintHeight));
1334
}
14-
return 0;
35+
return 0.0;
1536
}
1637

1738
public object?[] ConvertBack(object? value, Type[] targetTypes, object? parameter, CultureInfo culture)

src/MaterialDesignThemes.Wpf/TextFieldAssist.cs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -356,13 +356,31 @@ private static void PasswordBoxOnPasswordChanged(object sender, RoutedEventArgs
356356

357357
internal static readonly DependencyProperty PasswordBoxCharacterCountProperty = DependencyProperty.RegisterAttached(
358358
"PasswordBoxCharacterCount", typeof(int), typeof(TextFieldAssist), new PropertyMetadata(default(int)));
359-
internal static void SetPasswordBoxCharacterCount(DependencyObject element, int value) => element.SetValue(PasswordBoxCharacterCountProperty, value);
360-
internal static int GetPasswordBoxCharacterCount(DependencyObject element) => (int)element.GetValue(PasswordBoxCharacterCountProperty);
359+
internal static void SetPasswordBoxCharacterCount(DependencyObject element, int value)
360+
=> element.SetValue(PasswordBoxCharacterCountProperty, value);
361+
internal static int GetPasswordBoxCharacterCount(DependencyObject element)
362+
=> (int)element.GetValue(PasswordBoxCharacterCountProperty);
361363

362364
public static readonly DependencyProperty OutlinedBorderActiveThicknessProperty = DependencyProperty.RegisterAttached(
363365
"OutlinedBorderActiveThickness", typeof(Thickness), typeof(TextFieldAssist), new FrameworkPropertyMetadata(Constants.DefaultOutlinedBorderActiveThickness, FrameworkPropertyMetadataOptions.Inherits));
364-
public static void SetOutlinedBorderActiveThickness(DependencyObject element, Thickness value) => element.SetValue(OutlinedBorderActiveThicknessProperty, value);
365-
public static Thickness GetOutlinedBorderActiveThickness(DependencyObject element) => (Thickness)element.GetValue(OutlinedBorderActiveThicknessProperty);
366+
public static void SetOutlinedBorderActiveThickness(DependencyObject element, Thickness value)
367+
=> element.SetValue(OutlinedBorderActiveThicknessProperty, value);
368+
public static Thickness GetOutlinedBorderActiveThickness(DependencyObject element)
369+
=> (Thickness)element.GetValue(OutlinedBorderActiveThicknessProperty);
370+
371+
internal static readonly DependencyProperty LineCountProperty = DependencyProperty.RegisterAttached(
372+
"LineCount", typeof(int), typeof(TextFieldAssist), new PropertyMetadata(0));
373+
internal static void SetLineCount(DependencyObject element, int value)
374+
=> element.SetValue(LineCountProperty, value);
375+
internal static int GetLineCount(DependencyObject element)
376+
=> (int) element.GetValue(LineCountProperty);
377+
378+
internal static readonly DependencyProperty IsMultiLineProperty = DependencyProperty.RegisterAttached(
379+
"IsMultiLine", typeof(bool), typeof(TextFieldAssist), new PropertyMetadata(false));
380+
internal static void SetIsMultiLine(DependencyObject element, bool value)
381+
=> element.SetValue(IsMultiLineProperty, value);
382+
internal static bool GetIsMultiLine(DependencyObject element)
383+
=> (bool) element.GetValue(IsMultiLineProperty);
366384

367385
#region Methods
368386

src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.AutoSuggestBox.xaml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
33
xmlns:converters="clr-namespace:MaterialDesignThemes.Wpf.Converters"
44
xmlns:internal="clr-namespace:MaterialDesignThemes.Wpf.Internal"
5-
xmlns:wpf="clr-namespace:MaterialDesignThemes.Wpf">
5+
xmlns:wpf="clr-namespace:MaterialDesignThemes.Wpf"
6+
xmlns:behaviors="clr-namespace:MaterialDesignThemes.Wpf.Behaviors">
67
<ResourceDictionary.MergedDictionaries>
78
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.TextBox.xaml" />
89
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Card.xaml" />
@@ -323,7 +324,14 @@
323324
</MultiTrigger.Conditions>
324325
<Setter TargetName="PrefixTextBlock" Property="VerticalAlignment" Value="Stretch" />
325326
<Setter TargetName="SuffixTextBlock" Property="VerticalAlignment" Value="Stretch" />
326-
<Setter TargetName="Hint" Property="VerticalAlignment" Value="Top" />
327+
</MultiTrigger>
328+
<MultiTrigger>
329+
<MultiTrigger.Conditions>
330+
<Condition Property="VerticalContentAlignment" Value="Stretch" />
331+
<Condition Property="wpf:TextFieldAssist.IsMultiLine" Value="True" />
332+
</MultiTrigger.Conditions>
333+
<Setter TargetName="PrefixTextBlock" Property="VerticalAlignment" Value="Stretch" />
334+
<Setter TargetName="SuffixTextBlock" Property="VerticalAlignment" Value="Stretch" />
327335
</MultiTrigger>
328336

329337
<!-- Floating hint -->
@@ -508,6 +516,7 @@
508516
<MultiBinding Converter="{StaticResource FloatingHintInitialVerticalOffsetConverter}">
509517
<Binding ElementName="PART_ContentHost" Path="ActualHeight" />
510518
<Binding ElementName="Hint" Path="ActualHeight" />
519+
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="(wpf:TextFieldAssist.LineCount)" />
511520
</MultiBinding>
512521
</Setter.Value>
513522
</Setter>
@@ -580,6 +589,13 @@
580589
TargetType="{x:Type wpf:AutoSuggestBox}"
581590
BasedOn="{StaticResource MaterialDesignAutoSuggestBoxBase}">
582591
<Setter Property="Padding" Value="{x:Static wpf:Constants.TextBoxDefaultPadding}" />
592+
<Setter Property="wpf:BehaviorsAssist.Behaviors">
593+
<Setter.Value>
594+
<wpf:BehaviorCollection>
595+
<behaviors:TextBoxLineCountBehavior />
596+
</wpf:BehaviorCollection>
597+
</Setter.Value>
598+
</Setter>
583599
</Style>
584600

585601
<Style x:Key="MaterialDesignFloatingHintAutoSuggestBox"

src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.PasswordBox.xaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<Style x:Key="MaterialDesignPasswordBox" TargetType="{x:Type PasswordBox}">
1616
<Style.Resources>
1717
<system:Boolean x:Key="TrueValue">True</system:Boolean>
18+
<system:Int32 x:Key="One">1</system:Int32>
1819
<converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
1920
<converters:CursorConverter x:Key="ArrowCursorConverter" FallbackCursor="Arrow" />
2021
<converters:CursorConverter x:Key="IBeamCursorConverter" FallbackCursor="IBeam" />
@@ -484,6 +485,7 @@
484485
<MultiBinding Converter="{StaticResource FloatingHintInitialVerticalOffsetConverter}">
485486
<Binding ElementName="PART_ContentHost" Path="ActualHeight" />
486487
<Binding ElementName="Hint" Path="ActualHeight" />
488+
<Binding Source="{StaticResource One}" />
487489
</MultiBinding>
488490
</Setter.Value>
489491
</Setter>
@@ -572,6 +574,7 @@
572574
<Style x:Key="MaterialDesignRevealPasswordBox" TargetType="{x:Type PasswordBox}">
573575
<Style.Resources>
574576
<system:Boolean x:Key="TrueValue">True</system:Boolean>
577+
<system:Int32 x:Key="One">1</system:Int32>
575578
<converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
576579
<converters:BooleanToVisibilityConverter x:Key="InverseBooleanToVisibilityConverter"
577580
FalseValue="Visible"
@@ -1123,6 +1126,7 @@
11231126
<MultiBinding Converter="{StaticResource FloatingHintInitialVerticalOffsetConverter}">
11241127
<Binding ElementName="PART_ContentHost" Path="ActualHeight" />
11251128
<Binding ElementName="Hint" Path="ActualHeight" />
1129+
<Binding Source="{StaticResource One}" />
11261130
</MultiBinding>
11271131
</Setter.Value>
11281132
</Setter>

src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.RichTextBox.xaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
<Setter Property="wpf:TextFieldAssist.CharacterCounterStyle" Value="{x:Null}" />
1212
<Setter Property="wpf:TextFieldAssist.TextBoxViewMargin" Value="-4 0 1 0" />
1313
<Setter Property="Padding" Value="{x:Static wpf:Constants.TextBoxDefaultPadding}" />
14+
<!-- VerticalContentAlignment=Center is the best default value for RichTextBox when it comes to handling floating hint placement -->
15+
<Setter Property="VerticalContentAlignment" Value="Center" />
1416
</Style>
1517

1618
<Style x:Key="MaterialDesignFloatingHintRichTextBox"

src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.TextBox.xaml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
33
xmlns:converters="clr-namespace:MaterialDesignThemes.Wpf.Converters"
44
xmlns:internal="clr-namespace:MaterialDesignThemes.Wpf.Internal"
5-
xmlns:wpf="clr-namespace:MaterialDesignThemes.Wpf">
5+
xmlns:wpf="clr-namespace:MaterialDesignThemes.Wpf"
6+
xmlns:behaviors="clr-namespace:MaterialDesignThemes.Wpf.Behaviors">
67
<ResourceDictionary.MergedDictionaries>
78
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ValidationErrorTemplate.xaml" />
89
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Button.xaml" />
@@ -335,7 +336,14 @@
335336
</MultiTrigger.Conditions>
336337
<Setter TargetName="PrefixTextBlock" Property="VerticalAlignment" Value="Stretch" />
337338
<Setter TargetName="SuffixTextBlock" Property="VerticalAlignment" Value="Stretch" />
338-
<Setter TargetName="Hint" Property="VerticalAlignment" Value="Top" />
339+
</MultiTrigger>
340+
<MultiTrigger>
341+
<MultiTrigger.Conditions>
342+
<Condition Property="VerticalContentAlignment" Value="Stretch" />
343+
<Condition Property="wpf:TextFieldAssist.IsMultiLine" Value="True" />
344+
</MultiTrigger.Conditions>
345+
<Setter TargetName="PrefixTextBlock" Property="VerticalAlignment" Value="Stretch" />
346+
<Setter TargetName="SuffixTextBlock" Property="VerticalAlignment" Value="Stretch" />
339347
</MultiTrigger>
340348

341349
<!-- Floating hint -->
@@ -514,6 +522,7 @@
514522
<MultiBinding Converter="{StaticResource FloatingHintInitialVerticalOffsetConverter}">
515523
<Binding ElementName="PART_ContentHost" Path="ActualHeight" />
516524
<Binding ElementName="Hint" Path="ActualHeight" />
525+
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="(wpf:TextFieldAssist.LineCount)" />
517526
</MultiBinding>
518527
</Setter.Value>
519528
</Setter>
@@ -591,6 +600,13 @@
591600
TargetType="{x:Type TextBox}"
592601
BasedOn="{StaticResource MaterialDesignTextBoxBase}">
593602
<Setter Property="Padding" Value="{x:Static wpf:Constants.TextBoxDefaultPadding}" />
603+
<Setter Property="wpf:BehaviorsAssist.Behaviors">
604+
<Setter.Value>
605+
<wpf:BehaviorCollection>
606+
<behaviors:TextBoxLineCountBehavior />
607+
</wpf:BehaviorCollection>
608+
</Setter.Value>
609+
</Setter>
594610
</Style>
595611

596612
<Style x:Key="MaterialDesignFloatingHintTextBox"

0 commit comments

Comments
 (0)