Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
89 changes: 68 additions & 21 deletions src/Wpf.Ui/Controls/SplitButton/SplitButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@

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

// ReSharper disable once CheckNamespace
namespace Wpf.Ui.Controls;

/// <summary>
/// Represents a button with two parts that can be invoked separately. One part behaves like a standard button and the other part invokes a flyout.
/// </summary>
[TemplatePart(Name = TemplateElementToggle, Type = typeof(Border))]
[TemplatePart(Name = TemplateElementToggleButton, Type = typeof(ToggleButton))]
[TemplatePart(Name = TemplateElementContent, Type = typeof(Border))]
public class SplitButton : Button
{
private const string TemplateElementContent = "PART_Content";
private const string TemplateElementToggle = "PART_Toggle";

/// <summary>
/// Template element represented by the <c>ToggleButton</c> name.
/// </summary>
Expand All @@ -27,6 +33,9 @@ public class SplitButton : Button
/// </summary>
protected ToggleButton SplitButtonToggleButton { get; set; } = null!;

private Border _splitButtonToggleBorder;
private Border _splitButtonContentBorder;

/// <summary>Identifies the <see cref="Flyout"/> dependency property.</summary>
public static readonly DependencyProperty FlyoutProperty = DependencyProperty.Register(
nameof(Flyout),
Expand Down Expand Up @@ -85,29 +94,12 @@ public SplitButton()
};
}

protected virtual void AttachTemplateResources()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unused, so remove.

{
base.OnApplyTemplate();

if (GetTemplateChild(TemplateElementToggleButton) is ToggleButton toggleButton)
{
SplitButtonToggleButton = toggleButton;
AttachToggleButtonClick();
}
else
{
throw new NullReferenceException(
$"Element {nameof(TemplateElementToggleButton)} of type {typeof(ToggleButton)} not found in {typeof(SplitButton)}"
);
}
}

private void AttachToggleButtonClick()
{
if (SplitButtonToggleButton != null)
{
SplitButtonToggleButton.Click -= OnSplitButtonToggleButtonOnClick;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This had to be changed to PreviewMouseLeftButtonUp for two reasons.

  1. If the Click event is used, there's hardly any visible Pressed state as it instantly presses.
  2. The SplitButton's context menu would immediately open on mouse down, this isn't the same in WinUI, where a mouse up is required.

SplitButtonToggleButton.Click += OnSplitButtonToggleButtonOnClick;
SplitButtonToggleButton.PreviewMouseLeftButtonUp -= OnSplitButtonToggleButtonOnPreviewMouseLeftButtonUp;
SplitButtonToggleButton.PreviewMouseLeftButtonUp += OnSplitButtonToggleButtonOnPreviewMouseLeftButtonUp;
}
}

Expand Down Expand Up @@ -169,23 +161,78 @@ public override void OnApplyTemplate()
$"Element {nameof(TemplateElementToggleButton)} of type {typeof(ToggleButton)} not found in {typeof(SplitButton)}"
);
}

if (GetTemplateChild(TemplateElementContent) is Border contentBorder)
{
_splitButtonContentBorder = contentBorder;
}

if (GetTemplateChild(TemplateElementToggle) is Border toggleBorder)
{
_splitButtonToggleBorder = toggleBorder;
}

PreviewMouseMove += OnPreviewMouseMove;
MouseLeave += OnMouseLeave;
}

private void OnMouseLeave(object sender, MouseEventArgs e)
{
if (_splitButtonToggleBorder != null)
{
_splitButtonToggleBorder.Tag = null;
}

if (_splitButtonContentBorder != null)
{
_splitButtonContentBorder.Tag = null;
}
}

private void OnPreviewMouseMove(object sender, MouseEventArgs args)
{
if (_splitButtonToggleBorder != null)
{
var position = args.GetPosition(_splitButtonToggleBorder);
HitTestResult hitTestResult = VisualTreeHelper.HitTest(_splitButtonToggleBorder, position);
_splitButtonToggleBorder.Tag = hitTestResult?.VisualHit != null ? "IsMouseOver" : null;
}

if (_splitButtonContentBorder != null)
{
var position = args.GetPosition(_splitButtonContentBorder);
HitTestResult hitTestResult = VisualTreeHelper.HitTest(_splitButtonContentBorder, position);
_splitButtonContentBorder.Tag = hitTestResult?.VisualHit != null ? "IsMouseOver" : null;
}
}

/// <summary>
/// Triggered when the control is unloaded. Releases resource bindings.
/// </summary>
protected virtual void ReleaseTemplateResources()
{
SplitButtonToggleButton.Click -= OnSplitButtonToggleButtonOnClick;
if (SplitButtonToggleButton != null)
SplitButtonToggleButton.PreviewMouseLeftButtonUp -= OnSplitButtonToggleButtonOnPreviewMouseLeftButtonUp;

PreviewMouseMove -= OnPreviewMouseMove;
MouseLeave -= OnMouseLeave;
}

