Skip to content

Commit acdac76

Browse files
Add AP to control placement of navigation panel
Update UI tests accordingly
1 parent 512b66a commit acdac76

File tree

7 files changed

+248
-25
lines changed

7 files changed

+248
-25
lines changed

src/MaterialDesignThemes.Wpf/Behaviors/Internal/TabControlHeaderScrollBehavior.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,22 @@ private static void AdditionalHeaderPanelContentWidthChanged(DependencyObject d,
6868
behavior.AddPaddingToScrollableContentIfWiderThanViewPort();
6969
}
7070

71+
public double NavigationPanelLeftWidth
72+
{
73+
get => (double)GetValue(NavigationPanelLeftWidthProperty);
74+
set => SetValue(NavigationPanelLeftWidthProperty, value);
75+
}
76+
77+
public static readonly DependencyProperty NavigationPanelLeftWidthProperty =
78+
DependencyProperty.Register(nameof(NavigationPanelLeftWidth), typeof(double),
79+
typeof(TabControlHeaderScrollBehavior), new PropertyMetadata(0d, NavigationPanelLeftWidthChanged));
80+
81+
private static void NavigationPanelLeftWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
82+
{
83+
var behavior = (TabControlHeaderScrollBehavior)d;
84+
behavior.AddPaddingToScrollableContentIfWiderThanViewPort();
85+
}
86+
7187
public FrameworkElement ScrollableContent
7288
{
7389
get => (FrameworkElement)GetValue(ScrollableContentProperty);
@@ -193,7 +209,7 @@ private void AddPaddingToScrollableContentIfWiderThanViewPort()
193209
AssociatedObject.SetCurrentValue(TabControlHeaderScrollBehavior.CustomHorizontalOffsetProperty, 0d);
194210
TabAssist.SetIsOverflowing(TabControl, false);
195211
}
196-
AssociatedObject.Margin = new(0, 0, AdditionalHeaderPanelContentWidth, 0);
212+
AssociatedObject.Margin = new(NavigationPanelLeftWidth, 0, AdditionalHeaderPanelContentWidth, 0);
197213
}
198214

199215
private void OnTabControlPreviewKeyDown(object sender, KeyEventArgs e)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Globalization;
2+
using System.Windows.Data;
3+
4+
namespace MaterialDesignThemes.Wpf.Converters.Internal;
5+
6+
public class TabControlAdditionalHeaderPanelContentMarginConverter : IMultiValueConverter
7+
{
8+
public object? Convert(object?[]? values, Type targetType, object? parameter, CultureInfo culture)
9+
{
10+
if (values is [double scrollableContentWidthDefault, double scrollableContentWidthNonUniform, double controlWidth, StackPanel navPanel, ..])
11+
{
12+
double scrollableContentWidth = Math.Max(scrollableContentWidthDefault, scrollableContentWidthNonUniform);
13+
double xOffset = Math.Min(controlWidth, scrollableContentWidth + navPanel.ActualWidth) - navPanel.ActualWidth;
14+
return new Thickness(xOffset, 0, 0, 0);
15+
}
16+
return new Thickness(0);
17+
}
18+
19+
public object?[]? ConvertBack(object? value, Type[] targetTypes, object? parameter, CultureInfo culture)
20+
=> throw new NotImplementedException();
21+
}

