mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
#3753: when many documents are open, the tab strip scrolls a single row, hiding most tabs behind the overflow scroller. Dock's DocumentTabStrip hardcodes a horizontal StackPanel inside a PART_ScrollViewer in its theme template and ignores an ItemsPanel set via a Style, so a MultiRowTabStripBehavior sets the WrapPanel ItemsPanel directly and disables the scroll-viewer's horizontal scrolling, giving the WrapPanel a bounded width so tabs wrap onto new rows. Enabled on the document strip (not tool tabs) from App.axaml. Drag-reorder across rows is not yet validated (Dock's drop-position logic assumes a single horizontal strip), so this stays an opt-in prototype for now rather than a closing fix for the issue. Assisted-by: Claude:claude-opus-4-8:Claude Codepull/3755/head
3 changed files with 166 additions and 0 deletions
@ -0,0 +1,78 @@ |
|||||||
|
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||||
|
// software and associated documentation files (the "Software"), to deal in the Software
|
||||||
|
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||||
|
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||||
|
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
// substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||||
|
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||||
|
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
// DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
using System.Linq; |
||||||
|
using System.Threading.Tasks; |
||||||
|
|
||||||
|
using Avalonia.Controls; |
||||||
|
using Avalonia.Headless.NUnit; |
||||||
|
using Avalonia.Threading; |
||||||
|
using Avalonia.VisualTree; |
||||||
|
|
||||||
|
using AwesomeAssertions; |
||||||
|
|
||||||
|
using Dock.Avalonia.Controls; |
||||||
|
|
||||||
|
using ILSpy.AppEnv; |
||||||
|
using ILSpy.TextView; |
||||||
|
using ILSpy.ViewModels; |
||||||
|
using ILSpy.Views; |
||||||
|
|
||||||
|
using NUnit.Framework; |
||||||
|
|
||||||
|
namespace ICSharpCode.ILSpy.Tests.Docking; |
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PROTOTYPE check: with MultiRowTabStripBehavior enabled (App.axaml), many document tabs flow onto
|
||||||
|
/// multiple rows (a WrapPanel) instead of a single scrolling row.
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture] |
||||||
|
public class MultiRowTabStripTests |
||||||
|
{ |
||||||
|
[AvaloniaTest] |
||||||
|
public async Task Many_Document_Tabs_Wrap_To_Multiple_Rows() |
||||||
|
{ |
||||||
|
var window = AppComposition.Current.GetExport<MainWindow>(); |
||||||
|
window.Show(); |
||||||
|
var vm = (MainWindowViewModel)window.DataContext!; |
||||||
|
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); |
||||||
|
|
||||||
|
for (int i = 0; i < 40; i++) |
||||||
|
vm.DockWorkspace.OpenNewTab(new DecompilerTabPageModel { Title = $"Tab number {i:00}" }); |
||||||
|
|
||||||
|
for (int i = 0; i < 12; i++) |
||||||
|
{ |
||||||
|
Dispatcher.UIThread.RunJobs(); |
||||||
|
window.UpdateLayout(); |
||||||
|
await Task.Delay(20); |
||||||
|
} |
||||||
|
|
||||||
|
var strip = window.GetVisualDescendants().OfType<DocumentTabStrip>().FirstOrDefault(); |
||||||
|
strip.Should().NotBeNull("the document tab strip must be realised"); |
||||||
|
|
||||||
|
strip!.GetVisualDescendants().OfType<WrapPanel>().Should().NotBeEmpty( |
||||||
|
"the behaviour must swap the strip's ItemsPanel to a WrapPanel"); |
||||||
|
|
||||||
|
var rows = strip.GetVisualDescendants().OfType<DocumentTabStripItem>() |
||||||
|
.Where(it => it.Bounds.Width > 0) |
||||||
|
.Select(it => System.Math.Round(it.Bounds.Y)) |
||||||
|
.Distinct().ToList(); |
||||||
|
|
||||||
|
rows.Count.Should().BeGreaterThan(1, "40 tabs must wrap onto more than one row"); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,84 @@ |
|||||||
|
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||||
|
// software and associated documentation files (the "Software"), to deal in the Software
|
||||||
|
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||||
|
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||||
|
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
// substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||||
|
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||||
|
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
// DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
using System.Linq; |
||||||
|
|
||||||
|
using Avalonia; |
||||||
|
using Avalonia.Controls; |
||||||
|
using Avalonia.Controls.Primitives; |
||||||
|
using Avalonia.Controls.Templates; |
||||||
|
using Avalonia.Layout; |
||||||
|
using Avalonia.VisualTree; |
||||||
|
|
||||||
|
using Dock.Avalonia.Controls; |
||||||
|
|
||||||
|
namespace ILSpy.Themes |
||||||
|
{ |
||||||
|
/// <summary>
|
||||||
|
/// PROTOTYPE: makes the document tab strip lay its tabs out on multiple rows instead of a single
|
||||||
|
/// scrolling row. Dock's <see cref="DocumentTabStrip"/> hardcodes a horizontal StackPanel inside a
|
||||||
|
/// PART_ScrollViewer in its theme template and ignores an ItemsPanel set via a Style, so this
|
||||||
|
/// behaviour sets the WrapPanel ItemsPanel directly and disables the scroll-viewer's horizontal
|
||||||
|
/// scrolling (so the WrapPanel gets a bounded width and actually wraps). Toggle via the attached
|
||||||
|
/// Enable property in App.axaml.
|
||||||
|
/// </summary>
|
||||||
|
public static class MultiRowTabStripBehavior |
||||||
|
{ |
||||||
|
public static readonly AttachedProperty<bool> EnableProperty = |
||||||
|
AvaloniaProperty.RegisterAttached<DocumentTabStrip, bool>( |
||||||
|
"Enable", |
||||||
|
typeof(MultiRowTabStripBehavior)); |
||||||
|
|
||||||
|
public static void SetEnable(DocumentTabStrip element, bool value) |
||||||
|
=> element.SetValue(EnableProperty, value); |
||||||
|
|
||||||
|
public static bool GetEnable(DocumentTabStrip element) |
||||||
|
=> element.GetValue(EnableProperty); |
||||||
|
|
||||||
|
static MultiRowTabStripBehavior() |
||||||
|
{ |
||||||
|
EnableProperty.Changed.AddClassHandler<DocumentTabStrip>(OnEnableChanged); |
||||||
|
} |
||||||
|
|
||||||
|
static void OnEnableChanged(DocumentTabStrip strip, AvaloniaPropertyChangedEventArgs e) |
||||||
|
{ |
||||||
|
if (e.NewValue is not true) |
||||||
|
return; |
||||||
|
|
||||||
|
// The presenter builds its panel from ItemsPanel; setting it here (before/at styling time)
|
||||||
|
// makes the tabs flow through a WrapPanel.
|
||||||
|
strip.ItemsPanel = new FuncTemplate<Panel?>( |
||||||
|
() => new WrapPanel { Orientation = Orientation.Horizontal }); |
||||||
|
|
||||||
|
// The PART_ScrollViewer only exists once the template is applied. Disabling its horizontal
|
||||||
|
// scroll constrains the WrapPanel to the strip width so it wraps onto new rows.
|
||||||
|
strip.TemplateApplied += (_, _) => ConstrainScroller(strip); |
||||||
|
ConstrainScroller(strip); |
||||||
|
} |
||||||
|
|
||||||
|
static void ConstrainScroller(DocumentTabStrip strip) |
||||||
|
{ |
||||||
|
var scroller = strip.GetVisualDescendants().OfType<ScrollViewer>() |
||||||
|
.FirstOrDefault(s => s.Name == "PART_ScrollViewer"); |
||||||
|
if (scroller is null) |
||||||
|
return; |
||||||
|
scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; |
||||||
|
scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue