Browse Source

Restore Close entries on the document-tab context menu

The freeze work pointed Dock's DocumentContextMenu (which it shows on a
document-tab right-click, overriding the standard ContextMenu) at a menu
holding only "Freeze tab". That hid the Close / Close all but this /
Close all entries defined in App.axaml, and on a frozen tab — where the
Freeze item is hidden — the whole menu came up empty.

Build the full menu (Close / Close all but this / Close all, plus the
preview-only Freeze entry) in PreviewTabContextMenuBehavior and drop the
now-superseded App.axaml ContextMenu setter, so there is a single source
for the document-tab menu.

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3755/head
Siegfried Pammer 1 month ago
parent
commit
774cef5cb0
  1. 28
      ILSpy.Tests/Docking/DocumentTabContextMenuTests.cs
  2. 8
      ILSpy.Tests/Docking/PreviewTabPromotionTests.cs
  3. 16
      ILSpy/App.axaml
  4. 49
      ILSpy/Themes/PreviewTabContextMenuBehavior.cs

28
ILSpy.Tests/Docking/DocumentTabContextMenuTests.cs

@ -51,6 +51,34 @@ public class DocumentTabContextMenuTests @@ -51,6 +51,34 @@ public class DocumentTabContextMenuTests
return (a, b, c);
}
[AvaloniaTest]
public async Task Menu_Carries_The_Close_Entries_Bound_To_The_Tab_Commands()
{
// Regression: the per-tab DocumentContextMenu (which Dock shows, overriding the standard
// ContextMenu) once held only "Freeze tab", so the Close / Close all entries went missing
// (and the whole menu was empty on a frozen tab). It must carry all three close entries.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var (a, _, _) = OpenThreeTabs(vm.DockWorkspace);
var menu = global::ILSpy.Themes.PreviewTabContextMenuBehavior.BuildDocumentContextMenu(a);
var items = menu.ItemsSource!.OfType<global::Avalonia.Controls.MenuItem>().ToList();
items.Select(m => m.Header).Should().Contain(new object[] {
ICSharpCode.ILSpy.Properties.Resources.Close,
ICSharpCode.ILSpy.Properties.Resources.CloseAllButThisTab,
ICSharpCode.ILSpy.Properties.Resources.CloseAllTabs,
});
items.Single(m => Equals(m.Header, ICSharpCode.ILSpy.Properties.Resources.Close)).Command
.Should().BeSameAs(a.CloseCommand);
items.Single(m => Equals(m.Header, ICSharpCode.ILSpy.Properties.Resources.CloseAllButThisTab)).Command
.Should().BeSameAs(a.CloseAllButThisCommand);
items.Single(m => Equals(m.Header, ICSharpCode.ILSpy.Properties.Resources.CloseAllTabs)).Command
.Should().BeSameAs(a.CloseAllCommand);
}
[AvaloniaTest]
public async Task Close_All_But_This_Leaves_Only_The_Target_Tab()
{

8
ILSpy.Tests/Docking/PreviewTabPromotionTests.cs

@ -415,11 +415,13 @@ public class PreviewTabPromotionTests @@ -415,11 +415,13 @@ public class PreviewTabPromotionTests
var carveOutItem = window.GetVisualDescendants().OfType<DocumentTabStripItem>()
.Single(item => item.DataContext is ContentTabPage t && t.SourceNode == typeNode);
var mainFreezeEntry = mainTabItem.DocumentContextMenu!.Items.OfType<global::Avalonia.Controls.MenuItem>().Single();
mainFreezeEntry.Header.Should().Be("Freeze tab");
// The menu now also carries Close / Close all but this / Close all; pick the Freeze entry.
var mainFreezeEntry = mainTabItem.DocumentContextMenu!.Items.OfType<global::Avalonia.Controls.MenuItem>()
.Single(m => (string?)m.Header == "Freeze tab");
mainFreezeEntry.IsVisible.Should().BeTrue("MainTab is preview — Freeze tab entry must be visible");
var carveFreezeEntry = carveOutItem.DocumentContextMenu!.Items.OfType<global::Avalonia.Controls.MenuItem>().Single();
var carveFreezeEntry = carveOutItem.DocumentContextMenu!.Items.OfType<global::Avalonia.Controls.MenuItem>()
.Single(m => (string?)m.Header == "Freeze tab");
carveFreezeEntry.IsVisible.Should().BeFalse(
"carve-out tabs are already frozen — the Freeze entry must hide");
}

16
ILSpy/App.axaml