src/MaterialDesignThemes.Wpf/Converters/Internal/TabControlNavButtonPanelMarginConverter.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@ public class TabControlNavButtonPanelMarginConverter : IMultiValueConverter
77
{
88
public object? Convert(object?[]? values, Type targetType, object? parameter, CultureInfo culture)
99
{
10-
if (values is [double scrollableContentWidthDefault, double scrollableContentWidthNonUniform, double controlWidth, StackPanel navPanel, ..])
10+
if (values is [Thickness margin, Visibility previousButtonVisibility, Visibility nextButtonVisibility])
1111
{
12-
double scrollableContentWidth = Math.Max(scrollableContentWidthDefault, scrollableContentWidthNonUniform);
13-
double xOffset = Math.Min(controlWidth, scrollableContentWidth + navPanel.ActualWidth) - navPanel.ActualWidth;
14-
return new Thickness(xOffset, 0, 0, 0);
12+
return previousButtonVisibility == Visibility.Collapsed && nextButtonVisibility == Visibility.Collapsed ? new Thickness(0) : margin;
1513
}
1614
return new Thickness(0);
1715
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System.ComponentModel;
2+
using System.Globalization;
3+
using System.Windows.Data;
4+
5+
namespace MaterialDesignThemes.Wpf.Converters.Internal;
6+
7+
public class TabControlNavButtonVisibilityConverter : IValueConverter
8+
{
9+
[TypeConverter(typeof(NavigationPanelPlacementTypeConverter))]
10+
public NavigationPanelPlacement[] VisiblePlacements { get; set; } = [];
11+
12+
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
13+
{
14+
if (value is NavigationPanelPlacement placement)
15+
{
16+
return VisiblePlacements.Contains(placement) ? Visibility.Visible : Visibility.Collapsed;
17+
}
18+
return Visibility.Visible;
19+
}
20+
21+
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
22+
=> throw new NotImplementedException();
23+
24+
class NavigationPanelPlacementTypeConverter : TypeConverter
25+
{
26+
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
27+
=> sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
28+
29+
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
30+
{
31+
if (value is string s)
32+
{
33+
return s.Split([',', ' '], StringSplitOptions.RemoveEmptyEntries)
34+
.Select(x => Enum.Parse(typeof(NavigationPanelPlacement), x.Trim()))
35+
.Cast<NavigationPanelPlacement>()
36+
.ToArray();
37+
}
38+
return base.ConvertFrom(context, culture, value);
39+
}
40+
}
41+
}

src/MaterialDesignThemes.Wpf/TabAssist.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,26 @@ public static bool GetUseNavigationPanel(DependencyObject obj)
5757
public static void SetUseNavigationPanel(DependencyObject obj, bool value)
5858
=> obj.SetValue(UseNavigationPanelProperty, value);
5959

60-
public static readonly DependencyProperty UseNavigationPanelProperty =
61-
DependencyProperty.RegisterAttached("UseNavigationPanel", typeof(bool), typeof(TabAssist), new PropertyMetadata(false));
60+
public static readonly DependencyProperty UseNavigationPanelProperty = DependencyProperty.RegisterAttached(
61+
"UseNavigationPanel", typeof(bool), typeof(TabAssist), new PropertyMetadata(false));
6262

6363
public static Thickness GetNavigationPanelMargin(DependencyObject obj)
6464
=> (Thickness)obj.GetValue(NavigationPanelMarginProperty);
6565

6666
public static void SetNavigationPanelMargin(DependencyObject obj, Thickness value)
6767
=> obj.SetValue(NavigationPanelMarginProperty, value);
6868

69-
public static readonly DependencyProperty NavigationPanelMarginProperty =
70-
DependencyProperty.RegisterAttached("NavigationPanelMargin", typeof(Thickness), typeof(TabAssist), new PropertyMetadata(default(Thickness)));
69+
public static readonly DependencyProperty NavigationPanelMarginProperty = DependencyProperty.RegisterAttached(
70+
"NavigationPanelMargin", typeof(Thickness), typeof(TabAssist), new PropertyMetadata(default(Thickness)));
71+
72+
public static NavigationPanelPlacement GetNavigationPanelPlacement(DependencyObject obj)
73+
=> (NavigationPanelPlacement)obj.GetValue(NavigationPanelPlacementProperty);
74+
75+
public static void SetNavigationPanelPlacement(DependencyObject obj, NavigationPanelPlacement value)
76+
=> obj.SetValue(NavigationPanelPlacementProperty, value);
77+
78+
public static readonly DependencyProperty NavigationPanelPlacementProperty = DependencyProperty.RegisterAttached(
79+
"NavigationPanelPlacement", typeof(NavigationPanelPlacement), typeof(TabAssist), new PropertyMetadata(default(NavigationPanelPlacement)));
7180

7281
internal static void SetIsOverflowing(DependencyObject obj, bool value)
7382
=> obj.SetValue(IsOverflowingPropertyKey, value);
@@ -137,3 +146,10 @@ public static void SetScrollDuration(DependencyObject obj, TimeSpan value)
137146
DependencyProperty.RegisterAttached("ScrollDuration", typeof(TimeSpan),
138147
typeof(TabAssist), new PropertyMetadata(TimeSpan.Zero));
139148
}
149+
150+
public enum NavigationPanelPlacement
151+
{
152+
Split,
153+
Left,
154+
Right
155+
}

src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.TabControl.xaml

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,13 @@
2727

2828
<ControlTemplate x:Key="ScrollingTabControlTemplate" TargetType="{x:Type TabControl}">
2929
<ControlTemplate.Resources>
30-
<convertersInternal:TabControlNavButtonPanelMarginConverter x:Key="TabControlNavButtonPanelMarginConverter" />
30+
<convertersInternal:TabControlAdditionalHeaderPanelContentMarginConverter x:Key="TabControlAdditionalHeaderPanelContentMarginConverter" />
3131
<convertersInternal:TabControlNavButtonPanelVisibilityConverter x:Key="TabControlNavButtonPanelVisibilityConverter" />
32+
<convertersInternal:TabControlNavButtonVisibilityConverter x:Key="LeftPreviousTabControlNavButtonVisibilityConverter" VisiblePlacements="Split, Left" />
33+
<convertersInternal:TabControlNavButtonVisibilityConverter x:Key="LeftNextTabControlNavButtonVisibilityConverter" VisiblePlacements="Left" />
34+
<convertersInternal:TabControlNavButtonVisibilityConverter x:Key="RightPreviousTabControlNavButtonVisibilityConverter" VisiblePlacements="Right" />
35+
<convertersInternal:TabControlNavButtonVisibilityConverter x:Key="RightNextTabControlNavButtonVisibilityConverter" VisiblePlacements="Split, Right" />
36+
<convertersInternal:TabControlNavButtonPanelMarginConverter x:Key="TabControlNavButtonPanelMarginConverter" />
3237
</ControlTemplate.Resources>
3338
<DockPanel KeyboardNavigation.TabNavigation="Local">
3439
<wpf:ColorZone x:Name="PART_HeaderZone"
@@ -49,7 +54,8 @@
4954
<behaviorsInternal:TabControlHeaderScrollBehavior x:Name="TabControlHeaderScrollBehavior"
5055
TabControl="{Binding RelativeSource={RelativeSource TemplatedParent}}"
5156
ScrollableContent="{Binding ElementName=ScrollableContent}"
52-
AdditionalHeaderPanelContentWidth="{Binding ElementName=AdditionalHeaderPanelContent, Path=ActualWidth}"/>
57+
AdditionalHeaderPanelContentWidth="{Binding ElementName=AdditionalHeaderPanelContent, Path=ActualWidth}"
58+
NavigationPanelLeftWidth="{Binding ElementName=NavigationPanelLeft, Path=ActualWidth}"/>
5359
</b:Interaction.Behaviors>
5460
<internal:PaddedBringIntoViewStackPanel x:Name="ScrollableContent"
5561
ScrollDirection="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(behaviorsInternal:TabControlHeaderScrollBehavior.ScrollDirection)}"
@@ -73,10 +79,10 @@
7379
Orientation="Horizontal" />
7480
</internal:PaddedBringIntoViewStackPanel>
7581
</ScrollViewer>
76-
82+
7783
<StackPanel x:Name="AdditionalHeaderPanelContent" Orientation="Horizontal" HorizontalAlignment="Left">
7884
<StackPanel.Margin>
79-
<MultiBinding Converter="{StaticResource TabControlNavButtonPanelMarginConverter}">
85+
<MultiBinding Converter="{StaticResource TabControlAdditionalHeaderPanelContentMarginConverter}">
8086
<Binding ElementName="CenteredHeaderPanel" Path="ActualWidth" />
8187
<Binding ElementName="HeaderPanel" Path="ActualWidth" />
8288
<Binding ElementName="PART_HeaderZone" Path="ActualWidth" />
@@ -86,33 +92,79 @@
8692
</MultiBinding>
8793
</StackPanel.Margin>
8894

89-
<!-- Navigation panel -->
90-
<StackPanel x:Name="NavigationPanel" Orientation="Horizontal" Margin="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(wpf:TabAssist.NavigationPanelMargin)}">
95+
<!-- Navigation panel (right) -->
96+
<StackPanel x:Name="NavigationPanelRight" Orientation="Horizontal">
97+
<StackPanel.Margin>
98+
<MultiBinding Converter="{StaticResource TabControlNavButtonPanelMarginConverter}">
99+
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="(wpf:TabAssist.NavigationPanelMargin)" />
100+
<Binding ElementName="RightPreviousButton" Path="Visibility" />
101+
<Binding ElementName="RightNextButton" Path="Visibility" />
102+
</MultiBinding>
103+
</StackPanel.Margin>
91104
<StackPanel.Visibility>
92105
<MultiBinding Converter="{StaticResource TabControlNavButtonPanelVisibilityConverter}">
93106
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="(wpf:TabAssist.UseNavigationPanel)" />
94107
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="(wpf:TabAssist.IsOverflowing)" />
95108
</MultiBinding>
96109
</StackPanel.Visibility>
97-
<Button Width="16"
110+
<Button x:Name="RightPreviousButton"
111+
Width="16"
98112
Height="16"
99113
Padding="0"
100-
Command="{Binding ElementName=TabControlHeaderScrollBehavior, Path=PreviousTabCommand}">
114+
Margin="0,0,8,0"
115+
Command="{Binding ElementName=TabControlHeaderScrollBehavior, Path=PreviousTabCommand}"
116+
Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(wpf:TabAssist.NavigationPanelPlacement), Converter={StaticResource RightPreviousTabControlNavButtonVisibilityConverter}}">
101117
<wpf:PackIcon Kind="ChevronLeft" Width="12" Height="12" />
102118
</Button>
103-
<Button Width="16"
119+
<Button x:Name="RightNextButton"
120+
Width="16"
104121
Height="16"
105122
Padding="0"
106-
Margin="8,0,0,0"
107-
Command="{Binding ElementName=TabControlHeaderScrollBehavior, Path=NextTabCommand}">
123+
Command="{Binding ElementName=TabControlHeaderScrollBehavior, Path=NextTabCommand}"
124+
Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(wpf:TabAssist.NavigationPanelPlacement), Converter={StaticResource RightNextTabControlNavButtonVisibilityConverter}}">
108125
<wpf:PackIcon Kind="ChevronRight" Width="12" Height="12" />
109126
</Button>
110127
</StackPanel>
111128

