Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions MainDemo.Wpf/Domain/SmartHintViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ internal class SmartHintViewModel : ViewModelBase
private bool _showClearButton = true;
private bool _showLeadingIcon = true;
private string _hintText = "Hint text";
private Point _selectedFloatingOffset = new (0, -16);
private string _helperText = "Helper text";
private Point _selectedFloatingOffset = new(0, -16);
private bool _applyCustomPadding;
private Thickness _selectedCustomPadding = new(5);

public IEnumerable<FloatingHintHorizontalAlignment> HorizontalAlignmentOptions { get; } = Enum.GetValues(typeof(FloatingHintHorizontalAlignment)).OfType<FloatingHintHorizontalAlignment>();
public IEnumerable<double> FloatingScaleOptions { get; } = new[] {0.25, 0.5, 0.75, 1.0};

public IEnumerable<Point> FloatingOffsetOptions { get; } = new[] { new Point(0, -16), new Point(0, 16), new Point(16, 16), new Point(-16, -16) };

public IEnumerable<string> ComboBoxOptions { get; } = new[] {"Option 1", "Option 2", "Option 3"};
public IEnumerable<Thickness> CustomPaddingOptions { get; } = new [] { new Thickness(0), new Thickness(5), new Thickness(10), new Thickness(15) };

public bool FloatHint
{
Expand Down Expand Up @@ -88,4 +90,34 @@ public string HintText
OnPropertyChanged();
}
}

public string HelperText
{
get => _helperText;
set
{
_helperText = value;
OnPropertyChanged();
}
}

public bool ApplyCustomPadding
{
get => _applyCustomPadding;
set
{
_applyCustomPadding = value;
OnPropertyChanged();
}
}

public Thickness SelectedCustomPadding
{
get => _selectedCustomPadding;
set
{
_selectedCustomPadding = value;
OnPropertyChanged();
}
}
}
439 changes: 425 additions & 14 deletions MainDemo.Wpf/SmartHint.xaml

Large diffs are not rendered by default.

86 changes: 85 additions & 1 deletion MainDemo.Wpf/SmartHint.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
using MaterialDesignDemo.Domain;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
using MaterialDesignDemo.Domain;
using MaterialDesignThemes.Wpf;

namespace MaterialDesignDemo;

Expand All @@ -7,9 +11,89 @@ namespace MaterialDesignDemo;
/// </summary>
public partial class SmartHint : UserControl
{
// Attached property used for binding in the RichTextBox to enable triggering of validation errors.
internal static readonly DependencyProperty RichTextBoxTextProperty = DependencyProperty.RegisterAttached(
"RichTextBoxText", typeof(object), typeof(SmartHint), new PropertyMetadata(null));
internal static void SetRichTextBoxText(DependencyObject element, object value) => element.SetValue(RichTextBoxTextProperty, value);
internal static object GetRichTextBoxText(DependencyObject element) => element.GetValue(RichTextBoxTextProperty);

public SmartHint()
{
DataContext = new SmartHintViewModel();
InitializeComponent();
}

private void HasErrors_OnToggled(object sender, RoutedEventArgs e)
{
CheckBox c = (CheckBox) sender;

foreach (FrameworkElement element in FindVisualChildren<FrameworkElement>(ControlsPanel))
{
var binding = GetBinding(element);
if (binding is null)
continue;

if (c.IsChecked.GetValueOrDefault(false))
{
var error = new ValidationError(new ExceptionValidationRule(), binding)
{
ErrorContent = "Invalid input, please fix it!"
};
Validation.MarkInvalid(binding, error);
}
else
{
Validation.ClearInvalid(binding);
}
}
}

private static BindingExpression? GetBinding(FrameworkElement element)
{
if (element is TextBox textBox)
return textBox.GetBindingExpression(TextBox.TextProperty);
if (element is RichTextBox richTextBox)
return richTextBox.GetBindingExpression(RichTextBoxTextProperty);
if (element is PasswordBox passwordBox)
return passwordBox.GetBindingExpression(PasswordBoxAssist.PasswordProperty);
if (element is ComboBox comboBox)
return comboBox.GetBindingExpression(ComboBox.TextProperty);
if (element is DatePicker datePicker)
return datePicker.GetBindingExpression(DatePicker.TextProperty);
if (element is TimePicker timePicker)
return timePicker.GetBindingExpression(TimePicker.TextProperty);
return default;
}

private static IEnumerable<T> FindVisualChildren<T>(DependencyObject? dependencyObject) where T : DependencyObject
{
if (dependencyObject is null)
yield break;

if (dependencyObject is T obj)
yield return obj;

for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObject); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(dependencyObject, i);
foreach (T childOfChild in FindVisualChildren<T>(child))
{
yield return childOfChild;
}
}
}
}

