Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Clean-up NumberBox: focus repeat event-args debug-output cursor #1328

Merged
merged 5 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
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
Loading