Skip to content

Commit 607ee6a

Browse files
Refactor SmartHint alignment (#3056)
* Refactor SmartHint alignment Alignment now respects the HorizontalContentAlignment of the "owner"/"proxy". By default, the hint will float to the same alignment (left/center/right) as the HorizontalContentAlignment. Because I suspect there may be cases a user would want to float the hint differently that the resting position, I added an attached property HintAssist.FloatingHintHorizontalAlignment (inherited in the visual tree) which can be used to override the hint position when the hint is floated. By default, this property returns "Inherit" which means it follows the HorizontalContentAlignment, but it can also be set to Left/Center/Right/Stretch allowing explicit override of the default floating position. * Update MaterialDesignThemes.Wpf/Converters/FloatingHintTextBlockMarginConverter.cs Co-authored-by: Kevin B <[email protected]> --------- Co-authored-by: Kevin B <[email protected]>
1 parent a0bacbf commit 607ee6a

11 files changed

+770
-13
lines changed

MainDemo.Wpf/Domain/MainWindowViewModel.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,15 @@ private static IEnumerable<DemoItem> GenerateDemoItems(ISnackbarMessageQueue sna
419419
DocumentationLink.StyleLink("Shadows"),
420420
DocumentationLink.SpecsLink("https://material.io/design/environment/elevation.html", "Elevation")
421421
});
422+
423+
yield return new DemoItem(
424+
"Smart Hint",
425+
typeof(SmartHint),
426+
new[]
427+
{
428+
DocumentationLink.DemoPageLink<SmartHint>(),
429+
DocumentationLink.StyleLink("SmartHint"),
430+
});
422431
}
423432

424433
private bool DemoItemsFilter(object obj)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using MaterialDesignThemes.Wpf;
2+
3+
namespace MaterialDesignDemo.Domain;
4+
5+
internal class SmartHintViewModel : ViewModelBase
6+
{
7+
private FloatingHintHorizontalAlignment _selectedAlignment = FloatingHintHorizontalAlignment.Inherit;
8+
private double _selectedFloatingScale = 0.75;
9+
private bool _showClearButton = true;
10+
private bool _showLeadingIcon = true;
11+
private string _hintText = "Hint text";
12+
private Point _selectedFloatingOffset = new (0, -16);
13+
14+
public IEnumerable<FloatingHintHorizontalAlignment> HorizontalAlignmentOptions { get; } = Enum.GetValues(typeof(FloatingHintHorizontalAlignment)).OfType<FloatingHintHorizontalAlignment>();
15+
public IEnumerable<double> FloatingScaleOptions { get; } = new[] {0.25, 0.5, 0.75, 1.0};
16+
17+
public IEnumerable<Point> FloatingOffsetOptions { get; } = new[] { new Point(0, -16), new Point(0, 16), new Point(16, 16), new Point(-16, -16) };
18+
19+
public IEnumerable<string> ComboBoxOptions { get; } = new[] {"Option 1", "Option 2", "Option 3"};
20+
21+
public FloatingHintHorizontalAlignment SelectedAlignment
22+
{
23+
get => _selectedAlignment;
24+
set
25+
{
26+
_selectedAlignment = value;
27+
OnPropertyChanged();
28+
}
29+
}
30+
31+
public double SelectedFloatingScale
32+
{
33+
get => _selectedFloatingScale;
34+
set
35+
{
36+
_selectedFloatingScale = value;
37+
OnPropertyChanged();
38+
}
39+
}
40+
41+
public Point SelectedFloatingOffset
42+
{
43+
get => _selectedFloatingOffset;
44+
set
45+
{
46+
_selectedFloatingOffset = value;
47+
OnPropertyChanged();
48+
}
49+
}
50+
51+
public bool ShowClearButton
52+
{
53+
get => _showClearButton;
54+
set
55+
{
56+
_showClearButton = value;
57+
OnPropertyChanged();
58+
}
59+
}
60+
61+
public bool ShowLeadingIcon
62+
{
63+
get => _showLeadingIcon;
64+
set
65+
{
66+
_showLeadingIcon = value;
67+
OnPropertyChanged();
68+
}
69+
}
70+
71+
public string HintText
72+
{
73+
get => _hintText;
74+
set
75+
{
76+
_hintText = value;
77+
OnPropertyChanged();
78+
}
79+
}
80+
}

MainDemo.Wpf/SmartHint.xaml

Lines changed: 491 additions & 0 deletions
Large diffs are not rendered by default.