internal class CustomPaddingConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
Thickness? defaultPadding = values[0] as Thickness?;
bool applyCustomPadding = (bool) values[1];
Thickness customPadding = (Thickness) values[2];
return applyCustomPadding ? customPadding : defaultPadding ?? DependencyProperty.UnsetValue;
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
91 changes: 91 additions & 0 deletions MaterialDesignThemes.UITests/WPF/ComboBoxes/ComboBoxTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel;
using MaterialDesignThemes.UITests.WPF.TextBoxes;

namespace MaterialDesignThemes.UITests.WPF.ComboBoxes;

Expand Down Expand Up @@ -166,4 +167,94 @@ public async Task OnEditableComboBox_ClickInTextArea_FocusesTextBox()

recorder.Success();
}

[Theory]
[InlineData("MaterialDesignFloatingHintComboBox", null)]
[InlineData("MaterialDesignFloatingHintComboBox", 5)]
[InlineData("MaterialDesignFloatingHintComboBox", 20)]
[InlineData("MaterialDesignFilledComboBox", null)]
[InlineData("MaterialDesignFilledComboBox", 5)]
[InlineData("MaterialDesignFilledComboBox", 20)]
[InlineData("MaterialDesignOutlinedComboBox", null)]
[InlineData("MaterialDesignOutlinedComboBox", 5)]
[InlineData("MaterialDesignOutlinedComboBox", 20)]
public async Task ComboBox_WithHintAndHelperText_RespectsPadding(string styleName, int? padding)
{
await using var recorder = new TestRecorder(App);

// FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases.
const double tolerance = 1.5;

string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\"";
string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty;

var comboBox = await LoadXaml<ComboBox>($@"
<ComboBox {styleAttribute} {paddingAttribute}
materialDesign:HintAssist.Hint=""Hint text""
materialDesign:HintAssist.HelperText=""Helper text""
Width=""200"" VerticalAlignment=""Center"" HorizontalAlignment=""Center"" />
");

var contentHost = await comboBox.GetElement<ScrollViewer>("PART_ContentHost");
var hint = await comboBox.GetElement<SmartHint>("Hint");
var helperText = await comboBox.GetElement<TextBlock>("HelperTextTextBlock");

Rect? contentHostCoordinates = await contentHost.GetCoordinates();
Rect? hintCoordinates = await hint.GetCoordinates();
Rect? helperTextCoordinates = await helperText.GetCoordinates();

Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance);
Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - helperTextCoordinates.Value.Left), 0, tolerance);

recorder.Success();
}

[Theory]
[InlineData("MaterialDesignFloatingHintComboBox", null)]
[InlineData("MaterialDesignFloatingHintComboBox", 5)]
[InlineData("MaterialDesignFloatingHintComboBox", 20)]
[InlineData("MaterialDesignFilledComboBox", null)]
[InlineData("MaterialDesignFilledComboBox", 5)]
[InlineData("MaterialDesignFilledComboBox", 20)]
[InlineData("MaterialDesignOutlinedComboBox", null)]
[InlineData("MaterialDesignOutlinedComboBox", 5)]
[InlineData("MaterialDesignOutlinedComboBox", 20)]
public async Task ComboBox_WithHintAndValidationError_RespectsPadding(string styleName, int? padding)
{
await using var recorder = new TestRecorder(App);

// FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases.
const double tolerance = 1.5;

string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\"";
string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty;

var comboBox = await LoadXaml<ComboBox>($@"
<ComboBox {styleAttribute} {paddingAttribute}
materialDesign:HintAssist.Hint=""Hint text""
materialDesign:HintAssist.HelperText=""Helper text""
Width=""200"" VerticalAlignment=""Center"" HorizontalAlignment=""Center"">
<ComboBox.Text>
<Binding RelativeSource=""{{RelativeSource Self}}"" Path=""Tag"" UpdateSourceTrigger=""PropertyChanged"">
<Binding.ValidationRules>
<local:NotEmptyValidationRule ValidatesOnTargetUpdated=""True""/>
</Binding.ValidationRules>
</Binding>
</ComboBox.Text>
</ComboBox>
", ("local", typeof(NotEmptyValidationRule)));

var contentHost = await comboBox.GetElement<ScrollViewer>("PART_ContentHost");
var hint = await comboBox.GetElement<SmartHint>("Hint");
var errorViewer = await comboBox.GetElement<Border>("DefaultErrorViewer");

Rect? contentHostCoordinates = await contentHost.GetCoordinates();
Rect? hintCoordinates = await hint.GetCoordinates();
Rect? errorViewerCoordinates = await errorViewer.GetCoordinates();

Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance);
Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - errorViewerCoordinates.Value.Left), 0, tolerance);

recorder.Success();
}
}
91 changes: 91 additions & 0 deletions MaterialDesignThemes.UITests/WPF/DatePickers/DatePickerTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.ComponentModel;
using System.Globalization;
using MaterialDesignThemes.UITests.WPF.TextBoxes;