@ -306,19 +306,9 @@ @@ -306,19 +306,9 @@
names, so this is document-only. -->
<Style Selector="dockControls|DocumentTabStripItem">
<Setter Property="ToolTip.Tip" Value="{Binding Title, Mode=OneWay, x:CompileBindings=False}" />
<!-- Right-click a document tab: Close / Close all but this / Close all. The tab strip item's
DataContext is the ContentTabPage, which carries these commands (delegating to DockWorkspace). -->
<Setter Property="ContextMenu">
<ContextMenu>
<MenuItem Header="{x:Static res:Resources.Close}"
Command="{Binding CloseCommand, x:CompileBindings=False}" />
<Separator />
<MenuItem Header="{x:Static res:Resources.CloseAllButThisTab}"
Command="{Binding CloseAllButThisCommand, x:CompileBindings=False}" />
<MenuItem Header="{x:Static res:Resources.CloseAllTabs}"
Command="{Binding CloseAllCommand, x:CompileBindings=False}" />
</ContextMenu>
</Setter>
<!-- The document-tab right-click menu (Close / Close all but this / Close all / Freeze tab)
is built per-tab by PreviewTabContextMenuBehavior and assigned to Dock's
DocumentContextMenu, which takes precedence over the standard ContextMenu. -->
</Style>
<!-- A merely-selected (but not focused) tab gets the seam-matching fill. The active
tab keeps Dock's accent highlight (see below). Splitting the two states is

49
ILSpy/Themes/PreviewTabContextMenuBehavior.cs

@ -23,6 +23,8 @@ using Avalonia.Controls; @@ -23,6 +23,8 @@ using Avalonia.Controls;
using Dock.Avalonia.Controls;
using ICSharpCode.ILSpy.Properties;
using ILSpy.AppEnv;
using ILSpy.Docking;
using ILSpy.ViewModels;
@ -30,12 +32,14 @@ using ILSpy.ViewModels; @@ -30,12 +32,14 @@ using ILSpy.ViewModels;
namespace ILSpy.Themes
{
/// <summary>
/// Attached property that wires a per-instance <see cref="ContextMenu"/> onto every
/// <see cref="DocumentTabStripItem"/>. The menu hosts a single "Freeze tab" entry whose
/// <see cref="MenuItem.IsVisible"/> tracks <see cref="ContentTabPage.IsPreview"/> on
/// the tab's underlying viewmodel — invoking it routes to
/// <see cref="DockWorkspace.FreezeCurrentTab"/>. Each tab gets a fresh ContextMenu so
/// the popup has a unique parent (Avalonia visuals must have at most one parent).
/// Attached property that wires a per-instance context menu onto every
/// <see cref="DocumentTabStripItem"/> via Dock's <c>DocumentContextMenu</c> (the property Dock
/// actually shows on a document-tab right-click — it overrides the standard
/// <see cref="Control.ContextMenu"/>). The menu carries Close / Close all but this / Close all
/// (routing to the tab's commands) plus a "Freeze tab" entry shown only while the tab is a
/// preview (<see cref="ContentTabPage.IsPreview"/>); invoking Freeze routes to
/// <see cref="DockWorkspace.FreezeCurrentTab"/>. Each tab gets a fresh ContextMenu so the popup
/// has a unique parent (Avalonia visuals must have at most one parent).
/// </summary>
public static class PreviewTabContextMenuBehavior
{
@ -74,24 +78,49 @@ namespace ILSpy.Themes @@ -74,24 +78,49 @@ namespace ILSpy.Themes
item.DocumentContextMenu = null;
return;
}
item.DocumentContextMenu = BuildDocumentContextMenu(tab);
}
/// <summary>
/// Builds the document-tab context menu for <paramref name="tab"/>: Close / Close all but
/// this / Close all, plus a "Freeze tab" entry (and its separator) shown only while the tab
/// is a preview. Exposed for tests so the menu's composition can be verified without the
/// realised tab-strip visual.
/// </summary>
internal static ContextMenu BuildDocumentContextMenu(ContentTabPage tab)
{
var close = new MenuItem { Header = Resources.Close, Command = tab.CloseCommand };
var closeAllButThis = new MenuItem { Header = Resources.CloseAllButThisTab, Command = tab.CloseAllButThisCommand };
var closeAll = new MenuItem { Header = Resources.CloseAllTabs, Command = tab.CloseAllCommand };
var freezeSeparator = new Separator { IsVisible = tab.IsPreview };
var freezeItem = new MenuItem {
Header = "Freeze tab",
IsVisible = tab.IsPreview,
};
freezeItem.Click += (_, _) => TryGetDockWorkspace()?.FreezeCurrentTab();
// Keep IsVisible in sync with IsPreview so the entry hides once the tab is
// frozen (freeze is one-way — there's no matching unfreeze entry).
// Keep the Freeze entry (and its separator) in sync with IsPreview so they hide once the
// tab is frozen (freeze is one-way — there's no matching unfreeze entry).
tab.PropertyChanged += OnTabPropertyChanged;
void OnTabPropertyChanged(object? _, PropertyChangedEventArgs args)
{
if (args.PropertyName == nameof(ContentTabPage.IsPreview))
{
freezeItem.IsVisible = tab.IsPreview;
freezeSeparator.IsVisible = tab.IsPreview;
}
}
item.DocumentContextMenu = new ContextMenu {
ItemsSource = new[] { freezeItem },
return new ContextMenu {
ItemsSource = new Control[] {
close,
new Separator(),
closeAllButThis,
closeAll,
freezeSeparator,
freezeItem,
},
};
}

Loading…
Cancel
Save