Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions docfx/articles/dock-controls-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Unless noted otherwise, the properties listed are Avalonia styled properties and
| `IsActive` | `bool` | Active tab strip state (drives `:active`). |
| `EnableWindowDrag` | `bool` | Allows dragging the host window by the tab strip. |
| `Orientation` | `Orientation` | Tab strip orientation. |
| `MouseWheelScrollOrientation` | `Orientation` | Mouse-wheel scroll axis for tab overflow (`Horizontal` by default). |
| `CreateButtonTheme` | `ControlTheme?` | Theme for the create document button. |

### DocumentTabStripItem
Expand All @@ -97,6 +98,7 @@ Unless noted otherwise, the properties listed are Avalonia styled properties and
| Property | Type | Description |
| --- | --- | --- |
| `CanCreateItem` | `bool` | `true` when the new-tool button is available. |
| `MouseWheelScrollOrientation` | `Orientation` | Mouse-wheel scroll axis for tab overflow (`Horizontal` by default). |

### ToolTabStripItem

Expand Down
44 changes: 44 additions & 0 deletions src/Dock.Avalonia/Controls/DocumentTabStrip.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public class DocumentTabStrip : TabStrip
{
private HostWindow? _attachedWindow;
private Control? _grip;
private ScrollViewer? _scrollViewer;
private IDisposable? _scrollViewerWheelSubscription;
private WindowDragHelper? _windowDragHelper;

/// <summary>
Expand All @@ -45,6 +47,14 @@ public class DocumentTabStrip : TabStrip
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<DocumentTabStrip, Orientation>(nameof(Orientation));

/// <summary>
/// Defines the <see cref="MouseWheelScrollOrientation"/> property.
/// </summary>
public static readonly StyledProperty<Orientation> MouseWheelScrollOrientationProperty =
AvaloniaProperty.Register<DocumentTabStrip, Orientation>(
nameof(MouseWheelScrollOrientation),
defaultValue: Orientation.Horizontal);

/// <summary>
/// Define the <see cref="CreateButtonTheme"/> property.
/// </summary>
Expand Down Expand Up @@ -96,6 +106,15 @@ public Orientation Orientation
set => SetValue(OrientationProperty, value);
}

/// <summary>
/// Gets or sets orientation used for mouse wheel scrolling in the tab strip.
/// </summary>
public Orientation MouseWheelScrollOrientation
{
get => GetValue(MouseWheelScrollOrientationProperty);
set => SetValue(MouseWheelScrollOrientationProperty, value);
}

/// <inheritdoc/>
protected override Type StyleKeyOverride => typeof(DocumentTabStrip);

Expand All @@ -112,7 +131,13 @@ public DocumentTabStrip()
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);

DetachScrollViewerWheel();

_grip = e.NameScope.Find<Control>("PART_BorderFill");
_scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer");
AttachScrollViewerWheel();

AttachToWindow();
}

Expand All @@ -121,13 +146,15 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);

AttachScrollViewerWheel();
AttachToWindow();
}

/// <inheritdoc/>
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
DetachScrollViewerWheel();
DetachFromWindow();
}

Expand Down Expand Up @@ -169,6 +196,11 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
DetachFromWindow();
}
}

if (change.Property == MouseWheelScrollOrientationProperty)
{
AttachScrollViewerWheel();
}
}

private void UpdatePseudoClassesCreate(bool canCreate)
Expand Down Expand Up @@ -237,4 +269,16 @@ private void DetachFromWindow()
_windowDragHelper = null;
}
}

private void DetachScrollViewerWheel()
{
_scrollViewerWheelSubscription?.Dispose();
_scrollViewerWheelSubscription = null;
}

private void AttachScrollViewerWheel()
{
_scrollViewerWheelSubscription?.Dispose();
_scrollViewerWheelSubscription = ScrollViewerMouseWheelHookHelper.Attach(_scrollViewer, MouseWheelScrollOrientation);
}
}
48 changes: 48 additions & 0 deletions src/Dock.Avalonia/Controls/ToolTabStrip.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Reactive;
using Dock.Avalonia.Internal;

namespace Dock.Avalonia.Controls;

Expand All @@ -23,13 +25,22 @@ public class ToolTabStrip : TabStrip
private Border? _borderRightFill;
private ItemsPresenter? _itemsPresenter;
private ScrollViewer? _scrollViewer;
private IDisposable? _scrollViewerWheelSubscription;

/// <summary>
/// Defines the <see cref="CanCreateItem"/> property.
/// </summary>
public static readonly StyledProperty<bool> CanCreateItemProperty =
AvaloniaProperty.Register<ToolTabStrip, bool>(nameof(CanCreateItem));

/// <summary>
/// Defines the <see cref="MouseWheelScrollOrientation"/> property.
/// </summary>
public static readonly StyledProperty<Orientation> MouseWheelScrollOrientationProperty =
AvaloniaProperty.Register<ToolTabStrip, Orientation>(
nameof(MouseWheelScrollOrientation),
defaultValue: Orientation.Horizontal);

/// <summary>
/// Gets or sets if tab strop dock can create new items.
/// </summary>
Expand All @@ -39,6 +50,15 @@ public bool CanCreateItem
set => SetValue(CanCreateItemProperty, value);
}

/// <summary>
/// Gets or sets orientation used for mouse wheel scrolling in the tab strip.
/// </summary>
public Orientation MouseWheelScrollOrientation
{
get => GetValue(MouseWheelScrollOrientationProperty);
set => SetValue(MouseWheelScrollOrientationProperty, value);
}

