From 029884a85755044a676ff7ff4f84c08f1d078bb4 Mon Sep 17 00:00:00 2001 From: Nicolai Henriksen Date: Fri, 27 Jan 2023 11:24:33 +0100 Subject: [PATCH 1/2] 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. --- MainDemo.Wpf/Domain/MainWindowViewModel.cs | 9 + MainDemo.Wpf/Domain/SmartHintViewModel.cs | 80 +++ MainDemo.Wpf/SmartHint.xaml | 491 ++++++++++++++++++ MainDemo.Wpf/SmartHint.xaml.cs | 15 + .../FloatingHintTextBlockMarginConverter.cs | 116 +++++ .../FloatingHintHorizontalAlignment.cs | 10 + MaterialDesignThemes.Wpf/HintAssist.cs | 11 + .../Themes/MaterialDesignTheme.ComboBox.xaml | 7 +- .../MaterialDesignTheme.PasswordBox.xaml | 22 +- .../Themes/MaterialDesignTheme.SmartHint.xaml | 12 + .../Themes/MaterialDesignTheme.TextBox.xaml | 10 +- 11 files changed, 770 insertions(+), 13 deletions(-) create mode 100644 MainDemo.Wpf/Domain/SmartHintViewModel.cs create mode 100644 MainDemo.Wpf/SmartHint.xaml create mode 100644 MainDemo.Wpf/SmartHint.xaml.cs create mode 100644 MaterialDesignThemes.Wpf/Converters/FloatingHintTextBlockMarginConverter.cs create mode 100644 MaterialDesignThemes.Wpf/FloatingHintHorizontalAlignment.cs diff --git a/MainDemo.Wpf/Domain/MainWindowViewModel.cs b/MainDemo.Wpf/Domain/MainWindowViewModel.cs index f56ea9e4bd..2da13acc3b 100644 --- a/MainDemo.Wpf/Domain/MainWindowViewModel.cs +++ b/MainDemo.Wpf/Domain/MainWindowViewModel.cs @@ -419,6 +419,15 @@ private static IEnumerable GenerateDemoItems(ISnackbarMessageQueue sna DocumentationLink.StyleLink("Shadows"), DocumentationLink.SpecsLink("https://material.io/design/environment/elevation.html", "Elevation") }); + + yield return new DemoItem( + "Smart Hint", + typeof(SmartHint), + new[] + { + DocumentationLink.DemoPageLink(), + DocumentationLink.StyleLink("SmartHint"), + }); } private bool DemoItemsFilter(object obj) diff --git a/MainDemo.Wpf/Domain/SmartHintViewModel.cs b/MainDemo.Wpf/Domain/SmartHintViewModel.cs new file mode 100644 index 0000000000..3dd9470826 --- /dev/null +++ b/MainDemo.Wpf/Domain/SmartHintViewModel.cs @@ -0,0 +1,80 @@ +using MaterialDesignThemes.Wpf; + +namespace MaterialDesignDemo.Domain; + +internal class SmartHintViewModel : ViewModelBase +{ + private FloatingHintHorizontalAlignment _selectedAlignment = FloatingHintHorizontalAlignment.Inherit; + private double _selectedFloatingScale = 0.75; + private bool _showClearButton = true; + private bool _showLeadingIcon = true; + private string _hintText = "Hint text"; + private Point _selectedFloatingOffset = new (0, -16); + + public IEnumerable HorizontalAlignmentOptions { get; } = Enum.GetValues(typeof(FloatingHintHorizontalAlignment)).OfType(); + public IEnumerable FloatingScaleOptions { get; } = new[] {0.25, 0.5, 0.75, 1.0}; + + public IEnumerable FloatingOffsetOptions { get; } = new[] { new Point(0, -16), new Point(0, 16), new Point(16, 16), new Point(-16, -16) }; + + public IEnumerable ComboBoxOptions { get; } = new[] {"Option 1", "Option 2", "Option 3"}; + + public FloatingHintHorizontalAlignment SelectedAlignment + { + get => _selectedAlignment; + set + { + _selectedAlignment = value; + OnPropertyChanged(); + } + } + + public double SelectedFloatingScale + { + get => _selectedFloatingScale; + set + { + _selectedFloatingScale = value; + OnPropertyChanged(); + } + } + + public Point SelectedFloatingOffset + { + get => _selectedFloatingOffset; + set + { + _selectedFloatingOffset = value; + OnPropertyChanged(); + } + } + + public bool ShowClearButton + { + get => _showClearButton; + set + { + _showClearButton = value; + OnPropertyChanged(); + } + } + + public bool ShowLeadingIcon + { + get => _showLeadingIcon; + set + { + _showLeadingIcon = value; + OnPropertyChanged(); + } + } + + public string HintText + { + get => _hintText; + set + { + _hintText = value; + OnPropertyChanged(); + } + } +} diff --git a/MainDemo.Wpf/SmartHint.xaml b/MainDemo.Wpf/SmartHint.xaml new file mode 100644 index 0000000000..2441801fd3 --- /dev/null +++ b/MainDemo.Wpf/SmartHint.xaml @@ -0,0 +1,491 @@ + + + + + + + Github + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignFloatingHintTextBox + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignFilledTextBox + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignOutlinedTextBox + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignRichTextBox + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignFloatingHintPasswordBox + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignFilledPasswordBox + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignOutlinedPasswordBox + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignFloatingHintRevealPasswordBox + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignFilledRevealPasswordBox + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignOutlinedRevealPasswordBox + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignFloatingHintComboBox + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignFilledComboBox + + + + + + + + + + + + + + + + + + + + + + + + + + MaterialDesignOutlinedComboBox + + + + + + + + + + + + + + + + + diff --git a/MainDemo.Wpf/SmartHint.xaml.cs b/MainDemo.Wpf/SmartHint.xaml.cs new file mode 100644 index 0000000000..6fba47dd90 --- /dev/null +++ b/MainDemo.Wpf/SmartHint.xaml.cs @@ -0,0 +1,15 @@ +using MaterialDesignDemo.Domain; + +namespace MaterialDesignDemo; + +/// +/// Interaction logic for SmartHint.xaml +/// +public partial class SmartHint : UserControl +{ + public SmartHint() + { + DataContext = new SmartHintViewModel(); + InitializeComponent(); + } +} diff --git a/MaterialDesignThemes.Wpf/Converters/FloatingHintTextBlockMarginConverter.cs b/MaterialDesignThemes.Wpf/Converters/FloatingHintTextBlockMarginConverter.cs new file mode 100644 index 0000000000..9035dad285 --- /dev/null +++ b/MaterialDesignThemes.Wpf/Converters/FloatingHintTextBlockMarginConverter.cs @@ -0,0 +1,116 @@ +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; + +namespace MaterialDesignThemes.Wpf.Converters; + +internal class FloatingHintTextBlockMarginConverter : IMultiValueConverter +{ + public object? Convert(object?[]? values, Type targetType, object? parameter, CultureInfo culture) + { + if (values == null || values.Length != 7 || values.Any(v => v == null) + || values[0] is not FloatingHintHorizontalAlignment floatingAlignment + || values[1] is not HorizontalAlignment restingAlignment + || !double.TryParse(values[2]!.ToString(), out double desiredWidth) + || !double.TryParse(values[3]!.ToString(), out double availableWidth) + || !double.TryParse(values[4]!.ToString(), out double scale) + || !double.TryParse(values[5]!.ToString(), out double lower) + || !double.TryParse(values[6]!.ToString(), out double upper)) + { + return Transform.Identity; + } + + double scaleMultiplier = upper + (lower - upper) * scale; + + HorizontalAlignment alignment = restingAlignment; + if (scale != 0) + { + switch (floatingAlignment) + { + case FloatingHintHorizontalAlignment.Inherit: + alignment = restingAlignment; + break; + case FloatingHintHorizontalAlignment.Left: + alignment = HorizontalAlignment.Left; + break; + case FloatingHintHorizontalAlignment.Center: + alignment = HorizontalAlignment.Center; + break; + case FloatingHintHorizontalAlignment.Right: + alignment = HorizontalAlignment.Right; + break; + case FloatingHintHorizontalAlignment.Stretch: + alignment = HorizontalAlignment.Stretch; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + switch (alignment) + { + case HorizontalAlignment.Right: + return FloatRight(); + case HorizontalAlignment.Center: + return FloatCenter(); + default: + return FloatLeft(); + } + + Thickness FloatLeft() + { + if (restingAlignment == HorizontalAlignment.Center) + { + // Animate from center to left + double offset = Math.Max(0, (availableWidth - desiredWidth) / 2); + return new Thickness(offset - offset * scale, 0, 0, 0); + } + if (restingAlignment == HorizontalAlignment.Right) + { + // Animate from right to left + double offset = Math.Max(0, availableWidth - desiredWidth); + return new Thickness(offset - offset * scale, 0, 0, 0); + } + return new Thickness(0); + } + + Thickness FloatCenter() + { + if (restingAlignment == HorizontalAlignment.Left || restingAlignment == HorizontalAlignment.Stretch) + { + // Animate from left to center + double offset = Math.Max(0, (availableWidth - desiredWidth * scaleMultiplier) / 2); + return new Thickness(offset * scale, 0, 0, 0); + } + if (restingAlignment == HorizontalAlignment.Right) + { + // Animate from right to center + double startOffset = Math.Max(0, availableWidth - desiredWidth); + double endOffset = Math.Max(0, (availableWidth - desiredWidth) / 2); + double endOffsetDelta = startOffset - endOffset; + return new Thickness(endOffset + endOffsetDelta * (1 - scale), 0, 0, 0); + } + return new Thickness(Math.Max(0, availableWidth - desiredWidth * scaleMultiplier) / 2, 0, 0, 0); + } + + Thickness FloatRight() + { + if (restingAlignment == HorizontalAlignment.Left || restingAlignment == HorizontalAlignment.Stretch) + { + // Animate from left to right + double offset = Math.Max(0, availableWidth - desiredWidth * scaleMultiplier); + return new Thickness(offset * scale, 0, 0, 0); + } + if (restingAlignment == HorizontalAlignment.Center) + { + // Animate from center to right + double startOffset = Math.Max(0, (availableWidth - desiredWidth) / 2); + double endOffsetDelta = Math.Max(0, availableWidth - desiredWidth * scaleMultiplier) - startOffset; + return new Thickness(startOffset + endOffsetDelta * scale, 0, 0, 0); + } + return new Thickness(Math.Max(0, availableWidth - desiredWidth * scaleMultiplier), 0, 0, 0); + } + } + + public object?[]? ConvertBack(object? value, Type[] targetTypes, object? parameter, CultureInfo culture) + => throw new NotImplementedException(); +} diff --git a/MaterialDesignThemes.Wpf/FloatingHintHorizontalAlignment.cs b/MaterialDesignThemes.Wpf/FloatingHintHorizontalAlignment.cs new file mode 100644 index 0000000000..bd67e19c0c --- /dev/null +++ b/MaterialDesignThemes.Wpf/FloatingHintHorizontalAlignment.cs @@ -0,0 +1,10 @@ +namespace MaterialDesignThemes.Wpf; + +public enum FloatingHintHorizontalAlignment +{ + Inherit, + Left, + Center, + Right, + Stretch +} diff --git a/MaterialDesignThemes.Wpf/HintAssist.cs b/MaterialDesignThemes.Wpf/HintAssist.cs index 8941caa287..29802224de 100644 --- a/MaterialDesignThemes.Wpf/HintAssist.cs +++ b/MaterialDesignThemes.Wpf/HintAssist.cs @@ -66,6 +66,17 @@ public static void SetHintOpacity(DependencyObject element, double value) => element.SetValue(HintOpacityProperty, value); #endregion + #region AttachedProperty : FloatingHintHorizontalAlignment + public static readonly DependencyProperty FloatingHintHorizontalAlignmentProperty + = DependencyProperty.RegisterAttached("FloatingHintHorizontalAlignment", typeof(FloatingHintHorizontalAlignment), typeof(HintAssist), + new FrameworkPropertyMetadata(FloatingHintHorizontalAlignment.Inherit, FrameworkPropertyMetadataOptions.Inherits)); + + public static void SetFloatingHintHorizontalAlignment(DependencyObject element, FloatingHintHorizontalAlignment value) + => element.SetValue(FloatingHintHorizontalAlignmentProperty, value); + public static FloatingHintHorizontalAlignment GetFloatingHintHorizontalAlignment(DependencyObject element) + => (FloatingHintHorizontalAlignment) element.GetValue(FloatingHintHorizontalAlignmentProperty); + #endregion + #region AttachedProperty : HintFontFamilyProperty public static readonly DependencyProperty FontFamilyProperty = DependencyProperty.RegisterAttached("FontFamily", typeof(FontFamily), typeof(HintAssist), diff --git a/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.ComboBox.xaml b/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.ComboBox.xaml index de83e984d2..8a2b069c8e 100644 --- a/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.ComboBox.xaml +++ b/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.ComboBox.xaml @@ -326,7 +326,7 @@ Grid.Column="0" Padding="{TemplateBinding Padding}"> @@ -353,7 +353,7 @@ + UseLayoutRounding="{TemplateBinding UseLayoutRounding}" + HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"> - + + VerticalAlignment="Center" + HorizontalAlignment="Stretch"> @@ -155,6 +156,7 @@ wpf:ScrollViewerAssist.IgnorePadding="True" Cursor="{TemplateBinding Cursor, Converter={StaticResource IBeamCursorConverter}}" Focusable="false" + HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" HorizontalScrollBarVisibility="Hidden" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" UseLayoutRounding="{TemplateBinding UseLayoutRounding}" @@ -168,7 +170,8 @@ FontSize="{TemplateBinding FontSize}" HintOpacity="{TemplateBinding wpf:HintAssist.HintOpacity}" HintProxy="{Binding RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static converters:HintProxyFabricConverter.Instance}}" - UseFloating="{TemplateBinding wpf:HintAssist.IsFloating}"> + UseFloating="{TemplateBinding wpf:HintAssist.IsFloating}" + HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"> - + - + + VerticalAlignment="Center" + HorizontalAlignment="Stretch"> @@ -618,6 +622,7 @@ @@ -657,7 +664,8 @@ FontSize="{TemplateBinding FontSize}" HintOpacity="{TemplateBinding wpf:HintAssist.HintOpacity}" HintProxy="{Binding RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static converters:HintProxyFabricConverter.Instance}}" - UseFloating="{TemplateBinding wpf:HintAssist.IsFloating}"> + UseFloating="{TemplateBinding wpf:HintAssist.IsFloating}" + HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"> + 1.0 @@ -159,6 +160,17 @@ Opacity="{TemplateBinding HintOpacity}" RenderTransformOrigin="0,0" Visibility="{TemplateBinding UseFloating, Converter={StaticResource BoolToVisConverter}}"> + + + + + + + + + + + 0.0 diff --git a/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.TextBox.xaml b/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.TextBox.xaml index efc4d6bcda..669ad54900 100644 --- a/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.TextBox.xaml +++ b/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.TextBox.xaml @@ -127,7 +127,7 @@ BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding wpf:TextFieldAssist.TextFieldCornerRadius}" SnapsToDevicePixels="True"> - + @@ -145,7 +145,9 @@ + MinWidth="1" + VerticalAlignment="Center" + HorizontalAlignment="Stretch"> @@ -170,6 +172,7 @@ + UseFloating="{TemplateBinding wpf:HintAssist.IsFloating}" + HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"> Date: Mon, 30 Jan 2023 09:35:09 +0100 Subject: [PATCH 2/2] Update MaterialDesignThemes.Wpf/Converters/FloatingHintTextBlockMarginConverter.cs Co-authored-by: Kevin B --- .../Converters/FloatingHintTextBlockMarginConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MaterialDesignThemes.Wpf/Converters/FloatingHintTextBlockMarginConverter.cs b/MaterialDesignThemes.Wpf/Converters/FloatingHintTextBlockMarginConverter.cs index 9035dad285..0510d235bb 100644 --- a/MaterialDesignThemes.Wpf/Converters/FloatingHintTextBlockMarginConverter.cs +++ b/MaterialDesignThemes.Wpf/Converters/FloatingHintTextBlockMarginConverter.cs @@ -8,7 +8,7 @@ internal class FloatingHintTextBlockMarginConverter : IMultiValueConverter { public object? Convert(object?[]? values, Type targetType, object? parameter, CultureInfo culture) { - if (values == null || values.Length != 7 || values.Any(v => v == null) + if (values?.Length != 7 || values.Any(v => v == null) || values[0] is not FloatingHintHorizontalAlignment floatingAlignment || values[1] is not HorizontalAlignment restingAlignment || !double.TryParse(values[2]!.ToString(), out double desiredWidth)