Skip to content

Commit

Permalink
fix: Clean-up NumberBox: focus repeat event-args debug-output cursor (#…
Browse files Browse the repository at this point in the history
…1328)

* fix the NumberBox focus, keyboard, repeat isses, ValueChanged event, and debuggingcode

* fix: build warning for not documenting a constructor param

* Update NumberBoxValueChangedEventArgs.cs

---------

Co-authored-by: pomian <[email protected]>
  • Loading branch information
JohnTasler and pomianowski authored Feb 1, 2025
1 parent 1cf0e0c commit 5db3c71
Show file tree
Hide file tree
Showing 4 changed files with 326 additions and 162 deletions.
104 changes: 92 additions & 12 deletions src/Wpf.Ui/Controls/Button/Button.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,78 @@
<Thickness x:Key="ButtonBorderThemeThickness">1</Thickness>
<Thickness x:Key="ButtonIconMargin">0,0,8,0</Thickness>

<Style x:Key="DefaultRepeatButtonStyle" TargetType="{x:Type RepeatButton}">
<!-- Universal WPF UI focus -->
<Setter Property="FocusVisualStyle" Value="{DynamicResource DefaultControlFocusVisualStyle}" />
<!-- Universal WPF UI focus -->
<Setter Property="Background" Value="{DynamicResource ButtonBackground}" />
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
<Setter Property="BorderBrush" Value="{DynamicResource ControlElevationBorderBrush}" />
<Setter Property="BorderThickness" Value="{StaticResource ButtonBorderThemeThickness}" />
<Setter Property="Padding" Value="{StaticResource ButtonPadding}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Focusable" Value="False" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="Border.CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RepeatButton}">
<Border
x:Name="ContentBorder"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
MinWidth="{TemplateBinding MinWidth}"
MinHeight="{TemplateBinding MinHeight}"
Padding="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding Border.CornerRadius}">
<ContentPresenter
x:Name="ContentPresenter"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
TextElement.Foreground="{TemplateBinding Foreground}" />
</Border>
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsPressed" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="ContentBorder" Property="Background" Value="{DynamicResource ButtonBackgroundPointerOver}" />
<Setter TargetName="ContentBorder" Property="BorderBrush" Value="{DynamicResource ControlElevationBorderBrush}" />
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsPressed" Value="True" />
</MultiTrigger.Conditions>
<Setter TargetName="ContentBorder" Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter TargetName="ContentBorder" Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPressed}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</MultiTrigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="ContentBorder" Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter TargetName="ContentBorder" Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<Style x:Key="DefaultButtonStyle" TargetType="{x:Type Button}">
<!-- Universal WPF UI focus -->
<Setter Property="FocusVisualStyle" Value="{DynamicResource DefaultControlFocusVisualStyle}" />
Expand Down Expand Up @@ -109,21 +181,28 @@
-->
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource ControlElevationBorderBrush}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsPressed" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="ContentBorder" Property="Background" Value="{DynamicResource ButtonBackgroundPointerOver}" />
<Setter TargetName="ContentBorder" Property="BorderBrush" Value="{DynamicResource ControlElevationBorderBrush}" />
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsPressed" Value="True" />
</MultiTrigger.Conditions>
<Setter TargetName="ContentBorder" Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter TargetName="ContentBorder" Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPressed}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</MultiTrigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
<Setter TargetName="ContentBorder" Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter TargetName="ContentBorder" Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPressed}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
Expand Down Expand Up @@ -397,6 +476,7 @@
</Style.Triggers>
</Style>

<Style BasedOn="{StaticResource DefaultRepeatButtonStyle}" TargetType="{x:Type RepeatButton}" />
<Style BasedOn="{StaticResource DefaultButtonStyle}" TargetType="{x:Type Button}" />
<Style BasedOn="{StaticResource DefaultUiButtonStyle}" TargetType="{x:Type controls:Button}" />

Expand Down
142 changes: 94 additions & 48 deletions src/Wpf.Ui/Controls/NumberBox/NumberBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@

