Skip to content

Commit 8f02def

Browse files
Improves tab header scrolling behavior
Adds a behavior to handle tab selection changes intended to adjust scrolling for better user experience. Introduces a custom panel to hijack the BringIntoView event, allowing for offsetting the Rect being scrolled to based on the tab selection direction.
1 parent df185e4 commit 8f02def

File tree

4 files changed

+126
-22
lines changed

4 files changed

+126
-22
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System.Diagnostics;
2+
using Microsoft.Xaml.Behaviors;
3+
4+
namespace MaterialDesignThemes.Wpf.Behaviors.Internal;
5+
6+
public class TabControlHeaderScrollBehavior : Behavior<ScrollViewer>
7+
{
8+
public static readonly DependencyProperty TabScrollDirectionProperty =
9+
DependencyProperty.RegisterAttached("TabScrollDirection", typeof(TabScrollDirection), typeof(TabControlHeaderScrollBehavior), new PropertyMetadata(TabScrollDirection.Unknown));
10+
public static TabScrollDirection GetTabScrollDirection(DependencyObject obj) => (TabScrollDirection)obj.GetValue(TabScrollDirectionProperty);
11+
public static void SetTabScrollDirection(DependencyObject obj, TabScrollDirection value) => obj.SetValue(TabScrollDirectionProperty, value);
12+
13+
public TabControl TabControl
14+
{
15+
get => (TabControl)GetValue(TabControlProperty);
16+
set => SetValue(TabControlProperty, value);
17+
}
18+
19+
public static readonly DependencyProperty TabControlProperty =
20+
DependencyProperty.Register(nameof(TabControl), typeof(TabControl),
21+
typeof(TabControlHeaderScrollBehavior), new PropertyMetadata(null, OnTabControlChanged));
22+
23+
private static void OnTabControlChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
24+
{
25+
var behavior = (TabControlHeaderScrollBehavior)d;
26+
if (e.OldValue is TabControl oldTabControl)
27+
{
28+
oldTabControl.SelectionChanged -= behavior.OnTabChanged;
29+
}
30+
if (e.NewValue is TabControl newTabControl)
31+
{
32+
newTabControl.SelectionChanged += behavior.OnTabChanged;
33+
}
34+
}
35+
36+
private void OnTabChanged(object sender, SelectionChangedEventArgs e)
37+
{
38+
TabControl tabControl = (TabControl)sender;
39+
40+
if (e.AddedItems.Count > 0)
41+
{
42+
SetTabScrollDirection(tabControl, (IsMovingForward() ? TabScrollDirection.Forward : TabScrollDirection.Backward));
43+
}
44+
45+
bool IsMovingForward()
46+
{
47+
if (e.RemovedItems.Count == 0) return true;
48+
int previousIndex = GetItemIndex(e.RemovedItems[0]);
49+
int nextIndex = GetItemIndex(e.AddedItems[^1]);
50+
return nextIndex > previousIndex;
51+
}
52+
53+
int GetItemIndex(object? item) => tabControl.Items.IndexOf(item);
54+
}
55+
56+
protected override void OnAttached()
57+
{
58+
base.OnAttached();
59+
AssociatedObject.ScrollChanged += AssociatedObject_ScrollChanged;
60+
}
61+
62+
protected override void OnDetaching()
63+
{
64+
base.OnDetaching();
65+
if (AssociatedObject is { } ao)
66+
{
67+
ao.ScrollChanged -= AssociatedObject_ScrollChanged;
68+
}
69+
}
70+
71+
private void AssociatedObject_ScrollChanged(object sender, ScrollChangedEventArgs e)
72+
{
73+
Debug.WriteLine($"HorizontalOffset: {e.HorizontalOffset}, ViewportWidth: {e.ViewportWidth}, ExtentWidth: {e.ExtentWidth}");
74+
}
75+
}
76+
77+
public enum TabScrollDirection
78+
{
79+
Unknown,
80+
Backward,
81+
Forward
82+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+

2+
using MaterialDesignThemes.Wpf.Behaviors.Internal;
3+
4+
namespace MaterialDesignThemes.Wpf.Internal;
5+
6+
public class BringIntoViewHijackingStackPanel : StackPanel
7+
{
8+
public TabScrollDirection TabScrollDirection
9+
{
10+
get => (TabScrollDirection)GetValue(TabScrollDirectionProperty);
11+
set => SetValue(TabScrollDirectionProperty, value);
12+
}
13+
14+
public static readonly DependencyProperty TabScrollDirectionProperty =
15+
DependencyProperty.Register(nameof(TabScrollDirection), typeof(TabScrollDirection),
16+
typeof(BringIntoViewHijackingStackPanel), new PropertyMetadata(TabScrollDirection.Unknown));
17+
18+
public BringIntoViewHijackingStackPanel()
19+
=> AddHandler(FrameworkElement.RequestBringIntoViewEvent, new RoutedEventHandler(OnRequestBringIntoView), false);
20+
21+
private void OnRequestBringIntoView(object sender, RoutedEventArgs e)
22+
{
23+
if (e.OriginalSource is FrameworkElement child && child != this)
24+
{
25+
e.Handled = true;
26+
double offset = TabScrollDirection switch {
27+
TabScrollDirection.Backward => -52,
28+
TabScrollDirection.Forward => 52,
29+
_ => 0
30+
};
31+
var point = child.TranslatePoint(new Point(), this);
32+
var newTargetRect = new Rect(new Point(point.X + offset, point.Y), child.RenderSize);
33+
BringIntoView(newTargetRect);
34+
}
35+
}
36+
}

src/MaterialDesignThemes.Wpf/Internal/BringIntoViewHijackingVirtualizingStackPanel.cs

Lines changed: 0 additions & 19 deletions
This file was deleted.

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
33
xmlns:converters="clr-namespace:MaterialDesignThemes.Wpf.Converters"
44
xmlns:internal="clr-namespace:MaterialDesignThemes.Wpf.Internal"
5+
xmlns:behaviorsInternal="clr-namespace:MaterialDesignThemes.Wpf.Behaviors.Internal"
6+
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
57
xmlns:wpf="clr-namespace:MaterialDesignThemes.Wpf">
68

79
<ResourceDictionary.MergedDictionaries>
@@ -37,7 +39,10 @@
3739
wpf:ScrollViewerAssist.PaddingMode="{Binding Path=(wpf:ScrollViewerAssist.PaddingMode), RelativeSource={RelativeSource TemplatedParent}}"
3840
HorizontalScrollBarVisibility="Hidden"
3941
VerticalScrollBarVisibility="Hidden">
40-
<StackPanel>
42+
<b:Interaction.Behaviors>
43+
<behaviorsInternal:TabControlHeaderScrollBehavior TabControl="{Binding RelativeSource={RelativeSource TemplatedParent}}" />
44+
</b:Interaction.Behaviors>
45+
<internal:BringIntoViewHijackingStackPanel TabScrollDirection="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(behaviorsInternal:TabControlHeaderScrollBehavior.TabScrollDirection)}">
4146
<UniformGrid x:Name="CenteredHeaderPanel"
4247
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
4348
Margin="{Binding Path=(wpf:TabAssist.HeaderPanelMargin), RelativeSource={RelativeSource TemplatedParent}}"
@@ -46,15 +51,15 @@
4651
Focusable="False"
4752
KeyboardNavigation.TabIndex="1"
4853
Rows="1" />
49-
<internal:BringIntoViewHijackingVirtualizingStackPanel x:Name="HeaderPanel"
54+
<VirtualizingStackPanel x:Name="HeaderPanel"
5055
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
5156
Margin="{Binding Path=(wpf:TabAssist.HeaderPanelMargin), RelativeSource={RelativeSource TemplatedParent}}"
5257
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
5358
wpf:TabAssist.BindableIsItemsHost="{Binding Visibility, RelativeSource={RelativeSource Self}}"
5459
Focusable="False"
5560
KeyboardNavigation.TabIndex="1"
5661
Orientation="Horizontal" />
57-
</StackPanel>
62+
</internal:BringIntoViewHijackingStackPanel>
5863
</ScrollViewer>
5964
</wpf:ColorZone>
6065

0 commit comments

Comments
 (0)