mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
182 lines
7.8 KiB
182 lines
7.8 KiB
// 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.Shapes; |
|
using Avalonia.Data; |
|
using Avalonia.Layout; |
|
using Avalonia.Media; |
|
using Avalonia.VisualTree; |
|
|
|
using Dock.Avalonia.Controls; |
|
|
|
using ILSpy.AppEnv; |
|
using ILSpy.Docking; |
|
using ILSpy.ViewModels; |
|
|
|
namespace ILSpy.Themes |
|
{ |
|
/// <summary> |
|
/// Injects an inline "pin" Button into the tab header of every <see cref="DocumentTabStripItem"/>. |
|
/// 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 |
|
/// <see cref="Visual.IsVisible"/> is bound to <see cref="ContentTabPage.IsPreview"/> on the |
|
/// tab's underlying viewmodel and the click handler routes to |
|
/// <see cref="DockWorkspace.PinCurrentTab"/>. Styling lives in App.axaml under the |
|
/// <c>Button.preview-pin</c> class selector so the theme's <c>:pointerover</c> states still |
|
/// apply (local Background/BorderBrush values here would block them). |
|
/// </summary> |
|
public static class PreviewTabPinButtonBehavior |
|
{ |
|
const string PinButtonTag = "PreviewTabPinButton"; |
|
|
|
public static readonly AttachedProperty<bool> EnableProperty = |
|
AvaloniaProperty.RegisterAttached<DocumentTabStripItem, bool>( |
|
"Enable", |
|
typeof(PreviewTabPinButtonBehavior)); |
|
|
|
public static void SetEnable(DocumentTabStripItem element, bool value) |
|
=> element.SetValue(EnableProperty, value); |
|
|
|
public static bool GetEnable(DocumentTabStripItem element) |
|
=> element.GetValue(EnableProperty); |
|
|
|
static PreviewTabPinButtonBehavior() |
|
{ |
|
EnableProperty.Changed.AddClassHandler<DocumentTabStripItem>(OnEnableChanged); |
|
} |
|
|
|
static void OnEnableChanged(DocumentTabStripItem item, AvaloniaPropertyChangedEventArgs e) |
|
{ |
|
if (e.NewValue is not true) |
|
return; |
|
item.TemplateApplied += (_, _) => TryInject(item); |
|
item.AttachedToVisualTree += (_, _) => TryInject(item); |
|
TryInject(item); |
|
} |
|
|
|
static void TryInject(DocumentTabStripItem item) |
|
{ |
|
// Idempotent: bail if our button is already in the tree. |
|
if (item.GetVisualDescendants().OfType<Button>().Any(b => (b.Tag as string) == PinButtonTag)) |
|
return; |
|
if (item.DataContext is not ContentTabPage) |
|
return; |
|
// Anchor: the Grid that's the parent of the title StackPanel. The Grid also hosts |
|
// the close-button ContentPresenter as a sibling column. We inject pin as a new |
|
// Auto-sized column between the two so the title can't push pin past the tab's |
|
// right edge — appending to the StackPanel puts pin inside the title column, |
|
// where the unconstrained title TextBlock claims all the space and the pin |
|
// overflows behind the close button. |
|
var titleStack = item.GetVisualDescendants().OfType<StackPanel>().FirstOrDefault(); |
|
if (titleStack is null) |
|
return; |
|
var grid = titleStack.Parent as Grid; |
|
if (grid is null || grid.ColumnDefinitions.Count == 0) |
|
return; |
|
|
|
// Clip the StackPanel's content so the title TextBlock (which measures at its |
|
// full unconstrained text width — 900px+ for long type signatures) doesn't |
|
// render OVER the pin and close buttons that sit in adjacent Grid columns. |
|
// Without this, the pin appears "cut off" because the title's pixels paint |
|
// on top of it. |
|
titleStack.ClipToBounds = true; |
|
|
|
// Tilted-pushpin silhouette matching the Visual Studio preview-tab affordance. |
|
// Vector path (not a font glyph): the previous Segoe Fluent Icons U+E718 |
|
// fallback was Windows-only and rendered as a tofu box on Linux / macOS. |
|
// Path with bound Fill is what restores per-tab Foreground inheritance that |
|
// the intermediate SVG-image variant lost - Avalonia.Svg.Skia honors the |
|
// asset's literal fill, not the consuming control's theme color. RotateTransform |
|
// at -45 deg orients the upright pin path so its tip points to the lower-left. |
|
var glyph = new Path { |
|
Data = StreamGeometry.Parse("M5 2h6v1h-1v4l2 2v1H9v3l-1 2-1-2v-3H4V9l2-2V3H5z"), |
|
Stretch = Stretch.Uniform, |
|
Width = 12, |
|
Height = 12, |
|
HorizontalAlignment = HorizontalAlignment.Center, |
|
VerticalAlignment = VerticalAlignment.Center, |
|
RenderTransform = new RotateTransform(45), |
|
RenderTransformOrigin = RelativePoint.Center, |
|
}; |
|
glyph.Bind(Shape.FillProperty, new Binding(nameof(TemplatedControl.Foreground)) { |
|
Source = item, |
|
Mode = BindingMode.OneWay, |
|
}); |
|
var pin = new Button { |
|
Tag = PinButtonTag, |
|
Content = glyph, |
|
// Left margin preserves the gap between pin and close (~4px) regardless of |
|
// what the inherited ControlTheme decides for its own Margin. |
|
Margin = new Thickness(4, 0, 0, 0), |
|
VerticalAlignment = VerticalAlignment.Center, |
|
HorizontalAlignment = HorizontalAlignment.Center, |
|
Focusable = false, |
|
IsTabStop = false, |
|
// Belt-and-suspenders with the glyph's own ClipToBounds=false above; the |
|
// Avalonia Button defaults ClipToBounds=true which clipped the glyph's |
|
// right edge at the content-area boundary. |
|
ClipToBounds = false, |
|
}; |
|
// Copy the close button's ControlTheme so pin renders at the same size and uses |
|
// the same :pointerover background. The close button (a plain Avalonia.Controls.Button |
|
// with no classes, theme applied by Dock's tab template) sits as a sibling under |
|
// the same Grid. Without this, pin defaulted to a class-styled custom look that |
|
// was bigger and used a different hover tint than its neighbour. |
|
var closeButton = item.GetVisualDescendants().OfType<Button>() |
|
.FirstOrDefault(b => (b.Tag as string) != PinButtonTag); |
|
if (closeButton?.Theme is { } closeTheme) |
|
pin.Theme = closeTheme; |
|
ToolTip.SetTip(pin, "Pin tab"); |
|
|
|
// IsVisible follows IsPreview — the button hides once the tab is pinned. |
|
pin.Bind(Visual.IsVisibleProperty, new Binding(nameof(ContentTabPage.IsPreview)) { |
|
Source = item.DataContext, |
|
Mode = BindingMode.OneWay, |
|
}); |
|
|
|
pin.Click += (_, _) => TryGetDockWorkspace()?.PinCurrentTab(); |
|
|
|
// Insert a new Auto-sized column right before the last (close) column, bump any |
|
// existing children at or past that index by +1, and dock pin there. This gives |
|
// pin a dedicated cell the title can't encroach on. |
|
int newColIndex = grid.ColumnDefinitions.Count - 1; |
|
grid.ColumnDefinitions.Insert(newColIndex, new ColumnDefinition(GridLength.Auto)); |
|
foreach (var child in grid.Children) |
|
{ |
|
var col = Grid.GetColumn(child); |
|
if (col >= newColIndex) |
|
Grid.SetColumn(child, col + 1); |
|
} |
|
Grid.SetColumn(pin, newColIndex); |
|
grid.Children.Add(pin); |
|
} |
|
|
|
static DockWorkspace? TryGetDockWorkspace() |
|
{ |
|
try |
|
{ return AppComposition.Current.GetExport<DockWorkspace>(); } |
|
catch { return null; } |
|
} |
|
} |
|
}
|
|
|