MainDemo.Wpf/SmartHint.xaml.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using MaterialDesignDemo.Domain;
2+
3+
namespace MaterialDesignDemo;
4+
5+
/// <summary>
6+
/// Interaction logic for SmartHint.xaml
7+
/// </summary>
8+
public partial class SmartHint : UserControl
9+
{
10+
public SmartHint()
11+
{
12+
DataContext = new SmartHintViewModel();
13+
InitializeComponent();
14+
}
15+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using System.Globalization;
2+
using System.Windows.Data;
3+
using System.Windows.Media;
4+
5+
namespace MaterialDesignThemes.Wpf.Converters;
6+
7+
internal class FloatingHintTextBlockMarginConverter : IMultiValueConverter
8+
{
9+
public object? Convert(object?[]? values, Type targetType, object? parameter, CultureInfo culture)
10+
{
11+
if (values?.Length != 7 || values.Any(v => v == null)
12+
|| values[0] is not FloatingHintHorizontalAlignment floatingAlignment
13+
|| values[1] is not HorizontalAlignment restingAlignment
14+
|| !double.TryParse(values[2]!.ToString(), out double desiredWidth)
15+
|| !double.TryParse(values[3]!.ToString(), out double availableWidth)
16+
|| !double.TryParse(values[4]!.ToString(), out double scale)
17+
|| !double.TryParse(values[5]!.ToString(), out double lower)
18+
|| !double.TryParse(values[6]!.ToString(), out double upper))
19+
{
20+
return Transform.Identity;
21+
}
22+
23+
double scaleMultiplier = upper + (lower - upper) * scale;
24+
25+
HorizontalAlignment alignment = restingAlignment;
26+
if (scale != 0)
27+
{
28+
switch (floatingAlignment)
29+
{
30+
case FloatingHintHorizontalAlignment.Inherit:
31+
alignment = restingAlignment;
32+
break;
33+
case FloatingHintHorizontalAlignment.Left:
34+
alignment = HorizontalAlignment.Left;
35+
break;
36+
case FloatingHintHorizontalAlignment.Center:
37+
alignment = HorizontalAlignment.Center;
38+
break;
39+
case FloatingHintHorizontalAlignment.Right:
40+
alignment = HorizontalAlignment.Right;
41+
break;
42+
case FloatingHintHorizontalAlignment.Stretch:
43+
alignment = HorizontalAlignment.Stretch;
44+
break;
45+
default:
46+
throw new ArgumentOutOfRangeException();
47+
}
48+
}
49+
switch (alignment)
50+
{
51+
case HorizontalAlignment.Right:
52+
return FloatRight();
53+
case HorizontalAlignment.Center:
54+
return FloatCenter();
55+
default:
56+
return FloatLeft();
57+
}
58+
59+
Thickness FloatLeft()
60+
{
61+
if (restingAlignment == HorizontalAlignment.Center)
62+
{
63+
// Animate from center to left
64+
double offset = Math.Max(0, (availableWidth - desiredWidth) / 2);
65+
return new Thickness(offset - offset * scale, 0, 0, 0);
66+
}
67+
if (restingAlignment == HorizontalAlignment.Right)
68+
{
69+
// Animate from right to left
70+
double offset = Math.Max(0, availableWidth - desiredWidth);
71+
return new Thickness(offset - offset * scale, 0, 0, 0);
72+
}
73+
return new Thickness(0);
74+
}
75+
76+
Thickness FloatCenter()
77+
{
78+
if (restingAlignment == HorizontalAlignment.Left || restingAlignment == HorizontalAlignment.Stretch)
79+
{
80+
// Animate from left to center
81+
double offset = Math.Max(0, (availableWidth - desiredWidth * scaleMultiplier) / 2);
82+
return new Thickness(offset * scale, 0, 0, 0);
83+
}
84+
if (restingAlignment == HorizontalAlignment.Right)
85+
{
86+
// Animate from right to center
87+
double startOffset = Math.Max(0, availableWidth - desiredWidth);
88+
double endOffset = Math.Max(0, (availableWidth - desiredWidth) / 2);
89+
double endOffsetDelta = startOffset - endOffset;
90+
return new Thickness(endOffset + endOffsetDelta * (1 - scale), 0, 0, 0);
91+
}
92+
return new Thickness(Math.Max(0, availableWidth - desiredWidth * scaleMultiplier) / 2, 0, 0, 0);
93+
}
94+
95+
Thickness FloatRight()
96+
{
97+
if (restingAlignment == HorizontalAlignment.Left || restingAlignment == HorizontalAlignment.Stretch)
98+
{
99+
// Animate from left to right
100+
double offset = Math.Max(0, availableWidth - desiredWidth * scaleMultiplier);
101+
return new Thickness(offset * scale, 0, 0, 0);
102+
}
103+
if (restingAlignment == HorizontalAlignment.Center)
104+
{
105+
// Animate from center to right
106+
double startOffset = Math.Max(0, (availableWidth - desiredWidth) / 2);
107+
double endOffsetDelta = Math.Max(0, availableWidth - desiredWidth * scaleMultiplier) - startOffset;
108+
return new Thickness(startOffset + endOffsetDelta * scale, 0, 0, 0);
109+
}
110+
return new Thickness(Math.Max(0, availableWidth - desiredWidth * scaleMultiplier), 0, 0, 0);
111+
}
112+
}
113+
114+
public object?[]? ConvertBack(object? value, Type[] targetTypes, object? parameter, CultureInfo culture)
115+
=> throw new NotImplementedException();
116+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace MaterialDesignThemes.Wpf;
2+
3+
public enum FloatingHintHorizontalAlignment
4+
{
5+
Inherit,
6+
Left,
7+
Center,
8+
Right,
9+
Stretch
10+
}

MaterialDesignThemes.Wpf/HintAssist.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,17 @@ public static void SetHintOpacity(DependencyObject element, double value)
6666
=> element.SetValue(HintOpacityProperty, value);
6767
#endregion
6868

69+
#region AttachedProperty : FloatingHintHorizontalAlignment
70+
public static readonly DependencyProperty FloatingHintHorizontalAlignmentProperty
71+
= DependencyProperty.RegisterAttached("FloatingHintHorizontalAlignment", typeof(FloatingHintHorizontalAlignment), typeof(HintAssist),
72+
new FrameworkPropertyMetadata(FloatingHintHorizontalAlignment.Inherit, FrameworkPropertyMetadataOptions.Inherits));
73+
74+
public static void SetFloatingHintHorizontalAlignment(DependencyObject element, FloatingHintHorizontalAlignment value)
75+
=> element.SetValue(FloatingHintHorizontalAlignmentProperty, value);
76+
public static FloatingHintHorizontalAlignment GetFloatingHintHorizontalAlignment(DependencyObject element)
77+
=> (FloatingHintHorizontalAlignment) element.GetValue(FloatingHintHorizontalAlignmentProperty);
78+
#endregion
79+
6980
#region AttachedProperty : HintFontFamilyProperty
7081
public static readonly DependencyProperty FontFamilyProperty
7182
= DependencyProperty.RegisterAttached("FontFamily", typeof(FontFamily), typeof(HintAssist),

MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.ComboBox.xaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@
326326
Grid.Column="0"
327327
Padding="{TemplateBinding Padding}">
328328
<Grid x:Name="InnerRoot"
329-
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
329+
HorizontalAlignment="Stretch"
330330
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
331331
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
332332
UseLayoutRounding="{TemplateBinding UseLayoutRounding}">
@@ -353,7 +353,7 @@
353353
<TextBox x:Name="PART_EditableTextBox"
354354
Grid.Column="1"
355355
MinWidth="10"
356-
HorizontalAlignment="Stretch"
356+
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
357357
HorizontalContentAlignment="Stretch"
358358
CaretBrush="{TemplateBinding Foreground}"
359359
IsReadOnly="{TemplateBinding IsReadOnly}"
@@ -372,7 +372,8 @@
372372
HintProxy="{Binding RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static converters:HintProxyFabricConverter.Instance}}"
373373
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
374374
UseFloating="{TemplateBinding wpf:HintAssist.IsFloating}"
375-
UseLayoutRounding="{TemplateBinding UseLayoutRounding}">
375+
UseLayoutRounding="{TemplateBinding UseLayoutRounding}"
376+
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}">
376377
<wpf:SmartHint.Hint>
377378
<Border x:Name="HintBackgroundBorder"
378379
Background="{TemplateBinding wpf:HintAssist.Background}"

MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.PasswordBox.xaml

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,15 @@
131131
BorderThickness="{TemplateBinding BorderThickness}"
132132
CornerRadius="{TemplateBinding wpf:TextFieldAssist.TextFieldCornerRadius}"
133133
SnapsToDevicePixels="True">
134-
<Grid HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
134+
<Grid HorizontalAlignment="Stretch" VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
135135
<Grid.ColumnDefinitions>
136136
<ColumnDefinition />
137137
<ColumnDefinition Width="Auto" />
138138
</Grid.ColumnDefinitions>
139139
<Grid x:Name="grid"
140140
MinWidth="1"
141-
VerticalAlignment="Center">
141+
VerticalAlignment="Center"
142+
HorizontalAlignment="Stretch">
142143
<Grid.ColumnDefinitions>
143144
<ColumnDefinition Width="Auto" />
144145
<ColumnDefinition Width="*" />
@@ -155,6 +156,7 @@
155156
wpf:ScrollViewerAssist.IgnorePadding="True"
156157
Cursor="{TemplateBinding Cursor, Converter={StaticResource IBeamCursorConverter}}"
157158
Focusable="false"
159+
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
158160
HorizontalScrollBarVisibility="Hidden"
159161
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
160162
UseLayoutRounding="{TemplateBinding UseLayoutRounding}"
@@ -168,7 +170,8 @@
168170
FontSize="{TemplateBinding FontSize}"
169171
HintOpacity="{TemplateBinding wpf:HintAssist.HintOpacity}"
170172
HintProxy="{Binding RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static converters:HintProxyFabricConverter.Instance}}"
171-
UseFloating="{TemplateBinding wpf:HintAssist.IsFloating}">
173+
UseFloating="{TemplateBinding wpf:HintAssist.IsFloating}"
174+
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}">
172175
<wpf:SmartHint.Hint>
173176
<Border x:Name="HintBackgroundBorder"
174177
Background="{TemplateBinding wpf:HintAssist.Background}"
@@ -595,15 +598,16 @@
595598
BorderThickness="{TemplateBinding BorderThickness}"
596599
CornerRadius="{TemplateBinding wpf:TextFieldAssist.TextFieldCornerRadius}"
597600
SnapsToDevicePixels="True">
598-
<Grid HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
601+
<Grid HorizontalAlignment="Stretch" VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
599602
<Grid.ColumnDefinitions>
600-
<ColumnDefinition />
603+
<ColumnDefinition Width="*" />
601604
<ColumnDefinition Width="Auto" />
602605
<ColumnDefinition Width="Auto" />
603606
</Grid.ColumnDefinitions>
604607
<Grid x:Name="grid"
605608
MinWidth="1"
606-
VerticalAlignment="Center">
609+
VerticalAlignment="Center"
610+
HorizontalAlignment="Stretch">
607611
<Grid.ColumnDefinitions>
608612
<ColumnDefinition Width="Auto" />
609613
<ColumnDefinition Width="*" />
@@ -618,6 +622,7 @@
618622
<Grid x:Name="ContentGrid"
619623
Grid.Column="1"
620624
Panel.ZIndex="1"
625+
HorizontalAlignment="Stretch"
621626
wpf:PasswordBoxAssist.IsPasswordRevealed="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(wpf:PasswordBoxAssist.IsPasswordRevealed)}">
622627

623628
<ScrollViewer x:Name="PART_ContentHost"
@@ -628,6 +633,7 @@
628633
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
629634
UseLayoutRounding="{TemplateBinding UseLayoutRounding}"
630635
VerticalScrollBarVisibility="Hidden"
636+
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
631637
Visibility="{Binding ElementName=ContentGrid, Path=(wpf:PasswordBoxAssist.IsPasswordRevealed), Converter={StaticResource InverseBooleanToVisibilityConverter}}" />
632638
<TextBox x:Name="RevealPasswordTextBox"
633639
Grid.Column="0"
@@ -639,6 +645,7 @@
639645
SelectionBrush="{TemplateBinding SelectionBrush}"
640646
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
641647
Style="{StaticResource MaterialDesignRawTextBox}"
648+
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
642649
Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(wpf:PasswordBoxAssist.Password), UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
643650
UseLayoutRounding="{TemplateBinding UseLayoutRounding}"
644651
Visibility="{Binding ElementName=ContentGrid, Path=(wpf:PasswordBoxAssist.IsPasswordRevealed), Converter={StaticResource BooleanToVisibilityConverter}}">
@@ -657,7 +664,8 @@
657664
FontSize="{TemplateBinding FontSize}"
658665
HintOpacity="{TemplateBinding wpf:HintAssist.HintOpacity}"
659666
HintProxy="{Binding RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static converters:HintProxyFabricConverter.Instance}}"
660-
UseFloating="{TemplateBinding wpf:HintAssist.IsFloating}">
667+
UseFloating="{TemplateBinding wpf:HintAssist.IsFloating}"
668+
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}">
661669
<wpf:SmartHint.Hint>
662670
<Border x:Name="HintBackgroundBorder"
663671
Background="{TemplateBinding wpf:HintAssist.Background}"

0 commit comments

Comments
 (0)