namespace MaterialDesignThemes.UITests.WPF.DatePickers;

Expand Down Expand Up @@ -112,6 +113,96 @@ public async Task OutlinedDatePicker_RespectsActiveAndInactiveBorderThickness_Wh

recorder.Success();
}

[Theory]
[InlineData("MaterialDesignFloatingHintDatePicker", null)]
[InlineData("MaterialDesignFloatingHintDatePicker", 5)]
[InlineData("MaterialDesignFloatingHintDatePicker", 20)]
[InlineData("MaterialDesignFilledDatePicker", null)]
[InlineData("MaterialDesignFilledDatePicker", 5)]
[InlineData("MaterialDesignFilledDatePicker", 20)]
[InlineData("MaterialDesignOutlinedDatePicker", null)]
[InlineData("MaterialDesignOutlinedDatePicker", 5)]
[InlineData("MaterialDesignOutlinedDatePicker", 20)]
public async Task DatePicker_WithHintAndHelperText_RespectsPadding(string styleName, int? padding)
{
await using var recorder = new TestRecorder(App);

// FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases.
const double tolerance = 1.5;

string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\"";
string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty;

var datePicker = await LoadXaml<DatePicker>($@"
<DatePicker {styleAttribute} {paddingAttribute}
materialDesign:HintAssist.Hint=""Hint text""
materialDesign:HintAssist.HelperText=""Helper text""
Width=""200"" VerticalAlignment=""Center"" HorizontalAlignment=""Center"" />
");

var contentHost = await datePicker.GetElement<ScrollViewer>("PART_ContentHost");
var hint = await datePicker.GetElement<SmartHint>("Hint");
var helperText = await datePicker.GetElement<TextBlock>("HelperTextTextBlock");

Rect? contentHostCoordinates = await contentHost.GetCoordinates();
Rect? hintCoordinates = await hint.GetCoordinates();
Rect? helperTextCoordinates = await helperText.GetCoordinates();

Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance);
Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - helperTextCoordinates.Value.Left), 0, tolerance);

recorder.Success();
}

[Theory]
[InlineData("MaterialDesignFloatingHintDatePicker", null)]
[InlineData("MaterialDesignFloatingHintDatePicker", 5)]
[InlineData("MaterialDesignFloatingHintDatePicker", 20)]
[InlineData("MaterialDesignFilledDatePicker", null)]
[InlineData("MaterialDesignFilledDatePicker", 5)]
[InlineData("MaterialDesignFilledDatePicker", 20)]
[InlineData("MaterialDesignOutlinedDatePicker", null)]
[InlineData("MaterialDesignOutlinedDatePicker", 5)]
[InlineData("MaterialDesignOutlinedDatePicker", 20)]
public async Task DatePicker_WithHintAndValidationError_RespectsPadding(string styleName, int? padding)
{
await using var recorder = new TestRecorder(App);

// FIXME: Tolerance needed because TextFieldAssist.TextBoxViewMargin is in play and slightly modifies the hint text placement in certain cases.
const double tolerance = 1.5;

string styleAttribute = $"Style=\"{{StaticResource {styleName}}}\"";
string paddingAttribute = padding.HasValue ? $"Padding=\"{padding.Value}\"" : string.Empty;

var datePicker = await LoadXaml<DatePicker>($@"
<DatePicker {styleAttribute} {paddingAttribute}
materialDesign:HintAssist.Hint=""Hint text""
materialDesign:HintAssist.HelperText=""Helper text""
Width=""200"" VerticalAlignment=""Center"" HorizontalAlignment=""Center"">
<DatePicker.Text>
<Binding RelativeSource=""{{RelativeSource Self}}"" Path=""Tag"" UpdateSourceTrigger=""PropertyChanged"">
<Binding.ValidationRules>
<local:NotEmptyValidationRule ValidatesOnTargetUpdated=""True""/>
</Binding.ValidationRules>
</Binding>
</DatePicker.Text>
</DatePicker>
", ("local", typeof(NotEmptyValidationRule)));

var contentHost = await datePicker.GetElement<ScrollViewer>("PART_ContentHost");
var hint = await datePicker.GetElement<SmartHint>("Hint");
var errorViewer = await datePicker.GetElement<Border>("DefaultErrorViewer");

Rect? contentHostCoordinates = await contentHost.GetCoordinates();
Rect? hintCoordinates = await hint.GetCoordinates();
Rect? errorViewerCoordinates = await errorViewer.GetCoordinates();

Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - hintCoordinates.Value.Left), 0, tolerance);
Assert.InRange(Math.Abs(contentHostCoordinates.Value.Left - errorViewerCoordinates.Value.Left), 0, tolerance);

recorder.Success();
}
}

public class FutureDateValidationRule : ValidationRule
Expand Down
Loading