Browse Source

Pin the preview tab to index 0 and make it immovable

The One must stay leftmost: a frozen tab dragged before it, or the One dragged
out of slot 0, would break the 'preview is always first' rule. Dock's in-strip
reorder commits through FactoryBase.MoveDockable (the tab strip ignores
IDockable.CanDrag), so override it: refuse moving the One, and clamp any tab
targeting the One's slot to position 1 instead. Override the cross-dock variant
too, and add an idempotent OnDockableMoved guard that re-asserts the One at
index 0 against any drag path.

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3755/head
Siegfried Pammer 1 month ago
parent
commit
738b79807a
  1. 51
      ILSpy.Tests/Docking/PreviewTabPromotionTests.cs
  2. 49
      ILSpy/Docking/ILSpyDockFactory.cs

51
ILSpy.Tests/Docking/PreviewTabPromotionTests.cs

@ -625,4 +625,55 @@ public class PreviewTabPromotionTests @@ -625,4 +625,55 @@ public class PreviewTabPromotionTests
docs.VisibleDockables!.IndexOf(frozen).Should().BeGreaterThan(0,
"the frozen ex-One slides to the right of the new One");
}
[AvaloniaTest]
public async Task Preview_Cannot_Be_Reordered_Out_Of_Slot_0()
{
// The in-strip reorder drag commits through the same-dock MoveDockable; it's overridden to
// refuse moving the One out of slot 0. Drive that commit directly (what ItemDragHelper does).
var (_, vm) = await TestHarness.BootAsync(3);
var factory = (ILSpyDockFactory)vm.DockWorkspace.Factory;
var docs = vm.DockWorkspace.Documents!;
var typeA = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
vm.AssemblyTreeModel.SelectNode(typeA);
vm.DockWorkspace.SettleSelection();
var theOne = factory.MainTab!;
var typeC = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Private.Uri", "System", "System.Uri");
vm.DockWorkspace.OpenNodeInNewTab(typeC);
var frozen = docs.VisibleDockables!.OfType<ContentTabPage>().Last(t => t.SourceNode == typeC);
docs.VisibleDockables!.IndexOf(theOne).Should().Be(0, "precondition: the One is at index 0");
factory.MoveDockable(docs, theOne, frozen);
docs.VisibleDockables!.IndexOf(theOne).Should().Be(0,
"the One must stay at index 0 -- it is immovable");
}
[AvaloniaTest]
public async Task Frozen_Tab_Cannot_Be_Reordered_Before_The_Preview()
{
var (_, vm) = await TestHarness.BootAsync(3);
var factory = (ILSpyDockFactory)vm.DockWorkspace.Factory;
var docs = vm.DockWorkspace.Documents!;
var typeA = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
vm.AssemblyTreeModel.SelectNode(typeA);
vm.DockWorkspace.SettleSelection();
var theOne = factory.MainTab!;
var typeC = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Private.Uri", "System", "System.Uri");
vm.DockWorkspace.OpenNodeInNewTab(typeC);
var frozen = docs.VisibleDockables!.OfType<ContentTabPage>().Last(t => t.SourceNode == typeC);
factory.MoveDockable(docs, frozen, theOne);
docs.VisibleDockables!.IndexOf(theOne).Should().Be(0,
"the One must remain at index 0; a frozen tab cannot slip before it");
docs.VisibleDockables!.IndexOf(frozen).Should().BeGreaterThan(0,
"the frozen tab is clamped to a position after the One");
}
}

49
ILSpy/Docking/ILSpyDockFactory.cs

@ -83,6 +83,55 @@ namespace ILSpy.Docking @@ -83,6 +83,55 @@ namespace ILSpy.Docking
return fresh;
}
// The One is immovable at documents-dock index 0. A within-strip reorder drag commits
// through this same-dock MoveDockable (Dock's DocumentTabStripItem/ItemDragHelper does NOT
// consult IDockable.CanDrag), so overriding it is the choke point that pins the One to
// slot 0 and keeps frozen tabs to its right.
public override void MoveDockable(IDock dock, IDockable sourceDockable, IDockable targetDockable)
{
if (ReferenceEquals(dock, Documents))
{
// Never drag the One out of slot 0.
if (ReferenceEquals(sourceDockable, MainTab))
return;
// A frozen tab must not land at or before the One: retarget to the tab at index 1
// so the source lands at position 1, not 0.
if (ReferenceEquals(targetDockable, MainTab))
{
var kids = dock.VisibleDockables;
if (kids is { Count: > 1 })
targetDockable = kids[1];
else
return;
}
}
base.MoveDockable(dock, sourceDockable, targetDockable);
}
// Cross-dock move: never pull the One into another document dock (only reachable if the
// document area is ever split).
public override void MoveDockable(IDock sourceDock, IDock targetDock, IDockable sourceDockable, IDockable? targetDockable)
{
if (ReferenceEquals(sourceDockable, MainTab))
return;
base.MoveDockable(sourceDock, targetDock, sourceDockable, targetDockable);
}
// Self-healing insurance: whatever drag path ran, re-assert the One at index 0. Idempotent
// and cheap -- guards against a future Dock version routing a reorder past MoveDockable.
public override void OnDockableMoved(IDockable? dockable)
{
base.OnDockableMoved(dockable);
if (MainTab is not { } one || Documents?.VisibleDockables is not { } kids)
return;
int idx = kids.IndexOf(one);
if (idx > 0)
{
kids.RemoveAt(idx);
kids.Insert(0, one);
}
}
public ILSpyDockFactory(ToolPaneRegistry registry)
{
this.panes = registry.Panes;

Loading…
Cancel
Save