From 1659b8e9564741a1559e2a7b7c2e2c01c1822991 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 3 Jun 2026 08:52:55 +0200 Subject: [PATCH] Rename the preview-tab 'pin' concept to 'freeze' Pure rename, no behavior change. 'Pin' conventionally means stuck-at-front-of- strip + excluded-from-close-all, which is a different (future) feature; the gesture that makes the preview tab stop following the tree selection is now called Freeze throughout: FreezeCurrentTab / FreezeCurrentMainTab, the PreviewTabFreezeButtonBehavior, the 'Freeze tab' menu entry/tooltip, and the Window_FreezeCurrentTab command + resource. The pushpin glyph and accent colour are unchanged here -- they move in the following commit. Assisted-by: Claude:claude-opus-4-8:Claude Code --- .../Docking/PreviewTabPromotionTests.cs | 228 +++++++++--------- ILSpy/App.axaml | 4 +- ILSpy/Commands/WindowCommands.cs | 12 +- ILSpy/Docking/DockWorkspace.cs | 26 +- ILSpy/Docking/ILSpyDockFactory.cs | 16 +- ILSpy/Properties/Resources.Designer.cs | 4 +- ILSpy/Properties/Resources.resx | 4 +- ILSpy/Themes/PreviewTabContextMenuBehavior.cs | 16 +- ...r.cs => PreviewTabFreezeButtonBehavior.cs} | 74 +++--- ILSpy/ViewModels/ContentTabPage.cs | 4 +- 10 files changed, 194 insertions(+), 194 deletions(-) rename ILSpy/Themes/{PreviewTabPinButtonBehavior.cs => PreviewTabFreezeButtonBehavior.cs} (70%) diff --git a/ILSpy.Tests/Docking/PreviewTabPromotionTests.cs b/ILSpy.Tests/Docking/PreviewTabPromotionTests.cs index d0662bd83..f84cac3c9 100644 --- a/ILSpy.Tests/Docking/PreviewTabPromotionTests.cs +++ b/ILSpy.Tests/Docking/PreviewTabPromotionTests.cs @@ -44,21 +44,21 @@ public class PreviewTabPromotionTests public void MainTab_Starts_In_Preview_State() { // The persistent MainTab is the preview slot; tree-node clicks should replace its - // Content in place until the user explicitly pins it. + // Content in place until the user explicitly freezes it. var window = AppComposition.Current.GetExport(); window.Show(); var vm = (MainWindowViewModel)window.DataContext!; TestCapture.Step("booted"); var mainTab = ((ILSpyDockFactory)vm.DockWorkspace.Factory).MainTab!; mainTab.IsPreview.Should().BeTrue( - "the freshly-created MainTab is the preview slot until the user pins it"); + "the freshly-created MainTab is the preview slot until the user freezes it"); } [AvaloniaTest] - public async Task Carve_Out_Tabs_Are_Born_Pinned() + public async Task Carve_Out_Tabs_Are_Born_Frozen() { // Open-in-new-tab tabs are explicit user intent — they should never be replaced - // by subsequent tree-node selections, so they're born pinned (IsPreview=false). + // by subsequent tree-node selections, so they're born frozen (IsPreview=false). var (_, vm) = await TestHarness.BootAsync(3); var typeNode = vm.AssemblyTreeModel.FindNode( @@ -74,19 +74,19 @@ public class PreviewTabPromotionTests } [AvaloniaTest] - public async Task PinCurrentTab_Flips_IsPreview_Without_Spawning_A_New_Tab() + public async Task FreezeCurrentTab_Flips_IsPreview_Without_Spawning_A_New_Tab() { - // New semantics: Pin only flips IsPreview=false on the current MainTab. No new - // preview tab spawns at pin time. A fresh preview tab opens lazily later, when a + // New semantics: Freeze only flips IsPreview=false on the current MainTab. No new + // preview tab spawns at freeze time. A fresh preview tab opens lazily later, when a // tree-selection change finds the active tab frozen — see - // Selecting_A_Different_Node_After_Pin_Opens_A_New_Preview_Tab below. + // Selecting_A_Different_Node_After_Freeze_Opens_A_New_Preview_Tab below. var (_, vm) = await TestHarness.BootAsync(3); var factory = (ILSpyDockFactory)vm.DockWorkspace.Factory; var previousMainTab = factory.MainTab!; previousMainTab.IsPreview.Should().BeTrue("baseline"); - // Load content into MainTab so the pin has something meaningful to keep. + // Load content into MainTab so the freeze has something meaningful to keep. var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); vm.AssemblyTreeModel.SelectNode(typeNode); @@ -97,31 +97,31 @@ public class PreviewTabPromotionTests var tabCountBefore = vm.DockWorkspace.Documents!.VisibleDockables!.Count; - // Pin. - vm.DockWorkspace.PinCurrentTab(); - TestCapture.Step("tab-pinned"); + // Freeze. + vm.DockWorkspace.FreezeCurrentTab(); + TestCapture.Step("tab-frozen"); - // Tab is now pinned, content preserved. + // Tab is now frozen, content preserved. previousMainTab.IsPreview.Should().BeFalse( - "after pin, the previously-preview MainTab becomes a regular pinned tab"); + "after freeze, the previously-preview MainTab becomes a regular frozen tab"); ReferenceEquals(previousMainTab.SourceNode, typeNode).Should().BeTrue( - "pin must not throw away the tab's content"); + "freeze must not throw away the tab's content"); - // factory.MainTab still points at the same (now-pinned) tab — no rotation. + // factory.MainTab still points at the same (now-frozen) tab — no rotation. factory.MainTab.Should().BeSameAs(previousMainTab, - "pin alone must not rotate factory.MainTab — the pinned tab keeps the slot until a selection change spawns a new preview"); + "freeze alone must not rotate factory.MainTab — the frozen tab keeps the slot until a selection change spawns a new preview"); // And critically: no new tab spawned. vm.DockWorkspace.Documents!.VisibleDockables!.Count.Should().Be(tabCountBefore, - "pinning alone must not open a new tab; new tabs open lazily on the next selection change"); + "freezing alone must not open a new tab; new tabs open lazily on the next selection change"); } [AvaloniaTest] - public async Task Selecting_A_Different_Node_After_Pin_Opens_A_New_Preview_Tab() + public async Task Selecting_A_Different_Node_After_Freeze_Opens_A_New_Preview_Tab() { - // Pin holds the current tab's content. Selecting a different tree node while - // the (now-pinned) tab is active must spawn a fresh preview tab beside it and - // route the new content there — never overwrite the pinned tab. + // Freeze holds the current tab's content. Selecting a different tree node while + // the (now-frozen) tab is active must spawn a fresh preview tab beside it and + // route the new content there — never overwrite the frozen tab. var (_, vm) = await TestHarness.BootAsync(3); var factory = (ILSpyDockFactory)vm.DockWorkspace.Factory; @@ -132,15 +132,15 @@ public class PreviewTabPromotionTests vm.AssemblyTreeModel.SelectNode(typeA); vm.DockWorkspace.SettleSelection(); TestCapture.Step("type-a-selected"); - var pinnedTab = factory.MainTab!; - var pinnedContent = pinnedTab.Content; + var frozenTab = factory.MainTab!; + var frozenContent = frozenTab.Content; - // Phase 2: pin. factory.MainTab still points at the same (now-pinned) tab — - // no spawn yet (covered by PinCurrentTab_Flips_IsPreview_Without_Spawning_A_New_Tab). + // Phase 2: freeze. factory.MainTab still points at the same (now-frozen) tab — + // no spawn yet (covered by FreezeCurrentTab_Flips_IsPreview_Without_Spawning_A_New_Tab). var tabCountBeforeSelection = vm.DockWorkspace.Documents!.VisibleDockables!.Count; - vm.DockWorkspace.PinCurrentTab(); - TestCapture.Step("tab-pinned"); - factory.MainTab.Should().BeSameAs(pinnedTab, "pin alone keeps the slot"); + vm.DockWorkspace.FreezeCurrentTab(); + TestCapture.Step("tab-frozen"); + factory.MainTab.Should().BeSameAs(frozenTab, "freeze alone keeps the slot"); // Phase 3: select a different type — NOW a new preview tab should spawn AND // receive the new content. @@ -150,21 +150,21 @@ public class PreviewTabPromotionTests vm.DockWorkspace.SettleSelection(); TestCapture.Step("type-b-opens-new-preview"); - // New tab spawned beside the pinned one. + // New tab spawned beside the frozen one. vm.DockWorkspace.Documents!.VisibleDockables!.Count.Should().Be(tabCountBeforeSelection + 1, - "selecting a different node while the active tab is pinned must spawn a new preview tab"); + "selecting a different node while the active tab is frozen must spawn a new preview tab"); var newMainTab = factory.MainTab!; - newMainTab.Should().NotBeSameAs(pinnedTab, + newMainTab.Should().NotBeSameAs(frozenTab, "factory.MainTab must rotate to the freshly-spawned preview tab once a selection change forces it"); newMainTab.IsPreview.Should().BeTrue( - "the freshly-spawned tab is itself a preview tab (the user can pin it next)"); + "the freshly-spawned tab is itself a preview tab (the user can freeze it next)"); ReferenceEquals(newMainTab.SourceNode, typeB).Should().BeTrue( "new tree selection must land in the freshly-spawned preview tab"); - ReferenceEquals(pinnedTab.SourceNode, typeA).Should().BeTrue( - "the pinned tab must keep type A — not be overwritten by the new selection"); - pinnedTab.Content.Should().BeSameAs(pinnedContent, - "pinned tab's Content reference must survive subsequent tree selections"); + ReferenceEquals(frozenTab.SourceNode, typeA).Should().BeTrue( + "the frozen tab must keep type A — not be overwritten by the new selection"); + frozenTab.Content.Should().BeSameAs(frozenContent, + "frozen tab's Content reference must survive subsequent tree selections"); } [AvaloniaTest] @@ -172,7 +172,7 @@ public class PreviewTabPromotionTests { // Visual contract: the DocumentTabStripItem for the preview MainTab binds its // FontStyle to ContentTabPage.IsPreview via BoolToFontStyleConverter.Italic. - // Pinned tabs and tool-pane tabs fall through to FontStyle.Normal. + // Frozen tabs and tool-pane tabs fall through to FontStyle.Normal. var (window, vm) = await TestHarness.BootAsync(); // Wait for the tab strip to realise its items. @@ -213,13 +213,13 @@ public class PreviewTabPromotionTests } [AvaloniaTest] - public async Task Pin_Button_Fits_Inside_The_Tab_When_Close_Button_Is_Visible() + public async Task Freeze_Button_Fits_Inside_The_Tab_When_Close_Button_Is_Visible() { // The single-tab scenario hides the close button (UpdateLastDocumentCanClose sets - // CanClose=false), so the pin has lots of room. The bug shows up only when the + // CanClose=false), so the freeze has lots of room. The bug shows up only when the // close button is visible and competes for horizontal space. Open a carve-out // tab so both MainTab and carve-out exist; close button becomes visible; verify - // the pin's right edge stays inside the tab's right edge. + // the freeze's right edge stays inside the tab's right edge. var (window, vm) = await TestHarness.BootAsync(3); @@ -242,12 +242,12 @@ public class PreviewTabPromotionTests mainTabItem.Arrange(new global::Avalonia.Rect(mainTabItem.DesiredSize)); global::Avalonia.Threading.Dispatcher.UIThread.RunJobs(); - var pinButton = mainTabItem.GetVisualDescendants() + var freezeButton = mainTabItem.GetVisualDescendants() .OfType() - .Single(b => (b.Tag as string) == "PreviewTabPinButton"); + .Single(b => (b.Tag as string) == "PreviewTabFreezeButton"); // Walk up the visual tree summing each ancestor's bounds origin until we reach - // mainTabItem — gives the pin's top-left in tab coordinates without depending on + // mainTabItem — gives the freeze's top-left in tab coordinates without depending on // TranslatePoint (which lives on different namespaces across Avalonia versions). static global::Avalonia.Point OriginRelativeTo(global::Avalonia.Visual node, global::Avalonia.Visual ancestor) { @@ -261,36 +261,36 @@ public class PreviewTabPromotionTests } return new global::Avalonia.Point(x, y); } - var pinTopLeft = OriginRelativeTo(pinButton, mainTabItem); - var pinRight = pinTopLeft.X + pinButton.Bounds.Width; + var freezeTopLeft = OriginRelativeTo(freezeButton, mainTabItem); + var freezeRight = freezeTopLeft.X + freezeButton.Bounds.Width; var tabRight = mainTabItem.Bounds.Width; // Also find the close button so we can include it in the failure message for context. var closeButton = mainTabItem.GetVisualDescendants() .OfType() - .Where(b => (b.Tag as string) != "PreviewTabPinButton") + .Where(b => (b.Tag as string) != "PreviewTabFreezeButton") .Select(b => new { b, p = OriginRelativeTo(b, mainTabItem) }) .OrderByDescending(x => x.p.X) .FirstOrDefault(); var closeInfo = closeButton == null ? "no close button found" : $"close at x={closeButton.p.X:0}-{closeButton.p.X + closeButton.b.Bounds.Width:0}"; - TestContext.WriteLine($"tab width={tabRight:0}; pin at x={pinTopLeft.X:0}-{pinRight:0}; {closeInfo}"); + TestContext.WriteLine($"tab width={tabRight:0}; freeze at x={freezeTopLeft.X:0}-{freezeRight:0}; {closeInfo}"); - pinRight.Should().BeLessThanOrEqualTo(tabRight, - "pin button's right edge must fit inside the tab's right edge (cut-off bug)"); + freezeRight.Should().BeLessThanOrEqualTo(tabRight, + "freeze button's right edge must fit inside the tab's right edge (cut-off bug)"); if (closeButton != null && closeButton.b.Bounds.Width > 0) { - pinRight.Should().BeLessThanOrEqualTo(closeButton.p.X + 0.5, - "pin button must sit to the LEFT of the close button, not overlap it"); + freezeRight.Should().BeLessThanOrEqualTo(closeButton.p.X + 0.5, + "freeze button must sit to the LEFT of the close button, not overlap it"); } } [AvaloniaTest] - public async Task Pin_Button_Fits_Inside_The_Tab_Even_With_A_Long_Title() + public async Task Freeze_Button_Fits_Inside_The_Tab_Even_With_A_Long_Title() { // Production stress case: tabs with long titles can exceed the tab strip's available - // width if the title isn't capped. The pin button must still fit inside the tab's + // width if the title isn't capped. The freeze button must still fit inside the tab's // right edge regardless of title length. var (window, vm) = await TestHarness.BootAsync(3); @@ -320,9 +320,9 @@ public class PreviewTabPromotionTests mainTabItem.Arrange(new global::Avalonia.Rect(0, 0, 200, mainTabItem.DesiredSize.Height)); global::Avalonia.Threading.Dispatcher.UIThread.RunJobs(); - var pinButton = mainTabItem.GetVisualDescendants() + var freezeButton = mainTabItem.GetVisualDescendants() .OfType() - .Single(b => (b.Tag as string) == "PreviewTabPinButton"); + .Single(b => (b.Tag as string) == "PreviewTabFreezeButton"); static global::Avalonia.Point OriginRelativeTo(global::Avalonia.Visual node, global::Avalonia.Visual ancestor) { @@ -336,29 +336,29 @@ public class PreviewTabPromotionTests } return new global::Avalonia.Point(x, y); } - var pinTopLeft = OriginRelativeTo(pinButton, mainTabItem); - var pinRight = pinTopLeft.X + pinButton.Bounds.Width; + var freezeTopLeft = OriginRelativeTo(freezeButton, mainTabItem); + var freezeRight = freezeTopLeft.X + freezeButton.Bounds.Width; var tabRight = mainTabItem.Bounds.Width; - TestContext.WriteLine($"long-title scenario: tab={tabRight:0}, pin x={pinTopLeft.X:0}-{pinRight:0}"); + TestContext.WriteLine($"long-title scenario: tab={tabRight:0}, freeze x={freezeTopLeft.X:0}-{freezeRight:0}"); - pinRight.Should().BeLessThanOrEqualTo(tabRight, - $"pin must fit inside tab width even with long titles (tab={tabRight:0}, pinRight={pinRight:0})"); + freezeRight.Should().BeLessThanOrEqualTo(tabRight, + $"freeze must fit inside tab width even with long titles (tab={tabRight:0}, freezeRight={freezeRight:0})"); // Lock the rendering fixes in: // 1. The title StackPanel must clip — otherwise a long title TextBlock measures - // at its full unconstrained width and paints over the pin and close buttons. + // at its full unconstrained width and paints over the freeze and close buttons. var titleStack = mainTabItem.GetVisualDescendants().OfType().First(); titleStack.ClipToBounds.Should().BeTrue( - "the title StackPanel must clip its content so a long title TextBlock doesn't render over the pin button"); - // 2. The pin Button itself must NOT clip — Avalonia Buttons default to ClipToBounds=true + "the title StackPanel must clip its content so a long title TextBlock doesn't render over the freeze button"); + // 2. The freeze Button itself must NOT clip — Avalonia Buttons default to ClipToBounds=true // which cuts the right edge of Segoe Fluent Icon glyphs (whose visual extent // exceeds the reported advance width). - pinButton.ClipToBounds.Should().BeFalse( - "pin button must not clip so the glyph's visual extent (often wider than the font advance width) survives"); + freezeButton.ClipToBounds.Should().BeFalse( + "freeze button must not clip so the glyph's visual extent (often wider than the font advance width) survives"); } [AvaloniaTest] - public async Task Inline_Pin_Button_Appears_On_Preview_Tab_And_Pins_When_Clicked() + public async Task Inline_Freeze_Button_Appears_On_Preview_Tab_And_Freezes_When_Clicked() { var (window, vm) = await TestHarness.BootAsync(); @@ -369,39 +369,39 @@ public class PreviewTabPromotionTests var mainTabItem = window.GetVisualDescendants().OfType() .Single(item => ReferenceEquals(item.DataContext, factory.MainTab)); - // Pin button is injected via PreviewTabPinButtonBehavior — find it by Tag. + // Freeze button is injected via PreviewTabFreezeButtonBehavior — find it by Tag. await Waiters.WaitForAsync(() => mainTabItem.GetVisualDescendants() .OfType() - .Any(b => (b.Tag as string) == "PreviewTabPinButton"), + .Any(b => (b.Tag as string) == "PreviewTabFreezeButton"), System.TimeSpan.FromSeconds(5)); - var pinButton = mainTabItem.GetVisualDescendants() + var freezeButton = mainTabItem.GetVisualDescendants() .OfType() - .Single(b => (b.Tag as string) == "PreviewTabPinButton"); - pinButton.IsVisible.Should().BeTrue("pin button must show while the tab is preview"); + .Single(b => (b.Tag as string) == "PreviewTabFreezeButton"); + freezeButton.IsVisible.Should().BeTrue("freeze button must show while the tab is preview"); var previousMainTab = factory.MainTab!; var tabCountBefore = vm.DockWorkspace.Documents!.VisibleDockables!.Count; - // Simulate a user click on the pin button. - pinButton.RaiseEvent(new global::Avalonia.Interactivity.RoutedEventArgs( + // Simulate a user click on the freeze button. + freezeButton.RaiseEvent(new global::Avalonia.Interactivity.RoutedEventArgs( global::Avalonia.Controls.Button.ClickEvent)); - TestCapture.Step("pin-button-clicked"); + TestCapture.Step("freeze-button-clicked"); - // New semantics: clicking pin flips IsPreview, doesn't spawn a new tab. + // New semantics: clicking freeze flips IsPreview, doesn't spawn a new tab. previousMainTab.IsPreview.Should().BeFalse( - "clicking the pin button must flip IsPreview=false on the current MainTab"); + "clicking the freeze button must flip IsPreview=false on the current MainTab"); factory.MainTab.Should().BeSameAs(previousMainTab, - "pin alone must not rotate factory.MainTab; the new preview tab opens lazily on the next tree selection"); + "freeze alone must not rotate factory.MainTab; the new preview tab opens lazily on the next tree selection"); vm.DockWorkspace.Documents!.VisibleDockables!.Count.Should().Be(tabCountBefore, - "pin alone must not change tab count"); + "freeze alone must not change tab count"); } [AvaloniaTest] - public async Task Pin_Entry_Appears_In_Tab_Context_Menu_Only_When_Preview() + public async Task Freeze_Entry_Appears_In_Tab_Context_Menu_Only_When_Preview() { - // The right-click context-menu Pin entry must be visible on the preview MainTab - // and hidden (its IsVisible flips) on pinned carve-out tabs. + // The right-click context-menu Freeze entry must be visible on the preview MainTab + // and hidden (its IsVisible flips) on frozen carve-out tabs. var (window, vm) = await TestHarness.BootAsync(3); var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); @@ -417,17 +417,17 @@ public class PreviewTabPromotionTests var carveOutItem = window.GetVisualDescendants().OfType() .Single(item => item.DataContext is ContentTabPage t && t.SourceNode == typeNode); - var mainPinEntry = mainTabItem.DocumentContextMenu!.Items.OfType().Single(); - mainPinEntry.Header.Should().Be("Pin tab"); - mainPinEntry.IsVisible.Should().BeTrue("MainTab is preview — Pin tab entry must be visible"); + var mainFreezeEntry = mainTabItem.DocumentContextMenu!.Items.OfType().Single(); + mainFreezeEntry.Header.Should().Be("Freeze tab"); + mainFreezeEntry.IsVisible.Should().BeTrue("MainTab is preview — Freeze tab entry must be visible"); - var carvePinEntry = carveOutItem.DocumentContextMenu!.Items.OfType().Single(); - carvePinEntry.IsVisible.Should().BeFalse( - "carve-out tabs are already pinned — the Pin entry must hide"); + var carveFreezeEntry = carveOutItem.DocumentContextMenu!.Items.OfType().Single(); + carveFreezeEntry.IsVisible.Should().BeFalse( + "carve-out tabs are already frozen — the Freeze entry must hide"); } [AvaloniaTest] - public async Task Pinned_Carve_Out_Tab_Header_Renders_Upright() + public async Task Frozen_Carve_Out_Tab_Header_Renders_Upright() { // Regression guard for the bug where every DocumentTabStripItem got italicised // because the App.axaml setter used a static `FontStyle="Italic"` value instead @@ -447,38 +447,38 @@ public class PreviewTabPromotionTests var carveOutModel = vm.DockWorkspace.Documents!.VisibleDockables! .OfType() .Last(t => t.SourceNode == typeNode); - carveOutModel.IsPreview.Should().BeFalse("baseline: carve-out tab is pinned"); + carveOutModel.IsPreview.Should().BeFalse("baseline: carve-out tab is frozen"); var carveOutItem = window.GetVisualDescendants().OfType() .Single(item => ReferenceEquals(item.DataContext, carveOutModel)); carveOutItem.FontStyle.Should().Be(FontStyle.Normal, - "a pinned (IsPreview=false) document tab must render with upright FontStyle"); + "a frozen (IsPreview=false) document tab must render with upright FontStyle"); } [AvaloniaTest] - public void PinCurrentTab_Is_Noop_When_MainTab_Already_Pinned() + public void FreezeCurrentTab_Is_Noop_When_MainTab_Already_Frozen() { - // Idempotency: a second pin against an already-pinned MainTab is a no-op. The new - // "pin = flip-only, spawn-lazy" semantics make this simpler than the old version: - // PinCurrentMainTab() returns null when MainTab.IsPreview is already false, and - // PinCurrentTab early-returns. + // Idempotency: a second freeze against an already-frozen MainTab is a no-op. The new + // "freeze = flip-only, spawn-lazy" semantics make this simpler than the old version: + // FreezeCurrentMainTab() returns null when MainTab.IsPreview is already false, and + // FreezeCurrentTab early-returns. var window = AppComposition.Current.GetExport(); window.Show(); var vm = (MainWindowViewModel)window.DataContext!; var factory = (ILSpyDockFactory)vm.DockWorkspace.Factory; TestCapture.Step("booted"); - // Manually flip the current MainTab to pinned, simulating the post-pin state. + // Manually flip the current MainTab to frozen, simulating the post-freeze state. factory.MainTab!.IsPreview = false; var before = factory.MainTab; - vm.DockWorkspace.PinCurrentTab(); - TestCapture.Step("pin-noop"); + vm.DockWorkspace.FreezeCurrentTab(); + TestCapture.Step("freeze-noop"); factory.MainTab.Should().BeSameAs(before, - "with no preview MainTab to flip, PinCurrentTab must leave the factory state untouched"); + "with no preview MainTab to flip, FreezeCurrentTab must leave the factory state untouched"); factory.MainTab.IsPreview.Should().BeFalse( - "the already-pinned tab stays pinned"); + "the already-frozen tab stays frozen"); } [AvaloniaTest] @@ -501,15 +501,15 @@ public class PreviewTabPromotionTests } [AvaloniaTest] - public async Task Pin_Button_Inherits_The_Close_Button_Theme() + public async Task Freeze_Button_Inherits_The_Close_Button_Theme() { - // Visual parity: the pin button should look like the close button — same size, same + // Visual parity: the freeze button should look like the close button — same size, same // hover background. Both are plain Avalonia.Controls.Button instances; the close // button gets its visual identity from a ControlTheme applied by Dock's tab - // template. Pin should copy that Theme rather than carry its own custom style. + // template. Freeze should copy that Theme rather than carry its own custom style. var (window, vm) = await TestHarness.BootAsync(3); - // Open a carve-out so the close button is visible alongside the pin (single-tab + // Open a carve-out so the close button is visible alongside the freeze (single-tab // scenario hides the close button). var typeNode = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); @@ -524,24 +524,24 @@ public class PreviewTabPromotionTests var mainTabItem = window.GetVisualDescendants().OfType() .Single(item => ReferenceEquals(item.DataContext, factory.MainTab)); - var pinButton = mainTabItem.GetVisualDescendants() + var freezeButton = mainTabItem.GetVisualDescendants() .OfType() - .Single(b => (b.Tag as string) == "PreviewTabPinButton"); + .Single(b => (b.Tag as string) == "PreviewTabFreezeButton"); var closeButton = mainTabItem.GetVisualDescendants() .OfType() - .FirstOrDefault(b => (b.Tag as string) != "PreviewTabPinButton"); - closeButton.Should().NotBeNull("baseline: close button exists as a sibling of the pin"); + .FirstOrDefault(b => (b.Tag as string) != "PreviewTabFreezeButton"); + closeButton.Should().NotBeNull("baseline: close button exists as a sibling of the freeze"); - pinButton.Theme.Should().BeSameAs(closeButton!.Theme, - "pin button must use the same ControlTheme as the close button so size + hover bg match"); - pinButton.Classes.Should().NotContain("preview-pin", + freezeButton.Theme.Should().BeSameAs(closeButton!.Theme, + "freeze button must use the same ControlTheme as the close button so size + hover bg match"); + freezeButton.Classes.Should().NotContain("preview-freeze", "after the Theme-copy refactor the custom class-based styling is unused; the Theme drives all visuals"); } [AvaloniaTest] public async Task Tree_Selection_While_Frozen_Tab_Active_Opens_A_New_Preview_Tab() { - // "Frozen" covers: (a) user-pinned tabs (IsPreview=false via PinCurrentTab), + // "Frozen" covers: (a) user-frozen tabs (IsPreview=false via FreezeCurrentTab), // (b) carve-out tabs (born IsPreview=false via OpenNodeInNewTab), and (c) static // content tabs (Options, About) whose Content is a static viewmodel. Tree-node // selections while any of those are active must spawn a fresh preview tab and @@ -551,7 +551,7 @@ public class PreviewTabPromotionTests var factory = (ILSpyDockFactory)vm.DockWorkspace.Factory; - // Open a carve-out (born pinned) and let it become the active dockable. + // Open a carve-out (born frozen) and let it become the active dockable. var typeA = vm.AssemblyTreeModel.FindNode( "System.Linq", "System.Linq", "System.Linq.Enumerable"); vm.DockWorkspace.OpenNodeInNewTab(typeA); diff --git a/ILSpy/App.axaml b/ILSpy/App.axaml index 1bef450a4..9c3001b3f 100644 --- a/ILSpy/App.axaml +++ b/ILSpy/App.axaml @@ -387,7 +387,7 @@ Value="{Binding IsPreview, Converter={x:Static themes:BoolToBrushConverter.PreviewAccent}, FallbackValue={x:Null}, Mode=OneWay, x:CompileBindings=False}" /> - @@ -395,7 +395,7 @@ theme DLL; the Behavior walks the visual tree and injects a Button between the title and the close button. Bound to IsPreview so the icon hides once the tab is pinned. --> - + diff --git a/ILSpy/Commands/WindowCommands.cs b/ILSpy/Commands/WindowCommands.cs index a108e7ed7..1759d913d 100644 --- a/ILSpy/Commands/WindowCommands.cs +++ b/ILSpy/Commands/WindowCommands.cs @@ -42,16 +42,16 @@ namespace ILSpy.Commands /// /// VS-style "promote preview tab" command. The persistent MainTab starts as preview - /// (its content is replaced by tree-node selections); pinning makes it a regular tab + /// (its content is replaced by tree-node selections); freezing makes it a regular tab /// that keeps its content, and a fresh preview MainTab takes over the - /// tree-selection slot. is a no-op when - /// there's nothing pinnable (no MainTab, or MainTab already pinned). + /// tree-selection slot. is a no-op when + /// there's nothing freezable (no MainTab, or MainTab already frozen). /// - [ExportMainMenuCommand(Header = nameof(Resources.Window_PinCurrentTab), ParentMenuID = nameof(Resources._Window), MenuCategory = "Window", MenuOrder = 0)] + [ExportMainMenuCommand(Header = nameof(Resources.Window_FreezeCurrentTab), ParentMenuID = nameof(Resources._Window), MenuCategory = "Window", MenuOrder = 0)] [Shared] [method: ImportingConstructor] - sealed class PinCurrentTabCommand(DockWorkspace dockWorkspace) : SimpleCommand + sealed class FreezeCurrentTabCommand(DockWorkspace dockWorkspace) : SimpleCommand { - public override void Execute(object? parameter) => dockWorkspace.PinCurrentTab(); + public override void Execute(object? parameter) => dockWorkspace.FreezeCurrentTab(); } } diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs index d4974612f..b85017405 100644 --- a/ILSpy/Docking/DockWorkspace.cs +++ b/ILSpy/Docking/DockWorkspace.cs @@ -696,8 +696,8 @@ namespace ILSpy.Docking // here on a single click, so dedupe to avoid creating two TabPageModels for the // same selection — the second one's columns would replace the first's, but the // first's filter wiring would be left dangling on stale ColumnFilter instances. - // Dedupe is also what makes "re-clicking the same node after pin" a no-op: the - // pinned tab stays active, no new preview tab spawns. + // Dedupe is also what makes "re-clicking the same node after freeze" a no-op: the + // frozen tab stays active, no new preview tab spawns. if (lastShownNodes is { } prev && prev.SequenceEqual(nodes)) { if (factory.MainTab is { } existingMain) @@ -708,7 +708,7 @@ namespace ILSpy.Docking // Pick or spawn the target tab. If the currently-active document tab is a // writable preview ContentTabPage we replace its content in place. Anything - // else — pinned tab, Options page, About page, no Documents dock — counts as + // else — frozen tab, Options page, About page, no Documents dock — counts as // frozen and gets a brand-new preview tab spawned beside it. ContentTabPage? main; if (IsWritablePreview(factory.Documents?.ActiveDockable) is ContentTabPage activePreview) @@ -854,7 +854,7 @@ namespace ILSpy.Docking /// is spawned and asked to decompile the /// node. Shared between the assembly-tree MMB handler, the metadata-grid MMB /// handler, and the "Decompile to new tab" context-menu entry. The created tab - /// is pinned (born with false) — see + /// is frozen (born with false) — see /// . /// public void OpenNodeInNewTab(ILSpyTreeNode node) @@ -1093,7 +1093,7 @@ namespace ILSpy.Docking public ContentTabPage OpenNewTab(object content, SharpTreeNode? sourceNode = null) { - // Carve-outs are born pinned — they survive tree-node selections instead of + // Carve-outs are born frozen — they survive tree-node selections instead of // being replaced like the preview MainTab. var tab = new ContentTabPage { Content = content, SourceNode = sourceNode, IsPreview = false }; if (factory.Documents != null) @@ -1138,7 +1138,7 @@ namespace ILSpy.Docking /// /// Renders in the reusable MainTab as a transient welcome /// page (the About page shown at startup when nothing is selected). Unlike Help > - /// About this does not pin a static page: the content is non-static, so the first + /// About this does not freeze a static page: the content is non-static, so the first /// tree-node selection reuses the MainTab and replaces it, leaving the user with a /// single tab rather than a stray empty one. /// @@ -1162,12 +1162,12 @@ namespace ILSpy.Docking } /// - /// VS-style "pin tab" gesture. The current factory.MainTab keeps its position, - /// content, and active state but flips to pinned - /// ( = false). No new tab spawns at pin time: + /// VS-style "freeze tab" gesture. The current factory.MainTab keeps its position, + /// content, and active state but flips to frozen + /// ( = false). No new tab spawns at freeze time: /// the next tree-node selection that finds the active tab frozen will lazily spawn /// a fresh preview tab inside . Re-clicking the same - /// node after pin is a no-op (the dedupe activates the pinned tab). + /// node after freeze is a no-op (the dedupe activates the frozen tab). /// /// /// Pump dispatcher jobs so synchronously-set selection state (SourceNode, IsPreview, @@ -1180,11 +1180,11 @@ namespace ILSpy.Docking Avalonia.Threading.Dispatcher.UIThread.RunJobs(); } - public void PinCurrentTab() + public void FreezeCurrentTab() { - if (factory.PinCurrentMainTab() is null) + if (factory.FreezeCurrentMainTab() is null) return; - // Cached decompiler viewmodel was bound to the just-pinned tab — drop the cache + // Cached decompiler viewmodel was bound to the just-frozen tab — drop the cache // so the next selection-change-into-frozen path materialises a fresh // DecompilerTabPageModel for the newly-spawned preview tab. decompilerContent = null; diff --git a/ILSpy/Docking/ILSpyDockFactory.cs b/ILSpy/Docking/ILSpyDockFactory.cs index ce7e5267a..3687f3d47 100644 --- a/ILSpy/Docking/ILSpyDockFactory.cs +++ b/ILSpy/Docking/ILSpyDockFactory.cs @@ -42,21 +42,21 @@ namespace ILSpy.Docking /// The single persistent Document the host puts in . Its /// swaps between viewmodels (decompiler text / /// metadata grid) on tree-node selection — DockWorkspace owns the population. - /// Identity rotates through — after a pin, the - /// just-pinned tab keeps its position and content while MainTab points at a fresh + /// Identity rotates through — after a freeze, the + /// just-frozen tab keeps its position and content while MainTab points at a fresh /// preview tab beside it. /// public ContentTabPage? MainTab { get; internal set; } /// - /// Flips the current to pinned - /// ( = false). Returns the pinned tab, or - /// when there's nothing to pin (no current MainTab, MainTab - /// already pinned). Does NOT spawn a new preview tab — the next selection change + /// Flips the current to frozen + /// ( = false). Returns the frozen tab, or + /// when there's nothing to freeze (no current MainTab, MainTab + /// already frozen). Does NOT spawn a new preview tab — the next selection change /// that finds the active tab frozen will spawn one lazily via /// . /// - public ContentTabPage? PinCurrentMainTab() + public ContentTabPage? FreezeCurrentMainTab() { if (MainTab is not { IsPreview: true } current) return null; @@ -67,7 +67,7 @@ namespace ILSpy.Docking /// /// Creates a fresh preview , attaches it to /// , and sets to it. Used when a - /// tree-node selection changes while the active tab is frozen (pinned, Options, + /// tree-node selection changes while the active tab is frozen (frozen, Options, /// About, …) — a brand-new preview slot is needed to host the new content. /// Returns the new tab, or if is /// missing. diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index 07d9a9960..9670d6a37 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -3163,9 +3163,9 @@ namespace ICSharpCode.ILSpy.Properties { /// /// Looks up a localized string similar to _Pin current tab. /// - public static string Window_PinCurrentTab { + public static string Window_FreezeCurrentTab { get { - return ResourceManager.GetString("Window_PinCurrentTab", resourceCulture); + return ResourceManager.GetString("Window_FreezeCurrentTab", resourceCulture); } } diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index 735a7ee7a..7b5a898b5 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -1084,8 +1084,8 @@ Do you want to continue? _Close all documents - - _Pin current tab + + _Freeze current tab _Reset layout diff --git a/ILSpy/Themes/PreviewTabContextMenuBehavior.cs b/ILSpy/Themes/PreviewTabContextMenuBehavior.cs index 88a5fe51d..67d15ebfc 100644 --- a/ILSpy/Themes/PreviewTabContextMenuBehavior.cs +++ b/ILSpy/Themes/PreviewTabContextMenuBehavior.cs @@ -31,10 +31,10 @@ namespace ILSpy.Themes { /// /// Attached property that wires a per-instance onto every - /// . The menu hosts a single "Pin tab" entry whose + /// . The menu hosts a single "Freeze tab" entry whose /// tracks on /// the tab's underlying viewmodel — invoking it routes to - /// . Each tab gets a fresh ContextMenu so + /// . Each tab gets a fresh ContextMenu so /// the popup has a unique parent (Avalonia visuals must have at most one parent). /// public static class PreviewTabContextMenuBehavior @@ -75,23 +75,23 @@ namespace ILSpy.Themes return; } - var pinItem = new MenuItem { - Header = "Pin tab", + var freezeItem = new MenuItem { + Header = "Freeze tab", IsVisible = tab.IsPreview, }; - pinItem.Click += (_, _) => TryGetDockWorkspace()?.PinCurrentTab(); + freezeItem.Click += (_, _) => TryGetDockWorkspace()?.FreezeCurrentTab(); // Keep IsVisible in sync with IsPreview so the entry hides once the tab is - // pinned (or after promotion happens through some other code path). + // 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)) - pinItem.IsVisible = tab.IsPreview; + freezeItem.IsVisible = tab.IsPreview; } item.DocumentContextMenu = new ContextMenu { - ItemsSource = new[] { pinItem }, + ItemsSource = new[] { freezeItem }, }; } diff --git a/ILSpy/Themes/PreviewTabPinButtonBehavior.cs b/ILSpy/Themes/PreviewTabFreezeButtonBehavior.cs similarity index 70% rename from ILSpy/Themes/PreviewTabPinButtonBehavior.cs rename to ILSpy/Themes/PreviewTabFreezeButtonBehavior.cs index bef2a0424..c2b8a2c6a 100644 --- a/ILSpy/Themes/PreviewTabPinButtonBehavior.cs +++ b/ILSpy/Themes/PreviewTabFreezeButtonBehavior.cs @@ -36,24 +36,24 @@ using ILSpy.ViewModels; namespace ILSpy.Themes { /// - /// Injects an inline "pin" Button into the tab header of every . + /// Injects an inline "freeze" Button into the tab header of every . /// Dock's tab template is compiled into the theme DLL and not directly overridable without /// reproducing the whole template, so the button is added at runtime by walking the visual /// tree to the inner title StackPanel and appending a child there. The button's /// is bound to on the - /// tab's underlying viewmodel and the click handler routes to - /// . Styling lives in App.axaml under the - /// Button.preview-pin class selector so the theme's :pointerover states still - /// apply (local Background/BorderBrush values here would block them). + /// tab's underlying viewmodel — so it shows only on the One preview tab — and the click + /// handler routes to . It reuses the sibling + /// close button's ControlTheme so it renders at the same size and inherits the same + /// :pointerover states. /// - public static class PreviewTabPinButtonBehavior + public static class PreviewTabFreezeButtonBehavior { - const string PinButtonTag = "PreviewTabPinButton"; + const string FreezeButtonTag = "PreviewTabFreezeButton"; public static readonly AttachedProperty EnableProperty = AvaloniaProperty.RegisterAttached( "Enable", - typeof(PreviewTabPinButtonBehavior)); + typeof(PreviewTabFreezeButtonBehavior)); public static void SetEnable(DocumentTabStripItem element, bool value) => element.SetValue(EnableProperty, value); @@ -61,7 +61,7 @@ namespace ILSpy.Themes public static bool GetEnable(DocumentTabStripItem element) => element.GetValue(EnableProperty); - static PreviewTabPinButtonBehavior() + static PreviewTabFreezeButtonBehavior() { EnableProperty.Changed.AddClassHandler(OnEnableChanged); } @@ -78,16 +78,16 @@ namespace ILSpy.Themes static void TryInject(DocumentTabStripItem item) { // Idempotent: bail if our button is already in the tree. - if (item.GetVisualDescendants().OfType