112129
<!-- Custom content -->
113130
<ContentControl Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(wpf:TabAssist.HeaderPanelCustomContent)}" />
114-
115131
</StackPanel>
132+
133+
<!-- Navigation panel (left) - for some reason the extra Grid is needed to trigger updates to NavigationPanelLeft.ActualWidth in the binding above -->
134+
<Grid x:Name="NavigationPanelLeft" HorizontalAlignment="Left">
135+
<StackPanel Orientation="Horizontal">
136+
<StackPanel.Margin>
137+
<MultiBinding Converter="{StaticResource TabControlNavButtonPanelMarginConverter}">
138+
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="(wpf:TabAssist.NavigationPanelMargin)" />
139+
<Binding ElementName="LeftPreviousButton" Path="Visibility" />
140+
<Binding ElementName="LeftNextButton" Path="Visibility" />
141+
</MultiBinding>
142+
</StackPanel.Margin>
143+
<StackPanel.Visibility>
144+
<MultiBinding Converter="{StaticResource TabControlNavButtonPanelVisibilityConverter}">
145+
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="(wpf:TabAssist.UseNavigationPanel)" />
146+
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="(wpf:TabAssist.IsOverflowing)" />
147+
</MultiBinding>
148+
</StackPanel.Visibility>
149+
<Button x:Name="LeftPreviousButton"
150+
Width="16"
151+
Height="16"
152+
Padding="0"
153+
Command="{Binding ElementName=TabControlHeaderScrollBehavior, Path=PreviousTabCommand}"
154+
Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(wpf:TabAssist.NavigationPanelPlacement), Converter={StaticResource LeftPreviousTabControlNavButtonVisibilityConverter}}">
155+
<wpf:PackIcon Kind="ChevronLeft" Width="12" Height="12" />
156+
</Button>
157+
<Button x:Name="LeftNextButton"
158+
Width="16"
159+
Height="16"
160+
Padding="0"
161+
Margin="8,0,0,0"
162+
Command="{Binding ElementName=TabControlHeaderScrollBehavior, Path=NextTabCommand}"
163+
Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(wpf:TabAssist.NavigationPanelPlacement), Converter={StaticResource LeftNextTabControlNavButtonVisibilityConverter}}">
164+
<wpf:PackIcon Kind="ChevronRight" Width="12" Height="12" />
165+
</Button>
166+
</StackPanel>
167+
</Grid>
116168
</Grid>
117169
</wpf:ColorZone>
118170

0 commit comments

Comments
 (0)