// This Source Code is partially based on the source code provided by the .NET Foundation.

/* TODO: Mask (with placeholder); Clipboard paste; */
/* TODO: Constant decimals when formatting. Although this can actually be done with NumberFormatter. */
/* TODO: Disable expression by default */
/* TODO: Lock to digit characters only by property */
// TODO: Mask (with placeholder); Clipboard paste;
// TODO: Constant decimals when formatting. Although this can actually be done with NumberFormatter.
// TODO: Disable expression by default
// TODO: Lock to digit characters only by property

using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;

Expand All @@ -19,8 +20,16 @@ namespace Wpf.Ui.Controls;
/// <summary>
/// Represents a control that can be used to display and edit numbers.
/// </summary>
public class NumberBox : Wpf.Ui.Controls.TextBox
[TemplatePart(Name = PART_ClearButton, Type = typeof(Button))]
[TemplatePart(Name = PART_InlineIncrementButton, Type = typeof(RepeatButton))]
[TemplatePart(Name = PART_InlineDecrementButton, Type = typeof(RepeatButton))]
public partial class NumberBox : Wpf.Ui.Controls.TextBox
{
// Template part names
private const string PART_ClearButton = nameof(PART_ClearButton);
private const string PART_InlineIncrementButton = nameof(PART_InlineIncrementButton);
private const string PART_InlineDecrementButton = nameof(PART_InlineDecrementButton);

private bool _valueUpdating;

/// <summary>Identifies the <see cref="Value"/> dependency property.</summary>
Expand Down Expand Up @@ -114,7 +123,7 @@ public class NumberBox : Wpf.Ui.Controls.TextBox
public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent(
nameof(ValueChanged),
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(NumberBoxValueChangedEvent),
typeof(NumberBox)
);

Expand Down Expand Up @@ -211,7 +220,7 @@ public NumberBoxValidationMode ValidationMode
/// <summary>
/// Occurs after the user triggers evaluation of new input by pressing the Enter key, clicking a spin button, or by changing focus.
/// </summary>
public event RoutedEventHandler ValueChanged
public event NumberBoxValueChangedEvent ValueChanged
{
add => AddHandler(ValueChangedEvent, value);
remove => RemoveHandler(ValueChangedEvent, value);
Expand All @@ -233,10 +242,8 @@ public NumberBox()
}

/// <inheritdoc />
protected override void OnKeyUp(KeyEventArgs e)
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
base.OnKeyUp(e);

if (IsReadOnly)
{
return;
Expand All @@ -246,53 +253,48 @@ protected override void OnKeyUp(KeyEventArgs e)
{
case Key.PageUp:
StepValue(LargeChange);
e.Handled = true;
break;
case Key.PageDown:
StepValue(-LargeChange);
e.Handled = true;
break;
case Key.Up:
StepValue(SmallChange);
e.Handled = true;
break;
case Key.Down:
StepValue(-SmallChange);
break;
case Key.Enter:
if (TextWrapping != TextWrapping.Wrap)
{
ValidateInput();
MoveCaretToTextEnd();
}

e.Handled = true;
break;
}

base.OnPreviewKeyDown(e);
}

/// <inheritdoc />
protected override void OnTemplateButtonClick(string? parameter)
protected override void OnPreviewKeyUp(KeyEventArgs e)
{
System.Diagnostics.Debug.WriteLine(
$"INFO: {typeof(NumberBox)} button clicked with param: {parameter}",
"Wpf.Ui.NumberBox"
);

switch (parameter)
switch (e.Key)
{
case "clear":
OnClearButtonClick();
case Key.Enter:
if (TextWrapping != TextWrapping.Wrap)
{
ValidateInput();
MoveCaretToTextEnd();
}

e.Handled = true;
break;
case "increment":
StepValue(SmallChange);

case Key.Escape:
UpdateTextToValue();
e.Handled = true;
break;
case "decrement":
StepValue(-SmallChange);

break;
}

// NOTE: Focus looks and works well with mouse and Clear button. But it sucks for spin buttons
_ = Focus();
base.OnPreviewKeyUp(e);
}

/// <inheritdoc />
Expand All @@ -316,12 +318,11 @@ protected override void OnTextChanged(System.Windows.Controls.TextChangedEventAr
}*/

/// <inheritdoc />
protected override void OnTemplateChanged(
System.Windows.Controls.ControlTemplate oldTemplate,
System.Windows.Controls.ControlTemplate newTemplate
)
public override void OnApplyTemplate()
{
base.OnTemplateChanged(oldTemplate, newTemplate);
SubscribeToButtonClickEvent<System.Windows.Controls.Button>(PART_ClearButton, () => OnClearButtonClick());
SubscribeToButtonClickEvent<RepeatButton>(PART_InlineIncrementButton, () => StepValue(SmallChange));
SubscribeToButtonClickEvent<RepeatButton>(PART_InlineDecrementButton, () => StepValue(-SmallChange));

// If Text has been set, but Value hasn't, update Value based on Text.
if (string.IsNullOrEmpty(Text) && Value != null)
Expand All @@ -332,6 +333,21 @@ System.Windows.Controls.ControlTemplate newTemplate
{
UpdateTextToValue();
}

base.OnApplyTemplate();
}

private void SubscribeToButtonClickEvent<TButton>(string elementName, Action action)
where TButton : ButtonBase
{
if (GetTemplateChild(elementName) is TButton button)
{
button.Click += (s, e) =>
{
Debug.InfoWriteLineForButtonClick(s);
action();
};
}
}

/// <summary>
Expand Down Expand Up @@ -360,7 +376,7 @@ protected virtual void OnValueChanged(DependencyObject d, double? oldValue)

if (!Equals(newValue, oldValue))
{
RaiseEvent(new RoutedEventArgs(ValueChangedEvent));
RaiseEvent(new NumberBoxValueChangedEventArgs(oldValue, newValue, this));
}

UpdateTextToValue();
Expand All @@ -384,10 +400,7 @@ protected virtual void OnClipboardPaste(object sender, DataObjectPastingEventArg

private void StepValue(double? change)
{
System.Diagnostics.Debug.WriteLine(
$"INFO: {typeof(NumberBox)} {nameof(StepValue)} raised, change {change}",
"Wpf.Ui.NumberBox"
);
Debug.InfoWriteLine($"{typeof(NumberBox)} {nameof(StepValue)} raised, change {change}");

// Before adjusting the value, validate the contents of the textbox so we don't override it.
ValidateInput();
Expand Down Expand Up @@ -469,12 +482,10 @@ private static INumberFormatter GetRegionalSettingsAwareDecimalFormatter()

private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not NumberBox numberBox)
if (d is NumberBox numberBox)
{
return;
numberBox.OnValueChanged(d, (double?)e.OldValue);
}

numberBox.OnValueChanged(d, (double?)e.OldValue);
}

private static void OnNumberFormatterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
Expand All @@ -486,4 +497,39 @@ private static void OnNumberFormatterChanged(DependencyObject d, DependencyPrope
);
}
}

private static partial class Debug
{
public static partial void InfoWriteLine(string debugLine);

public static partial void InfoWriteLineForButtonClick(object sender);

#if DEBUG
public static partial void InfoWriteLine(string debugLine)
{
System.Diagnostics.Debug.WriteLine($"INFO: {debugLine}", "Wpf.Ui.NumberBox");
}

public static partial void InfoWriteLineForButtonClick(object sender)
{
var buttonName = (sender is System.Windows.Controls.Primitives.ButtonBase element)
? element.Name
: throw new InvalidCastException(nameof(sender));

InfoWriteLine($"{typeof(NumberBox)} {buttonName} clicked");
}

#else
public static partial void InfoWriteLine(string debugLine)
{
// Do nothing in non-DEBUG builds
}

public static partial void InfoWriteLineForButtonClick(object sender)
{
// Do nothing in non-DEBUG builds
}

#endif // DEBUG
}
}
Loading

0 comments on commit 5db3c71

Please sign in to comment.