private void OnSplitButtonToggleButtonOnClick(object sender, RoutedEventArgs e)
private void OnSplitButtonToggleButtonOnPreviewMouseLeftButtonUp(object sender, MouseEventArgs e)
{
if (sender is not ToggleButton || _contextMenu is null)
{
return;
}

// Ensure mouse up actually happened inside the toggler, and not outside.
var position = e.GetPosition(_splitButtonToggleBorder);
HitTestResult hitTestResult = VisualTreeHelper.HitTest(_splitButtonToggleBorder, position);
if (hitTestResult?.VisualHit == null)
{
return;
}

_contextMenu.SetCurrentValue(MinWidthProperty, ActualWidth);
_contextMenu.SetCurrentValue(ContextMenu.PlacementTargetProperty, this);
_contextMenu.SetCurrentValue(ContextMenu.PlacementProperty, PlacementMode.Bottom);
Expand Down
38 changes: 28 additions & 10 deletions src/Wpf.Ui/Controls/SplitButton/SplitButton.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border
x:Name="ContentBorder"
Margin="0,-1,-1,-1"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This caused the toggle button to be off-center, there's no need for this, so just remove it.

Padding="11,0"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
Expand Down Expand Up @@ -72,24 +71,24 @@
<ControlTemplate TargetType="{x:Type controls:SplitButton}">
<Border
x:Name="ContentBorder"
Grid.Row="0"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't needed, there's no Grid, so I removed it as well.

Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
MinWidth="{TemplateBinding MinWidth}"
MinHeight="{TemplateBinding MinHeight}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border
Name="PART_Content"
Grid.Column="0"
Margin="0"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness,
Converter={StaticResource LeftSplitThicknessConverter}}"
Expand All @@ -115,12 +114,15 @@
Grid.Column="1"
VerticalAlignment="Center"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also added this so ContentTemplate can be used.

TextElement.Foreground="{TemplateBinding Foreground}" />
</Grid>
</Border>
<Border
Name="PART_Toggle"
Grid.Column="1"
Margin="0"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness,
Converter={StaticResource RightSplitThicknessConverter}}"
Expand All @@ -134,7 +136,7 @@
Focusable="False"
Foreground="{TemplateBinding Foreground}"
IsChecked="{TemplateBinding IsDropDownOpen}"
Style="{StaticResource DefaultSplitButtonToggleButtonStyle}">
Style="{DynamicResource DefaultSplitButtonToggleButtonStyle}">
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we can override the style of the toggle button without having to redeclare the entire template.

<controls:SymbolIcon FontSize="10" Symbol="ChevronDown24" />
</ToggleButton>
</Border>
Expand All @@ -143,19 +145,35 @@
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition SourceName="PART_Toggle" Property="Tag" Value="IsMouseOver" />
<Condition Property="IsPressed" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="ContentBorder" Property="Background" Value="{Binding MouseOverBackground, RelativeSource={RelativeSource TemplatedParent}}" />
<Setter TargetName="ContentBorder" Property="BorderBrush" Value="{Binding MouseOverBorderBrush, RelativeSource={RelativeSource TemplatedParent}}" />
<Setter TargetName="PART_Toggle" Property="Background" Value="{Binding MouseOverBackground, RelativeSource={RelativeSource TemplatedParent}}" />
<Setter TargetName="PART_Toggle" Property="BorderBrush" Value="{Binding MouseOverBorderBrush, RelativeSource={RelativeSource TemplatedParent}}" />
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition SourceName="PART_Content" Property="Tag" Value="IsMouseOver" />
<Condition Property="IsPressed" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="PART_Content" Property="Background" Value="{Binding MouseOverBackground, RelativeSource={RelativeSource TemplatedParent}}" />
<Setter TargetName="PART_Content" Property="BorderBrush" Value="{Binding MouseOverBorderBrush, RelativeSource={RelativeSource TemplatedParent}}" />
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition SourceName="PART_Toggle" Property="Tag" Value="IsMouseOver" />
<Condition SourceName="PART_ToggleButton" Property="IsPressed" Value="True" />
</MultiTrigger.Conditions>
<Setter TargetName="PART_Toggle" Property="Background" Value="{Binding PressedBackground, RelativeSource={RelativeSource TemplatedParent}}" />
<Setter TargetName="PART_Toggle" Property="BorderBrush" Value="{Binding PressedBorderBrush, RelativeSource={RelativeSource TemplatedParent}}" />
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition SourceName="PART_Content" Property="Tag" Value="IsMouseOver" />
<Condition Property="IsPressed" Value="True" />
</MultiTrigger.Conditions>
<Setter TargetName="ContentBorder" Property="Background" Value="{Binding PressedBackground, RelativeSource={RelativeSource TemplatedParent}}" />
<Setter TargetName="ContentBorder" Property="BorderBrush" Value="{Binding PressedBorderBrush, RelativeSource={RelativeSource TemplatedParent}}" />
<Setter TargetName="PART_Content" Property="Background" Value="{Binding PressedBackground, RelativeSource={RelativeSource TemplatedParent}}" />
<Setter TargetName="PART_Content" Property="BorderBrush" Value="{Binding PressedBorderBrush, RelativeSource={RelativeSource TemplatedParent}}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{Binding PressedForeground, RelativeSource={RelativeSource TemplatedParent}}" />
<Setter TargetName="ControlIcon" Property="TextElement.Foreground" Value="{Binding PressedForeground, RelativeSource={RelativeSource TemplatedParent}}" />
</MultiTrigger>
Expand Down