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
76 changes: 75 additions & 1 deletion src/Dock.Avalonia/Internal/ItemDragHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ internal class ItemDragHelper
private const double AutoScrollSpeed = 200.0; // pixels per second
private const double AutoScrollTimerInterval = 16.0; // ~60fps
private PointerEventArgs? _pressedArgs;
private SelectingItemsControl? _selectingItemsControl;
private int _selectedIndexOnPress = -1;
private object? _selectedItemOnPress;
private bool _selectionRestored;

public ItemDragHelper(
Control owner,
Expand Down Expand Up @@ -76,6 +80,7 @@ public void Detach()
_owner.RemoveHandler(InputElement.PointerCaptureLostEvent, PointerCaptureLost);

StopAutoScroll();
ResetSelectionTracking();
}

private void PointerPressed(object? sender, PointerPressedEventArgs e)
Expand All @@ -90,6 +95,7 @@ private void PointerPressed(object? sender, PointerPressedEventArgs e)
_targetIndex = -1;
_itemsControl = itemsControl;
_draggedContainer = _owner;
CaptureSelection(itemsControl);

if (_draggedContainer is not null)
{
Expand Down Expand Up @@ -174,6 +180,7 @@ private void Released()
_itemsControl = null;

_draggedContainer = null;
ResetSelectionTracking();
}

private void AddTransforms(ItemsControl? itemsControl)
Expand Down Expand Up @@ -448,6 +455,7 @@ private void PointerMoved(object? sender, PointerEventArgs e)
if (!IsPositionWithinDragBounds(position, _itemsControl))
{
_dragStarted = false;
RestoreSelectionIfNeeded();
Released();
_captured = false;
_dragOutside?.Invoke(_pressedArgs, e);
Expand All @@ -469,6 +477,7 @@ private void PointerMoved(object? sender, PointerEventArgs e)
if (Math.Abs(diff.X) > horizontalDragThreshold)
{
_dragStarted = true;
RestoreSelectionIfNeeded();
}
else
{
Expand All @@ -480,6 +489,7 @@ private void PointerMoved(object? sender, PointerEventArgs e)
if (Math.Abs(diff.Y) > verticalDragThreshold)
{
_dragStarted = true;
RestoreSelectionIfNeeded();
}
else
{
Expand Down Expand Up @@ -564,6 +574,71 @@ private void PointerMoved(object? sender, PointerEventArgs e)
}
}

private void CaptureSelection(ItemsControl itemsControl)
{
if (itemsControl is SelectingItemsControl selectingItemsControl)
{
_selectingItemsControl = selectingItemsControl;
_selectedIndexOnPress = selectingItemsControl.SelectedIndex;
_selectedItemOnPress = selectingItemsControl.SelectedItem;
_selectionRestored = false;
}
else
{
ResetSelectionTracking();
}
}

private void RestoreSelectionIfNeeded()
{
if (_selectionRestored || _selectingItemsControl is null)
{
return;
}

var selectingItemsControl = _selectingItemsControl;

if (_selectedItemOnPress is not null)
{
if (!Equals(selectingItemsControl.SelectedItem, _selectedItemOnPress))
{
if (selectingItemsControl.Items is IList list && list.Contains(_selectedItemOnPress))
{
selectingItemsControl.SelectedItem = _selectedItemOnPress;
_selectionRestored = true;
return;
}
}
else
{
_selectionRestored = true;
return;
}
}

if (_selectedIndexOnPress >= 0 &&
selectingItemsControl.Items is { } items &&
_selectedIndexOnPress < items.Count)
{
if (selectingItemsControl.SelectedIndex != _selectedIndexOnPress)
{
selectingItemsControl.SelectedIndex = _selectedIndexOnPress;
}
_selectionRestored = true;
return;
}

_selectionRestored = true;
}

private void ResetSelectionTracking()
{
_selectingItemsControl = null;
_selectedIndexOnPress = -1;
_selectedItemOnPress = null;
_selectionRestored = false;
}

private void SetDraggingPseudoClasses(Control control, bool isDragging)
{
if (isDragging)
Expand All @@ -583,4 +658,3 @@ private void SetTranslateTransform(Control control, double x, double y)
control.RenderTransform = transformBuilder.Build();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.Input;
using Dock.Avalonia.Controls;
using Xunit;

namespace Dock.Avalonia.HeadlessTests;

public class DocumentTabStripSelectionRestoreTests
{
[AvaloniaFact]
public void DragOutside_RestoresSelection()
{
var (window, tabStrip) = CreateTabStrip();
try
{
tabStrip.SelectedIndex = 0;
var originalIndex = tabStrip.SelectedIndex;

var tabItem = GetTabItem(tabStrip, 1);

var pointer = new Pointer(1, PointerType.Mouse, true);
var pressed = CreatePressedArgs(tabItem, tabStrip, new Point(5, 5), pointer);
tabItem.RaiseEvent(pressed);

tabStrip.SelectedIndex = 1;
Assert.Equal(1, tabStrip.SelectedIndex);

var moved = CreateMovedArgs(tabItem, tabStrip, new Point(-50, 5), pointer);
tabItem.RaiseEvent(moved);

Assert.Equal(originalIndex, tabStrip.SelectedIndex);
}
finally
{
window.Close();
}
}

[AvaloniaFact]
public void DragThreshold_RestoresSelection()
{
var (window, tabStrip) = CreateTabStrip();
try
{
tabStrip.SelectedIndex = 0;
var originalIndex = tabStrip.SelectedIndex;

var tabItem = GetTabItem(tabStrip, 1);

var pointer = new Pointer(2, PointerType.Mouse, true);
var pressed = CreatePressedArgs(tabItem, tabStrip, new Point(5, 5), pointer);
tabItem.RaiseEvent(pressed);

tabStrip.SelectedIndex = 1;
Assert.Equal(1, tabStrip.SelectedIndex);

var moved = CreateMovedArgs(tabItem, tabStrip, new Point(20, 5), pointer);
tabItem.RaiseEvent(moved);

Assert.Equal(originalIndex, tabStrip.SelectedIndex);
}
finally
{
window.Close();
}
}

private static (Window window, DocumentTabStrip tabStrip) CreateTabStrip()
{
var tabStrip = new DocumentTabStrip
{
Width = 400,
Height = 32,
ItemsSource = new AvaloniaList<string> { "Doc1", "Doc2" }
};

var window = new Window
{
Width = 400,
Height = 200,
Content = tabStrip
};

window.Show();
tabStrip.ApplyTemplate();
window.UpdateLayout();
tabStrip.UpdateLayout();

return (window, tabStrip);
}

private static DocumentTabStripItem GetTabItem(DocumentTabStrip tabStrip, int index)
{
var tabItem = tabStrip.ContainerFromIndex(index) as DocumentTabStripItem;
Assert.NotNull(tabItem);
return tabItem!;
}

private static PointerPressedEventArgs CreatePressedArgs(Control source, Visual rootVisual, Point position, Pointer pointer)
{
var properties = new PointerPointProperties(RawInputModifiers.LeftMouseButton, PointerUpdateKind.LeftButtonPressed);
return new PointerPressedEventArgs(source, pointer, rootVisual, position, 0, properties, KeyModifiers.None, 1);
}

private static PointerEventArgs CreateMovedArgs(Control source, Visual rootVisual, Point position, Pointer pointer)
{
var properties = new PointerPointProperties(RawInputModifiers.LeftMouseButton, PointerUpdateKind.Other);
return new PointerEventArgs(InputElement.PointerMovedEvent, source, pointer, rootVisual, position, 0, properties, KeyModifiers.None);
}
}