/// <inheritdoc/>
protected override Type StyleKeyOverride => typeof(ToolTabStrip);

Expand All @@ -55,10 +75,13 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);

AttachScrollViewerWheel(null);

_borderLeftFill = e.NameScope.Find<Border>("PART_BorderLeftFill");
_borderRightFill = e.NameScope.Find<Border>("PART_BorderRightFill");
_itemsPresenter = e.NameScope.Find<ItemsPresenter>("PART_ItemsPresenter");
_scrollViewer = e.NameScope.Find<ScrollViewer>("PART_ScrollViewer");
AttachScrollViewerWheel(_scrollViewer);

_itemsPresenter?.GetObservable(Border.BoundsProperty)
.Subscribe(new AnonymousObserver<Rect>(_ => UpdateBorders()));
Expand All @@ -75,6 +98,20 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
UpdateBorders();
}

/// <inheritdoc/>
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
AttachScrollViewerWheel(null);
}

/// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
AttachScrollViewerWheel(_scrollViewer);
}

private void OnContainerAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
if (sender is ToolTabStripItem tabStripItem)
Expand Down Expand Up @@ -175,10 +212,21 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
{
UpdatePseudoClasses(change.GetNewValue<bool>());
}

if (change.Property == MouseWheelScrollOrientationProperty)
{
AttachScrollViewerWheel(_scrollViewer);
}
}

private void UpdatePseudoClasses(bool canCreate)
{
PseudoClasses.Set(":create", canCreate);
}

private void AttachScrollViewerWheel(ScrollViewer? scrollViewer)
{
_scrollViewerWheelSubscription?.Dispose();
_scrollViewerWheelSubscription = ScrollViewerMouseWheelHookHelper.Attach(scrollViewer, MouseWheelScrollOrientation);
}
}
51 changes: 51 additions & 0 deletions src/Dock.Avalonia/Internal/ScrollViewerMouseWheelHookHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Wiesław Šoltés. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
using System;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;

namespace Dock.Avalonia.Internal;

internal static class ScrollViewerMouseWheelHookHelper
{
public static IDisposable? Attach(ScrollViewer? scrollViewer, Orientation orientation)
{
if (scrollViewer is null)
{
return null;
}

void OnPointerWheelChanged(object? sender, PointerWheelEventArgs e)
{
if (e.Handled)
{
return;
}

if (TabStripMouseWheelScrollHelper.TryHandle(scrollViewer, orientation, e.Delta))
{
e.Handled = true;
}
}

scrollViewer.PointerWheelChanged += OnPointerWheelChanged;
return new DelegateDisposable(() => scrollViewer.PointerWheelChanged -= OnPointerWheelChanged);
}

private sealed class DelegateDisposable : IDisposable
{
private Action? _dispose;

public DelegateDisposable(Action dispose)
{
_dispose = dispose;
}

public void Dispose()
{
_dispose?.Invoke();
_dispose = null;
}
}
}
73 changes: 73 additions & 0 deletions src/Dock.Avalonia/Internal/TabStripMouseWheelScrollHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) Wiesław Šoltés. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;

namespace Dock.Avalonia.Internal;

internal static class TabStripMouseWheelScrollHelper
{
public static bool TryHandle(ScrollViewer? scrollViewer, Orientation orientation, Vector delta)
{
if (scrollViewer is null)
{
return false;
}

var deltaY = delta.Y;
if (Math.Abs(deltaY) <= double.Epsilon)
{
return false;
}

var steps = Math.Max(1, (int)Math.Ceiling(Math.Abs(deltaY)));

switch (orientation)
{
case Orientation.Horizontal:
if (scrollViewer.Extent.Width <= scrollViewer.Viewport.Width)
{
return false;
}

var initialHorizontalOffset = scrollViewer.Offset;
for (var i = 0; i < steps; i++)
{
if (deltaY > 0)
{
scrollViewer.LineLeft();
}
else
{
scrollViewer.LineRight();
}
}

return !scrollViewer.Offset.Equals(initialHorizontalOffset);
case Orientation.Vertical:
if (scrollViewer.Extent.Height <= scrollViewer.Viewport.Height)
{
return false;
}

var initialVerticalOffset = scrollViewer.Offset;
for (var i = 0; i < steps; i++)
{
if (deltaY > 0)
{
scrollViewer.LineUp();
}
else
{
scrollViewer.LineDown();
}
}

return !scrollViewer.Offset.Equals(initialVerticalOffset);
default:
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ public void DocumentTabStrip_Default_Orientation_Horizontal()
Assert.Equal(LayoutOrientation.Horizontal, control.Orientation);
}

[AvaloniaFact]
public void DocumentTabStrip_Default_MouseWheelScrollOrientation_Horizontal()
{
var control = new DocumentTabStrip();
Assert.Equal(LayoutOrientation.Horizontal, control.MouseWheelScrollOrientation);
}

[AvaloniaFact]
public void DockableControl_Default_TrackingMode_Visible()
{
Expand Down Expand Up @@ -197,6 +204,13 @@ public void ToolTabStrip_Can_Instantiate()
Assert.NotNull(control);
}

[AvaloniaFact]
public void ToolTabStrip_Default_MouseWheelScrollOrientation_Horizontal()
{
var control = new ToolTabStrip();
Assert.Equal(LayoutOrientation.Horizontal, control.MouseWheelScrollOrientation);
}

[AvaloniaFact]
public void ToolTabStripItem_Can_Instantiate()
{
Expand Down
Loading
Loading