committed by
GitHub
113 changed files with 18 additions and 7718 deletions
@ -1 +0,0 @@ |
|||
Subproject commit 85a0b32ef6d963c1d67619ca3e2f6da0bc43ac9a |
|||
Binary file not shown.
@ -1,39 +0,0 @@ |
|||
<ResourceDictionary xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:ClassModifier="internal"> |
|||
<ResourceDictionary.ThemeDictionaries> |
|||
<ResourceDictionary x:Key="Default"> |
|||
<SolidColorBrush Color="Black" x:Key="PinColor"/> |
|||
</ResourceDictionary> |
|||
<ResourceDictionary x:Key="Dark"> |
|||
<SolidColorBrush Color="White" x:Key="PinColor"/> |
|||
</ResourceDictionary> |
|||
</ResourceDictionary.ThemeDictionaries> |
|||
|
|||
<!-- Add Resources Here --> |
|||
<DrawingGroup x:Key="Pin_xaml"> |
|||
<DrawingGroup.ClipGeometry> |
|||
<RectangleGeometry Rect="0.0,-960.0,960.0,960.0"/> |
|||
</DrawingGroup.ClipGeometry> |
|||
<GeometryDrawing Brush="{DynamicResource PinColor}"> |
|||
<GeometryDrawing.Geometry> |
|||
<PathGeometry Figures="m 640 -480 l 80 80 v 80 H 520 v 240 l -40 40 l -40 -40 v -240 H 240 v -80 l 80 -80 v -280 h -40 v -80 h 400 v 80 h -40 v 280 Z m -286 80 h 252 l -46 -46 v -314 H 400 v 314 l -46 46 Z m 126 0 Z" FillRule="NonZero"/> |
|||
</GeometryDrawing.Geometry> |
|||
</GeometryDrawing> |
|||
</DrawingGroup> |
|||
|
|||
<DrawingGroup x:Key="unpin_xaml"> |
|||
<DrawingGroup.ClipGeometry> |
|||
<RectangleGeometry Rect="0.0,-960.0,960.0,960.0"/> |
|||
</DrawingGroup.ClipGeometry> |
|||
<GeometryDrawing Brush="{DynamicResource PinColor}"> |
|||
<GeometryDrawing.Geometry> |
|||
<PathGeometry Figures="m 440 -600 l 80 -80 h 80 v 200 h 240 l 40 40 l -40 40 H 600 v 200 h -80 l -80 -80 H 160 v 40 H 80 v -400 h 80 v 40 z m 80 286 v -252 l -46 46 H 160 v 160 h 314 z m 0 -126 z" FillRule="NonZero"/> |
|||
</GeometryDrawing.Geometry> |
|||
</GeometryDrawing> |
|||
</DrawingGroup> |
|||
|
|||
<DrawingImage Drawing="{StaticResource unpin_xaml}" x:Key="UnpinIcon"/> |
|||
<DrawingImage Drawing="{StaticResource Pin_xaml}" x:Key="PinIcon"/> |
|||
|
|||
</ResourceDictionary> |
|||
@ -1,32 +0,0 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>$(AvsCurrentTargetFramework);$(AvsLegacyTargetFrameworks)</TargetFrameworks> |
|||
<RootNamespace>Avalonia</RootNamespace> |
|||
<DefineConstants>$(DefineConstants);DATAGRID_INTERNAL</DefineConstants> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<Compile Update="**/*.xaml.cs" |
|||
DependentUpon="%(Filename)" /> |
|||
<Compile Include="../../external/Avalonia.Controls.DataGrid/src/Avalonia.Controls.DataGrid/**/*.cs" |
|||
LinkBase="Diagnostics/Controls/DataGrid" /> |
|||
<AvaloniaXaml Include="../../external/Avalonia.Controls.DataGrid/src/Avalonia.Controls.DataGrid/Themes/Simple.xaml" |
|||
Link="Diagnostics/Controls/DataGrid/Themes/Simple.xaml" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" /> |
|||
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" /> |
|||
<ProjectReference Include="..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" /> |
|||
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" /> |
|||
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" /> |
|||
<ProjectReference Include="..\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<Import Project="..\..\build\EmbedXaml.props" /> |
|||
<Import Project="..\..\build\BuildTargets.targets" /> |
|||
<Import Project="..\..\build\NullableEnable.props" /> |
|||
<Import Project="..\..\build\DevAnalyzers.props" /> |
|||
|
|||
</Project> |
|||
@ -1,85 +0,0 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Diagnostics; |
|||
using Avalonia.Input; |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
/// <summary>
|
|||
/// Extension methods for attaching DevTools..
|
|||
/// </summary>
|
|||
public static class DevToolsExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Attaches DevTools to a window, to be opened with the F12 key.
|
|||
/// </summary>
|
|||
/// <param name="root">The window to attach DevTools to.</param>
|
|||
public static void AttachDevTools(this TopLevel root) |
|||
{ |
|||
DevTools.Attach(root, new DevToolsOptions()); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Attaches DevTools to a window, to be opened with the specified key gesture.
|
|||
/// </summary>
|
|||
/// <param name="root">The window to attach DevTools to.</param>
|
|||
/// <param name="gesture">The key gesture to open DevTools.</param>
|
|||
public static void AttachDevTools(this TopLevel root, KeyGesture gesture) |
|||
{ |
|||
DevTools.Attach(root, gesture); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Attaches DevTools to a window, to be opened with the specified options.
|
|||
/// </summary>
|
|||
/// <param name="root">The window to attach DevTools to.</param>
|
|||
/// <param name="options">Additional settings of DevTools.</param>
|
|||
public static void AttachDevTools(this TopLevel root, DevToolsOptions options) |
|||
{ |
|||
DevTools.Attach(root, options); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Attaches DevTools to a Application, to be opened with the specified options.
|
|||
/// </summary>
|
|||
/// <param name="application">The Application to attach DevTools to.</param>
|
|||
public static void AttachDevTools(this Application application) |
|||
{ |
|||
DevTools.Attach(application, new DevToolsOptions()); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Attaches DevTools to a Application, to be opened with the specified options.
|
|||
/// </summary>
|
|||
/// <param name="application">The Application to attach DevTools to.</param>
|
|||
/// <param name="options">Additional settings of DevTools.</param>
|
|||
/// <remarks>
|
|||
/// Attach DevTools should only be called after application initialization is complete. A good point is <see cref="Application.OnFrameworkInitializationCompleted"/>
|
|||
/// </remarks>
|
|||
/// <example>
|
|||
/// <code>
|
|||
/// public class App : Application
|
|||
/// {
|
|||
/// public override void OnFrameworkInitializationCompleted()
|
|||
/// {
|
|||
/// if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
|
|||
/// {
|
|||
/// desktopLifetime.MainWindow = new MainWindow();
|
|||
/// }
|
|||
/// else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
|
|||
/// singleViewLifetime.MainView = new MainView();
|
|||
///
|
|||
/// base.OnFrameworkInitializationCompleted();
|
|||
/// this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()
|
|||
/// {
|
|||
/// StartupScreenIndex = 1,
|
|||
/// });
|
|||
/// }
|
|||
/// }
|
|||
/// </code>
|
|||
/// </example>
|
|||
public static void AttachDevTools(this Application application, DevToolsOptions options) |
|||
{ |
|||
DevTools.Attach(application, options); |
|||
} |
|||
} |
|||
} |
|||
@ -1,47 +0,0 @@ |
|||
namespace Avalonia.Diagnostics.Behaviors |
|||
{ |
|||
/// <summary>
|
|||
/// See discussion https://github.com/AvaloniaUI/Avalonia/discussions/6773
|
|||
/// </summary>
|
|||
static class ColumnDefinition |
|||
{ |
|||
private readonly static Avalonia.Controls.GridLength ZeroWidth = |
|||
new Avalonia.Controls.GridLength(0, Avalonia.Controls.GridUnitType.Pixel); |
|||
|
|||
private readonly static AttachedProperty<Avalonia.Controls.GridLength?> LastWidthProperty = |
|||
AvaloniaProperty.RegisterAttached<Avalonia.Controls.ColumnDefinition, Avalonia.Controls.GridLength?>("LastWidth" |
|||
, typeof(ColumnDefinition) |
|||
, default); |
|||
|
|||
public readonly static AttachedProperty<bool> IsVisibleProperty = |
|||
AvaloniaProperty.RegisterAttached<Avalonia.Controls.ColumnDefinition, bool>("IsVisible" |
|||
, typeof(ColumnDefinition) |
|||
, true |
|||
, coerce: (element, visibility) => |
|||
{ |
|||
|
|||
var lastWidth = element.GetValue(LastWidthProperty); |
|||
if (visibility == true && lastWidth is { }) |
|||
{ |
|||
element.SetValue(Avalonia.Controls.ColumnDefinition.WidthProperty, lastWidth); |
|||
} |
|||
else if (visibility == false) |
|||
{ |
|||
element.SetValue(LastWidthProperty, element.GetValue(Avalonia.Controls.ColumnDefinition.WidthProperty)); |
|||
element.SetValue(Avalonia.Controls.ColumnDefinition.WidthProperty, ZeroWidth); |
|||
} |
|||
return visibility; |
|||
} |
|||
); |
|||
|
|||
public static bool GetIsVisible(Avalonia.Controls.ColumnDefinition columnDefinition) |
|||
{ |
|||
return columnDefinition.GetValue(IsVisibleProperty); |
|||
} |
|||
|
|||
public static void SetIsVisible(Avalonia.Controls.ColumnDefinition columnDefinition, bool visibility) |
|||
{ |
|||
columnDefinition.SetValue(IsVisibleProperty, visibility); |
|||
} |
|||
} |
|||
} |
|||
@ -1,140 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Styling; |
|||
using Lifetimes = Avalonia.Controls.ApplicationLifetimes; |
|||
|
|||
namespace Avalonia.Diagnostics.Controls |
|||
{ |
|||
internal class Application : TopLevelGroup |
|||
, Input.ICloseable, IDisposable |
|||
|
|||
{ |
|||
private readonly Avalonia.Application _application; |
|||
|
|||
public event EventHandler? Closed; |
|||
|
|||
public static readonly StyledProperty<ThemeVariant?> RequestedThemeVariantProperty = |
|||
ThemeVariantScope.RequestedThemeVariantProperty.AddOwner<Application>(); |
|||
|
|||
public Application(ClassicDesktopStyleApplicationLifetimeTopLevelGroup group, Avalonia.Application application) |
|||
: base(group) |
|||
{ |
|||
_application = application; |
|||
|
|||
if (_application.ApplicationLifetime is Lifetimes.IControlledApplicationLifetime controller) |
|||
{ |
|||
EventHandler<Lifetimes.ControlledApplicationLifetimeExitEventArgs> eh = default!; |
|||
eh = (s, e) => |
|||
{ |
|||
controller.Exit -= eh; |
|||
Closed?.Invoke(s, e); |
|||
}; |
|||
controller.Exit += eh; |
|||
} |
|||
RendererRoot = application.ApplicationLifetime switch |
|||
{ |
|||
Lifetimes.IClassicDesktopStyleApplicationLifetime classic => classic.MainWindow?.Renderer, |
|||
Lifetimes.ISingleViewApplicationLifetime single => single.MainView?.VisualRoot?.Renderer, |
|||
_ => null |
|||
}; |
|||
|
|||
SetCurrentValue(RequestedThemeVariantProperty, application.RequestedThemeVariant); |
|||
_application.PropertyChanged += ApplicationOnPropertyChanged; |
|||
} |
|||
|
|||
internal Avalonia.Application Instance => _application; |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="DataContext"/> property.
|
|||
/// </summary>
|
|||
public object? DataContext => |
|||
_application.DataContext; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the application's global data templates.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The application's global data templates.
|
|||
/// </value>
|
|||
public Avalonia.Controls.Templates.DataTemplates DataTemplates => |
|||
_application.DataTemplates; |
|||
|
|||
/// <summary>
|
|||
/// Gets the application's input manager.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The application's input manager.
|
|||
/// </value>
|
|||
public Input.InputManager? InputManager => |
|||
_application.InputManager; |
|||
|
|||
/// <summary>
|
|||
/// Gets the application's global resource dictionary.
|
|||
/// </summary>
|
|||
public IResourceDictionary Resources => |
|||
_application.Resources; |
|||
|
|||
/// <summary>
|
|||
/// Gets the application's global styles.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The application's global styles.
|
|||
/// </value>
|
|||
/// <remarks>
|
|||
/// Global styles apply to all windows in the application.
|
|||
/// </remarks>
|
|||
public Styling.Styles Styles => |
|||
_application.Styles; |
|||
|
|||
/// <summary>
|
|||
/// Application lifetime, use it for things like setting the main window and exiting the app from code
|
|||
/// Currently supported lifetimes are:
|
|||
/// - <see cref="Lifetimes.IClassicDesktopStyleApplicationLifetime"/>
|
|||
/// - <see cref="Lifetimes.ISingleViewApplicationLifetime"/>
|
|||
/// - <see cref="Lifetimes.IControlledApplicationLifetime"/>
|
|||
/// </summary>
|
|||
public Lifetimes.IApplicationLifetime? ApplicationLifetime => |
|||
_application.ApplicationLifetime; |
|||
|
|||
/// <summary>
|
|||
/// Application name to be used for various platform-specific purposes
|
|||
/// </summary>
|
|||
public string? Name => |
|||
_application.Name; |
|||
|
|||
/// <summary>
|
|||
/// Gets the root of the visual tree, if the control is attached to a visual tree.
|
|||
/// </summary>
|
|||
internal Rendering.IRenderer? RendererRoot { get; } |
|||
|
|||
/// <inheritdoc cref="ThemeVariantScope.RequestedThemeVariant" />
|
|||
public ThemeVariant? RequestedThemeVariant |
|||
{ |
|||
get => GetValue(RequestedThemeVariantProperty); |
|||
set => SetValue(RequestedThemeVariantProperty, value); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_application.PropertyChanged -= ApplicationOnPropertyChanged; |
|||
} |
|||
|
|||
private void ApplicationOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (e.Property == Avalonia.Application.RequestedThemeVariantProperty) |
|||
{ |
|||
SetCurrentValue(RequestedThemeVariantProperty, e.GetNewValue<ThemeVariant>()); |
|||
} |
|||
} |
|||
|
|||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) |
|||
{ |
|||
base.OnPropertyChanged(change); |
|||
|
|||
if (change.Property == RequestedThemeVariantProperty) |
|||
{ |
|||
_application.RequestedThemeVariant = change.GetNewValue<ThemeVariant>(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
<Styles xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:controls="using:Avalonia.Diagnostics.Controls" |
|||
x:ClassModifier="internal"> |
|||
<Design.PreviewWith> |
|||
<controls:BrushEditor /> |
|||
</Design.PreviewWith> |
|||
|
|||
<Style Selector=":is(controls|BrushEditor)"> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Grid> |
|||
<Button Theme="{StaticResource SimpleTextBoxClearButtonTheme}" |
|||
x:Name="PART_ClearButton" |
|||
HorizontalAlignment="Right" |
|||
VerticalContentAlignment="Center"/> |
|||
</Grid> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
</Styles> |
|||
@ -1,128 +0,0 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Metadata; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Input; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Immutable; |
|||
|
|||
namespace Avalonia.Diagnostics.Controls |
|||
{ |
|||
[TemplatePart("PART_ClearButton", typeof(Button))] |
|||
partial class BrushEditor : TemplatedControl |
|||
{ |
|||
private readonly EventHandler<RoutedEventArgs> clearHandler; |
|||
private Button? _clearButton = default; |
|||
private readonly ColorView _colorView = new() |
|||
{ |
|||
HexInputAlphaPosition = AlphaComponentPosition.Leading, // Always match XAML
|
|||
}; |
|||
|
|||
public BrushEditor() |
|||
{ |
|||
FlyoutBase.SetAttachedFlyout(this, new Flyout { Content = _colorView }); |
|||
_colorView.ColorChanged += (_, e) => |
|||
{ |
|||
// Avoid unnecessary value setters by checking if color was actually changed.
|
|||
if (Brush is null || Brush is not ISolidColorBrush oldSolidBrush || oldSolidBrush.Color != e.NewColor) |
|||
{ |
|||
Brush = new ImmutableSolidColorBrush(e.NewColor); |
|||
} |
|||
}; |
|||
clearHandler = (s, e) => Brush = default; |
|||
} |
|||
|
|||
protected override Type StyleKeyOverride => typeof(BrushEditor); |
|||
|
|||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) |
|||
{ |
|||
base.OnApplyTemplate(e); |
|||
if (_clearButton is not null) |
|||
{ |
|||
_clearButton.Click -= clearHandler; |
|||
} |
|||
_clearButton = e.NameScope.Find<Button>("PART_ClearButton"); |
|||
if (_clearButton is Button button) |
|||
{ |
|||
button.Click += clearHandler; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Brush" /> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<BrushEditor, IBrush?> BrushProperty = |
|||
AvaloniaProperty.RegisterDirect<BrushEditor, IBrush?>( |
|||
nameof(Brush), o => o.Brush, (o, v) => o.Brush = v); |
|||
|
|||
private IBrush? _brush; |
|||
|
|||
public IBrush? Brush |
|||
{ |
|||
get => _brush; |
|||
set => SetAndRaise(BrushProperty, ref _brush, value); |
|||
} |
|||
|
|||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) |
|||
{ |
|||
base.OnPropertyChanged(change); |
|||
|
|||
if (change.Property == BrushProperty) |
|||
{ |
|||
if (Brush is ISolidColorBrush scb) |
|||
{ |
|||
_colorView.Color = scb.Color; |
|||
} |
|||
ToolTip.SetTip(this, Brush?.GetType().Name ?? "(null)"); |
|||
InvalidateVisual(); |
|||
} |
|||
} |
|||
|
|||
protected override void OnPointerPressed(PointerPressedEventArgs e) |
|||
{ |
|||
base.OnPointerPressed(e); |
|||
|
|||
FlyoutBase.ShowAttachedFlyout(this); |
|||
} |
|||
|
|||
public override void Render(DrawingContext context) |
|||
{ |
|||
base.Render(context); |
|||
|
|||
var brush = Brush ?? Brushes.Black; |
|||
|
|||
context.FillRectangle(brush, Bounds); |
|||
|
|||
var text = (Brush as ISolidColorBrush)?.Color.ToString() ?? "(null)"; |
|||
|
|||
var ft = new FormattedText(text, |
|||
CultureInfo.CurrentCulture, |
|||
FlowDirection.LeftToRight, |
|||
Typeface.Default, |
|||
10, |
|||
GetTextBrush(brush)); |
|||
|
|||
context.DrawText(ft, |
|||
new Point(Bounds.Width / 2 - ft.Width / 2, Bounds.Height / 2 - ft.Height / 2)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get Contrasted Text Color
|
|||
/// </summary>
|
|||
/// <param name="brush"></param>
|
|||
/// <returns></returns>
|
|||
private static IBrush GetTextBrush(IBrush brush) |
|||
{ |
|||
if (brush is ISolidColorBrush solid) |
|||
{ |
|||
var color = solid.Color; |
|||
var l = ColorHelper.GetRelativeLuminance(color); |
|||
|
|||
return l < 0.5 ? Brushes.White : Brushes.Black; |
|||
} |
|||
return Brushes.White; |
|||
} |
|||
} |
|||
} |
|||
@ -1,89 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Input; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Styling; |
|||
|
|||
namespace Avalonia.Diagnostics.Controls |
|||
{ |
|||
//TODO: UpdateSourceTrigger & Binding.ValidationRules could help removing the need for this control.
|
|||
internal sealed class CommitTextBox : TextBox |
|||
{ |
|||
protected override Type StyleKeyOverride => typeof(TextBox); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="CommittedText" /> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<CommitTextBox, string?> CommittedTextProperty = |
|||
AvaloniaProperty.RegisterDirect<CommitTextBox, string?>( |
|||
nameof(CommittedText), o => o.CommittedText, (o, v) => o.CommittedText = v); |
|||
|
|||
private string? _committedText; |
|||
|
|||
public string? CommittedText |
|||
{ |
|||
get => _committedText; |
|||
set => SetAndRaise(CommittedTextProperty, ref _committedText, value); |
|||
} |
|||
|
|||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) |
|||
{ |
|||
base.OnPropertyChanged(change); |
|||
|
|||
if (change.Property == CommittedTextProperty) |
|||
{ |
|||
Text = CommittedText; |
|||
} |
|||
} |
|||
|
|||
protected override void OnKeyUp(KeyEventArgs e) |
|||
{ |
|||
base.OnKeyUp(e); |
|||
|
|||
switch (e.Key) |
|||
{ |
|||
case Key.Enter: |
|||
|
|||
TryCommit(); |
|||
|
|||
e.Handled = true; |
|||
|
|||
break; |
|||
|
|||
case Key.Escape: |
|||
|
|||
Cancel(); |
|||
|
|||
e.Handled = true; |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
protected override void OnLostFocus(RoutedEventArgs e) |
|||
{ |
|||
base.OnLostFocus(e); |
|||
|
|||
TryCommit(); |
|||
} |
|||
|
|||
private void Cancel() |
|||
{ |
|||
Text = CommittedText; |
|||
DataValidationErrors.ClearErrors(this); |
|||
} |
|||
|
|||
private void TryCommit() |
|||
{ |
|||
if (!DataValidationErrors.GetHasErrors(this)) |
|||
{ |
|||
CommittedText = Text; |
|||
} |
|||
else |
|||
{ |
|||
Text = CommittedText; |
|||
DataValidationErrors.ClearErrors(this); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,104 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Input; |
|||
using Avalonia.Media; |
|||
using Avalonia.Reactive; |
|||
|
|||
namespace Avalonia.Diagnostics.Controls; |
|||
|
|||
internal class ControlHighlightAdorner : Control |
|||
{ |
|||
private static readonly Panel _layoutHighlightAdorner; |
|||
static ControlHighlightAdorner() |
|||
{ |
|||
_layoutHighlightAdorner = new Panel |
|||
{ |
|||
ClipToBounds = false, |
|||
Children = |
|||
{ |
|||
//Padding frame
|
|||
new Border { BorderBrush = new SolidColorBrush(Colors.Green, 0.5) }, |
|||
//Content frame
|
|||
new Border { Background = new SolidColorBrush(Color.FromRgb(160, 197, 232), 0.5) }, |
|||
//Margin frame
|
|||
new Border { BorderBrush = new SolidColorBrush(Colors.Yellow, 0.5) } |
|||
}, |
|||
}; |
|||
AdornerLayer.SetIsClipEnabled(_layoutHighlightAdorner, false); |
|||
} |
|||
readonly IPen _pen; |
|||
|
|||
private ControlHighlightAdorner(IPen pen) |
|||
{ |
|||
_pen = pen; |
|||
this.Clip = null; |
|||
} |
|||
|
|||
public static IDisposable? Add(InputElement owner, IBrush highlightBrush) |
|||
{ |
|||
|
|||
if (AdornerLayer.GetAdornerLayer(owner) is { } layer) |
|||
{ |
|||
var pen = new Pen(highlightBrush, 2).ToImmutable(); |
|||
var adorner = new ControlHighlightAdorner(pen) |
|||
{ |
|||
[AdornerLayer.AdornedElementProperty] = owner |
|||
}; |
|||
layer.Children.Add(adorner); |
|||
|
|||
return Disposable.Create((layer, adorner), state => |
|||
{ |
|||
state.layer.Children.Remove(state.adorner); |
|||
}); |
|||
} |
|||
return default; |
|||
} |
|||
|
|||
public override void Render(DrawingContext context) |
|||
{ |
|||
base.Render(context); |
|||
context.DrawRectangle(_pen, Bounds.Deflate(2)); |
|||
} |
|||
|
|||
internal static IDisposable? Add(Visual visual, bool visualizeMarginPadding) |
|||
{ |
|||
if (AdornerLayer.GetAdornerLayer(visual) is { } layer) |
|||
{ |
|||
if (layer.Children.Contains(_layoutHighlightAdorner)) |
|||
{ |
|||
return default; |
|||
} |
|||
layer.Children.Add(_layoutHighlightAdorner); |
|||
AdornerLayer.SetAdornedElement(_layoutHighlightAdorner, visual); |
|||
var paddingBorder = (Border)_layoutHighlightAdorner.Children[0]; |
|||
var contentBorder = (Border)_layoutHighlightAdorner.Children[1]; |
|||
var marginBorder = (Border)_layoutHighlightAdorner.Children[2]; |
|||
if (visualizeMarginPadding) |
|||
{ |
|||
paddingBorder.BorderThickness = visual.GetValue(TemplatedControl.PaddingProperty); |
|||
contentBorder.Margin = visual.GetValue(TemplatedControl.PaddingProperty); |
|||
marginBorder.BorderThickness = visual.GetValue(MarginProperty); |
|||
marginBorder.Margin = InvertThickness(visual.GetValue(TemplatedControl.MarginProperty)); |
|||
} |
|||
else |
|||
{ |
|||
paddingBorder.BorderThickness = default; |
|||
contentBorder.Margin = default; |
|||
marginBorder.BorderThickness = default; |
|||
marginBorder.Margin = default; |
|||
} |
|||
return Disposable.Create((Layer: layer, Adorner: _layoutHighlightAdorner), state => |
|||
{ |
|||
state.Layer.Children.Remove(state.Adorner); |
|||
}); |
|||
} |
|||
return default; |
|||
} |
|||
|
|||
|
|||
private static Thickness InvertThickness(Thickness input) |
|||
{ |
|||
return new Thickness(-input.Left, -input.Top, -input.Right, -input.Bottom); |
|||
} |
|||
} |
|||
@ -1,105 +0,0 @@ |
|||
<Styles xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:controls="using:Avalonia.Diagnostics.Controls" |
|||
x:DataType="controls:FilterTextBox" |
|||
x:ClassModifier="internal"> |
|||
|
|||
<Design.PreviewWith> |
|||
<Border Padding="10"> |
|||
<controls:FilterTextBox Width="200" Text="Hello World" /> |
|||
</Border> |
|||
</Design.PreviewWith> |
|||
|
|||
<Style Selector="TextBox.filter-text-box"> |
|||
<Setter Property="VerticalContentAlignment" Value="Center" /> |
|||
<Setter Property="InnerRightContent"> |
|||
<Template> |
|||
<StackPanel Orientation="Horizontal" Spacing="1"> |
|||
<Button Margin="0,0,2,0" |
|||
Classes="textBoxClearButton" |
|||
Theme="{StaticResource SimpleTextBoxClearButtonTheme}" |
|||
Focusable="False" |
|||
ToolTip.Tip="Clear" |
|||
Cursor="Hand" |
|||
Command="{Binding $parent[TextBox].Clear}" |
|||
Opacity="0.5" /> |
|||
<ToggleButton Classes="filter-text-box-toggle" |
|||
ToolTip.Tip="Match Case" |
|||
IsChecked="{Binding $parent[controls:FilterTextBox].UseCaseSensitiveFilter}"> |
|||
<Image> |
|||
<DrawingImage> |
|||
<GeometryDrawing Brush="{Binding $parent[ToggleButton].Foreground}"> |
|||
<GeometryDrawing.Geometry> |
|||
M7.495 9.052L8.386 11.402H9.477L6.237 3H5.217L2 11.402H3.095L3.933 9.052H7.495ZM5.811 4.453L5.855 4.588L7.173 8.162H4.255L5.562 4.588L5.606 4.453L5.644 4.297L5.676 4.145L5.697 4.019H5.72L5.744 4.145L5.773 4.297L5.811 4.453ZM13.795 10.464V11.4H14.755V7.498C14.755 6.779 14.575 6.226 14.216 5.837C13.857 5.448 13.327 5.254 12.628 5.254C12.429 5.254 12.227 5.273 12.022 5.31C11.817 5.347 11.622 5.394 11.439 5.451C11.256 5.508 11.091 5.569 10.944 5.636C10.797 5.703 10.683 5.765 10.601 5.824V6.808C10.867 6.578 11.167 6.397 11.505 6.268C11.843 6.139 12.194 6.075 12.557 6.075C12.745 6.075 12.915 6.103 13.07 6.16C13.225 6.217 13.357 6.306 13.466 6.427C13.575 6.548 13.659 6.706 13.718 6.899C13.777 7.092 13.806 7.326 13.806 7.599L11.995 7.851C11.651 7.898 11.355 7.977 11.107 8.088C10.859 8.199 10.654 8.339 10.492 8.507C10.33 8.675 10.21 8.868 10.132 9.087C10.054 9.306 10.015 9.546 10.015 9.808C10.015 10.054 10.057 10.283 10.139 10.496C10.221 10.709 10.342 10.893 10.502 11.047C10.662 11.201 10.862 11.323 11.1 11.413C11.338 11.503 11.613 11.548 11.926 11.548C12.328 11.548 12.686 11.456 13.001 11.27C13.316 11.084 13.573 10.816 13.772 10.464H13.795ZM11.667 8.721C11.843 8.657 12.068 8.607 12.341 8.572L13.806 8.367V8.976C13.806 9.222 13.765 9.451 13.683 9.664C13.601 9.877 13.486 10.063 13.34 10.221C13.194 10.379 13.019 10.503 12.816 10.593C12.613 10.683 12.39 10.728 12.148 10.728C11.961 10.728 11.795 10.703 11.653 10.652C11.511 10.601 11.392 10.53 11.296 10.441C11.2 10.352 11.127 10.247 11.076 10.125C11.025 10.003 11 9.873 11 9.732C11 9.568 11.018 9.421 11.055 9.292C11.092 9.163 11.16 9.051 11.257 8.958C11.354 8.865 11.491 8.785 11.667 8.721Z |
|||
</GeometryDrawing.Geometry> |
|||
</GeometryDrawing> |
|||
</DrawingImage> |
|||
</Image> |
|||
</ToggleButton> |
|||
<ToggleButton Classes="filter-text-box-toggle" |
|||
ToolTip.Tip="Match Whole Word" |
|||
IsChecked="{Binding $parent[controls:FilterTextBox].UseWholeWordFilter}"> |
|||
<Image> |
|||
<DrawingImage> |
|||
<GeometryDrawing Brush="{Binding $parent[ToggleButton].Foreground}"> |
|||
<GeometryDrawing.Geometry> |
|||
M1 2H15V3H1V2ZM14 4H13V12H14V4ZM11.272 8.387C11.194 8.088 11.073 7.825 10.912 7.601C10.751 7.377 10.547 7.2 10.303 7.071C10.059 6.942 9.769 6.878 9.437 6.878C9.239 6.878 9.057 6.902 8.89 6.951C8.725 7 8.574 7.068 8.437 7.156C8.301 7.244 8.18 7.35 8.072 7.474L7.893 7.732V4.578H7V12H7.893V11.425L8.019 11.6C8.106 11.702 8.208 11.79 8.323 11.869C8.44 11.947 8.572 12.009 8.721 12.055C8.87 12.101 9.035 12.123 9.219 12.123C9.572 12.123 9.885 12.052 10.156 11.911C10.428 11.768 10.655 11.573 10.838 11.325C11.021 11.075 11.159 10.782 11.252 10.446C11.345 10.108 11.392 9.743 11.392 9.349C11.391 9.007 11.352 8.686 11.272 8.387ZM9.793 7.78C9.944 7.851 10.075 7.956 10.183 8.094C10.292 8.234 10.377 8.407 10.438 8.611C10.489 8.785 10.52 8.982 10.527 9.198L10.52 9.323C10.52 9.65 10.487 9.943 10.42 10.192C10.353 10.438 10.259 10.645 10.142 10.806C10.025 10.968 9.882 11.091 9.721 11.172C9.399 11.334 8.961 11.338 8.652 11.187C8.499 11.112 8.366 11.012 8.259 10.891C8.174 10.795 8.103 10.675 8.041 10.524C8.041 10.524 7.862 10.077 7.862 9.577C7.862 9.077 8.041 8.575 8.041 8.575C8.103 8.398 8.177 8.257 8.265 8.145C8.379 8.002 8.521 7.886 8.689 7.8C8.857 7.714 9.054 7.671 9.276 7.671C9.466 7.671 9.64 7.708 9.793 7.78ZM15 13H1V14H15V13ZM2.813 10L2.085 12.031H1L1.025 11.959L3.466 4.87305H4.407L6.892 12.031H5.81L5.032 10H2.813ZM3.934 6.42205H3.912L3.007 9.17505H4.848L3.934 6.42205Z |
|||
</GeometryDrawing.Geometry> |
|||
</GeometryDrawing> |
|||
</DrawingImage> |
|||
</Image> |
|||
</ToggleButton> |
|||
<ToggleButton Classes="filter-text-box-toggle" |
|||
ToolTip.Tip="Use Regular Expression" |
|||
IsChecked="{Binding $parent[controls:FilterTextBox].UseRegexFilter}"> |
|||
<Image> |
|||
<DrawingImage> |
|||
<GeometryDrawing Brush="{Binding $parent[ToggleButton].Foreground}"> |
|||
<GeometryDrawing.Geometry> |
|||
M10.0122 2H10.9879V5.11346L13.5489 3.55609L14.034 4.44095L11.4702 6L14.034 7.55905L13.5489 8.44391L10.9879 6.88654V10H10.0122V6.88654L7.45114 8.44391L6.96606 7.55905L9.5299 6L6.96606 4.44095L7.45114 3.55609L10.0122 5.11346V2ZM2 10H6V14H2V10Z |
|||
</GeometryDrawing.Geometry> |
|||
</GeometryDrawing> |
|||
</DrawingImage> |
|||
</Image> |
|||
</ToggleButton> |
|||
</StackPanel> |
|||
</Template> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<Style Selector="TextBox.filter-text-box Button.textBoxClearButton"> |
|||
<Setter Property="IsVisible" Value="False" /> |
|||
</Style> |
|||
<Style Selector="TextBox.filter-text-box[AcceptsReturn=False][IsReadOnly=False]:focus:not(TextBox:empty) Button.textBoxClearButton"> |
|||
<Setter Property="IsVisible" Value="True" /> |
|||
</Style> |
|||
|
|||
<Style Selector="ToggleButton.filter-text-box-toggle > Image"> |
|||
<Setter Property="Width" Value="16" /> |
|||
<Setter Property="Height" Value="16" /> |
|||
<Setter Property="Opacity" Value="0.5" /> |
|||
</Style> |
|||
<Style Selector="ToggleButton.filter-text-box-toggle:pointerover > Image"> |
|||
<Setter Property="Opacity" Value="0.7" /> |
|||
</Style> |
|||
<Style Selector="ToggleButton.filter-text-box-toggle:pressed > Image, ToggleButton.filter-text-box-toggle:checked > Image"> |
|||
<Setter Property="Opacity" Value="0.7" /> |
|||
</Style> |
|||
|
|||
<Style Selector="ToggleButton.filter-text-box-toggle"> |
|||
<Setter Property="Cursor" Value="Hand" /> |
|||
<Setter Property="Padding" Value="0,1" /> |
|||
<Setter Property="Width" Value="24" /> |
|||
<Setter Property="BorderThickness" Value="1" /> |
|||
<Setter Property="VerticalAlignment" Value="Top" /> |
|||
</Style> |
|||
<Style Selector="ToggleButton.filter-text-box-toggle /template/ ContentPresenter"> |
|||
<Setter Property="Background" Value="Transparent" /> |
|||
<Setter Property="BorderBrush" Value="Transparent" /> |
|||
<Setter Property="CornerRadius" Value="0" /> |
|||
</Style> |
|||
<Style Selector="ToggleButton.filter-text-box-toggle:pressed /template/ ContentPresenter, ToggleButton.filter-text-box-toggle:checked /template/ ContentPresenter"> |
|||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentColor}" /> |
|||
<Setter Property="Background" Value="{DynamicResource ThemeAccentColor4}" /> |
|||
</Style> |
|||
</Styles> |
|||
@ -1,47 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Data; |
|||
using Avalonia.Styling; |
|||
|
|||
namespace Avalonia.Diagnostics.Controls |
|||
{ |
|||
internal class FilterTextBox : TextBox |
|||
{ |
|||
public static readonly StyledProperty<bool> UseRegexFilterProperty = |
|||
AvaloniaProperty.Register<FilterTextBox, bool>(nameof(UseRegexFilter), |
|||
defaultBindingMode: BindingMode.TwoWay); |
|||
|
|||
public static readonly StyledProperty<bool> UseCaseSensitiveFilterProperty = |
|||
AvaloniaProperty.Register<FilterTextBox, bool>(nameof(UseCaseSensitiveFilter), |
|||
defaultBindingMode: BindingMode.TwoWay); |
|||
|
|||
public static readonly StyledProperty<bool> UseWholeWordFilterProperty = |
|||
AvaloniaProperty.Register<FilterTextBox, bool>(nameof(UseWholeWordFilter), |
|||
defaultBindingMode: BindingMode.TwoWay); |
|||
|
|||
public FilterTextBox() |
|||
{ |
|||
Classes.Add("filter-text-box"); |
|||
} |
|||
|
|||
public bool UseRegexFilter |
|||
{ |
|||
get => GetValue(UseRegexFilterProperty); |
|||
set => SetValue(UseRegexFilterProperty, value); |
|||
} |
|||
|
|||
public bool UseCaseSensitiveFilter |
|||
{ |
|||
get => GetValue(UseCaseSensitiveFilterProperty); |
|||
set => SetValue(UseCaseSensitiveFilterProperty,value); |
|||
} |
|||
|
|||
public bool UseWholeWordFilter |
|||
{ |
|||
get => GetValue(UseWholeWordFilterProperty); |
|||
set => SetValue(UseWholeWordFilterProperty, value); |
|||
} |
|||
|
|||
protected override Type StyleKeyOverride => typeof(TextBox); |
|||
} |
|||
} |
|||
@ -1,88 +0,0 @@ |
|||
<Styles xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:controls="using:Avalonia.Diagnostics.Controls" |
|||
x:ClassModifier="internal"> |
|||
|
|||
<Styles.Resources> |
|||
<SolidColorBrush x:Key="HighlightBorderBrush" Color="CornflowerBlue" /> |
|||
<SolidColorBrush x:Key="ThicknessBorderBrush" Color="#666666" /> |
|||
</Styles.Resources> |
|||
|
|||
<Style Selector="controls|ThicknessEditor"> |
|||
<Setter Property="HorizontalContentAlignment" Value="Center" /> |
|||
<Setter Property="VerticalContentAlignment" Value="Center" /> |
|||
<Setter Property="BorderThickness" Value="1" /> |
|||
<Setter Property="BorderBrush" Value="{StaticResource ThicknessBorderBrush}" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Panel> |
|||
<Rectangle x:Name="PART_Background" |
|||
Classes.no-content-pointerover="{Binding !#PART_ContentPresenter.IsPointerOver}" /> |
|||
<Border |
|||
x:Name="PART_Border" |
|||
Classes.no-content-pointerover="{Binding !#PART_ContentPresenter.IsPointerOver}" |
|||
BorderBrush="{TemplateBinding BorderBrush}" |
|||
BorderThickness="{TemplateBinding BorderThickness}"> |
|||
<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="Auto,*,Auto"> |
|||
<Grid.Styles> |
|||
<Style Selector="TextBox.thickness-edit"> |
|||
<Setter Property="Background" Value="Transparent" /> |
|||
<Setter Property="BorderThickness" Value="0" /> |
|||
<Setter Property="Margin" Value="2" /> |
|||
<Setter Property="HorizontalAlignment" Value="Stretch" /> |
|||
<Setter Property="VerticalAlignment" Value="Stretch" /> |
|||
<Setter Property="HorizontalContentAlignment" Value="Center" /> |
|||
<Setter Property="VerticalContentAlignment" Value="Center" /> |
|||
<Setter Property="(ScrollViewer.HorizontalScrollBarVisibility)" |
|||
Value="Disabled" /> |
|||
<Setter Property="(ScrollViewer.VerticalScrollBarVisibility)" Value="Disabled" /> |
|||
<Setter Property="IsVisible" |
|||
Value="{Binding $parent[controls:ThicknessEditor].IsPresent}" /> |
|||
</Style> |
|||
</Grid.Styles> |
|||
<TextBlock IsVisible="{TemplateBinding IsPresent}" Margin="4,0,0,0" |
|||
Text="{TemplateBinding Header}" Grid.Row="0" Grid.Column="0" |
|||
Grid.ColumnSpan="2" /> |
|||
<TextBox Grid.Row="1" Grid.Column="0" |
|||
Text="{Binding Left, RelativeSource={RelativeSource TemplatedParent}}" |
|||
Classes="thickness-edit" /> |
|||
<TextBox x:Name="Right" Grid.Row="0" Grid.Column="1" |
|||
Text="{Binding Top, RelativeSource={RelativeSource TemplatedParent}}" |
|||
Classes="thickness-edit" /> |
|||
<TextBox Grid.Row="1" Grid.Column="2" |
|||
Text="{Binding Right, RelativeSource={RelativeSource TemplatedParent}}" |
|||
Classes="thickness-edit" /> |
|||
<TextBox Grid.Row="2" Grid.Column="1" |
|||
Text="{Binding Bottom, RelativeSource={RelativeSource TemplatedParent}}" |
|||
Classes="thickness-edit" /> |
|||
<ContentPresenter Grid.Row="1" Grid.Column="1" |
|||
Name="PART_ContentPresenter" |
|||
ContentTemplate="{TemplateBinding ContentTemplate}" |
|||
Content="{TemplateBinding Content}" |
|||
Padding="{TemplateBinding Padding}" |
|||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" |
|||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" /> |
|||
</Grid> |
|||
</Border> |
|||
</Panel> |
|||
|
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<Style Selector="controls|ThicknessEditor[IsPresent=False]"> |
|||
<Setter Property="BorderThickness" Value="0" /> |
|||
</Style> |
|||
|
|||
<Style Selector="controls|ThicknessEditor /template/ Rectangle#PART_Background"> |
|||
<Setter Property="Fill" Value="{Binding Background, RelativeSource={RelativeSource TemplatedParent}}" /> |
|||
</Style> |
|||
|
|||
<Style Selector="controls|ThicknessEditor:pointerover /template/ Rectangle#PART_Background.no-content-pointerover"> |
|||
<Setter Property="Fill" Value="{Binding Highlight, RelativeSource={RelativeSource TemplatedParent}}" /> |
|||
</Style> |
|||
|
|||
<Style Selector="controls|ThicknessEditor:pointerover /template/ Border#PART_Border.no-content-pointerover"> |
|||
<Setter Property="BorderBrush" Value="{StaticResource HighlightBorderBrush}" /> |
|||
</Style> |
|||
</Styles> |
|||
@ -1,114 +0,0 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Data; |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Diagnostics.Controls |
|||
{ |
|||
internal class ThicknessEditor : ContentControl |
|||
{ |
|||
public static readonly StyledProperty<Thickness> ThicknessProperty = |
|||
AvaloniaProperty.Register<ThicknessEditor, Thickness>(nameof(Thickness), |
|||
defaultBindingMode: BindingMode.TwoWay); |
|||
|
|||
public static readonly StyledProperty<string?> HeaderProperty = |
|||
AvaloniaProperty.Register<ThicknessEditor, string?>(nameof(Header)); |
|||
|
|||
public static readonly StyledProperty<bool> IsPresentProperty = |
|||
AvaloniaProperty.Register<ThicknessEditor, bool>(nameof(IsPresent), true); |
|||
|
|||
public static readonly StyledProperty<double> LeftProperty = |
|||
AvaloniaProperty.Register<ThicknessEditor, double>(nameof(Left)); |
|||
|
|||
public static readonly StyledProperty<double> TopProperty = |
|||
AvaloniaProperty.Register<ThicknessEditor, double>(nameof(Top)); |
|||
|
|||
public static readonly StyledProperty<double> RightProperty = |
|||
AvaloniaProperty.Register<ThicknessEditor, double>(nameof(Right)); |
|||
|
|||
public static readonly StyledProperty<double> BottomProperty = |
|||
AvaloniaProperty.Register<ThicknessEditor, double>(nameof(Bottom)); |
|||
|
|||
public static readonly StyledProperty<IBrush> HighlightProperty = |
|||
AvaloniaProperty.Register<ThicknessEditor, IBrush>(nameof(Highlight)); |
|||
|
|||
private bool _isUpdatingThickness; |
|||
|
|||
public Thickness Thickness |
|||
{ |
|||
get => GetValue(ThicknessProperty); |
|||
set => SetValue(ThicknessProperty, value); |
|||
} |
|||
|
|||
public string? Header |
|||
{ |
|||
get => GetValue(HeaderProperty); |
|||
set => SetValue(HeaderProperty, value); |
|||
} |
|||
|
|||
public bool IsPresent |
|||
{ |
|||
get => GetValue(IsPresentProperty); |
|||
set => SetValue(IsPresentProperty, value); |
|||
} |
|||
|
|||
public double Left |
|||
{ |
|||
get => GetValue(LeftProperty); |
|||
set => SetValue(LeftProperty, value); |
|||
} |
|||
|
|||
public double Top |
|||
{ |
|||
get => GetValue(TopProperty); |
|||
set => SetValue(TopProperty, value); |
|||
} |
|||
|
|||
public double Right |
|||
{ |
|||
get => GetValue(RightProperty); |
|||
set => SetValue(RightProperty, value); |
|||
} |
|||
|
|||
public double Bottom |
|||
{ |
|||
get => GetValue(BottomProperty); |
|||
set => SetValue(BottomProperty, value); |
|||
} |
|||
|
|||
public IBrush Highlight |
|||
{ |
|||
get => GetValue(HighlightProperty); |
|||
set => SetValue(HighlightProperty, value); |
|||
} |
|||
|
|||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) |
|||
{ |
|||
base.OnPropertyChanged(change); |
|||
|
|||
if (change.Property == ThicknessProperty) |
|||
{ |
|||
try |
|||
{ |
|||
_isUpdatingThickness = true; |
|||
|
|||
var value = change.GetNewValue<Thickness>(); |
|||
|
|||
SetCurrentValue(LeftProperty, value.Left); |
|||
SetCurrentValue(TopProperty, value.Top); |
|||
SetCurrentValue(RightProperty, value.Right); |
|||
SetCurrentValue(BottomProperty, value.Bottom); |
|||
} |
|||
finally |
|||
{ |
|||
_isUpdatingThickness = false; |
|||
} |
|||
} |
|||
else if (!_isUpdatingThickness && |
|||
(change.Property == LeftProperty || change.Property == TopProperty || |
|||
change.Property == RightProperty || change.Property == BottomProperty)) |
|||
{ |
|||
SetCurrentValue(ThicknessProperty, new(Left, Top, Right, Bottom)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,41 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using Avalonia.Controls; |
|||
|
|||
namespace Avalonia.Diagnostics.Controls; |
|||
|
|||
internal class TopLevelGroup : AvaloniaObject |
|||
{ |
|||
public TopLevelGroup(IDevToolsTopLevelGroup group) |
|||
{ |
|||
Group = group; |
|||
if (Group.Items is INotifyCollectionChanged incc) |
|||
{ |
|||
incc.CollectionChanged += (_, args) => |
|||
{ |
|||
if (args.OldItems is not null) |
|||
{ |
|||
foreach (TopLevel oldItem in args.OldItems) |
|||
{ |
|||
Removed?.Invoke(this, oldItem); |
|||
} |
|||
} |
|||
|
|||
if (args.NewItems is not null) |
|||
{ |
|||
foreach (TopLevel newItem in args.NewItems) |
|||
{ |
|||
Added?.Invoke(this, newItem); |
|||
} |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
|
|||
public IDevToolsTopLevelGroup Group { get; } |
|||
|
|||
public IReadOnlyList<TopLevel> Items => Group.Items; |
|||
public event EventHandler<TopLevel>? Added; |
|||
public event EventHandler<TopLevel>? Removed; |
|||
} |
|||
@ -1,8 +0,0 @@ |
|||
namespace Avalonia.Diagnostics |
|||
{ |
|||
internal static class Conventions |
|||
{ |
|||
public static IScreenshotHandler DefaultScreenshotHandler { get; } = |
|||
new Screenshots.FilePickerHandler(); |
|||
} |
|||
} |
|||
@ -1,25 +0,0 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using Avalonia.Data; |
|||
using Avalonia.Data.Converters; |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Diagnostics.Converters; |
|||
|
|||
internal class BoolToImageConverter : IValueConverter |
|||
{ |
|||
public IImage? TrueImage { get; set; } |
|||
|
|||
public IImage? FalseImage { get; set; } |
|||
|
|||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => |
|||
value switch |
|||
{ |
|||
true => TrueImage, |
|||
false => FalseImage, |
|||
_ => null |
|||
}; |
|||
|
|||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => |
|||
BindingOperations.DoNothing; |
|||
} |
|||
@ -1,26 +0,0 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using Avalonia.Data.Converters; |
|||
|
|||
namespace Avalonia.Diagnostics.Converters |
|||
{ |
|||
internal class BoolToOpacityConverter : IValueConverter |
|||
{ |
|||
public double Opacity { get; set; } |
|||
|
|||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
if (value is bool boolean && boolean) |
|||
{ |
|||
return 1d; |
|||
} |
|||
|
|||
return Opacity; |
|||
} |
|||
|
|||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,40 +0,0 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using Avalonia.Data; |
|||
using Avalonia.Data.Converters; |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Diagnostics.Converters; |
|||
|
|||
internal class BrushSelectorConveter : AvaloniaObject, IValueConverter |
|||
{ |
|||
public static readonly DirectProperty<BrushSelectorConveter, IBrush?> BrushProperty = |
|||
AvaloniaProperty.RegisterDirect<BrushSelectorConveter, IBrush?>(nameof(Brush) |
|||
, o => o.Brush |
|||
, (o, v) => o.Brush = v); |
|||
|
|||
public IBrush? Brush { get; set; } |
|||
|
|||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
if (ReferenceEquals(value, parameter)) |
|||
{ |
|||
return Brush; |
|||
} |
|||
else if (value is ISolidColorBrush a |
|||
&& parameter is ISolidColorBrush b |
|||
&& a.Color == b.Color |
|||
&& a.Transform == b.Transform |
|||
&& b.Opacity == a.Opacity |
|||
) |
|||
{ |
|||
return Brush; |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
return BindingOperations.DoNothing; |
|||
} |
|||
} |
|||
@ -1,25 +0,0 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using Avalonia.Data; |
|||
using Avalonia.Data.Converters; |
|||
|
|||
namespace Avalonia.Diagnostics.Converters |
|||
{ |
|||
internal class EnumToCheckedConverter : IValueConverter |
|||
{ |
|||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
return Equals(value, parameter); |
|||
} |
|||
|
|||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
if (value is bool isChecked && isChecked) |
|||
{ |
|||
return parameter; |
|||
} |
|||
|
|||
return BindingOperations.DoNothing; |
|||
} |
|||
} |
|||
} |
|||
@ -1,24 +0,0 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using Avalonia.Data; |
|||
using Avalonia.Data.Converters; |
|||
|
|||
namespace Avalonia.Diagnostics.Converters |
|||
{ |
|||
internal class GetTypeNameConverter : IValueConverter |
|||
{ |
|||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
if (value is Type type) |
|||
{ |
|||
return type.GetTypeName(); |
|||
} |
|||
return BindingOperations.DoNothing; |
|||
} |
|||
|
|||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) |
|||
{ |
|||
return BindingOperations.DoNothing; |
|||
} |
|||
} |
|||
} |
|||
@ -1,166 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using System.Linq; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Diagnostics.Views; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Reactive; |
|||
|
|||
namespace Avalonia.Diagnostics |
|||
{ |
|||
internal static class DevTools |
|||
{ |
|||
private static readonly Dictionary<IDevToolsTopLevelGroup, MainWindow> s_open = new(); |
|||
|
|||
public static IDisposable Attach(TopLevel root, KeyGesture gesture) |
|||
{ |
|||
return Attach(root, new DevToolsOptions() |
|||
{ |
|||
Gesture = gesture, |
|||
}); |
|||
} |
|||
|
|||
public static IDisposable Attach(TopLevel root, DevToolsOptions options) |
|||
{ |
|||
void PreviewKeyDown(object? sender, KeyEventArgs e) |
|||
{ |
|||
if (options.Gesture.Matches(e)) |
|||
{ |
|||
Open(root, options); |
|||
} |
|||
} |
|||
|
|||
return (root ?? throw new ArgumentNullException(nameof(root))).AddDisposableHandler( |
|||
InputElement.KeyDownEvent, |
|||
PreviewKeyDown, |
|||
RoutingStrategies.Tunnel); |
|||
} |
|||
|
|||
public static IDisposable Open(TopLevel root, DevToolsOptions options) => |
|||
Open(new SingleViewTopLevelGroup(root), options, root as Window, null); |
|||
|
|||
internal static IDisposable Open(IDevToolsTopLevelGroup group, DevToolsOptions options) => |
|||
Open(group, options, null, null); |
|||
|
|||
internal static IDisposable Attach(Application application, DevToolsOptions options) |
|||
{ |
|||
var openedDisposable = new SerialDisposableValue(); |
|||
var result = new CompositeDisposable(2); |
|||
result.Add(openedDisposable); |
|||
|
|||
// Skip if call on Design Mode
|
|||
if (!Design.IsDesignMode) |
|||
{ |
|||
var lifeTime = application.ApplicationLifetime |
|||
as Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime; |
|||
|
|||
if (lifeTime is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(application), "DevTools can only attach to applications that support IClassicDesktopStyleApplicationLifetime."); |
|||
} |
|||
|
|||
if (application.InputManager is not null) |
|||
{ |
|||
result.Add(application.InputManager.PreProcess.Subscribe(e => |
|||
{ |
|||
var owner = lifeTime.MainWindow; |
|||
|
|||
if (e is RawKeyEventArgs keyEventArgs |
|||
&& keyEventArgs.Type == RawKeyEventType.KeyUp |
|||
&& options.Gesture.Matches(keyEventArgs)) |
|||
{ |
|||
openedDisposable.Disposable = |
|||
Open(new ClassicDesktopStyleApplicationLifetimeTopLevelGroup(lifeTime), options, |
|||
owner, application); |
|||
e.Handled = true; |
|||
} |
|||
})); |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
private static IDisposable Open(IDevToolsTopLevelGroup topLevelGroup, DevToolsOptions options, |
|||
Window? owner, Application? app) |
|||
{ |
|||
var focusedControl = owner?.FocusManager?.GetFocusedElement() as Control; |
|||
AvaloniaObject root = topLevelGroup switch |
|||
{ |
|||
ClassicDesktopStyleApplicationLifetimeTopLevelGroup gr => new Controls.Application(gr, app ?? Application.Current!), |
|||
SingleViewTopLevelGroup gr => gr.Items.First(), |
|||
_ => new Controls.TopLevelGroup(topLevelGroup) |
|||
}; |
|||
|
|||
// If single static toplevel is already visible in another devtools window, focus it.
|
|||
if (s_open.TryGetValue(topLevelGroup, out var mainWindow)) |
|||
{ |
|||
mainWindow.Activate(); |
|||
mainWindow.SelectedControl(focusedControl); |
|||
return Disposable.Empty; |
|||
} |
|||
if (topLevelGroup.Items.Count == 1 && topLevelGroup.Items is not INotifyCollectionChanged) |
|||
{ |
|||
var singleTopLevel = topLevelGroup.Items.First(); |
|||
|
|||
foreach (var group in s_open) |
|||
{ |
|||
if (group.Key.Items.Contains(singleTopLevel)) |
|||
{ |
|||
group.Value.Activate(); |
|||
group.Value.SelectedControl(focusedControl); |
|||
return Disposable.Empty; |
|||
} |
|||
} |
|||
} |
|||
|
|||
var window = new MainWindow |
|||
{ |
|||
Root = root, |
|||
Width = options.Size.Width, |
|||
Height = options.Size.Height, |
|||
Tag = topLevelGroup |
|||
}; |
|||
window.SetOptions(options); |
|||
window.SelectedControl(focusedControl); |
|||
window.Closed += DevToolsClosed; |
|||
s_open.Add(topLevelGroup, window); |
|||
if (options.ShowAsChildWindow && owner is not null) |
|||
{ |
|||
window.Show(owner); |
|||
} |
|||
else |
|||
{ |
|||
window.Show(); |
|||
} |
|||
return Disposable.Create(() => window.Close()); |
|||
} |
|||
|
|||
private static void DevToolsClosed(object? sender, EventArgs e) |
|||
{ |
|||
var window = (MainWindow)sender!; |
|||
window.Closed -= DevToolsClosed; |
|||
s_open.Remove((IDevToolsTopLevelGroup)window.Tag!); |
|||
} |
|||
|
|||
internal static bool DoesBelongToDevTool(this Visual v) |
|||
{ |
|||
var topLevel = TopLevel.GetTopLevel(v); |
|||
|
|||
while (topLevel is not null && topLevel is not Views.MainWindow) |
|||
{ |
|||
if (topLevel is Avalonia.Controls.Primitives.PopupRoot pr) |
|||
{ |
|||
topLevel = pr.ParentTopLevel; |
|||
} |
|||
else |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Input; |
|||
|
|||
namespace Avalonia.Diagnostics; |
|||
|
|||
/// <summary>
|
|||
/// Contains data formats related to dev tools.
|
|||
/// </summary>
|
|||
public static class DevToolsDataFormats |
|||
{ |
|||
// TODO: this name isn't ideal. For instance, it's not a valid UTI for macOS.
|
|||
// We currently have a converter in place in native code for backwards compatibility with IDataObject,
|
|||
// but this should ideally be removed at some point.
|
|||
// Consider using DataFormat.CreateApplicationFormat() instead (breaking change).
|
|||
|
|||
/// <summary>
|
|||
/// Gets the clipboard data format representing a selector.
|
|||
/// It's used for quick format recognition in IDEs.
|
|||
/// </summary>
|
|||
public static DataFormat<string> Selector { get; } = DataFormat.CreateStringPlatformFormat("Avalonia_DevTools_Selector"); |
|||
} |
|||
@ -1,66 +0,0 @@ |
|||
using Avalonia.Input; |
|||
using Avalonia.Media; |
|||
using Avalonia.Styling; |
|||
|
|||
namespace Avalonia.Diagnostics |
|||
{ |
|||
/// <summary>
|
|||
/// Describes options used to customize DevTools.
|
|||
/// </summary>
|
|||
public class DevToolsOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets the key gesture used to open DevTools.
|
|||
/// </summary>
|
|||
public KeyGesture Gesture { get; set; } = new KeyGesture(Key.F12); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether DevTools should be displayed as a child window
|
|||
/// of the window being inspected. The default value is true.
|
|||
/// </summary>
|
|||
/// <remarks>This setting is ignored if DevTools is attached to <see cref="Application"/></remarks>
|
|||
public bool ShowAsChildWindow { get; set; } = true; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the initial size of the DevTools window. The default value is 1280x720.
|
|||
/// </summary>
|
|||
public Size Size { get; set; } = new Size(1280, 720); |
|||
|
|||
/// <summary>
|
|||
/// Get or set the startup screen index where the DevTools window will be displayed.
|
|||
/// </summary>
|
|||
public int? StartupScreenIndex { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether DevTools should be displayed implemented interfaces on Control details. The default value is true.
|
|||
/// </summary>
|
|||
public bool ShowImplementedInterfaces { get; set; } = true; |
|||
|
|||
/// <summary>
|
|||
/// Allow to customize SreenshotHandler
|
|||
/// </summary>
|
|||
/// <remarks>Default handler is <see cref="Screenshots.FilePickerHandler"/></remarks>
|
|||
public IScreenshotHandler ScreenshotHandler { get; set; } |
|||
= Conventions.DefaultScreenshotHandler; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets whether DevTools theme.
|
|||
/// </summary>
|
|||
public ThemeVariant? ThemeVariant { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Get or set Focus Highlighter <see cref="Brush"/>
|
|||
/// </summary>
|
|||
public IBrush? FocusHighlighterBrush { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Set the <see cref="DevToolsViewKind">kind</see> of diagnostic view that show at launch of DevTools
|
|||
/// </summary>
|
|||
public DevToolsViewKind LaunchView { get; init; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or inits the <see cref="HotKeyConfiguration" /> used to activate DevTools features
|
|||
/// </summary>
|
|||
public HotKeyConfiguration HotKeys { get; init; } = new(); |
|||
} |
|||
} |
|||
@ -1,20 +0,0 @@ |
|||
namespace Avalonia.Diagnostics; |
|||
|
|||
/// <summary>
|
|||
/// Kinds of diagnostic views available in DevTools
|
|||
/// </summary>
|
|||
public enum DevToolsViewKind |
|||
{ |
|||
/// <summary>
|
|||
/// The Logical Tree diagnostic view
|
|||
/// </summary>
|
|||
LogicalTree, |
|||
/// <summary>
|
|||
/// The Visual Tree diagnostic view
|
|||
/// </summary>
|
|||
VisualTree, |
|||
/// <summary>
|
|||
/// Events diagnostic view
|
|||
/// </summary>
|
|||
Events, |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
using Avalonia.Input; |
|||
|
|||
namespace Avalonia.Diagnostics |
|||
{ |
|||
public class HotKeyConfiguration |
|||
{ |
|||
/// <summary>
|
|||
/// Freezes refreshing the Value Frames inspector for the selected Control
|
|||
/// </summary>
|
|||
public KeyGesture ValueFramesFreeze { get; init; } = new(Key.S, KeyModifiers.Alt); |
|||
|
|||
/// <summary>
|
|||
/// Resumes refreshing the Value Frames inspector for the selected Control
|
|||
/// </summary>
|
|||
public KeyGesture ValueFramesUnfreeze { get; init; } = new(Key.D, KeyModifiers.Alt); |
|||
|
|||
/// <summary>
|
|||
/// Inspects the hovered Control in the Logical or Visual Tree Page
|
|||
/// </summary>
|
|||
public KeyGesture InspectHoveredControl { get; init; } = new(Key.None, KeyModifiers.Shift | KeyModifiers.Control); |
|||
|
|||
/// <summary>
|
|||
/// Toggles the freezing of Popups which prevents visible Popups from closing so they can be inspected
|
|||
/// </summary>
|
|||
public KeyGesture TogglePopupFreeze { get; init; } = new(Key.F, KeyModifiers.Alt | KeyModifiers.Control); |
|||
|
|||
/// <summary>
|
|||
/// Saves a Screenshot of the Selected Control in the Logical or Visual Tree Page
|
|||
/// </summary>
|
|||
public KeyGesture ScreenshotSelectedControl { get; init; } = new(Key.F8); |
|||
} |
|||
} |
|||
@ -1,56 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
|
|||
namespace Avalonia.Diagnostics; |
|||
|
|||
internal interface IDevToolsTopLevelGroup |
|||
{ |
|||
IReadOnlyList<TopLevel> Items { get; } |
|||
} |
|||
|
|||
internal class ClassicDesktopStyleApplicationLifetimeTopLevelGroup : IDevToolsTopLevelGroup |
|||
{ |
|||
private readonly IClassicDesktopStyleApplicationLifetime _lifetime; |
|||
|
|||
public ClassicDesktopStyleApplicationLifetimeTopLevelGroup(IClassicDesktopStyleApplicationLifetime lifetime) |
|||
{ |
|||
_lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime)); |
|||
} |
|||
|
|||
public IReadOnlyList<TopLevel> Items => _lifetime.Windows; |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
return _lifetime.GetHashCode(); |
|||
} |
|||
|
|||
public override bool Equals(object? obj) |
|||
{ |
|||
return obj is ClassicDesktopStyleApplicationLifetimeTopLevelGroup g && g._lifetime == _lifetime; |
|||
} |
|||
} |
|||
|
|||
internal class SingleViewTopLevelGroup : IDevToolsTopLevelGroup |
|||
{ |
|||
private readonly TopLevel _topLevel; |
|||
|
|||
public SingleViewTopLevelGroup(TopLevel topLevel) |
|||
{ |
|||
_topLevel = topLevel; |
|||
Items = new[] { topLevel ?? throw new ArgumentNullException(nameof(topLevel)) }; |
|||
} |
|||
|
|||
public IReadOnlyList<TopLevel> Items { get; } |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
return _topLevel.GetHashCode(); |
|||
} |
|||
|
|||
public override bool Equals(object? obj) |
|||
{ |
|||
return obj is SingleViewTopLevelGroup g && g._topLevel == _topLevel; |
|||
} |
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
|
|||
namespace Avalonia.Diagnostics |
|||
{ |
|||
/// <summary>
|
|||
/// Allowed to define custom handler for Shreeshot
|
|||
/// </summary>
|
|||
public interface IScreenshotHandler |
|||
{ |
|||
/// <summary>
|
|||
/// Handle the Screenshot
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
Task Take(Control control); |
|||
} |
|||
} |
|||
@ -1,27 +0,0 @@ |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
|
|||
namespace Avalonia.Diagnostics |
|||
{ |
|||
internal static class KeyGestureExtesions |
|||
{ |
|||
public static bool Matches(this KeyGesture gesture, RawKeyEventArgs keyEvent) => |
|||
(KeyModifiers)(keyEvent.Modifiers & RawInputModifiers.KeyboardMask) == gesture.KeyModifiers && |
|||
ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(gesture.Key); |
|||
|
|||
private static Key ResolveNumPadOperationKey(Key key) |
|||
{ |
|||
switch (key) |
|||
{ |
|||
case Key.Add: |
|||
return Key.OemPlus; |
|||
case Key.Subtract: |
|||
return Key.OemMinus; |
|||
case Key.Decimal: |
|||
return Key.OemPeriod; |
|||
default: |
|||
return key; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,36 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Diagnostics.Models |
|||
{ |
|||
internal class EventChainLink |
|||
{ |
|||
public EventChainLink(object handler, bool handled, RoutingStrategies route) |
|||
{ |
|||
Handler = handler ?? throw new ArgumentNullException(nameof(handler)); |
|||
Handled = handled; |
|||
Route = route; |
|||
} |
|||
|
|||
public object Handler { get; } |
|||
|
|||
public bool BeginsNewRoute { get; set; } |
|||
|
|||
public string HandlerName |
|||
{ |
|||
get |
|||
{ |
|||
if (Handler is INamed named && !string.IsNullOrEmpty(named.Name)) |
|||
{ |
|||
return named.Name + " (" + Handler.GetType().Name + ")"; |
|||
} |
|||
|
|||
return Handler.GetType().Name; |
|||
} |
|||
} |
|||
|
|||
public bool Handled { get; set; } |
|||
|
|||
public RoutingStrategies Route { get; } |
|||
} |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
|
|||
namespace Avalonia.Diagnostics.Screenshots |
|||
{ |
|||
/// <summary>
|
|||
/// Base class for render Screenshto to stream
|
|||
/// </summary>
|
|||
public abstract class BaseRenderToStreamHandler : IScreenshotHandler |
|||
{ |
|||
/// <summary>
|
|||
/// Get stream to write a screenshot to.
|
|||
/// </summary>
|
|||
/// <param name="control"></param>
|
|||
/// <returns>stream to render the control</returns>
|
|||
protected abstract Task<System.IO.Stream?> GetStream(Control control); |
|||
|
|||
public async Task Take(Control control) |
|||
{ |
|||
#if NET6_0_OR_GREATER
|
|||
await using var output = await GetStream(control); |
|||
#else
|
|||
using var output = await GetStream(control); |
|||
#endif
|
|||
if (output is { }) |
|||
{ |
|||
control.RenderTo(output); |
|||
await output.FlushAsync(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,82 +0,0 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Platform.Storage; |
|||
using Avalonia.Platform.Storage.FileIO; |
|||
using Lifetimes = Avalonia.Controls.ApplicationLifetimes; |
|||
|
|||
namespace Avalonia.Diagnostics.Screenshots |
|||
{ |
|||
/// <summary>
|
|||
/// Show a FileSavePicker to select where save screenshot
|
|||
/// </summary>
|
|||
public sealed class FilePickerHandler : BaseRenderToStreamHandler |
|||
{ |
|||
private readonly string _title; |
|||
private readonly string? _screenshotRoot; |
|||
|
|||
/// <summary>
|
|||
/// Instance FilePickerHandler
|
|||
/// </summary>
|
|||
public FilePickerHandler() : this(null, null) |
|||
{ |
|||
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Instance FilePickerHandler with specificated parameter
|
|||
/// </summary>
|
|||
/// <param name="title">SaveFilePicker Title</param>
|
|||
/// <param name="screenshotRoot"></param>
|
|||
public FilePickerHandler( |
|||
string? title, |
|||
string? screenshotRoot = default) |
|||
{ |
|||
_title = title ?? "Save Screenshot to ..."; |
|||
_screenshotRoot = screenshotRoot; |
|||
} |
|||
|
|||
private static TopLevel GetTopLevel(Control control) |
|||
{ |
|||
// If possible, use devtools main window.
|
|||
TopLevel? devToolsTopLevel = null; |
|||
if (Application.Current?.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime desktop) |
|||
{ |
|||
devToolsTopLevel = desktop.Windows.FirstOrDefault(w => w is Views.MainWindow); |
|||
} |
|||
|
|||
return devToolsTopLevel ?? TopLevel.GetTopLevel(control) |
|||
?? throw new InvalidOperationException("No TopLevel is available."); |
|||
} |
|||
|
|||
protected override async Task<Stream?> GetStream(Control control) |
|||
{ |
|||
var storageProvider = GetTopLevel(control).StorageProvider; |
|||
IStorageFolder? defaultFolder = null; |
|||
if (_screenshotRoot is not null) |
|||
{ |
|||
defaultFolder = await storageProvider.TryGetFolderFromPathAsync(_screenshotRoot); |
|||
} |
|||
if (defaultFolder is null) |
|||
{ |
|||
defaultFolder = await storageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Pictures); |
|||
} |
|||
|
|||
var result = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions |
|||
{ |
|||
SuggestedStartLocation = defaultFolder, |
|||
Title = _title, |
|||
FileTypeChoices = new [] { FilePickerFileTypes.ImagePng }, |
|||
DefaultExtension = ".png" |
|||
}); |
|||
if (result is null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
return await result.OpenWriteAsync(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.Diagnostics |
|||
{ |
|||
internal static class TypeExtesnions |
|||
{ |
|||
private static readonly ConditionalWeakTable<Type, string> s_getTypeNameCache = |
|||
new ConditionalWeakTable<Type, string>(); |
|||
|
|||
public static string GetTypeName(this Type type) |
|||
{ |
|||
if (!s_getTypeNameCache.TryGetValue(type, out var name)) |
|||
{ |
|||
name = type.Name; |
|||
if (Nullable.GetUnderlyingType(type) is Type nullable) |
|||
{ |
|||
name = nullable.Name + "?"; |
|||
} |
|||
else if (type.IsGenericType) |
|||
{ |
|||
var definition = type.GetGenericTypeDefinition(); |
|||
var arguments = type.GetGenericArguments(); |
|||
name = definition.Name.Substring(0, definition.Name.IndexOf('`')); |
|||
name = $"{name}<{string.Join(",", arguments.Select(GetTypeName))}>"; |
|||
} |
|||
s_getTypeNameCache.Add(type, name); |
|||
} |
|||
return name; |
|||
} |
|||
} |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Templates; |
|||
using Avalonia.Diagnostics.ViewModels; |
|||
|
|||
namespace Avalonia.Diagnostics |
|||
{ |
|||
internal class ViewLocator : IDataTemplate |
|||
{ |
|||
public Control? Build(object? data) |
|||
{ |
|||
if (data is null) |
|||
return null; |
|||
|
|||
var name = data.GetType().FullName!.Replace("ViewModel", "View"); |
|||
var type = Type.GetType(name); |
|||
|
|||
if (type != null) |
|||
{ |
|||
return (Control)Activator.CreateInstance(type)!; |
|||
} |
|||
else |
|||
{ |
|||
return new TextBlock { Text = name }; |
|||
} |
|||
} |
|||
|
|||
public bool Match(object? data) |
|||
{ |
|||
return data is ViewModelBase; |
|||
} |
|||
} |
|||
} |
|||
@ -1,128 +0,0 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class AvaloniaPropertyViewModel : PropertyViewModel |
|||
{ |
|||
private readonly AvaloniaObject _target; |
|||
private Type _assignedType; |
|||
private object? _value; |
|||
private string _priority; |
|||
private string _group; |
|||
private readonly Type _propertyType; |
|||
|
|||
#nullable disable |
|||
// Remove "nullable disable" after MemberNotNull will work on our CI.
|
|||
public AvaloniaPropertyViewModel(AvaloniaObject o, AvaloniaProperty property) |
|||
#nullable restore |
|||
{ |
|||
_target = o; |
|||
Property = property; |
|||
|
|||
Name = property.IsAttached ? |
|||
$"[{property.OwnerType.Name}.{property.Name}]" : |
|||
property.Name; |
|||
DeclaringType = property.OwnerType; |
|||
_propertyType = property.PropertyType; |
|||
Update(); |
|||
} |
|||
|
|||
public AvaloniaProperty Property { get; } |
|||
public override object Key => Property; |
|||
public override string Name { get; } |
|||
public override bool? IsAttached => Property.IsAttached; |
|||
public override string Priority => _priority; |
|||
public override Type AssignedType => _assignedType; |
|||
|
|||
public override object? Value |
|||
{ |
|||
get => _value; |
|||
set |
|||
{ |
|||
try |
|||
{ |
|||
_target.SetValue(Property, value); |
|||
Update(); |
|||
} |
|||
catch { } |
|||
} |
|||
} |
|||
|
|||
public override string Group => IsPinned ? "Pinned" : _group; |
|||
|
|||
public override Type? DeclaringType { get; } |
|||
public override Type PropertyType => _propertyType; |
|||
public override bool IsReadonly => Property.IsReadOnly; |
|||
|
|||
// [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))]
|
|||
public override void Update() |
|||
{ |
|||
if (Property.IsDirect) |
|||
{ |
|||
object? value; |
|||
Type? valueType = null; |
|||
|
|||
try |
|||
{ |
|||
value = _target.GetValue(Property); |
|||
valueType = value?.GetType(); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
value = e.GetBaseException(); |
|||
} |
|||
|
|||
RaiseAndSetIfChanged(ref _value, value, nameof(Value)); |
|||
RaiseAndSetIfChanged(ref _assignedType, valueType ?? Property.PropertyType, nameof(AssignedType)); |
|||
RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority)); |
|||
|
|||
_group = "Properties"; |
|||
} |
|||
else |
|||
{ |
|||
object? value; |
|||
Type? valueType = null; |
|||
BindingPriority? priority = null; |
|||
|
|||
try |
|||
{ |
|||
var diag = _target.GetDiagnostic(Property); |
|||
|
|||
value = diag.Value; |
|||
valueType = value?.GetType(); |
|||
priority = diag.Priority; |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
value = e.GetBaseException(); |
|||
} |
|||
|
|||
RaiseAndSetIfChanged(ref _value, value, nameof(Value)); |
|||
RaiseAndSetIfChanged(ref _assignedType, valueType ?? Property.PropertyType, nameof(AssignedType)); |
|||
|
|||
if (priority != null) |
|||
{ |
|||
RaiseAndSetIfChanged(ref _priority, priority.ToString()!, nameof(Priority)); |
|||
RaiseAndSetIfChanged(ref _group, IsAttached == true ? "Attached Properties" : "Properties", nameof(Group)); |
|||
} |
|||
else |
|||
{ |
|||
RaiseAndSetIfChanged(ref _priority, "Unset", nameof(Priority)); |
|||
RaiseAndSetIfChanged(ref _group, "Unset", nameof(Group)); |
|||
} |
|||
} |
|||
RaisePropertyChanged(nameof(Type)); |
|||
} |
|||
|
|||
protected override void OnPropertyChanged(PropertyChangedEventArgs e) |
|||
{ |
|||
base.OnPropertyChanged(e); |
|||
if (e.PropertyName == nameof(IsPinned)) |
|||
{ |
|||
RaisePropertyChanged(nameof(Group)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,57 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Markup.Xaml.MarkupExtensions; |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class BindingSetterViewModel : SetterViewModel |
|||
{ |
|||
public BindingSetterViewModel(AvaloniaProperty property, object? value, IClipboard? clipboard) : base(property, value, clipboard) |
|||
{ |
|||
switch (value) |
|||
{ |
|||
case Binding binding: |
|||
Path = binding.Path; |
|||
Tint = Brushes.CornflowerBlue; |
|||
ValueTypeTooltip = "Reflection Binding"; |
|||
|
|||
break; |
|||
case CompiledBindingExtension binding: |
|||
Path = binding.Path.ToString(); |
|||
Tint = Brushes.DarkGreen; |
|||
ValueTypeTooltip = "Compiled Binding"; |
|||
|
|||
break; |
|||
case TemplateBinding binding: |
|||
if (binding.Property is AvaloniaProperty templateProperty) |
|||
{ |
|||
Path = $"{templateProperty.OwnerType.Name}.{templateProperty.Name}"; |
|||
} |
|||
else |
|||
{ |
|||
Path = "Unassigned"; |
|||
} |
|||
|
|||
Tint = Brushes.OrangeRed; |
|||
ValueTypeTooltip = "Template Binding"; |
|||
|
|||
break; |
|||
default: |
|||
throw new ArgumentException("Invalid binding type", nameof(value)); |
|||
} |
|||
} |
|||
|
|||
public IBrush Tint { get; } |
|||
|
|||
public string ValueTypeTooltip { get; } |
|||
|
|||
public string Path { get; } |
|||
|
|||
public override void CopyValue() |
|||
{ |
|||
CopyToClipboard(Path); |
|||
} |
|||
} |
|||
} |
|||
@ -1,96 +0,0 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Reflection; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class ClrPropertyViewModel : PropertyViewModel |
|||
{ |
|||
private readonly object _target; |
|||
private Type _assignedType; |
|||
private object? _value; |
|||
private readonly Type _propertyType; |
|||
|
|||
#nullable disable |
|||
// Remove "nullable disable" after MemberNotNull will work on our CI.
|
|||
public ClrPropertyViewModel(object o, PropertyInfo property) |
|||
#nullable restore |
|||
{ |
|||
_target = o; |
|||
Property = property; |
|||
|
|||
if (property.DeclaringType == null || !property.DeclaringType.IsInterface) |
|||
{ |
|||
Name = property.Name; |
|||
} |
|||
else |
|||
{ |
|||
Name = property.DeclaringType.Name + '.' + property.Name; |
|||
} |
|||
|
|||
DeclaringType = property.DeclaringType; |
|||
_propertyType = property.PropertyType; |
|||
|
|||
Update(); |
|||
} |
|||
|
|||
public PropertyInfo Property { get; } |
|||
public override object Key => Name; |
|||
public override string Name { get; } |
|||
public override string Group => IsPinned ? "Pinned" : "CLR Properties"; |
|||
|
|||
public override Type AssignedType => _assignedType; |
|||
public override Type PropertyType => _propertyType; |
|||
public override bool IsReadonly => !Property.CanWrite; |
|||
|
|||
public override object? Value |
|||
{ |
|||
get => _value; |
|||
set |
|||
{ |
|||
try |
|||
{ |
|||
Property.SetValue(_target, value); |
|||
Update(); |
|||
} |
|||
catch { } |
|||
} |
|||
} |
|||
|
|||
public override string Priority => string.Empty; |
|||
|
|||
public override bool? IsAttached => default; |
|||
|
|||
public override Type? DeclaringType { get; } |
|||
|
|||
// [MemberNotNull(nameof(_type))]
|
|||
public override void Update() |
|||
{ |
|||
object? value; |
|||
Type? valueType = null; |
|||
|
|||
try |
|||
{ |
|||
value = Property.GetValue(_target); |
|||
valueType = value?.GetType(); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
value = e.GetBaseException(); |
|||
} |
|||
|
|||
RaiseAndSetIfChanged(ref _value, value, nameof(Value)); |
|||
RaiseAndSetIfChanged(ref _assignedType, valueType ?? Property.PropertyType, nameof(AssignedType)); |
|||
RaisePropertyChanged(nameof(Type)); |
|||
} |
|||
|
|||
protected override void OnPropertyChanged(PropertyChangedEventArgs e) |
|||
{ |
|||
base.OnPropertyChanged(e); |
|||
if (e.PropertyName == nameof(IsPinned)) |
|||
{ |
|||
RaisePropertyChanged(nameof(Group)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,548 +0,0 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Collections.ObjectModel; |
|||
using System.ComponentModel; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Metadata; |
|||
using Avalonia.Data; |
|||
using Avalonia.Markup.Xaml.MarkupExtensions; |
|||
using Avalonia.Styling; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class ControlDetailsViewModel : ViewModelBase, IDisposable, IClassesChangedListener |
|||
{ |
|||
private readonly AvaloniaObject _avaloniaObject; |
|||
private readonly ISet<string> _pinnedProperties; |
|||
private IDictionary<object, PropertyViewModel[]>? _propertyIndex; |
|||
private PropertyViewModel? _selectedProperty; |
|||
private DataGridCollectionView? _propertiesView; |
|||
private bool _snapshotFrames; |
|||
private bool _showInactiveFrames; |
|||
private string? _framesStatus; |
|||
private object? _selectedEntity; |
|||
private readonly Stack<(string Name, object Entry)> _selectedEntitiesStack = new(); |
|||
private string? _selectedEntityName; |
|||
private string? _selectedEntityType; |
|||
private bool _showImplementedInterfaces; |
|||
// new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group))
|
|||
private readonly static IReadOnlyList<DataGridPathGroupDescription> GroupDescriptors = new DataGridPathGroupDescription[] |
|||
{ |
|||
new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group)) |
|||
}; |
|||
|
|||
private readonly static IReadOnlyList<DataGridSortDescription> SortDescriptions = new DataGridSortDescription[] |
|||
{ |
|||
new DataGridComparerSortDescription(PropertyComparer.Instance!, ListSortDirection.Ascending), |
|||
}; |
|||
|
|||
public ControlDetailsViewModel(TreePageViewModel treePage, AvaloniaObject avaloniaObject, ISet<string> pinnedProperties) |
|||
{ |
|||
_avaloniaObject = avaloniaObject; |
|||
_pinnedProperties = pinnedProperties; |
|||
TreePage = treePage; |
|||
Layout = avaloniaObject is Visual visual |
|||
? new ControlLayoutViewModel(visual) |
|||
: default; |
|||
|
|||
NavigateToProperty(_avaloniaObject, (_avaloniaObject as Control)?.Name ?? _avaloniaObject.ToString()); |
|||
|
|||
AppliedFrames = new ObservableCollection<ValueFrameViewModel>(); |
|||
PseudoClasses = new ObservableCollection<PseudoClassViewModel>(); |
|||
|
|||
if (avaloniaObject is StyledElement styledElement) |
|||
{ |
|||
styledElement.Classes.AddListener(this); |
|||
|
|||
var pseudoClassAttributes = styledElement.GetType().GetCustomAttributes<PseudoClassesAttribute>(true); |
|||
|
|||
foreach (var classAttribute in pseudoClassAttributes) |
|||
{ |
|||
foreach (var className in classAttribute.PseudoClasses) |
|||
{ |
|||
PseudoClasses.Add(new PseudoClassViewModel(className, styledElement)); |
|||
} |
|||
} |
|||
|
|||
var styleDiagnostics = styledElement.GetValueStoreDiagnostic(); |
|||
|
|||
var clipboard = TopLevel.GetTopLevel(_avaloniaObject as Visual)?.Clipboard; |
|||
|
|||
foreach (var appliedStyle in styleDiagnostics.AppliedFrames.OrderBy(s => s.Priority)) |
|||
{ |
|||
AppliedFrames.Add(new ValueFrameViewModel(styledElement, appliedStyle, clipboard)); |
|||
} |
|||
|
|||
UpdateStyles(); |
|||
} |
|||
} |
|||
|
|||
public bool CanNavigateToParentProperty => _selectedEntitiesStack.Count >= 1; |
|||
|
|||
public TreePageViewModel TreePage { get; } |
|||
|
|||
public DataGridCollectionView? PropertiesView |
|||
{ |
|||
get => _propertiesView; |
|||
private set => RaiseAndSetIfChanged(ref _propertiesView, value); |
|||
} |
|||
|
|||
public ObservableCollection<ValueFrameViewModel> AppliedFrames { get; } |
|||
|
|||
public ObservableCollection<PseudoClassViewModel> PseudoClasses { get; } |
|||
|
|||
public object? SelectedEntity |
|||
{ |
|||
get => _selectedEntity; |
|||
set => RaiseAndSetIfChanged(ref _selectedEntity, value); |
|||
} |
|||
|
|||
public string? SelectedEntityName |
|||
{ |
|||
get => _selectedEntityName; |
|||
set => RaiseAndSetIfChanged(ref _selectedEntityName, value); |
|||
} |
|||
|
|||
public string? SelectedEntityType |
|||
{ |
|||
get => _selectedEntityType; |
|||
set => RaiseAndSetIfChanged(ref _selectedEntityType, value); |
|||
} |
|||
|
|||
public PropertyViewModel? SelectedProperty |
|||
{ |
|||
get => _selectedProperty; |
|||
set => RaiseAndSetIfChanged(ref _selectedProperty, value); |
|||
} |
|||
|
|||
public bool SnapshotFrames |
|||
{ |
|||
get => _snapshotFrames; |
|||
set => RaiseAndSetIfChanged(ref _snapshotFrames, value); |
|||
} |
|||
|
|||
public bool ShowInactiveFrames |
|||
{ |
|||
get => _showInactiveFrames; |
|||
set => RaiseAndSetIfChanged(ref _showInactiveFrames, value); |
|||
} |
|||
|
|||
public string? FramesStatus |
|||
{ |
|||
get => _framesStatus; |
|||
set => RaiseAndSetIfChanged(ref _framesStatus, value); |
|||
} |
|||
|
|||
public ControlLayoutViewModel? Layout { get; } |
|||
|
|||
protected override void OnPropertyChanged(PropertyChangedEventArgs e) |
|||
{ |
|||
base.OnPropertyChanged(e); |
|||
|
|||
if (e.PropertyName == nameof(SnapshotFrames)) |
|||
{ |
|||
if (!SnapshotFrames) |
|||
{ |
|||
UpdateStyles(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void UpdateStyleFilters() |
|||
{ |
|||
foreach (var style in AppliedFrames) |
|||
{ |
|||
var hasVisibleSetter = false; |
|||
|
|||
foreach (var setter in style.Setters) |
|||
{ |
|||
setter.IsVisible = TreePage.SettersFilter.Filter(setter.Name); |
|||
|
|||
hasVisibleSetter |= setter.IsVisible; |
|||
} |
|||
|
|||
style.IsVisible = hasVisibleSetter; |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (_avaloniaObject is INotifyPropertyChanged inpc) |
|||
{ |
|||
inpc.PropertyChanged -= ControlPropertyChanged; |
|||
} |
|||
|
|||
if (_avaloniaObject is AvaloniaObject ao) |
|||
{ |
|||
ao.PropertyChanged -= ControlPropertyChanged; |
|||
} |
|||
|
|||
if (_avaloniaObject is StyledElement se) |
|||
{ |
|||
se.Classes.RemoveListener(this); |
|||
} |
|||
} |
|||
|
|||
private static IEnumerable<PropertyViewModel> GetAvaloniaProperties(object o) |
|||
{ |
|||
if (o is AvaloniaObject ao) |
|||
{ |
|||
return AvaloniaPropertyRegistry.Instance.GetRegistered(ao) |
|||
.Union(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(ao.GetType())) |
|||
.Select(x => new AvaloniaPropertyViewModel(ao, x)); |
|||
} |
|||
else |
|||
{ |
|||
return Enumerable.Empty<AvaloniaPropertyViewModel>(); |
|||
} |
|||
} |
|||
|
|||
private static IEnumerable<PropertyViewModel> GetClrProperties(object o, bool showImplementedInterfaces) |
|||
{ |
|||
foreach (var p in GetClrProperties(o, o.GetType())) |
|||
{ |
|||
yield return p; |
|||
} |
|||
|
|||
if (showImplementedInterfaces) |
|||
{ |
|||
foreach (var i in o.GetType().GetInterfaces()) |
|||
{ |
|||
foreach (var p in GetClrProperties(o, i)) |
|||
{ |
|||
yield return p; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static IEnumerable<PropertyViewModel> GetClrProperties(object o, Type t) |
|||
{ |
|||
return t.GetProperties() |
|||
.Where(x => x.GetIndexParameters().Length == 0) |
|||
.Select(x => new ClrPropertyViewModel(o, x)); |
|||
} |
|||
|
|||
private void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (_propertyIndex is { } && _propertyIndex.TryGetValue(e.Property, out var properties)) |
|||
{ |
|||
foreach (var property in properties) |
|||
{ |
|||
property.Update(); |
|||
} |
|||
} |
|||
|
|||
Layout?.ControlPropertyChanged(sender, e); |
|||
} |
|||
|
|||
private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e) |
|||
{ |
|||
if (e.PropertyName != null |
|||
&& _propertyIndex is { } |
|||
&& _propertyIndex.TryGetValue(e.PropertyName, out var properties)) |
|||
{ |
|||
foreach (var property in properties) |
|||
{ |
|||
property.Update(); |
|||
} |
|||
} |
|||
|
|||
if (!SnapshotFrames) |
|||
{ |
|||
Dispatcher.UIThread.Post(UpdateStyles); |
|||
} |
|||
} |
|||
|
|||
void IClassesChangedListener.Changed() |
|||
{ |
|||
if (!SnapshotFrames) |
|||
{ |
|||
Dispatcher.UIThread.Post(UpdateStyles); |
|||
} |
|||
} |
|||
|
|||
private void UpdateStyles() |
|||
{ |
|||
int activeCount = 0; |
|||
|
|||
foreach (var style in AppliedFrames) |
|||
{ |
|||
style.Update(); |
|||
|
|||
if (style.IsActive) |
|||
{ |
|||
activeCount++; |
|||
} |
|||
} |
|||
|
|||
var propertyBuckets = new Dictionary<AvaloniaProperty, List<SetterViewModel>>(); |
|||
|
|||
foreach (var style in AppliedFrames.Reverse()) |
|||
{ |
|||
if (!style.IsActive) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
foreach (var setter in style.Setters) |
|||
{ |
|||
if (propertyBuckets.TryGetValue(setter.Property, out var setters)) |
|||
{ |
|||
foreach (var otherSetter in setters) |
|||
{ |
|||
otherSetter.IsActive = false; |
|||
} |
|||
|
|||
setter.IsActive = true; |
|||
|
|||
setters.Add(setter); |
|||
} |
|||
else |
|||
{ |
|||
setter.IsActive = true; |
|||
|
|||
setters = new List<SetterViewModel> { setter }; |
|||
|
|||
propertyBuckets.Add(setter.Property, setters); |
|||
} |
|||
} |
|||
} |
|||
|
|||
foreach (var pseudoClass in PseudoClasses) |
|||
{ |
|||
pseudoClass.Update(); |
|||
} |
|||
|
|||
FramesStatus = $"Value Frames ({activeCount}/{AppliedFrames.Count} active)"; |
|||
} |
|||
|
|||
private bool FilterProperty(object arg) |
|||
{ |
|||
return !(arg is PropertyViewModel property) || TreePage.PropertiesFilter.Filter(property.Name); |
|||
} |
|||
|
|||
private class PropertyComparer : IComparer<PropertyViewModel>, IComparer |
|||
{ |
|||
public static PropertyComparer Instance { get; } = new PropertyComparer(); |
|||
|
|||
public int Compare(PropertyViewModel? x, PropertyViewModel? y) |
|||
{ |
|||
if (x is null && y is null) |
|||
return 0; |
|||
|
|||
if (x is null && y is not null) |
|||
return -1; |
|||
|
|||
if (x is not null && y is null) |
|||
return 1; |
|||
|
|||
var groupX = GroupIndex(x!.Group); |
|||
var groupY = GroupIndex(y!.Group); |
|||
|
|||
if (groupX != groupY) |
|||
{ |
|||
return groupX - groupY; |
|||
} |
|||
else |
|||
{ |
|||
return string.CompareOrdinal(x.Name, y.Name); |
|||
} |
|||
} |
|||
|
|||
private static int GroupIndex(string? group) |
|||
{ |
|||
switch (group) |
|||
{ |
|||
case "Pinned": |
|||
return -1; |
|||
case "Properties": |
|||
return 0; |
|||
case "Attached Properties": |
|||
return 1; |
|||
case "CLR Properties": |
|||
return 2; |
|||
default: |
|||
return 3; |
|||
} |
|||
} |
|||
|
|||
public int Compare(object? x, object? y) => |
|||
Compare(x as PropertyViewModel, y as PropertyViewModel); |
|||
} |
|||
|
|||
private static IEnumerable<PropertyInfo> GetAllPublicProperties(Type type) |
|||
{ |
|||
return type |
|||
.GetProperties() |
|||
.Concat(type.GetInterfaces().SelectMany(i => i.GetProperties())); |
|||
} |
|||
|
|||
public void NavigateToSelectedProperty() |
|||
{ |
|||
var selectedProperty = SelectedProperty; |
|||
var selectedEntity = SelectedEntity; |
|||
var selectedEntityName = SelectedEntityName; |
|||
if (selectedEntity == null |
|||
|| selectedProperty == null |
|||
|| selectedProperty.PropertyType == typeof(string) |
|||
|| selectedProperty.PropertyType.IsValueType) |
|||
return; |
|||
|
|||
object? property = null; |
|||
|
|||
switch (selectedProperty) |
|||
{ |
|||
case AvaloniaPropertyViewModel avaloniaProperty: |
|||
|
|||
property = (_selectedEntity as Control)?.GetValue(avaloniaProperty.Property); |
|||
|
|||
break; |
|||
|
|||
case ClrPropertyViewModel clrProperty: |
|||
{ |
|||
property = GetAllPublicProperties(selectedEntity.GetType()) |
|||
.FirstOrDefault(pi => clrProperty.Property == pi)? |
|||
.GetValue(selectedEntity); |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (property == null) |
|||
return; |
|||
|
|||
_selectedEntitiesStack.Push((Name: selectedEntityName!, Entry: selectedEntity)); |
|||
|
|||
var propertyName = selectedProperty.Name; |
|||
|
|||
//Strip out interface names
|
|||
if (propertyName.LastIndexOf('.') is var p && p != -1) |
|||
{ |
|||
propertyName = propertyName.Substring(p + 1); |
|||
} |
|||
|
|||
NavigateToProperty(property, selectedEntityName + "." + propertyName); |
|||
|
|||
RaisePropertyChanged(nameof(CanNavigateToParentProperty)); |
|||
} |
|||
|
|||
public void NavigateToParentProperty() |
|||
{ |
|||
if (_selectedEntitiesStack.Count > 0) |
|||
{ |
|||
var property = _selectedEntitiesStack.Pop(); |
|||
NavigateToProperty(property.Entry, property.Name); |
|||
|
|||
RaisePropertyChanged(nameof(CanNavigateToParentProperty)); |
|||
} |
|||
} |
|||
|
|||
protected void NavigateToProperty(object o, string? entityName) |
|||
{ |
|||
var oldSelectedEntity = SelectedEntity; |
|||
|
|||
switch (oldSelectedEntity) |
|||
{ |
|||
case AvaloniaObject ao1: |
|||
ao1.PropertyChanged -= ControlPropertyChanged; |
|||
break; |
|||
|
|||
case INotifyPropertyChanged inpc1: |
|||
inpc1.PropertyChanged -= ControlPropertyChanged; |
|||
break; |
|||
} |
|||
|
|||
SelectedEntity = o; |
|||
SelectedEntityName = entityName; |
|||
SelectedEntityType = o.ToString(); |
|||
|
|||
var properties = GetAvaloniaProperties(o) |
|||
.Concat(GetClrProperties(o, _showImplementedInterfaces)) |
|||
.Do(p => |
|||
{ |
|||
p.IsPinned = _pinnedProperties.Contains(p.FullName); |
|||
}) |
|||
.ToArray(); |
|||
|
|||
_propertyIndex = properties |
|||
.GroupBy(x => x.Key) |
|||
.ToDictionary(x => x.Key, x => x.ToArray()); |
|||
|
|||
var view = new DataGridCollectionView(properties); |
|||
view.GroupDescriptions.AddRange(GroupDescriptors); |
|||
view.SortDescriptions.AddRange(SortDescriptions); |
|||
view.Filter = FilterProperty; |
|||
PropertiesView = view; |
|||
|
|||
switch (o) |
|||
{ |
|||
case AvaloniaObject ao2: |
|||
ao2.PropertyChanged += ControlPropertyChanged; |
|||
break; |
|||
|
|||
case INotifyPropertyChanged inpc2: |
|||
inpc2.PropertyChanged += ControlPropertyChanged; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
internal void SelectProperty(AvaloniaProperty property) |
|||
{ |
|||
SelectedProperty = null; |
|||
|
|||
if (SelectedEntity != _avaloniaObject) |
|||
{ |
|||
NavigateToProperty( |
|||
_avaloniaObject, |
|||
(_avaloniaObject as Control)?.Name ?? _avaloniaObject.ToString()); |
|||
} |
|||
|
|||
if (PropertiesView is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
foreach (object o in PropertiesView) |
|||
{ |
|||
if (o is AvaloniaPropertyViewModel propertyVm && propertyVm.Property == property) |
|||
{ |
|||
SelectedProperty = propertyVm; |
|||
|
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
internal void UpdatePropertiesView(bool showImplementedInterfaces) |
|||
{ |
|||
_showImplementedInterfaces = showImplementedInterfaces; |
|||
SelectedProperty = null; |
|||
NavigateToProperty(_avaloniaObject, (_avaloniaObject as Control)?.Name ?? _avaloniaObject.ToString()); |
|||
} |
|||
|
|||
public void TogglePinnedProperty(object parameter) |
|||
{ |
|||
if (parameter is PropertyViewModel model) |
|||
{ |
|||
var fullname = model.FullName; |
|||
if (_pinnedProperties.Contains(fullname)) |
|||
{ |
|||
_pinnedProperties.Remove(fullname); |
|||
model.IsPinned = false; |
|||
} |
|||
else |
|||
{ |
|||
_pinnedProperties.Add(fullname); |
|||
model.IsPinned = true; |
|||
} |
|||
PropertiesView?.Refresh(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,248 +0,0 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Text; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Layout; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class ControlLayoutViewModel : ViewModelBase |
|||
{ |
|||
private readonly Visual _control; |
|||
private Thickness _borderThickness; |
|||
private double _height; |
|||
private string? _heightConstraint; |
|||
private HorizontalAlignment _horizontalAlignment; |
|||
private Thickness _marginThickness; |
|||
private Thickness _paddingThickness; |
|||
private bool _updatingFromControl; |
|||
private VerticalAlignment _verticalAlignment; |
|||
private double _width; |
|||
private string? _widthConstraint; |
|||
|
|||
public ControlLayoutViewModel(Visual control) |
|||
{ |
|||
_control = control; |
|||
|
|||
HasPadding = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Decorator.PaddingProperty); |
|||
HasBorder = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Border.BorderThicknessProperty); |
|||
|
|||
if (control is AvaloniaObject ao) |
|||
{ |
|||
try |
|||
{ |
|||
_updatingFromControl = true; |
|||
MarginThickness = ao.GetValue(Layoutable.MarginProperty); |
|||
|
|||
if (HasPadding) |
|||
{ |
|||
PaddingThickness = ao.GetValue(Decorator.PaddingProperty); |
|||
} |
|||
|
|||
if (HasBorder) |
|||
{ |
|||
BorderThickness = ao.GetValue(Border.BorderThicknessProperty); |
|||
} |
|||
|
|||
HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty); |
|||
VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty); |
|||
} |
|||
finally |
|||
{ |
|||
_updatingFromControl = false; |
|||
} |
|||
} |
|||
|
|||
UpdateSize(); |
|||
UpdateSizeConstraints(); |
|||
} |
|||
|
|||
public Thickness MarginThickness |
|||
{ |
|||
get => _marginThickness; |
|||
set => RaiseAndSetIfChanged(ref _marginThickness, value); |
|||
} |
|||
|
|||
public Thickness BorderThickness |
|||
{ |
|||
get => _borderThickness; |
|||
set => RaiseAndSetIfChanged(ref _borderThickness, value); |
|||
} |
|||
|
|||
public Thickness PaddingThickness |
|||
{ |
|||
get => _paddingThickness; |
|||
set => RaiseAndSetIfChanged(ref _paddingThickness, value); |
|||
} |
|||
|
|||
public double Width |
|||
{ |
|||
get => _width; |
|||
private set => RaiseAndSetIfChanged(ref _width, value); |
|||
} |
|||
|
|||
public double Height |
|||
{ |
|||
get => _height; |
|||
private set => RaiseAndSetIfChanged(ref _height, value); |
|||
} |
|||
|
|||
public string? WidthConstraint |
|||
{ |
|||
get => _widthConstraint; |
|||
private set => RaiseAndSetIfChanged(ref _widthConstraint, value); |
|||
} |
|||
|
|||
public string? HeightConstraint |
|||
{ |
|||
get => _heightConstraint; |
|||
private set => RaiseAndSetIfChanged(ref _heightConstraint, value); |
|||
} |
|||
|
|||
public HorizontalAlignment HorizontalAlignment |
|||
{ |
|||
get => _horizontalAlignment; |
|||
set => RaiseAndSetIfChanged(ref _horizontalAlignment, value); |
|||
} |
|||
|
|||
public VerticalAlignment VerticalAlignment |
|||
{ |
|||
get => _verticalAlignment; |
|||
set => RaiseAndSetIfChanged(ref _verticalAlignment, value); |
|||
} |
|||
|
|||
public bool HasPadding { get; } |
|||
|
|||
public bool HasBorder { get; } |
|||
|
|||
private void UpdateSizeConstraints() |
|||
{ |
|||
if (_control is AvaloniaObject ao) |
|||
{ |
|||
string? CreateConstraintInfo(StyledProperty<double> minProperty, StyledProperty<double> maxProperty) |
|||
{ |
|||
bool hasMin = ao.IsSet(minProperty); |
|||
bool hasMax = ao.IsSet(maxProperty); |
|||
|
|||
if (hasMin || hasMax) |
|||
{ |
|||
var builder = new StringBuilder(); |
|||
|
|||
if (hasMin) |
|||
{ |
|||
var minValue = ao.GetValue(minProperty); |
|||
builder.AppendFormat("Min: {0}", Math.Round(minValue, 2)); |
|||
builder.AppendLine(); |
|||
} |
|||
|
|||
if (hasMax) |
|||
{ |
|||
var maxValue = ao.GetValue(maxProperty); |
|||
builder.AppendFormat("Max: {0}", Math.Round(maxValue, 2)); |
|||
} |
|||
|
|||
return builder.ToString(); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
WidthConstraint = CreateConstraintInfo(Layoutable.MinWidthProperty, Layoutable.MaxWidthProperty); |
|||
HeightConstraint = CreateConstraintInfo(Layoutable.MinHeightProperty, Layoutable.MaxHeightProperty); |
|||
} |
|||
} |
|||
|
|||
protected override void OnPropertyChanged(PropertyChangedEventArgs e) |
|||
{ |
|||
base.OnPropertyChanged(e); |
|||
|
|||
if (_updatingFromControl) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (_control is AvaloniaObject ao) |
|||
{ |
|||
if (e.PropertyName == nameof(MarginThickness)) |
|||
{ |
|||
ao.SetValue(Layoutable.MarginProperty, MarginThickness); |
|||
} |
|||
else if (HasPadding && e.PropertyName == nameof(PaddingThickness)) |
|||
{ |
|||
ao.SetValue(Decorator.PaddingProperty, PaddingThickness); |
|||
} |
|||
else if (HasBorder && e.PropertyName == nameof(BorderThickness)) |
|||
{ |
|||
ao.SetValue(Border.BorderThicknessProperty, BorderThickness); |
|||
} |
|||
else if (e.PropertyName == nameof(HorizontalAlignment)) |
|||
{ |
|||
ao.SetValue(Layoutable.HorizontalAlignmentProperty, HorizontalAlignment); |
|||
} |
|||
else if (e.PropertyName == nameof(VerticalAlignment)) |
|||
{ |
|||
ao.SetValue(Layoutable.VerticalAlignmentProperty, VerticalAlignment); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
try |
|||
{ |
|||
_updatingFromControl = true; |
|||
|
|||
if (e.Property == Visual.BoundsProperty) |
|||
{ |
|||
UpdateSize(); |
|||
} |
|||
else |
|||
{ |
|||
if (_control is AvaloniaObject ao) |
|||
{ |
|||
if (e.Property == Layoutable.MarginProperty) |
|||
{ |
|||
MarginThickness = ao.GetValue(Layoutable.MarginProperty); |
|||
} |
|||
else if (e.Property == Decorator.PaddingProperty) |
|||
{ |
|||
PaddingThickness = ao.GetValue(Decorator.PaddingProperty); |
|||
} |
|||
else if (e.Property == Border.BorderThicknessProperty) |
|||
{ |
|||
BorderThickness = ao.GetValue(Border.BorderThicknessProperty); |
|||
} |
|||
else if (e.Property == Layoutable.MinWidthProperty || |
|||
e.Property == Layoutable.MaxWidthProperty || |
|||
e.Property == Layoutable.MinHeightProperty || |
|||
e.Property == Layoutable.MaxHeightProperty) |
|||
{ |
|||
UpdateSizeConstraints(); |
|||
} |
|||
else if (e.Property == Layoutable.HorizontalAlignmentProperty) |
|||
{ |
|||
HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty); |
|||
} |
|||
else if (e.Property == Layoutable.VerticalAlignmentProperty) |
|||
{ |
|||
VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_updatingFromControl = false; |
|||
} |
|||
} |
|||
|
|||
private void UpdateSize() |
|||
{ |
|||
var size = _control.Bounds; |
|||
|
|||
Width = Math.Round(size.Width, 2); |
|||
Height = Math.Round(size.Height, 2); |
|||
} |
|||
} |
|||
} |
|||
@ -1,47 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class EventOwnerTreeNode : EventTreeNodeBase |
|||
{ |
|||
public EventOwnerTreeNode(Type type, IEnumerable<RoutedEvent> events, EventsPageViewModel vm) |
|||
: base(null, type.Name) |
|||
{ |
|||
Children = new AvaloniaList<EventTreeNodeBase>(events.OrderBy(e => e.Name) |
|||
.Select(e => new EventTreeNode(this, e, vm))); |
|||
IsExpanded = true; |
|||
} |
|||
|
|||
public override bool? IsEnabled |
|||
{ |
|||
get => base.IsEnabled; |
|||
set |
|||
{ |
|||
if (base.IsEnabled != value) |
|||
{ |
|||
base.IsEnabled = value; |
|||
|
|||
if (_updateChildren && value != null) |
|||
{ |
|||
foreach (var child in Children!) |
|||
{ |
|||
try |
|||
{ |
|||
child._updateParent = false; |
|||
child.IsEnabled = value; |
|||
} |
|||
finally |
|||
{ |
|||
child._updateParent = true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,131 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Diagnostics.Models; |
|||
using Avalonia.Diagnostics.Views; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Threading; |
|||
using Avalonia.VisualTree; |
|||
using Avalonia.Reactive; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class EventTreeNode : EventTreeNodeBase |
|||
{ |
|||
private readonly EventsPageViewModel _parentViewModel; |
|||
private bool _isRegistered; |
|||
private FiredEvent? _currentEvent; |
|||
|
|||
public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsPageViewModel vm) |
|||
: base(parent, @event.Name) |
|||
{ |
|||
Event = @event ?? throw new ArgumentNullException(nameof(@event)); |
|||
_parentViewModel = vm ?? throw new ArgumentNullException(nameof(vm)); |
|||
} |
|||
|
|||
public RoutedEvent Event { get; } |
|||
|
|||
public override bool? IsEnabled |
|||
{ |
|||
get => base.IsEnabled; |
|||
set |
|||
{ |
|||
if (base.IsEnabled != value) |
|||
{ |
|||
base.IsEnabled = value; |
|||
UpdateTracker(); |
|||
if (Parent != null && _updateParent) |
|||
{ |
|||
try |
|||
{ |
|||
Parent._updateChildren = false; |
|||
Parent.UpdateChecked(); |
|||
} |
|||
finally |
|||
{ |
|||
Parent._updateChildren = true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void UpdateTracker() |
|||
{ |
|||
if (IsEnabled.GetValueOrDefault() && !_isRegistered) |
|||
{ |
|||
var allRoutes = RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble; |
|||
|
|||
// FIXME: This leaks event handlers.
|
|||
Event.AddClassHandler(typeof(object), HandleEvent, allRoutes, handledEventsToo: true); |
|||
Event.RouteFinished.Subscribe(HandleRouteFinished); |
|||
|
|||
_isRegistered = true; |
|||
} |
|||
} |
|||
|
|||
private void HandleEvent(object? sender, RoutedEventArgs e) |
|||
{ |
|||
if (!_isRegistered || IsEnabled == false) |
|||
return; |
|||
if (sender is Visual v && v.DoesBelongToDevTool()) |
|||
return; |
|||
|
|||
var s = sender!; |
|||
var handled = e.Handled; |
|||
var route = e.Route; |
|||
var triggerTime = DateTime.Now; |
|||
|
|||
void handler() |
|||
{ |
|||
if (_currentEvent == null || !_currentEvent.IsPartOfSameEventChain(e)) |
|||
{ |
|||
_currentEvent = new FiredEvent(e, new EventChainLink(s, handled, route), triggerTime); |
|||
|
|||
_parentViewModel.RecordedEvents.Add(_currentEvent); |
|||
|
|||
while (_parentViewModel.RecordedEvents.Count > 100) |
|||
_parentViewModel.RecordedEvents.RemoveAt(0); |
|||
} |
|||
else |
|||
{ |
|||
_currentEvent.AddToChain(new EventChainLink(s, handled, route)); |
|||
} |
|||
}; |
|||
|
|||
if (!Dispatcher.UIThread.CheckAccess()) |
|||
Dispatcher.UIThread.Post(handler); |
|||
else |
|||
handler(); |
|||
} |
|||
|
|||
private void HandleRouteFinished(RoutedEventArgs e) |
|||
{ |
|||
if (!_isRegistered || IsEnabled == false) |
|||
return; |
|||
if (e.Source is Visual v && v.DoesBelongToDevTool()) |
|||
return; |
|||
|
|||
var s = e.Source; |
|||
var handled = e.Handled; |
|||
var route = e.Route; |
|||
|
|||
void handler() |
|||
{ |
|||
if (_currentEvent != null && handled) |
|||
{ |
|||
var linkIndex = _currentEvent.EventChain.Count - 1; |
|||
var link = _currentEvent.EventChain[linkIndex]; |
|||
|
|||
link.Handled = true; |
|||
_currentEvent.HandledBy ??= link; |
|||
} |
|||
} |
|||
|
|||
if (!Dispatcher.UIThread.CheckAccess()) |
|||
Dispatcher.UIThread.Post(handler); |
|||
else |
|||
handler(); |
|||
} |
|||
|
|||
|
|||
} |
|||
} |
|||
@ -1,84 +0,0 @@ |
|||
using Avalonia.Collections; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal abstract class EventTreeNodeBase : ViewModelBase |
|||
{ |
|||
internal bool _updateChildren = true; |
|||
internal bool _updateParent = true; |
|||
private bool _isExpanded; |
|||
private bool? _isEnabled = false; |
|||
private bool _isVisible; |
|||
|
|||
protected EventTreeNodeBase(EventTreeNodeBase? parent, string text) |
|||
{ |
|||
Parent = parent; |
|||
Text = text; |
|||
IsVisible = true; |
|||
} |
|||
|
|||
public IAvaloniaReadOnlyList<EventTreeNodeBase>? Children |
|||
{ |
|||
get; |
|||
protected set; |
|||
} |
|||
|
|||
public bool IsExpanded |
|||
{ |
|||
get => _isExpanded; |
|||
set => RaiseAndSetIfChanged(ref _isExpanded, value); |
|||
} |
|||
|
|||
public virtual bool? IsEnabled |
|||
{ |
|||
get => _isEnabled; |
|||
set => RaiseAndSetIfChanged(ref _isEnabled, value); |
|||
} |
|||
|
|||
public bool IsVisible |
|||
{ |
|||
get => _isVisible; |
|||
set => RaiseAndSetIfChanged(ref _isVisible, value); |
|||
} |
|||
|
|||
public EventTreeNodeBase? Parent |
|||
{ |
|||
get; |
|||
} |
|||
|
|||
public string Text |
|||
{ |
|||
get; |
|||
} |
|||
|
|||
internal void UpdateChecked() |
|||
{ |
|||
IsEnabled = GetValue(); |
|||
|
|||
bool? GetValue() |
|||
{ |
|||
if (Children == null) |
|||
return false; |
|||
|
|||
bool? value = false; |
|||
|
|||
for (int i = 0; i < Children.Count; i++) |
|||
{ |
|||
if (i == 0) |
|||
{ |
|||
value = Children[i].IsEnabled; |
|||
continue; |
|||
} |
|||
|
|||
if (value != Children[i].IsEnabled) |
|||
{ |
|||
value = null; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return value; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,177 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.ObjectModel; |
|||
using System.Linq; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Diagnostics.Models; |
|||
using Avalonia.Input; |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class EventsPageViewModel : ViewModelBase |
|||
{ |
|||
private static readonly HashSet<RoutedEvent> s_defaultEvents = new HashSet<RoutedEvent>() |
|||
{ |
|||
Button.ClickEvent, |
|||
InputElement.KeyDownEvent, |
|||
InputElement.KeyUpEvent, |
|||
InputElement.TextInputEvent, |
|||
InputElement.PointerReleasedEvent, |
|||
InputElement.PointerPressedEvent |
|||
}; |
|||
|
|||
private readonly MainViewModel _mainViewModel; |
|||
private FiredEvent? _selectedEvent; |
|||
private EventTreeNodeBase? _selectedNode; |
|||
|
|||
public EventsPageViewModel(MainViewModel mainViewModel) |
|||
{ |
|||
_mainViewModel = mainViewModel; |
|||
|
|||
Nodes = RoutedEventRegistry.Instance.GetAllRegistered() |
|||
.GroupBy(e => e.OwnerType) |
|||
.OrderBy(e => e.Key.Name) |
|||
.Select(g => new EventOwnerTreeNode(g.Key, g, this)) |
|||
.ToArray(); |
|||
|
|||
EventsFilter = new FilterViewModel(); |
|||
EventsFilter.RefreshFilter += (s, e) => UpdateEventFilters(); |
|||
|
|||
EnableDefault(); |
|||
} |
|||
|
|||
public string Name => "Events"; |
|||
|
|||
public EventTreeNodeBase[] Nodes { get; } |
|||
|
|||
public ObservableCollection<FiredEvent> RecordedEvents { get; } = new ObservableCollection<FiredEvent>(); |
|||
|
|||
public FiredEvent? SelectedEvent |
|||
{ |
|||
get => _selectedEvent; |
|||
set => RaiseAndSetIfChanged(ref _selectedEvent, value); |
|||
} |
|||
|
|||
public EventTreeNodeBase? SelectedNode |
|||
{ |
|||
get => _selectedNode; |
|||
set => RaiseAndSetIfChanged(ref _selectedNode, value); |
|||
} |
|||
|
|||
public FilterViewModel EventsFilter { get; } |
|||
|
|||
public void Clear() |
|||
{ |
|||
RecordedEvents.Clear(); |
|||
} |
|||
|
|||
public void DisableAll() |
|||
{ |
|||
EvaluateNodeEnabled(_ => false); |
|||
} |
|||
|
|||
public void EnableDefault() |
|||
{ |
|||
EvaluateNodeEnabled(node => s_defaultEvents.Contains(node.Event)); |
|||
} |
|||
|
|||
public void RequestTreeNavigateTo(EventChainLink navTarget) |
|||
{ |
|||
if (navTarget.Handler is Control control) |
|||
{ |
|||
_mainViewModel.RequestTreeNavigateTo(control, true); |
|||
} |
|||
} |
|||
|
|||
public void SelectEventByType(RoutedEvent evt) |
|||
{ |
|||
foreach (var node in Nodes) |
|||
{ |
|||
var result = FindNode(node, evt); |
|||
|
|||
if (result != null && result.IsVisible) |
|||
{ |
|||
SelectedNode = result; |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
static EventTreeNodeBase? FindNode(EventTreeNodeBase node, RoutedEvent eventType) |
|||
{ |
|||
if (node is EventTreeNode eventNode && eventNode.Event == eventType) |
|||
{ |
|||
return node; |
|||
} |
|||
|
|||
if (node.Children != null) |
|||
{ |
|||
foreach (var child in node.Children) |
|||
{ |
|||
var result = FindNode(child, eventType); |
|||
|
|||
if (result != null) |
|||
{ |
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
|
|||
private void EvaluateNodeEnabled(Func<EventTreeNode, bool> eval) |
|||
{ |
|||
void ProcessNode(EventTreeNodeBase node) |
|||
{ |
|||
if (node is EventTreeNode eventNode) |
|||
{ |
|||
node.IsEnabled = eval(eventNode); |
|||
} |
|||
|
|||
if (node.Children != null) |
|||
{ |
|||
foreach (var childNode in node.Children) |
|||
{ |
|||
ProcessNode(childNode); |
|||
} |
|||
} |
|||
} |
|||
|
|||
foreach (var node in Nodes) |
|||
{ |
|||
ProcessNode(node); |
|||
} |
|||
} |
|||
|
|||
private void UpdateEventFilters() |
|||
{ |
|||
foreach (var node in Nodes) |
|||
{ |
|||
FilterNode(node, false); |
|||
} |
|||
|
|||
bool FilterNode(EventTreeNodeBase node, bool isParentVisible) |
|||
{ |
|||
bool matchesFilter = EventsFilter.Filter(node.Text); |
|||
bool hasVisibleChild = false; |
|||
|
|||
if (node.Children != null) |
|||
{ |
|||
foreach (var childNode in node.Children) |
|||
{ |
|||
hasVisibleChild |= FilterNode(childNode, matchesFilter); |
|||
} |
|||
} |
|||
|
|||
node.IsVisible = hasVisibleChild || matchesFilter || isParentVisible; |
|||
|
|||
return node.IsVisible; |
|||
} |
|||
} |
|||
|
|||
public MainViewModel MainView => _mainViewModel; |
|||
} |
|||
} |
|||
@ -1,122 +0,0 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel; |
|||
using System.Text.RegularExpressions; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class FilterViewModel : ViewModelBase, INotifyDataErrorInfo |
|||
{ |
|||
private readonly Dictionary<string, string> _errors = new Dictionary<string, string>(); |
|||
private string _filterString = string.Empty; |
|||
private bool _useRegexFilter, _useCaseSensitiveFilter, _useWholeWordFilter; |
|||
private Regex? _filterRegex; |
|||
|
|||
public event EventHandler? RefreshFilter; |
|||
|
|||
public bool HasErrors => _errors.Count > 0; |
|||
|
|||
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged; |
|||
|
|||
public bool Filter(string input) |
|||
{ |
|||
return _filterRegex?.IsMatch(input) ?? true; |
|||
} |
|||
|
|||
private void UpdateFilterRegex() |
|||
{ |
|||
void ClearError() |
|||
{ |
|||
if (_errors.Remove(nameof(FilterString))) |
|||
{ |
|||
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(FilterString))); |
|||
} |
|||
} |
|||
|
|||
try |
|||
{ |
|||
var options = RegexOptions.Compiled; |
|||
var pattern = UseRegexFilter |
|||
? FilterString.Trim() : Regex.Escape(FilterString.Trim()); |
|||
if (!UseCaseSensitiveFilter) |
|||
{ |
|||
options |= RegexOptions.IgnoreCase; |
|||
} |
|||
if (UseWholeWordFilter) |
|||
{ |
|||
pattern = $"\\b(?:{pattern})\\b"; |
|||
} |
|||
|
|||
_filterRegex = new Regex(pattern, options); |
|||
ClearError(); |
|||
} |
|||
catch (Exception exception) |
|||
{ |
|||
_errors[nameof(FilterString)] = exception.Message; |
|||
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(FilterString))); |
|||
} |
|||
} |
|||
|
|||
public string FilterString |
|||
{ |
|||
get => _filterString; |
|||
set |
|||
{ |
|||
if (RaiseAndSetIfChanged(ref _filterString, value)) |
|||
{ |
|||
UpdateFilterRegex(); |
|||
RefreshFilter?.Invoke(this, EventArgs.Empty); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool UseRegexFilter |
|||
{ |
|||
get => _useRegexFilter; |
|||
set |
|||
{ |
|||
if (RaiseAndSetIfChanged(ref _useRegexFilter, value)) |
|||
{ |
|||
UpdateFilterRegex(); |
|||
RefreshFilter?.Invoke(this, EventArgs.Empty); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool UseCaseSensitiveFilter |
|||
{ |
|||
get => _useCaseSensitiveFilter; |
|||
set |
|||
{ |
|||
if (RaiseAndSetIfChanged(ref _useCaseSensitiveFilter, value)) |
|||
{ |
|||
UpdateFilterRegex(); |
|||
RefreshFilter?.Invoke(this, EventArgs.Empty); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool UseWholeWordFilter |
|||
{ |
|||
get => _useWholeWordFilter; |
|||
set |
|||
{ |
|||
if (RaiseAndSetIfChanged(ref _useWholeWordFilter, value)) |
|||
{ |
|||
UpdateFilterRegex(); |
|||
RefreshFilter?.Invoke(this, EventArgs.Empty); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public IEnumerable GetErrors(string? propertyName) |
|||
{ |
|||
if (propertyName != null |
|||
&& _errors.TryGetValue(propertyName, out var error)) |
|||
{ |
|||
yield return error; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,87 +0,0 @@ |
|||
using System; |
|||
using System.Collections.ObjectModel; |
|||
using Avalonia.Diagnostics.Models; |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class FiredEvent : ViewModelBase |
|||
{ |
|||
private readonly RoutedEventArgs _eventArgs; |
|||
private readonly RoutedEvent? _originalEvent; |
|||
private EventChainLink? _handledBy; |
|||
|
|||
public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator, DateTime triggerTime) |
|||
{ |
|||
_eventArgs = eventArgs ?? throw new ArgumentNullException(nameof(eventArgs)); |
|||
Originator = originator ?? throw new ArgumentNullException(nameof(originator)); |
|||
_originalEvent = _eventArgs.RoutedEvent; |
|||
AddToChain(originator); |
|||
TriggerTime = triggerTime; |
|||
} |
|||
|
|||
public bool IsPartOfSameEventChain(RoutedEventArgs e) |
|||
{ |
|||
// Note, Avalonia might reuse RoutedEventArgs for different events to avoid extra allocations.
|
|||
// Like, PointerEntered and PointerExited will use the same instance of RoutedEventArgs.
|
|||
return e == _eventArgs && e.RoutedEvent == _originalEvent; |
|||
} |
|||
|
|||
public DateTime TriggerTime { get; } |
|||
|
|||
public RoutedEvent Event => _originalEvent!; |
|||
|
|||
public bool IsHandled => HandledBy?.Handled == true; |
|||
|
|||
public ObservableCollection<EventChainLink> EventChain { get; } = new ObservableCollection<EventChainLink>(); |
|||
|
|||
public string DisplayText |
|||
{ |
|||
get |
|||
{ |
|||
if (IsHandled) |
|||
{ |
|||
return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine + |
|||
$"strategies: {Event.RoutingStrategies}; handled by: {HandledBy!.HandlerName}"; |
|||
} |
|||
|
|||
return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}"; |
|||
} |
|||
} |
|||
|
|||
public EventChainLink Originator { get; } |
|||
|
|||
public EventChainLink? HandledBy |
|||
{ |
|||
get => _handledBy; |
|||
set |
|||
{ |
|||
if (_handledBy != value) |
|||
{ |
|||
_handledBy = value; |
|||
RaisePropertyChanged(); |
|||
RaisePropertyChanged(nameof(IsHandled)); |
|||
RaisePropertyChanged(nameof(DisplayText)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void AddToChain(EventChainLink link) |
|||
{ |
|||
if (EventChain.Count > 0) |
|||
{ |
|||
var prevLink = EventChain[EventChain.Count - 1]; |
|||
|
|||
if (prevLink.Route != link.Route) |
|||
{ |
|||
link.BeginsNewRoute = true; |
|||
} |
|||
} |
|||
|
|||
EventChain.Add(link); |
|||
|
|||
if (HandledBy == null && link.Handled) |
|||
HandledBy = link; |
|||
} |
|||
} |
|||
} |
|||
@ -1,40 +0,0 @@ |
|||
using System.Collections.ObjectModel; |
|||
using Avalonia.Input; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal record HotKeyDescription(string Gesture, string BriefDescription, string? DetailedDescription = null); |
|||
|
|||
internal class HotKeyPageViewModel : ViewModelBase |
|||
{ |
|||
private ObservableCollection<HotKeyDescription>? _hotKeyDescriptions; |
|||
public ObservableCollection<HotKeyDescription>? HotKeyDescriptions |
|||
{ |
|||
get => _hotKeyDescriptions; |
|||
private set => RaiseAndSetIfChanged(ref _hotKeyDescriptions, value); |
|||
} |
|||
|
|||
public void SetOptions(DevToolsOptions options) |
|||
{ |
|||
var hotKeys = options.HotKeys; |
|||
|
|||
HotKeyDescriptions = new() |
|||
{ |
|||
new(CreateDescription(options.Gesture), "Launch DevTools", "Launches DevTools to inspect the TopLevel that received the hotkey input"), |
|||
new(CreateDescription(hotKeys.ValueFramesFreeze), "Freeze Value Frames", "Pauses refreshing the Value Frames inspector for the selected Control"), |
|||
new(CreateDescription(hotKeys.ValueFramesUnfreeze), "Unfreeze Value Frames", "Resumes refreshing the Value Frames inspector for the selected Control"), |
|||
new(CreateDescription(hotKeys.InspectHoveredControl), "Inspect Control Under Pointer", "Inspects the hovered Control in the Logical or Visual Tree Page"), |
|||
new(CreateDescription(hotKeys.TogglePopupFreeze), "Toggle Popup Freeze", "Prevents visible Popups from closing so they can be inspected"), |
|||
new(CreateDescription(hotKeys.ScreenshotSelectedControl), "Screenshot Selected Control", "Saves a Screenshot of the Selected Control in the Logical or Visual Tree Page") |
|||
}; |
|||
} |
|||
|
|||
private string CreateDescription(KeyGesture gesture) |
|||
{ |
|||
if (gesture.Key == Key.None && gesture.KeyModifiers != KeyModifiers.None) |
|||
return gesture.ToString().Replace("+None", ""); |
|||
else |
|||
return gesture.ToString(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,116 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Controls; |
|||
using Avalonia.LogicalTree; |
|||
using Lifetimes = Avalonia.Controls.ApplicationLifetimes; |
|||
using System.Linq; |
|||
using Avalonia.Reactive; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class LogicalTreeNode : TreeNode |
|||
{ |
|||
public LogicalTreeNode(AvaloniaObject avaloniaObject, TreeNode? parent) |
|||
: base(avaloniaObject, parent) |
|||
{ |
|||
Children = avaloniaObject switch |
|||
{ |
|||
ILogical logical => new LogicalTreeNodeCollection(this, logical), |
|||
Controls.TopLevelGroup host => new TopLevelGroupHostLogical(this, host), |
|||
_ => TreeNodeCollection.Empty |
|||
}; |
|||
} |
|||
|
|||
public override TreeNodeCollection Children { get; } |
|||
|
|||
public static LogicalTreeNode[] Create(object control) |
|||
{ |
|||
var logical = control as AvaloniaObject; |
|||
return logical != null ? new[] { new LogicalTreeNode(logical, null) } : Array.Empty<LogicalTreeNode>(); |
|||
} |
|||
|
|||
internal class LogicalTreeNodeCollection : TreeNodeCollection |
|||
{ |
|||
private readonly ILogical _control; |
|||
private IDisposable? _subscription; |
|||
|
|||
public LogicalTreeNodeCollection(TreeNode owner, ILogical control) |
|||
: base(owner) |
|||
{ |
|||
_control = control; |
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
base.Dispose(); |
|||
_subscription?.Dispose(); |
|||
} |
|||
|
|||
protected override void Initialize(AvaloniaList<TreeNode> nodes) |
|||
{ |
|||
_subscription = _control.LogicalChildren.ForEachItem( |
|||
(i, item) => nodes.Insert(i, new LogicalTreeNode((AvaloniaObject)item, Owner)), |
|||
(i, item) => nodes.RemoveAt(i), |
|||
() => nodes.Clear()); |
|||
} |
|||
} |
|||
|
|||
internal class TopLevelGroupHostLogical : TreeNodeCollection |
|||
{ |
|||
private readonly Controls.TopLevelGroup _group; |
|||
private readonly CompositeDisposable _subscriptions = new(1); |
|||
|
|||
public TopLevelGroupHostLogical(TreeNode owner, Controls.TopLevelGroup host) : |
|||
base(owner) |
|||
{ |
|||
_group = host; |
|||
} |
|||
|
|||
protected override void Initialize(AvaloniaList<TreeNode> nodes) |
|||
{ |
|||
for (var i = 0; i < _group.Items.Count; i++) |
|||
{ |
|||
var window = _group.Items[i]; |
|||
if (window is Views.MainWindow) |
|||
{ |
|||
continue; |
|||
} |
|||
nodes.Add(new LogicalTreeNode(window, Owner)); |
|||
} |
|||
void GroupOnAdded(object? sender, TopLevel e) |
|||
{ |
|||
if (e is Views.MainWindow) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
nodes.Add(new LogicalTreeNode(e, Owner)); |
|||
} |
|||
void GroupOnRemoved(object? sender, TopLevel e) |
|||
{ |
|||
if (e is Views.MainWindow) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
nodes.Add(new LogicalTreeNode(e, Owner)); |
|||
} |
|||
|
|||
_group.Added += GroupOnAdded; |
|||
_group.Removed += GroupOnRemoved; |
|||
|
|||
_subscriptions.Add(new Disposable.AnonymousDisposable(() => |
|||
{ |
|||
_group.Added -= GroupOnAdded; |
|||
_group.Removed -= GroupOnRemoved; |
|||
})); |
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
_subscriptions?.Dispose(); |
|||
base.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,387 +0,0 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Runtime.CompilerServices; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Input; |
|||
using Avalonia.Metadata; |
|||
using Avalonia.Threading; |
|||
using Avalonia.Reactive; |
|||
using Avalonia.Rendering; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class MainViewModel : ViewModelBase, IDisposable |
|||
{ |
|||
private readonly AvaloniaObject _root; |
|||
private readonly TreePageViewModel _logicalTree; |
|||
private readonly TreePageViewModel _visualTree; |
|||
private readonly EventsPageViewModel _events; |
|||
private readonly HotKeyPageViewModel _hotKeys; |
|||
private readonly IDisposable _pointerOverSubscription; |
|||
private ViewModelBase? _content; |
|||
private int _selectedTab; |
|||
private string? _focusedControl; |
|||
private IInputElement? _pointerOverElement; |
|||
private bool _shouldVisualizeMarginPadding = true; |
|||
private bool _freezePopups; |
|||
private string? _pointerOverElementName; |
|||
private IInputRoot? _pointerOverRoot; |
|||
private IScreenshotHandler? _screenshotHandler; |
|||
private bool _showPropertyType; |
|||
private bool _showImplementedInterfaces; |
|||
private readonly HashSet<string> _pinnedProperties = new(); |
|||
private IBrush? _FocusHighlighter; |
|||
private IDisposable? _currentFocusHighlightAdorner = default; |
|||
|
|||
public MainViewModel(AvaloniaObject root) |
|||
{ |
|||
_root = root; |
|||
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root), _pinnedProperties); |
|||
_visualTree = new TreePageViewModel(this, VisualTreeNode.Create(root), _pinnedProperties); |
|||
_events = new EventsPageViewModel(this); |
|||
_hotKeys = new HotKeyPageViewModel(); |
|||
|
|||
UpdateFocusedControl(); |
|||
|
|||
if (KeyboardDevice.Instance is not null) |
|||
KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged; |
|||
SelectedTab = 0; |
|||
if (root is TopLevel topLevel) |
|||
{ |
|||
_pointerOverRoot = topLevel; |
|||
_pointerOverSubscription = topLevel.GetObservable(TopLevel.PointerOverElementProperty) |
|||
.Subscribe(x => PointerOverElement = x); |
|||
|
|||
} |
|||
else |
|||
{ |
|||
_pointerOverSubscription = InputManager.Instance!.PreProcess |
|||
.Subscribe(e => |
|||
{ |
|||
if (e is Input.Raw.RawPointerEventArgs pointerEventArgs) |
|||
{ |
|||
PointerOverRoot = pointerEventArgs.Root; |
|||
PointerOverElement = pointerEventArgs.Root.InputHitTest(pointerEventArgs.Position); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public bool FreezePopups |
|||
{ |
|||
get => _freezePopups; |
|||
set => RaiseAndSetIfChanged(ref _freezePopups, value); |
|||
} |
|||
|
|||
public bool ShouldVisualizeMarginPadding |
|||
{ |
|||
get => _shouldVisualizeMarginPadding; |
|||
set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value); |
|||
} |
|||
|
|||
public void ToggleVisualizeMarginPadding() |
|||
=> ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding; |
|||
|
|||
private IRenderer? TryGetRenderer() |
|||
=> _root switch |
|||
{ |
|||
TopLevel topLevel => topLevel.Renderer, |
|||
Controls.Application app => app.RendererRoot, |
|||
_ => null |
|||
}; |
|||
|
|||
private bool GetDebugOverlay(RendererDebugOverlays overlay) |
|||
=> ((TryGetRenderer()?.Diagnostics.DebugOverlays ?? RendererDebugOverlays.None) & overlay) != 0; |
|||
|
|||
private void SetDebugOverlay(RendererDebugOverlays overlay, bool enable, |
|||
[CallerMemberName] string? propertyName = null) |
|||
{ |
|||
if (TryGetRenderer() is not { } renderer) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var oldValue = renderer.Diagnostics.DebugOverlays; |
|||
var newValue = enable ? oldValue | overlay : oldValue & ~overlay; |
|||
|
|||
if (oldValue == newValue) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
renderer.Diagnostics.DebugOverlays = newValue; |
|||
RaisePropertyChanged(propertyName); |
|||
} |
|||
|
|||
public bool ShowDirtyRectsOverlay |
|||
{ |
|||
get => GetDebugOverlay(RendererDebugOverlays.DirtyRects); |
|||
set => SetDebugOverlay(RendererDebugOverlays.DirtyRects, value); |
|||
} |
|||
|
|||
public void ToggleDirtyRectsOverlay() |
|||
=> ShowDirtyRectsOverlay = !ShowDirtyRectsOverlay; |
|||
|
|||
public bool ShowFpsOverlay |
|||
{ |
|||
get => GetDebugOverlay(RendererDebugOverlays.Fps); |
|||
set => SetDebugOverlay(RendererDebugOverlays.Fps, value); |
|||
} |
|||
|
|||
public void ToggleFpsOverlay() |
|||
=> ShowFpsOverlay = !ShowFpsOverlay; |
|||
|
|||
public bool ShowLayoutTimeGraphOverlay |
|||
{ |
|||
get => GetDebugOverlay(RendererDebugOverlays.LayoutTimeGraph); |
|||
set => SetDebugOverlay(RendererDebugOverlays.LayoutTimeGraph, value); |
|||
} |
|||
|
|||
public void ToggleLayoutTimeGraphOverlay() |
|||
=> ShowLayoutTimeGraphOverlay = !ShowLayoutTimeGraphOverlay; |
|||
|
|||
public bool ShowRenderTimeGraphOverlay |
|||
{ |
|||
get => GetDebugOverlay(RendererDebugOverlays.RenderTimeGraph); |
|||
set => SetDebugOverlay(RendererDebugOverlays.RenderTimeGraph, value); |
|||
} |
|||
|
|||
public void ToggleRenderTimeGraphOverlay() |
|||
=> ShowRenderTimeGraphOverlay = !ShowRenderTimeGraphOverlay; |
|||
|
|||
public ViewModelBase? Content |
|||
{ |
|||
get { return _content; } |
|||
private set |
|||
{ |
|||
if (_content is TreePageViewModel oldTree && |
|||
value is TreePageViewModel newTree && |
|||
oldTree?.SelectedNode?.Visual is Control control) |
|||
{ |
|||
// HACK: We want to select the currently selected control in the new tree, but
|
|||
// to select nested nodes in TreeView, currently the TreeView has to be able to
|
|||
// expand the parent nodes. Because at this point the TreeView isn't visible,
|
|||
// this will fail unless we schedule the selection to run after layout.
|
|||
DispatcherTimer.RunOnce( |
|||
() => |
|||
{ |
|||
try |
|||
{ |
|||
newTree.SelectControl(control); |
|||
} |
|||
catch { } |
|||
}, |
|||
TimeSpan.FromMilliseconds(0)); |
|||
} |
|||
|
|||
RaiseAndSetIfChanged(ref _content, value); |
|||
} |
|||
} |
|||
|
|||
public int SelectedTab |
|||
{ |
|||
get { return _selectedTab; } |
|||
// [MemberNotNull(nameof(_content))]
|
|||
set |
|||
{ |
|||
_selectedTab = value; |
|||
|
|||
switch (value) |
|||
{ |
|||
case 1: |
|||
Content = _visualTree; |
|||
break; |
|||
case 2: |
|||
Content = _events; |
|||
break; |
|||
case 3: |
|||
Content = _hotKeys; |
|||
break; |
|||
default: |
|||
Content = _logicalTree; |
|||
break; |
|||
} |
|||
|
|||
RaisePropertyChanged(); |
|||
} |
|||
} |
|||
|
|||
public string? FocusedControl |
|||
{ |
|||
get { return _focusedControl; } |
|||
private set { RaiseAndSetIfChanged(ref _focusedControl, value); } |
|||
} |
|||
|
|||
public IInputRoot? PointerOverRoot |
|||
{ |
|||
get => _pointerOverRoot; |
|||
private set => RaiseAndSetIfChanged(ref _pointerOverRoot, value); |
|||
} |
|||
|
|||
public IInputElement? PointerOverElement |
|||
{ |
|||
get { return _pointerOverElement; } |
|||
private set |
|||
{ |
|||
RaiseAndSetIfChanged(ref _pointerOverElement, value); |
|||
PointerOverElementName = value?.GetType()?.Name; |
|||
} |
|||
} |
|||
|
|||
public string? PointerOverElementName |
|||
{ |
|||
get => _pointerOverElementName; |
|||
private set => RaiseAndSetIfChanged(ref _pointerOverElementName, value); |
|||
} |
|||
|
|||
public void ShowHotKeys() |
|||
{ |
|||
SelectedTab = 3; |
|||
} |
|||
|
|||
public void SelectControl(Control control) |
|||
{ |
|||
var tree = Content as TreePageViewModel; |
|||
|
|||
tree?.SelectControl(control); |
|||
} |
|||
|
|||
public void EnableSnapshotStyles(bool enable) |
|||
{ |
|||
if (Content is TreePageViewModel treeVm && treeVm.Details != null) |
|||
{ |
|||
treeVm.Details.SnapshotFrames = enable; |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (KeyboardDevice.Instance is not null) |
|||
KeyboardDevice.Instance.PropertyChanged -= KeyboardPropertyChanged; |
|||
_pointerOverSubscription.Dispose(); |
|||
_logicalTree.Dispose(); |
|||
_visualTree.Dispose(); |
|||
_currentFocusHighlightAdorner?.Dispose(); |
|||
if (TryGetRenderer() is { } renderer) |
|||
{ |
|||
renderer.Diagnostics.DebugOverlays = RendererDebugOverlays.None; |
|||
} |
|||
} |
|||
|
|||
private void UpdateFocusedControl() |
|||
{ |
|||
var element = KeyboardDevice.Instance?.FocusedElement; |
|||
FocusedControl = element?.GetType().Name; |
|||
_currentFocusHighlightAdorner?.Dispose(); |
|||
if (FocusHighlighter is IBrush brush |
|||
&& element is InputElement input |
|||
&& !input.DoesBelongToDevTool() |
|||
) |
|||
{ |
|||
_currentFocusHighlightAdorner = Controls.ControlHighlightAdorner.Add(input, brush); |
|||
} |
|||
} |
|||
|
|||
private void KeyboardPropertyChanged(object? sender, PropertyChangedEventArgs e) |
|||
{ |
|||
if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) |
|||
{ |
|||
UpdateFocusedControl(); |
|||
} |
|||
} |
|||
|
|||
public void RequestTreeNavigateTo(Control control, bool isVisualTree) |
|||
{ |
|||
var tree = isVisualTree ? _visualTree : _logicalTree; |
|||
|
|||
var node = tree.FindNode(control); |
|||
|
|||
if (node != null) |
|||
{ |
|||
SelectedTab = isVisualTree ? 1 : 0; |
|||
|
|||
tree.SelectControl(control); |
|||
} |
|||
} |
|||
|
|||
public int? StartupScreenIndex { get; private set; } = default; |
|||
|
|||
[DependsOn(nameof(TreePageViewModel.SelectedNode))] |
|||
[DependsOn(nameof(Content))] |
|||
public bool CanShot(object? parameter) |
|||
{ |
|||
return Content is TreePageViewModel tree |
|||
&& tree.SelectedNode != null |
|||
&& tree.SelectedNode.Visual is Visual visual |
|||
&& visual.VisualRoot != null; |
|||
} |
|||
|
|||
public async void Shot(object? parameter) |
|||
{ |
|||
if ((Content as TreePageViewModel)?.SelectedNode?.Visual is Control control |
|||
&& _screenshotHandler is { } |
|||
) |
|||
{ |
|||
try |
|||
{ |
|||
await _screenshotHandler.Take(control); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
System.Diagnostics.Debug.WriteLine(ex.Message); |
|||
//TODO: Notify error
|
|||
} |
|||
} |
|||
} |
|||
|
|||
public void SetOptions(DevToolsOptions options) |
|||
{ |
|||
_screenshotHandler = options.ScreenshotHandler; |
|||
StartupScreenIndex = options.StartupScreenIndex; |
|||
ShowImplementedInterfaces = options.ShowImplementedInterfaces; |
|||
FocusHighlighter = options.FocusHighlighterBrush; |
|||
SelectedTab = (int)options.LaunchView; |
|||
|
|||
_hotKeys.SetOptions(options); |
|||
} |
|||
|
|||
public bool ShowImplementedInterfaces |
|||
{ |
|||
get => _showImplementedInterfaces; |
|||
private set => RaiseAndSetIfChanged(ref _showImplementedInterfaces, value); |
|||
} |
|||
|
|||
public void ToggleShowImplementedInterfaces(object parameter) |
|||
{ |
|||
ShowImplementedInterfaces = !ShowImplementedInterfaces; |
|||
if (Content is TreePageViewModel viewModel) |
|||
{ |
|||
viewModel.UpdatePropertiesView(); |
|||
} |
|||
} |
|||
|
|||
public bool ShowDetailsPropertyType |
|||
{ |
|||
get => _showPropertyType; |
|||
private set => RaiseAndSetIfChanged(ref _showPropertyType, value); |
|||
} |
|||
|
|||
public void ToggleShowDetailsPropertyType(object parameter) |
|||
{ |
|||
ShowDetailsPropertyType = !ShowDetailsPropertyType; |
|||
} |
|||
|
|||
public IBrush? FocusHighlighter |
|||
{ |
|||
get => _FocusHighlighter; |
|||
private set => RaiseAndSetIfChanged(ref _FocusHighlighter, value); |
|||
} |
|||
|
|||
public void SelectFocusHighlighter(object parameter) |
|||
{ |
|||
FocusHighlighter = parameter as IBrush; |
|||
} |
|||
} |
|||
} |
|||
@ -1,29 +0,0 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels; |
|||
|
|||
internal abstract class PropertyViewModel : ViewModelBase |
|||
{ |
|||
private bool _isPinned; |
|||
|
|||
public abstract object Key { get; } |
|||
public abstract string Name { get; } |
|||
public abstract string Group { get; } |
|||
public abstract Type AssignedType { get; } |
|||
public abstract Type? DeclaringType { get; } |
|||
public abstract object? Value { get; set; } |
|||
public abstract string Priority { get; } |
|||
public abstract bool? IsAttached { get; } |
|||
public abstract void Update(); |
|||
public abstract Type PropertyType { get; } |
|||
|
|||
public string Type => PropertyType == AssignedType ? |
|||
PropertyType.GetTypeName() : |
|||
$"{PropertyType.GetTypeName()} {{{AssignedType.GetTypeName()}}}"; |
|||
|
|||
public abstract bool IsReadonly { get; } |
|||
|
|||
public bool IsPinned { get => _isPinned; set => RaiseAndSetIfChanged(ref _isPinned, value); } |
|||
|
|||
public string FullName => $"{GetType().Name.Replace("PropertyViewModel","")}:{DeclaringType?.FullName}.{Name}"; |
|||
} |
|||
@ -1,51 +0,0 @@ |
|||
using Avalonia.Controls; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class PseudoClassViewModel : ViewModelBase |
|||
{ |
|||
private readonly IPseudoClasses _pseudoClasses; |
|||
private readonly StyledElement _source; |
|||
private bool _isActive; |
|||
private bool _isUpdating; |
|||
|
|||
public PseudoClassViewModel(string name, StyledElement source) |
|||
{ |
|||
Name = name; |
|||
_source = source; |
|||
_pseudoClasses = _source.Classes; |
|||
|
|||
Update(); |
|||
} |
|||
|
|||
public string Name { get; } |
|||
|
|||
public bool IsActive |
|||
{ |
|||
get => _isActive; |
|||
set |
|||
{ |
|||
RaiseAndSetIfChanged(ref _isActive, value); |
|||
|
|||
if (!_isUpdating) |
|||
{ |
|||
_pseudoClasses.Set(Name, value); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void Update() |
|||
{ |
|||
try |
|||
{ |
|||
_isUpdating = true; |
|||
|
|||
IsActive = _source.Classes.Contains(Name); |
|||
} |
|||
finally |
|||
{ |
|||
_isUpdating = false; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,57 +0,0 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Linq.Expressions; |
|||
using System.Reflection; |
|||
using Avalonia.Reactive; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal static class ReactiveExtensions |
|||
{ |
|||
public static IObservable<TValue> GetObservable<TOwner, TValue>( |
|||
this TOwner vm, |
|||
Expression<Func<TOwner, TValue>> property, |
|||
bool fireImmediately = true) |
|||
where TOwner : INotifyPropertyChanged |
|||
{ |
|||
return Observable.Create<TValue>(o => |
|||
{ |
|||
var propertyInfo = GetPropertyInfo(property); |
|||
|
|||
void Fire() |
|||
{ |
|||
o.OnNext((TValue) propertyInfo.GetValue(vm)!); |
|||
} |
|||
|
|||
void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) |
|||
{ |
|||
if (e.PropertyName == propertyInfo.Name) |
|||
{ |
|||
Fire(); |
|||
} |
|||
} |
|||
|
|||
if (fireImmediately) |
|||
{ |
|||
Fire(); |
|||
} |
|||
|
|||
vm.PropertyChanged += OnPropertyChanged; |
|||
|
|||
return Disposable.Create(() => vm.PropertyChanged -= OnPropertyChanged); |
|||
}); |
|||
} |
|||
|
|||
private static PropertyInfo GetPropertyInfo<TOwner, TValue>(this Expression<Func<TOwner, TValue>> property) |
|||
{ |
|||
if (property.Body is UnaryExpression unaryExpression) |
|||
{ |
|||
return (PropertyInfo)((MemberExpression)unaryExpression.Operand).Member; |
|||
} |
|||
|
|||
var memExpr = (MemberExpression)property.Body; |
|||
|
|||
return (PropertyInfo)memExpr.Member; |
|||
} |
|||
} |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class ResourceSetterViewModel : SetterViewModel |
|||
{ |
|||
public object Key { get; } |
|||
|
|||
public IBrush Tint { get; } |
|||
|
|||
public string ValueTypeTooltip { get; } |
|||
|
|||
public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object? resourceValue, bool isDynamic, IClipboard? clipboard) : base(property, resourceValue, clipboard) |
|||
{ |
|||
Key = resourceKey; |
|||
Tint = isDynamic ? Brushes.Orange : Brushes.Brown; |
|||
ValueTypeTooltip = isDynamic ? "Dynamic Resource" : "Static Resource"; |
|||
} |
|||
|
|||
public void CopyResourceKey() |
|||
{ |
|||
var textToCopy = Key?.ToString(); |
|||
|
|||
if (textToCopy is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
CopyToClipboard(textToCopy); |
|||
} |
|||
} |
|||
} |
|||
@ -1,63 +0,0 @@ |
|||
using Avalonia.Input.Platform; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class SetterViewModel : ViewModelBase |
|||
{ |
|||
private bool _isActive; |
|||
private bool _isVisible; |
|||
|
|||
public AvaloniaProperty Property { get; } |
|||
|
|||
public string Name { get; } |
|||
|
|||
public object? Value { get; } |
|||
|
|||
public bool IsActive |
|||
{ |
|||
get => _isActive; |
|||
set => RaiseAndSetIfChanged(ref _isActive, value); |
|||
} |
|||
|
|||
public bool IsVisible |
|||
{ |
|||
get => _isVisible; |
|||
set => RaiseAndSetIfChanged(ref _isVisible, value); |
|||
} |
|||
|
|||
private IClipboard? _clipboard; |
|||
|
|||
public SetterViewModel(AvaloniaProperty property, object? value, IClipboard? clipboard) |
|||
{ |
|||
Property = property; |
|||
Name = property.Name; |
|||
Value = value; |
|||
IsActive = true; |
|||
IsVisible = true; |
|||
|
|||
_clipboard = clipboard; |
|||
} |
|||
|
|||
public virtual void CopyValue() |
|||
{ |
|||
var textToCopy = Value?.ToString(); |
|||
|
|||
if (textToCopy is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
CopyToClipboard(textToCopy); |
|||
} |
|||
|
|||
public void CopyPropertyName() |
|||
{ |
|||
CopyToClipboard(Property.Name); |
|||
} |
|||
|
|||
protected void CopyToClipboard(string value) |
|||
{ |
|||
_clipboard?.SetTextAsync(value); |
|||
} |
|||
} |
|||
} |
|||
@ -1,96 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Specialized; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.LogicalTree; |
|||
using Avalonia.Media; |
|||
using Avalonia.Reactive; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal abstract class TreeNode : ViewModelBase, IDisposable |
|||
{ |
|||
private readonly IDisposable? _classesSubscription; |
|||
private string _classes; |
|||
private bool _isExpanded; |
|||
|
|||
protected TreeNode(AvaloniaObject avaloniaObject, TreeNode? parent, string? customTypeName = null) |
|||
{ |
|||
_classes = string.Empty; |
|||
Parent = parent; |
|||
Type = customTypeName ?? avaloniaObject.GetType().Name; |
|||
Visual = avaloniaObject; |
|||
FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal; |
|||
|
|||
ElementName = (avaloniaObject as INamed)?.Name; |
|||
|
|||
if (avaloniaObject is StyledElement { Classes: { } classes }) |
|||
{ |
|||
_classesSubscription = ((IObservable<object?>)classes.GetWeakCollectionChangedObservable()) |
|||
.StartWith(null) |
|||
.Subscribe(_ => |
|||
{ |
|||
if (classes.Count > 0) |
|||
{ |
|||
Classes = $"({string.Join(" ", classes)})"; |
|||
} |
|||
else |
|||
{ |
|||
Classes = string.Empty; |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private bool IsRoot => Visual is TopLevel || |
|||
Visual is ContextMenu || |
|||
Visual is IPopupHost; |
|||
|
|||
public FontWeight FontWeight { get; } |
|||
|
|||
public abstract TreeNodeCollection Children |
|||
{ |
|||
get; |
|||
} |
|||
|
|||
public string Classes |
|||
{ |
|||
get { return _classes; } |
|||
private set { RaiseAndSetIfChanged(ref _classes, value); } |
|||
} |
|||
|
|||
public string? ElementName |
|||
{ |
|||
get; |
|||
} |
|||
|
|||
public AvaloniaObject Visual |
|||
{ |
|||
get; |
|||
} |
|||
|
|||
public bool IsExpanded |
|||
{ |
|||
get { return _isExpanded; } |
|||
set { RaiseAndSetIfChanged(ref _isExpanded, value); } |
|||
} |
|||
|
|||
public TreeNode? Parent |
|||
{ |
|||
get; |
|||
} |
|||
|
|||
public string Type |
|||
{ |
|||
get; |
|||
private set; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_classesSubscription?.Dispose(); |
|||
Children.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,98 +0,0 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using System.ComponentModel; |
|||
|
|||
using Avalonia.Collections; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList<TreeNode>, IList, IDisposable |
|||
{ |
|||
private class EmptyTreeNodeCollection : TreeNodeCollection |
|||
{ |
|||
public EmptyTreeNodeCollection():base(default!) |
|||
{ |
|||
|
|||
} |
|||
protected override void Initialize(AvaloniaList<TreeNode> nodes) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
|
|||
static readonly internal TreeNodeCollection Empty = new EmptyTreeNodeCollection(); |
|||
|
|||
private AvaloniaList<TreeNode>? _inner; |
|||
|
|||
public TreeNodeCollection(TreeNode owner) => Owner = owner; |
|||
|
|||
public TreeNode this[int index] => EnsureInitialized()[index]; |
|||
|
|||
public int Count => EnsureInitialized().Count; |
|||
|
|||
protected TreeNode Owner { get; } |
|||
bool IList.IsFixedSize => false; |
|||
bool IList.IsReadOnly => true; |
|||
bool ICollection.IsSynchronized => false; |
|||
object ICollection.SyncRoot => this; |
|||
|
|||
object? IList.this[int index] |
|||
{ |
|||
get => this[index]; |
|||
set => throw new NotImplementedException(); |
|||
} |
|||
|
|||
public event NotifyCollectionChangedEventHandler? CollectionChanged |
|||
{ |
|||
add => EnsureInitialized().CollectionChanged += value; |
|||
remove => EnsureInitialized().CollectionChanged -= value; |
|||
} |
|||
|
|||
public event PropertyChangedEventHandler? PropertyChanged |
|||
{ |
|||
add => EnsureInitialized().PropertyChanged += value; |
|||
remove => EnsureInitialized().PropertyChanged -= value; |
|||
} |
|||
|
|||
public virtual void Dispose() |
|||
{ |
|||
if (_inner is object) |
|||
{ |
|||
foreach (var node in _inner) |
|||
{ |
|||
node.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public IEnumerator<TreeNode> GetEnumerator() |
|||
{ |
|||
return EnsureInitialized().GetEnumerator(); |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
|
|||
protected abstract void Initialize(AvaloniaList<TreeNode> nodes); |
|||
|
|||
private AvaloniaList<TreeNode> EnsureInitialized() |
|||
{ |
|||
if (_inner is null) |
|||
{ |
|||
_inner = new AvaloniaList<TreeNode>(); |
|||
Initialize(_inner); |
|||
} |
|||
return _inner; |
|||
} |
|||
|
|||
int IList.Add(object? value) => throw new NotImplementedException(); |
|||
void IList.Clear() => throw new NotImplementedException(); |
|||
bool IList.Contains(object? value) => EnsureInitialized().Contains((TreeNode)value!); |
|||
int IList.IndexOf(object? value) => EnsureInitialized().IndexOf((TreeNode)value!); |
|||
void IList.Insert(int index, object? value) => throw new NotImplementedException(); |
|||
void IList.Remove(object? value) => throw new NotImplementedException(); |
|||
void IList.RemoveAt(int index) => throw new NotImplementedException(); |
|||
void ICollection.CopyTo(Array array, int index) => throw new NotImplementedException(); |
|||
} |
|||
} |
|||
@ -1,249 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Controls; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class TreePageViewModel : ViewModelBase, IDisposable |
|||
{ |
|||
private TreeNode? _selectedNode; |
|||
private ControlDetailsViewModel? _details; |
|||
private readonly ISet<string> _pinnedProperties; |
|||
|
|||
public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes, ISet<string> pinnedProperties) |
|||
{ |
|||
MainView = mainView; |
|||
Nodes = nodes; |
|||
_pinnedProperties = pinnedProperties; |
|||
PropertiesFilter = new FilterViewModel(); |
|||
PropertiesFilter.RefreshFilter += (s, e) => Details?.PropertiesView?.Refresh(); |
|||
|
|||
SettersFilter = new FilterViewModel(); |
|||
SettersFilter.RefreshFilter += (s, e) => Details?.UpdateStyleFilters(); |
|||
} |
|||
|
|||
public event EventHandler<string>? ClipboardCopyRequested; |
|||
|
|||
public MainViewModel MainView { get; } |
|||
|
|||
public FilterViewModel PropertiesFilter { get; } |
|||
|
|||
public FilterViewModel SettersFilter { get; } |
|||
|
|||
public TreeNode[] Nodes { get; protected set; } |
|||
|
|||
public TreeNode? SelectedNode |
|||
{ |
|||
get => _selectedNode; |
|||
set |
|||
{ |
|||
if (RaiseAndSetIfChanged(ref _selectedNode, value)) |
|||
{ |
|||
Details = value != null ? |
|||
new ControlDetailsViewModel(this, value.Visual, _pinnedProperties) : |
|||
null; |
|||
Details?.UpdatePropertiesView(MainView.ShowImplementedInterfaces); |
|||
Details?.UpdateStyleFilters(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public ControlDetailsViewModel? Details |
|||
{ |
|||
get => _details; |
|||
private set |
|||
{ |
|||
var oldValue = _details; |
|||
|
|||
if (RaiseAndSetIfChanged(ref _details, value)) |
|||
{ |
|||
oldValue?.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
foreach (var node in Nodes) |
|||
{ |
|||
node.Dispose(); |
|||
} |
|||
|
|||
_details?.Dispose(); |
|||
} |
|||
|
|||
public TreeNode? FindNode(Control control) |
|||
{ |
|||
foreach (var node in Nodes) |
|||
{ |
|||
var result = FindNode(node, control); |
|||
|
|||
if (result != null) |
|||
{ |
|||
return result; |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public void SelectControl(Control control) |
|||
{ |
|||
var node = default(TreeNode); |
|||
Control? c = control; |
|||
|
|||
while (node == null && c != null) |
|||
{ |
|||
node = FindNode(c); |
|||
|
|||
if (node == null) |
|||
{ |
|||
c = c.GetVisualParent<Control>(); |
|||
} |
|||
} |
|||
|
|||
if (node != null) |
|||
{ |
|||
SelectedNode = node; |
|||
ExpandNode(node.Parent); |
|||
} |
|||
} |
|||
|
|||
public void CopySelector() |
|||
{ |
|||
var currentVisual = SelectedNode?.Visual as Visual; |
|||
if (currentVisual is not null) |
|||
{ |
|||
var selector = GetVisualSelector(currentVisual); |
|||
|
|||
ClipboardCopyRequested?.Invoke(this, selector); |
|||
} |
|||
} |
|||
|
|||
public void CopySelectorFromTemplateParent() |
|||
{ |
|||
var parts = new List<string>(); |
|||
|
|||
var currentVisual = SelectedNode?.Visual as Visual; |
|||
while (currentVisual is not null) |
|||
{ |
|||
parts.Add(GetVisualSelector(currentVisual)); |
|||
|
|||
currentVisual = currentVisual.TemplatedParent as Visual; |
|||
} |
|||
|
|||
if (parts.Any()) |
|||
{ |
|||
parts.Reverse(); |
|||
var selector = string.Join(" /template/ ", parts); |
|||
|
|||
ClipboardCopyRequested?.Invoke(this, selector); |
|||
} |
|||
} |
|||
|
|||
public void ExpandRecursively() |
|||
{ |
|||
if (SelectedNode is { } selectedNode) |
|||
{ |
|||
ExpandNode(selectedNode); |
|||
|
|||
var stack = new Stack<TreeNode>(); |
|||
stack.Push(selectedNode); |
|||
|
|||
while (stack.Count > 0) |
|||
{ |
|||
var item = stack.Pop(); |
|||
item.IsExpanded = true; |
|||
foreach (var child in item.Children) |
|||
{ |
|||
stack.Push(child); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void CollapseChildren() |
|||
{ |
|||
if (SelectedNode is { } selectedNode) |
|||
{ |
|||
var stack = new Stack<TreeNode>(); |
|||
stack.Push(selectedNode); |
|||
|
|||
while (stack.Count > 0) |
|||
{ |
|||
var item = stack.Pop(); |
|||
item.IsExpanded = false; |
|||
foreach (var child in item.Children) |
|||
{ |
|||
stack.Push(child); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void CaptureNodeScreenshot() |
|||
{ |
|||
MainView.Shot(null); |
|||
} |
|||
|
|||
public void BringIntoView() |
|||
{ |
|||
(SelectedNode?.Visual as Control)?.BringIntoView(); |
|||
} |
|||
|
|||
|
|||
public void Focus() |
|||
{ |
|||
(SelectedNode?.Visual as Control)?.Focus(); |
|||
} |
|||
|
|||
private static string GetVisualSelector(Visual visual) |
|||
{ |
|||
var name = string.IsNullOrEmpty(visual.Name) ? "" : $"#{visual.Name}"; |
|||
var classes = string.Concat(visual.Classes |
|||
.Where(c => !c.StartsWith(":")) |
|||
.Select(c => '.' + c)); |
|||
var pseudo = string.Concat(visual.Classes.Where(c => c[0] == ':').Select(c => c)); |
|||
var type = StyledElement.GetStyleKey(visual); |
|||
return $$"""{{{type.Assembly.FullName}}}{{type.Namespace}}|{{type.Name}}{{name}}{{classes}}{{pseudo}}"""; |
|||
} |
|||
|
|||
private void ExpandNode(TreeNode? node) |
|||
{ |
|||
if (node != null) |
|||
{ |
|||
node.IsExpanded = true; |
|||
ExpandNode(node.Parent); |
|||
} |
|||
} |
|||
|
|||
private TreeNode? FindNode(TreeNode node, Control control) |
|||
{ |
|||
if (node.Visual == control) |
|||
{ |
|||
return node; |
|||
} |
|||
else |
|||
{ |
|||
foreach (var child in node.Children) |
|||
{ |
|||
var result = FindNode(child, control); |
|||
|
|||
if (result != null) |
|||
{ |
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
internal void UpdatePropertiesView() |
|||
{ |
|||
Details?.UpdatePropertiesView(MainView?.ShowImplementedInterfaces ?? true); |
|||
} |
|||
} |
|||
} |
|||
@ -1,153 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Data; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Markup.Xaml.MarkupExtensions; |
|||
using Avalonia.Styling; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class ValueFrameViewModel : ViewModelBase |
|||
{ |
|||
private readonly IValueFrameDiagnostic _valueFrame; |
|||
private bool _isActive; |
|||
private bool _isVisible; |
|||
|
|||
public ValueFrameViewModel(StyledElement styledElement, IValueFrameDiagnostic valueFrame, IClipboard? clipboard) |
|||
{ |
|||
_valueFrame = valueFrame; |
|||
IsVisible = true; |
|||
|
|||
var source = SourceToString(_valueFrame.Source); |
|||
Description = (_valueFrame.Type, source) switch |
|||
{ |
|||
(IValueFrameDiagnostic.FrameType.Local, _) => "Local Values " + source, |
|||
(IValueFrameDiagnostic.FrameType.Template, _) => "Template " + source, |
|||
(IValueFrameDiagnostic.FrameType.Theme, _) => "Theme " + source, |
|||
(_, {Length:>0}) => source, |
|||
_ => _valueFrame.Priority.ToString() |
|||
}; |
|||
|
|||
Setters = new List<SetterViewModel>(); |
|||
|
|||
foreach (var (setterProperty, setterValue) in valueFrame.Values) |
|||
{ |
|||
var resourceInfo = GetResourceInfo(setterValue); |
|||
|
|||
SetterViewModel setterVm; |
|||
|
|||
if (resourceInfo.HasValue) |
|||
{ |
|||
var resourceKey = resourceInfo.Value.resourceKey; |
|||
var resourceValue = styledElement.FindResource(resourceKey); |
|||
|
|||
setterVm = new ResourceSetterViewModel(setterProperty, resourceKey, resourceValue, |
|||
resourceInfo.Value.isDynamic, clipboard); |
|||
} |
|||
else |
|||
{ |
|||
var isBinding = IsBinding(setterValue); |
|||
|
|||
if (isBinding) |
|||
{ |
|||
setterVm = new BindingSetterViewModel(setterProperty, setterValue, clipboard); |
|||
} |
|||
else |
|||
{ |
|||
setterVm = new SetterViewModel(setterProperty, setterValue, clipboard); |
|||
} |
|||
} |
|||
Setters.Add(setterVm); |
|||
} |
|||
|
|||
Update(); |
|||
} |
|||
|
|||
public bool IsActive |
|||
{ |
|||
get => _isActive; |
|||
set => RaiseAndSetIfChanged(ref _isActive, value); |
|||
} |
|||
|
|||
public bool IsVisible |
|||
{ |
|||
get => _isVisible; |
|||
set => RaiseAndSetIfChanged(ref _isVisible, value); |
|||
} |
|||
|
|||
public string? Description { get; } |
|||
|
|||
public List<SetterViewModel> Setters { get; } |
|||
|
|||
public void Update() |
|||
{ |
|||
IsActive = _valueFrame.IsActive; |
|||
} |
|||
|
|||
private static (object resourceKey, bool isDynamic)? GetResourceInfo(object? value) |
|||
{ |
|||
if (value is StaticResourceExtension staticResource |
|||
&& staticResource.ResourceKey != null) |
|||
{ |
|||
return (staticResource.ResourceKey, false); |
|||
} |
|||
else if (value is DynamicResourceExtension dynamicResource |
|||
&& dynamicResource.ResourceKey != null) |
|||
{ |
|||
return (dynamicResource.ResourceKey, true); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private static bool IsBinding(object? value) |
|||
{ |
|||
switch (value) |
|||
{ |
|||
case Binding: |
|||
case CompiledBindingExtension: |
|||
case TemplateBinding: |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private string? SourceToString(object? source) |
|||
{ |
|||
if (source is Style style) |
|||
{ |
|||
StyleBase? currentStyle = style; |
|||
var selectors = new Stack<string>(); |
|||
|
|||
while (currentStyle is not null) |
|||
{ |
|||
if (currentStyle is Style { Selector: { } selector }) |
|||
{ |
|||
selectors.Push(selector.ToString()); |
|||
} |
|||
if (currentStyle is ControlTheme theme) |
|||
{ |
|||
selectors.Push("Theme " + theme.TargetType?.Name); |
|||
} |
|||
|
|||
currentStyle = currentStyle.Parent as StyleBase; |
|||
} |
|||
|
|||
return string.Concat(selectors).Replace("^", ""); |
|||
} |
|||
else if (source is ControlTheme controlTheme) |
|||
{ |
|||
return controlTheme.TargetType?.Name; |
|||
} |
|||
else if (source is StyledElement styledElement) |
|||
{ |
|||
return styledElement.StyleKey?.Name; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -1,42 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class ViewModelBase : INotifyPropertyChanged |
|||
{ |
|||
private PropertyChangedEventHandler? _propertyChanged; |
|||
private List<string> events = new List<string>(); |
|||
|
|||
public event PropertyChangedEventHandler? PropertyChanged |
|||
{ |
|||
add { _propertyChanged += value; events.Add("added"); } |
|||
remove { _propertyChanged -= value; events.Add("removed"); } |
|||
} |
|||
|
|||
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) |
|||
{ |
|||
} |
|||
|
|||
protected bool RaiseAndSetIfChanged<T>([NotNullIfNotNull("value")] ref T field, T value, [CallerMemberName] string? propertyName = null) |
|||
{ |
|||
if (!EqualityComparer<T>.Default.Equals(field, value)) |
|||
{ |
|||
field = value; |
|||
RaisePropertyChanged(propertyName); |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null) |
|||
{ |
|||
var e = new PropertyChangedEventArgs(propertyName); |
|||
OnPropertyChanged(e); |
|||
_propertyChanged?.Invoke(this, e); |
|||
} |
|||
} |
|||
} |
|||
@ -1,238 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Diagnostics; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Styling; |
|||
using Avalonia.VisualTree; |
|||
using Avalonia.Reactive; |
|||
using Lifetimes = Avalonia.Controls.ApplicationLifetimes; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.Diagnostics.ViewModels |
|||
{ |
|||
internal class VisualTreeNode : TreeNode |
|||
{ |
|||
public VisualTreeNode(AvaloniaObject avaloniaObject, TreeNode? parent, string? customName = null) |
|||
: base(avaloniaObject, parent, customName) |
|||
{ |
|||
Children = avaloniaObject switch |
|||
{ |
|||
Visual visual => new VisualTreeNodeCollection(this, visual), |
|||
Controls.Application host => new ApplicationHostVisuals(this, host), |
|||
_ => TreeNodeCollection.Empty |
|||
}; |
|||
|
|||
if (Visual is StyledElement styleable) |
|||
IsInTemplate = styleable.TemplatedParent != null; |
|||
} |
|||
|
|||
public bool IsInTemplate { get; } |
|||
|
|||
public override TreeNodeCollection Children { get; } |
|||
|
|||
public static VisualTreeNode[] Create(object control) |
|||
{ |
|||
return control is AvaloniaObject visual ? |
|||
new[] { new VisualTreeNode(visual, null) } : |
|||
Array.Empty<VisualTreeNode>(); |
|||
} |
|||
|
|||
internal class VisualTreeNodeCollection : TreeNodeCollection |
|||
{ |
|||
private readonly Visual _control; |
|||
private readonly CompositeDisposable _subscriptions = new CompositeDisposable(2); |
|||
|
|||
public VisualTreeNodeCollection(TreeNode owner, Visual control) |
|||
: base(owner) |
|||
{ |
|||
_control = control; |
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
_subscriptions.Dispose(); |
|||
} |
|||
|
|||
private static IObservable<PopupRoot?>? GetHostedPopupRootObservable(Visual visual) |
|||
{ |
|||
static IObservable<PopupRoot?> GetPopupHostObservable( |
|||
IPopupHostProvider popupHostProvider, |
|||
string? providerName = null) |
|||
{ |
|||
return Observable.Create<IPopupHost?>(observer => |
|||
{ |
|||
void Handler(IPopupHost? args) => observer.OnNext(args); |
|||
popupHostProvider.PopupHostChanged += Handler; |
|||
return Disposable.Create(() => popupHostProvider.PopupHostChanged -= Handler); |
|||
}) |
|||
.StartWith(popupHostProvider.PopupHost) |
|||
.Select(popupHost => |
|||
{ |
|||
if (popupHost is Control control) |
|||
return new PopupRoot( |
|||
control, |
|||
providerName != null ? $"{providerName} ({control.GetType().Name})" : null); |
|||
|
|||
return (PopupRoot?)null; |
|||
}); |
|||
} |
|||
|
|||
return visual switch |
|||
{ |
|||
Popup p => GetPopupHostObservable(p), |
|||
Control c => Observable.CombineLatest( |
|||
new IObservable<object?>[] |
|||
{ |
|||
c.GetObservable(Control.ContextFlyoutProperty), |
|||
c.GetObservable(Control.ContextMenuProperty), |
|||
c.GetObservable(FlyoutBase.AttachedFlyoutProperty), |
|||
c.GetObservable(ToolTipDiagnostics.ToolTipProperty), |
|||
c.GetObservable(Button.FlyoutProperty) |
|||
}) |
|||
.Select( |
|||
items => |
|||
{ |
|||
var contextFlyout = items[0] as IPopupHostProvider; |
|||
var contextMenu = items[1] as ContextMenu; |
|||
var attachedFlyout = items[2] as IPopupHostProvider; |
|||
var toolTip = items[3] as IPopupHostProvider; |
|||
var buttonFlyout = items[4] as IPopupHostProvider; |
|||
|
|||
if (contextMenu != null) |
|||
//Note: ContextMenus are special since all the items are added as visual children.
|
|||
//So we don't need to go via Popup
|
|||
return Observable.Return<PopupRoot?>(new PopupRoot(contextMenu)); |
|||
|
|||
if (contextFlyout != null) |
|||
return GetPopupHostObservable(contextFlyout, "ContextFlyout"); |
|||
|
|||
if (attachedFlyout != null) |
|||
return GetPopupHostObservable(attachedFlyout, "AttachedFlyout"); |
|||
|
|||
if (toolTip != null) |
|||
return GetPopupHostObservable(toolTip, "ToolTip"); |
|||
|
|||
if (buttonFlyout != null) |
|||
return GetPopupHostObservable(buttonFlyout, "Flyout"); |
|||
|
|||
return Observable.Return<PopupRoot?>(null); |
|||
}) |
|||
.Switch(), |
|||
_ => null |
|||
}; |
|||
} |
|||
|
|||
protected override void Initialize(AvaloniaList<TreeNode> nodes) |
|||
{ |
|||
_subscriptions.Clear(); |
|||
|
|||
if (GetHostedPopupRootObservable(_control) is { } popupRootObservable) |
|||
{ |
|||
VisualTreeNode? childNode = null; |
|||
|
|||
_subscriptions.Add( |
|||
popupRootObservable |
|||
.Subscribe(popupRoot => |
|||
{ |
|||
if (popupRoot != null) |
|||
{ |
|||
childNode = new VisualTreeNode( |
|||
popupRoot.Value.Root, |
|||
Owner, |
|||
popupRoot.Value.CustomName); |
|||
|
|||
nodes.Add(childNode); |
|||
} |
|||
else if (childNode != null) |
|||
{ |
|||
nodes.Remove(childNode); |
|||
} |
|||
})); |
|||
} |
|||
|
|||
_subscriptions.Add( |
|||
_control.VisualChildren.ForEachItem( |
|||
(i, item) => nodes.Insert(i, new VisualTreeNode((AvaloniaObject)item, Owner)), |
|||
(i, item) => nodes.RemoveAt(i), |
|||
() => nodes.Clear())); |
|||
} |
|||
|
|||
private struct PopupRoot |
|||
{ |
|||
public PopupRoot(Control root, string? customName = null) |
|||
{ |
|||
Root = root; |
|||
CustomName = customName; |
|||
} |
|||
|
|||
public Control Root { get; } |
|||
public string? CustomName { get; } |
|||
} |
|||
} |
|||
|
|||
internal class ApplicationHostVisuals : TreeNodeCollection |
|||
{ |
|||
readonly Controls.Application _application; |
|||
CompositeDisposable _subscriptions = new CompositeDisposable(2); |
|||
public ApplicationHostVisuals(TreeNode owner, Controls.Application host) : |
|||
base(owner) |
|||
{ |
|||
_application = host; |
|||
} |
|||
|
|||
protected override void Initialize(AvaloniaList<TreeNode> nodes) |
|||
{ |
|||
if (_application.ApplicationLifetime is Lifetimes.ISingleViewApplicationLifetime single && |
|||
single.MainView is not null) |
|||
{ |
|||
nodes.Add(new VisualTreeNode(single.MainView, Owner)); |
|||
} |
|||
if (_application.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime classic) |
|||
{ |
|||
|
|||
for (int i = 0; i < classic.Windows.Count; i++) |
|||
{ |
|||
var window = classic.Windows[i]; |
|||
if (window is Views.MainWindow) |
|||
{ |
|||
continue; |
|||
} |
|||
nodes.Add(new VisualTreeNode(window, Owner)); |
|||
} |
|||
_subscriptions = new CompositeDisposable(2) |
|||
{ |
|||
Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (s,e)=> |
|||
{ |
|||
if (s is Views.MainWindow) |
|||
{ |
|||
return; |
|||
} |
|||
nodes.Add(new VisualTreeNode((AvaloniaObject)s!,Owner)); |
|||
}), |
|||
Window.WindowClosedEvent.AddClassHandler(typeof(Window), (s,e)=> |
|||
{ |
|||
if (s is Views.MainWindow) |
|||
{ |
|||
return; |
|||
} |
|||
var item = nodes.FirstOrDefault(node=>object.ReferenceEquals(node.Visual,s)); |
|||
if(!(item is null)) |
|||
{ |
|||
nodes.Remove(item); |
|||
} |
|||
}), |
|||
}; |
|||
|
|||
|
|||
} |
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
_subscriptions?.Dispose(); |
|||
base.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,321 +0,0 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:conv="using:Avalonia.Diagnostics.Converters" |
|||
xmlns:local="using:Avalonia.Diagnostics.Views" |
|||
xmlns:controls="using:Avalonia.Diagnostics.Controls" |
|||
xmlns:vm="using:Avalonia.Diagnostics.ViewModels" |
|||
xmlns:lb="using:Avalonia.Diagnostics.Behaviors" |
|||
xmlns:system="clr-namespace:System;assembly=System.Runtime" |
|||
x:Class="Avalonia.Diagnostics.Views.ControlDetailsView" |
|||
x:Name="Main" |
|||
x:DataType="vm:ControlDetailsViewModel"> |
|||
|
|||
<UserControl.Resources> |
|||
<ResourceDictionary> |
|||
<ResourceDictionary.MergedDictionaries> |
|||
<ResourceInclude Source="/Assets/Icons.axaml"/> |
|||
</ResourceDictionary.MergedDictionaries> |
|||
<conv:BoolToOpacityConverter x:Key="BoolToOpacity" Opacity="0.6"/> |
|||
<conv:GetTypeNameConverter x:Key="GetTypeName"/> |
|||
<conv:BoolToImageConverter x:Key="pc" |
|||
TrueImage="{StaticResource PinIcon}" |
|||
FalseImage="{StaticResource UnpinIcon}"/> |
|||
</ResourceDictionary> |
|||
</UserControl.Resources> |
|||
|
|||
<UserControl.Styles> |
|||
<Style Selector="TextBlock.Pinned"> |
|||
<Setter Property="FontWeight" Value="Bold"/> |
|||
</Style> |
|||
</UserControl.Styles> |
|||
<Grid> |
|||
<Grid.ColumnDefinitions> |
|||
<ColumnDefinition Width="*"/> |
|||
<ColumnDefinition Width="Auto"/> |
|||
<!-- |
|||
When selecting the Application node, we need this trick to hide Layout Visualizer |
|||
because when using the GridSplitter it sets the Width property of ColumnDefinition |
|||
(see https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Controls/GridSplitter.cs#L528) |
|||
and if we hide the contents of the column, the space is not reclaimed |
|||
(see discussion https://github.com/AvaloniaUI/Avalonia/discussions/6773). |
|||
--> |
|||
<ColumnDefinition Width="320" lb:ColumnDefinition.IsVisible="{Binding Layout, Converter={x:Static ObjectConverters.IsNotNull}}" /> |
|||
</Grid.ColumnDefinitions> |
|||
|
|||
<Grid Grid.Column="0" RowDefinitions="Auto,Auto,*"> |
|||
|
|||
<Grid ColumnDefinitions="Auto, *" RowDefinitions="Auto, Auto"> |
|||
<Button Grid.Column="0" Grid.RowSpan="2" Content="^" |
|||
IsEnabled="{Binding CanNavigateToParentProperty}" |
|||
Margin="0 0 4 0" |
|||
ToolTip.Tip="Navigate to parent property" |
|||
Command="{Binding NavigateToParentProperty}" /> |
|||
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding SelectedEntityName}" FontWeight="Bold" /> |
|||
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding SelectedEntityType}" FontStyle="Italic" /> |
|||
</Grid> |
|||
|
|||
<controls:FilterTextBox Grid.Row="1" |
|||
BorderThickness="0" |
|||
DataContext="{Binding TreePage.PropertiesFilter}" |
|||
Text="{Binding FilterString}" |
|||
Watermark="Filter properties" |
|||
UseCaseSensitiveFilter="{Binding UseCaseSensitiveFilter}" |
|||
UseWholeWordFilter="{Binding UseWholeWordFilter}" |
|||
UseRegexFilter="{Binding UseRegexFilter}"/> |
|||
|
|||
<DataGrid |
|||
x:Name="DataGrid" |
|||
ItemsSource="{Binding PropertiesView}" |
|||
Grid.Row="2" |
|||
BorderThickness="0" |
|||
RowBackground="Transparent" |
|||
SelectedItem="{Binding SelectedProperty, Mode=TwoWay}" |
|||
CanUserResizeColumns="true" |
|||
DoubleTapped="PropertiesGrid_OnDoubleTapped"> |
|||
<DataGrid.Columns> |
|||
<DataGridTemplateColumn Header="Property" IsReadOnly="True" x:DataType="vm:PropertyViewModel"> |
|||
<DataGridTemplateColumn.CellTemplate> |
|||
<DataTemplate DataType="vm:PropertyViewModel"> |
|||
<!-- |
|||
Do not remove the DockPanel background set to Transparent |
|||
because if Background is null HitTest on empty space returns false |
|||
--> |
|||
<DockPanel Background="Transparent"> |
|||
<DockPanel.Styles> |
|||
<Style Selector="DockPanel[IsPointerOver=False] > ToggleButton"> |
|||
<Setter Property="IsVisible" Value="False"/> |
|||
</Style> |
|||
</DockPanel.Styles> |
|||
<ToggleButton |
|||
IsChecked="{Binding IsPinned}" |
|||
Background="Transparent" |
|||
BorderBrush="Transparent" |
|||
DockPanel.Dock="Right" |
|||
Command="{Binding #Main.((vm:ControlDetailsViewModel)DataContext).TogglePinnedProperty}" |
|||
CommandParameter="{Binding}"> |
|||
<Image Source="{Binding IsPinned, Converter={StaticResource pc}}" |
|||
Width="12" |
|||
Height="12"/> |
|||
</ToggleButton> |
|||
<TextBlock Text="{Binding Name}" |
|||
Classes.Pinned="{Binding IsPinned}" |
|||
VerticalAlignment="Center" |
|||
/> |
|||
</DockPanel> |
|||
</DataTemplate> |
|||
</DataGridTemplateColumn.CellTemplate> |
|||
</DataGridTemplateColumn> |
|||
<DataGridTemplateColumn Header="Value" Width="100"> |
|||
<DataTemplate> |
|||
<local:PropertyValueEditorView /> |
|||
</DataTemplate> |
|||
</DataGridTemplateColumn> |
|||
<DataGridTextColumn Header="Type" Binding="{Binding Type}" |
|||
IsReadOnly="True" |
|||
IsVisible="{Binding !$parent[UserControl;2].((vm:MainViewModel)DataContext).ShowDetailsPropertyType}" |
|||
x:DataType="vm:PropertyViewModel" |
|||
/> |
|||
<DataGridTextColumn Header="Assigned Type" Binding="{Binding AssignedType, Converter={StaticResource GetTypeName}}" |
|||
IsReadOnly="True" |
|||
IsVisible="{Binding $parent[UserControl;2].((vm:MainViewModel)DataContext).ShowDetailsPropertyType}" |
|||
x:DataType="vm:PropertyViewModel" |
|||
/> |
|||
<DataGridTextColumn Header="Property Type" Binding="{Binding PropertyType, Converter={StaticResource GetTypeName}}" |
|||
IsReadOnly="True" |
|||
IsVisible="{Binding $parent[UserControl;2].((vm:MainViewModel)DataContext).ShowDetailsPropertyType}" |
|||
x:DataType="vm:PropertyViewModel" |
|||
/> |
|||
<DataGridTextColumn Header="Priority" Binding="{Binding Priority}" IsReadOnly="True" x:DataType="vm:PropertyViewModel" /> |
|||
</DataGrid.Columns> |
|||
|
|||
<DataGrid.Styles> |
|||
<Style Selector="DataGridRow TextBox"> |
|||
<Setter Property="SelectionBrush" Value="LightBlue" /> |
|||
</Style> |
|||
</DataGrid.Styles> |
|||
</DataGrid> |
|||
|
|||
</Grid> |
|||
|
|||
<GridSplitter Grid.Column="1"/> |
|||
|
|||
<Grid Grid.Column="2" RowDefinitions="*,Auto,*" |
|||
IsVisible="{Binding Layout, Converter={x:Static ObjectConverters.IsNotNull}}"> |
|||
|
|||
<Grid RowDefinitions="Auto,*" Grid.Row="0"> |
|||
<TextBlock FontWeight="Bold" Grid.Row="0" Text="Layout Visualizer" Margin="4" /> |
|||
<local:LayoutExplorerView Grid.Row="1" DataContext="{Binding Layout}" /> |
|||
</Grid> |
|||
|
|||
<GridSplitter Grid.Row="1" /> |
|||
|
|||
<Grid RowDefinitions="Auto,*,Auto" Grid.Row="2"> |
|||
|
|||
<Grid Grid.Row="0" Margin="4" RowDefinitions="Auto,Auto"> |
|||
|
|||
<Grid Grid.Row="0" Margin="2" ColumnDefinitions="Auto,*,Auto,Auto"> |
|||
<TextBlock FontWeight="Bold" Grid.Column="0" Text="{Binding FramesStatus}" VerticalAlignment="Center" /> |
|||
<CheckBox Margin="2,0,0,0" Grid.Column="2" Content="Show inactive" IsChecked="{Binding ShowInactiveFrames}" ToolTip.Tip="Show values that are currently inactive" /> |
|||
<ToggleButton Margin="2,0,0,0" Grid.Column="3" ToolTip.Tip="Snapshot current values (Alt+S/Alt+D to enable/disable within debugged window)" Content="Snapshot" IsChecked="{Binding SnapshotFrames}" /> |
|||
</Grid> |
|||
|
|||
<controls:FilterTextBox Grid.Row="1" Margin="2" Grid.Column="0" |
|||
DataContext="{Binding TreePage.SettersFilter}" |
|||
Text="{Binding FilterString}" |
|||
Watermark="Filter values" |
|||
UseCaseSensitiveFilter="{Binding UseCaseSensitiveFilter}" |
|||
UseWholeWordFilter="{Binding UseWholeWordFilter}" |
|||
UseRegexFilter="{Binding UseRegexFilter}" /> |
|||
</Grid> |
|||
|
|||
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled"> |
|||
<ItemsControl ItemsSource="{Binding AppliedFrames}" > |
|||
<ItemsControl.ItemTemplate> |
|||
<DataTemplate> |
|||
<Border BorderThickness="0,0,0,1" BorderBrush="#6C6C6C" Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}"> |
|||
<Border.IsVisible> |
|||
<MultiBinding Converter="{x:Static BoolConverters.And}"> |
|||
<MultiBinding Converter="{x:Static BoolConverters.Or}" > |
|||
<Binding Path="IsActive" /> |
|||
<!-- Rider marks it as an error, because it doesn't know about new binding rules yet. --> |
|||
<Binding Path="#Main.DataContext.ShowInactiveFrames" /> |
|||
</MultiBinding> |
|||
<Binding Path="IsVisible" /> |
|||
</MultiBinding> |
|||
</Border.IsVisible> |
|||
<Expander IsExpanded="True" Margin="0" Padding="8,0" ContentTransition="{x:Null}" > |
|||
<Expander.Header> |
|||
<TextBlock Grid.Row="0" Text="{Binding Description}" /> |
|||
</Expander.Header> |
|||
|
|||
<ItemsControl Margin="20,0,0,0" Grid.Row="1" ItemsSource="{Binding Setters}"> |
|||
|
|||
<ItemsControl.Styles> |
|||
<Style Selector="TextBlock.property-name"> |
|||
<Setter Property="FontWeight" Value="SemiBold" /> |
|||
<Setter Property="Background" Value="Transparent" /> |
|||
</Style> |
|||
|
|||
<Style Selector="TextBlock.property-name:pointerover"> |
|||
<Setter Property="TextDecorations" Value="Underline" /> |
|||
</Style> |
|||
|
|||
<Style Selector="Rectangle.property-inactive"> |
|||
<Setter Property="IsHitTestVisible" Value="False" /> |
|||
<Setter Property="Height" Value="1" /> |
|||
<Setter Property="Fill" Value="#6C6C6C" /> |
|||
<Setter Property="VerticalAlignment" Value="Center" /> |
|||
</Style> |
|||
|
|||
</ItemsControl.Styles> |
|||
|
|||
<ItemsControl.DataTemplates> |
|||
|
|||
<DataTemplate DataType="IBrush"> |
|||
<StackPanel Orientation="Horizontal" Spacing="2"> |
|||
<Border BorderThickness="1" BorderBrush="Black" Background="{Binding}" Width="8" Height="8"/> |
|||
<TextBlock Text="{Binding}" /> |
|||
</StackPanel> |
|||
</DataTemplate> |
|||
|
|||
<DataTemplate DataType="Color"> |
|||
<StackPanel Orientation="Horizontal" Spacing="2"> |
|||
<Border BorderThickness="1" BorderBrush="Black" Width="8" Height="8"> |
|||
<Border.Background> |
|||
<SolidColorBrush Color="{Binding}" /> |
|||
</Border.Background> |
|||
</Border> |
|||
<TextBlock Text="{Binding}" /> |
|||
</StackPanel> |
|||
</DataTemplate> |
|||
|
|||
<DataTemplate DataType="vm:BindingSetterViewModel"> |
|||
<Panel Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}" IsVisible="{Binding IsVisible}" HorizontalAlignment="Left"> |
|||
<Panel.ContextMenu> |
|||
<ContextMenu> |
|||
<MenuItem Header="Copy property name" Command="{Binding CopyPropertyName} "/> |
|||
<MenuItem Header="Copy value" Command="{Binding CopyValue} "/> |
|||
</ContextMenu> |
|||
</Panel.ContextMenu> |
|||
<StackPanel Orientation="Horizontal" Spacing="2"> |
|||
<TextBlock Classes="property-name" PointerPressed="PropertyNamePressed" Text="{Binding Name}" /> |
|||
<TextBlock Text=":" /> |
|||
<TextBlock>{</TextBlock> |
|||
<Rectangle Height="8" Width="8" VerticalAlignment="Center" Fill="{Binding Tint}" ToolTip.Tip="{Binding ValueTypeTooltip}"/> |
|||
<TextBlock Text="{Binding Path}" /> |
|||
<TextBlock>}</TextBlock> |
|||
</StackPanel> |
|||
<Rectangle Classes="property-inactive" IsVisible="{Binding !IsActive}" /> |
|||
</Panel> |
|||
</DataTemplate> |
|||
|
|||
<DataTemplate DataType="vm:ResourceSetterViewModel"> |
|||
<Panel Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}" IsVisible="{Binding IsVisible}" HorizontalAlignment="Left"> |
|||
<Panel.ContextMenu> |
|||
<ContextMenu> |
|||
<MenuItem Header="Copy property name" Command="{Binding CopyPropertyName} "/> |
|||
<MenuItem Header="Copy value" Command="{Binding CopyValue} "/> |
|||
<MenuItem Header="Copy resource key" Command="{Binding CopyResourceKey}" /> |
|||
</ContextMenu> |
|||
</Panel.ContextMenu> |
|||
<StackPanel Orientation="Horizontal" Spacing="2" HorizontalAlignment="Left"> |
|||
<TextBlock Classes="property-name" PointerPressed="PropertyNamePressed" Text="{Binding Name}" /> |
|||
<TextBlock Text=":" /> |
|||
<TextBlock Text="{Binding Value}" /> |
|||
<TextBlock>(</TextBlock> |
|||
<Ellipse Height="8" Width="8" VerticalAlignment="Center" Fill="{Binding Tint}" ToolTip.Tip="{Binding ValueTypeTooltip}"/> |
|||
<TextBlock FontStyle="Italic" Text="{Binding Key}" /> |
|||
<TextBlock>)</TextBlock> |
|||
</StackPanel> |
|||
<Rectangle Classes="property-inactive" IsVisible="{Binding !IsActive}" /> |
|||
</Panel> |
|||
</DataTemplate> |
|||
|
|||
<DataTemplate DataType="vm:SetterViewModel"> |
|||
<Panel Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}" IsVisible="{Binding IsVisible}" HorizontalAlignment="Left"> |
|||
<Panel.ContextMenu> |
|||
<ContextMenu> |
|||
<MenuItem Header="Copy property name" Command="{Binding CopyPropertyName} "/> |
|||
<MenuItem Header="Copy value" Command="{Binding CopyValue} "/> |
|||
</ContextMenu> |
|||
</Panel.ContextMenu> |
|||
<StackPanel Orientation="Horizontal" Spacing="2"> |
|||
<TextBlock Classes="property-name" PointerPressed="PropertyNamePressed" Text="{Binding Name}" /> |
|||
<TextBlock Text=":" /> |
|||
<TextBlock Text="{Binding Value}" /> |
|||
</StackPanel> |
|||
<Rectangle Classes="property-inactive" IsVisible="{Binding !IsActive}" /> |
|||
</Panel> |
|||
</DataTemplate> |
|||
|
|||
</ItemsControl.DataTemplates> |
|||
</ItemsControl> |
|||
|
|||
</Expander> |
|||
</Border> |
|||
</DataTemplate> |
|||
</ItemsControl.ItemTemplate> |
|||
</ItemsControl> |
|||
</ScrollViewer> |
|||
|
|||
<Expander Header="Pseudo Classes" Grid.Row="2"> |
|||
<ItemsControl ItemsSource="{Binding PseudoClasses}"> |
|||
<ItemsControl.ItemsPanel> |
|||
<ItemsPanelTemplate> |
|||
<WrapPanel /> |
|||
</ItemsPanelTemplate> |
|||
</ItemsControl.ItemsPanel> |
|||
<ItemsControl.ItemTemplate> |
|||
<DataTemplate> |
|||
<CheckBox Margin="2" Content="{Binding Name}" IsChecked="{Binding IsActive, Mode=TwoWay}" /> |
|||
</DataTemplate> |
|||
</ItemsControl.ItemTemplate> |
|||
</ItemsControl> |
|||
</Expander> |
|||
|
|||
</Grid> |
|||
|
|||
</Grid> |
|||
</Grid> |
|||
|
|||
</UserControl> |
|||
@ -1,53 +0,0 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Diagnostics.ViewModels; |
|||
using Avalonia.Input; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Avalonia.Diagnostics.Views |
|||
{ |
|||
internal class ControlDetailsView : UserControl |
|||
{ |
|||
private DataGrid _dataGrid; |
|||
|
|||
public ControlDetailsView() |
|||
{ |
|||
InitializeComponent(); |
|||
|
|||
_dataGrid = this.GetControl<DataGrid>("DataGrid"); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
private void PropertiesGrid_OnDoubleTapped(object sender, TappedEventArgs e) |
|||
{ |
|||
if (sender is DataGrid grid && grid.DataContext is ControlDetailsViewModel controlDetails) |
|||
{ |
|||
controlDetails.NavigateToSelectedProperty(); |
|||
} |
|||
|
|||
} |
|||
|
|||
public void PropertyNamePressed(object sender, PointerPressedEventArgs e) |
|||
{ |
|||
var mainVm = (ControlDetailsViewModel?) DataContext; |
|||
|
|||
if (mainVm is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (sender is Control control && control.DataContext is SetterViewModel setterVm) |
|||
{ |
|||
mainVm.SelectProperty(setterVm.Property); |
|||
|
|||
if (mainVm.SelectedProperty is not null) |
|||
{ |
|||
_dataGrid.ScrollIntoView(mainVm.SelectedProperty, null); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,153 +0,0 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:vm="using:Avalonia.Diagnostics.ViewModels" |
|||
xmlns:controls="using:Avalonia.Diagnostics.Controls" |
|||
xmlns:models="using:Avalonia.Diagnostics.Models" |
|||
x:Class="Avalonia.Diagnostics.Views.EventsPageView" |
|||
Margin="2" |
|||
x:DataType="vm:EventsPageViewModel"> |
|||
<UserControl.Styles> |
|||
<Style Selector="TextBlock.nav" > |
|||
<Setter Property="TextDecorations"> |
|||
<TextDecorationCollection> |
|||
<TextDecoration Location="Underline" Stroke="Black" StrokeThickness="1" StrokeDashArray="2,2"/> |
|||
</TextDecorationCollection> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<Style Selector="TextBlock.nav:pointerover" > |
|||
<Setter Property="Foreground" Value="{DynamicResource ThemeAccentBrush}" /> |
|||
<Setter Property="Cursor" Value="Help" /> |
|||
</Style> |
|||
|
|||
<Style Selector="ListBoxItem" > |
|||
<Setter Property="BorderThickness" Value="1" /> |
|||
</Style> |
|||
|
|||
<Style Selector="ListBoxItem:selected /template/ ContentPresenter" > |
|||
<Setter Property="BorderBrush" Value="Black" /> |
|||
</Style> |
|||
|
|||
<Style Selector="ListBoxItem.handled" > |
|||
<Setter Property="Background" Value="#34c940" /> |
|||
<Setter Property="Foreground" Value="Black" /> |
|||
</Style> |
|||
</UserControl.Styles> |
|||
|
|||
<Grid ColumnDefinitions="1.1*,4,3*"> |
|||
|
|||
<Grid Grid.Column="0" RowDefinitions="Auto,*,Auto"> |
|||
|
|||
<controls:FilterTextBox Grid.Row="0" Margin="0,0,0,2" |
|||
DataContext="{Binding EventsFilter}" |
|||
Text="{Binding FilterString}" |
|||
Watermark="Filter events" |
|||
UseCaseSensitiveFilter="{Binding UseCaseSensitiveFilter}" |
|||
UseWholeWordFilter="{Binding UseWholeWordFilter}" |
|||
UseRegexFilter="{Binding UseRegexFilter}" /> |
|||
|
|||
<TreeView Grid.Row="1" ItemsSource="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}" > |
|||
<TreeView.DataTemplates> |
|||
<TreeDataTemplate DataType="vm:EventTreeNodeBase" |
|||
ItemsSource="{Binding Children}"> |
|||
<CheckBox Content="{Binding Text}" IsChecked="{Binding IsEnabled, Mode=TwoWay}" /> |
|||
</TreeDataTemplate> |
|||
</TreeView.DataTemplates> |
|||
<TreeView.Styles> |
|||
<Style Selector="TreeViewItem" x:DataType="vm:EventTreeNodeBase"> |
|||
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" /> |
|||
<Setter Property="IsVisible" Value="{Binding IsVisible}" /> |
|||
</Style> |
|||
</TreeView.Styles> |
|||
</TreeView> |
|||
|
|||
<StackPanel Grid.Row="2" Margin="0,2" Orientation="Horizontal" Spacing="2"> |
|||
<Button Content="Disable all" Command="{Binding DisableAll}" /> |
|||
<Button Content="Enable default" Command="{Binding EnableDefault}" /> |
|||
</StackPanel> |
|||
|
|||
</Grid> |
|||
|
|||
<GridSplitter Width="4" Grid.Column="1" /> |
|||
|
|||
<Grid RowDefinitions="*,4,2*,Auto" Grid.Column="2"> |
|||
|
|||
<ListBox Name="EventsList" ItemsSource="{Binding RecordedEvents}" |
|||
SelectedItem="{Binding SelectedEvent, Mode=TwoWay}"> |
|||
<ListBox.Styles> |
|||
<Style Selector="ListBoxItem"> |
|||
<Setter Property="Classes.handled" Value="{Binding IsHandled, DataType=vm:FiredEvent}" /> |
|||
</Style> |
|||
</ListBox.Styles> |
|||
|
|||
<ListBox.ItemTemplate> |
|||
<DataTemplate> |
|||
<Grid ColumnDefinitions="Auto,Auto,Auto,*,Auto"> |
|||
|
|||
<TextBlock Grid.Column="0" Text="{Binding TriggerTime, StringFormat={}{0:HH:mm:ss.fff}}"/> |
|||
|
|||
<StackPanel Margin="10,0,0,0" Grid.Column="1" Spacing="2" Orientation="Horizontal" > |
|||
<TextBlock Tag="{Binding Event}" DoubleTapped="NavigateTo" Text="{Binding Event.Name}" FontWeight="Bold" Classes="nav" /> |
|||
<TextBlock Text="on" /> |
|||
<TextBlock Tag="{Binding Originator}" DoubleTapped="NavigateTo" Text="{Binding Originator.HandlerName}" Classes="nav" /> |
|||
</StackPanel> |
|||
|
|||
<StackPanel Margin="2,0,0,0" Grid.Column="2" Spacing="2" Orientation="Horizontal" IsVisible="{Binding IsHandled}" > |
|||
<TextBlock Text="::" /> |
|||
<TextBlock Text="Handled by" /> |
|||
<TextBlock Tag="{Binding HandledBy}" DoubleTapped="NavigateTo" Text="{Binding HandledBy.HandlerName}" Classes="nav" /> |
|||
</StackPanel> |
|||
|
|||
<StackPanel Grid.Column="4" Orientation="Horizontal" HorizontalAlignment="Right"> |
|||
<TextBlock Text="Routing (" /> |
|||
<TextBlock Text="{Binding Event.RoutingStrategies}"/> |
|||
<TextBlock Text=")"/> |
|||
</StackPanel> |
|||
|
|||
</Grid> |
|||
</DataTemplate> |
|||
</ListBox.ItemTemplate> |
|||
</ListBox> |
|||
|
|||
<GridSplitter Height="4" Grid.Row="1" /> |
|||
|
|||
<DockPanel Grid.Row="2" LastChildFill="True"> |
|||
<TextBlock DockPanel.Dock="Top" FontSize="16" Text="Event chain:" /> |
|||
|
|||
<ListBox ItemsSource="{Binding SelectedEvent.EventChain}"> |
|||
<ListBox.Styles> |
|||
<Style Selector="ListBoxItem"> |
|||
<Setter Property="Classes.handled" Value="{Binding Handled, DataType=models:EventChainLink}" /> |
|||
</Style> |
|||
</ListBox.Styles> |
|||
<ListBox.ItemTemplate> |
|||
<DataTemplate> |
|||
<StackPanel Orientation="Vertical" |
|||
Background="Transparent" |
|||
PointerEntered="ListBoxItem_PointerEntered" |
|||
PointerExited="ListBoxItem_PointerExited"> |
|||
|
|||
<Rectangle IsVisible="{Binding BeginsNewRoute}" StrokeDashArray="2,2" StrokeThickness="1" Stroke="Gray" /> |
|||
|
|||
<StackPanel Orientation="Horizontal" Spacing="2"> |
|||
<TextBlock Text="{Binding Route}" FontWeight="Bold" /> |
|||
<TextBlock Tag="{Binding}" |
|||
DoubleTapped="NavigateTo" |
|||
Text="{Binding HandlerName}" |
|||
Classes="nav" /> |
|||
</StackPanel> |
|||
|
|||
</StackPanel> |
|||
</DataTemplate> |
|||
</ListBox.ItemTemplate> |
|||
</ListBox> |
|||
|
|||
</DockPanel> |
|||
|
|||
<StackPanel Orientation="Horizontal" Grid.Row="3" Spacing="2" Margin="2"> |
|||
<Button Content="Clear" Command="{Binding Clear}" /> |
|||
</StackPanel> |
|||
|
|||
</Grid> |
|||
</Grid> |
|||
</UserControl> |
|||
@ -1,93 +0,0 @@ |
|||
using System; |
|||
using System.Collections.ObjectModel; |
|||
using System.Collections.Specialized; |
|||
using System.Linq; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Diagnostics.Models; |
|||
using Avalonia.Diagnostics.ViewModels; |
|||
using Avalonia.Input; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Diagnostics.Views |
|||
{ |
|||
internal class EventsPageView : UserControl |
|||
{ |
|||
private readonly ListBox _events; |
|||
private IDisposable? _adorner; |
|||
|
|||
public EventsPageView() |
|||
{ |
|||
InitializeComponent(); |
|||
_events = this.GetControl<ListBox>("EventsList"); |
|||
} |
|||
|
|||
public void NavigateTo(object sender, TappedEventArgs e) |
|||
{ |
|||
if (DataContext is EventsPageViewModel vm && sender is Control control) |
|||
{ |
|||
switch (control.Tag) |
|||
{ |
|||
case EventChainLink chainLink: |
|||
{ |
|||
vm.RequestTreeNavigateTo(chainLink); |
|||
break; |
|||
} |
|||
case RoutedEvent evt: |
|||
{ |
|||
vm.SelectEventByType(evt); |
|||
|
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected override void OnDataContextChanged(EventArgs e) |
|||
{ |
|||
base.OnDataContextChanged(e); |
|||
|
|||
if (DataContext is EventsPageViewModel vm) |
|||
{ |
|||
vm.RecordedEvents.CollectionChanged += OnRecordedEventsChanged; |
|||
} |
|||
} |
|||
|
|||
private void OnRecordedEventsChanged(object? sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
if (sender is ObservableCollection<FiredEvent> events) |
|||
{ |
|||
var evt = events.LastOrDefault(); |
|||
|
|||
if (evt is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
Dispatcher.UIThread.Post(() => _events.ScrollIntoView(evt)); |
|||
} |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
private void ListBoxItem_PointerEntered(object? sender, PointerEventArgs e) |
|||
{ |
|||
if (DataContext is EventsPageViewModel vm |
|||
&& sender is Control control |
|||
&& control.DataContext is EventChainLink chainLink |
|||
&& chainLink.Handler is Visual visual) |
|||
{ |
|||
_adorner = Controls.ControlHighlightAdorner.Add(visual, vm.MainView.ShouldVisualizeMarginPadding); |
|||
} |
|||
} |
|||
|
|||
private void ListBoxItem_PointerExited(object? sender, PointerEventArgs e) |
|||
{ |
|||
_adorner?.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,36 +0,0 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:vm="using:Avalonia.Diagnostics.ViewModels" |
|||
Padding="4" |
|||
x:Class="Avalonia.Diagnostics.Views.HotKeyPageView" |
|||
x:DataType="vm:HotKeyPageViewModel"> |
|||
<Grid RowDefinitions="auto,*" Grid.IsSharedSizeScope="True"> |
|||
<Grid> |
|||
<Grid.ColumnDefinitions> |
|||
<ColumnDefinition SharedSizeGroup="A" Width="auto" /> |
|||
<ColumnDefinition Width="8" /> |
|||
<ColumnDefinition SharedSizeGroup="B" Width="*" /> |
|||
</Grid.ColumnDefinitions> |
|||
|
|||
<TextBlock FontWeight="Bold" Text="Action" /> |
|||
<TextBlock Grid.Column="2" FontWeight="Bold" Text="Gesture" /> |
|||
</Grid> |
|||
|
|||
<ItemsControl Grid.Row="1" Grid.ColumnSpan="3" ItemsSource="{Binding HotKeyDescriptions}"> |
|||
<ItemsControl.ItemTemplate> |
|||
<DataTemplate> |
|||
<Grid> |
|||
<Grid.ColumnDefinitions> |
|||
<ColumnDefinition SharedSizeGroup="A" Width="auto" /> |
|||
<ColumnDefinition Width="8" /> |
|||
<ColumnDefinition SharedSizeGroup="B" Width="*" /> |
|||
</Grid.ColumnDefinitions> |
|||
|
|||
<TextBlock Text="{Binding BriefDescription}" ToolTip.Tip="{Binding DetailedDescription}" /> |
|||
<TextBlock Grid.Column="2" Text="{Binding Gesture}" /> |
|||
</Grid> |
|||
</DataTemplate> |
|||
</ItemsControl.ItemTemplate> |
|||
</ItemsControl> |
|||
</Grid> |
|||
</UserControl> |
|||
@ -1,18 +0,0 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace Avalonia.Diagnostics.Views |
|||
{ |
|||
internal class HotKeyPageView : UserControl |
|||
{ |
|||
public HotKeyPageView() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -1,213 +0,0 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="Avalonia.Diagnostics.Views.LayoutExplorerView" |
|||
xmlns:controls="using:Avalonia.Diagnostics.Controls" |
|||
xmlns:converters="using:Avalonia.Diagnostics.Converters" |
|||
xmlns:viewModels="using:Avalonia.Diagnostics.ViewModels" |
|||
x:DataType="viewModels:ControlLayoutViewModel"> |
|||
|
|||
<UserControl.Resources> |
|||
<SolidColorBrush x:Key="SizeGuidelineBrush" Color="#333333" /> |
|||
<SolidColorBrush x:Key="MarginBackgroundBrush" Color="#D78965" /> |
|||
<SolidColorBrush x:Key="MarginHighlightBrush" Color="#EA966F" /> |
|||
<SolidColorBrush x:Key="BorderBackgroundBrush" Color="#E3C381" /> |
|||
<SolidColorBrush x:Key="BorderHighlightBrush" Color="#EFCD88" /> |
|||
<SolidColorBrush x:Key="PaddingBackgroundBrush" Color="#B8C47F" /> |
|||
<SolidColorBrush x:Key="PaddingHighlightBrush" Color="#CEDA8E" /> |
|||
<SolidColorBrush x:Key="SizeBackgroundBrush" Color="#88B2BD" /> |
|||
<SolidColorBrush x:Key="SizeHighlightBrush" Color="#9ED0DC" /> |
|||
<SolidColorBrush x:Key="ThicknessEditorForeground" Color="Black" /> |
|||
</UserControl.Resources> |
|||
|
|||
<Grid RowDefinitions="*,Auto"> |
|||
|
|||
<Viewbox Grid.Row="0" StretchDirection="DownOnly"> |
|||
<Grid x:Name="LayoutRoot" Margin="8,0,8,8" RowDefinitions="Auto,Auto" |
|||
ColumnDefinitions="Auto,Auto"> |
|||
|
|||
<Grid.Styles> |
|||
<Style Selector="TextBlock.with-constraint"> |
|||
<Setter Property="TextDecorations" Value="Underline" /> |
|||
</Style> |
|||
|
|||
</Grid.Styles> |
|||
|
|||
<Border x:Name="VerticalSize" Grid.Row="0" Grid.Column="1"> |
|||
<TextBlock VerticalAlignment="Center" FontWeight="Bold" |
|||
Classes.with-constraint="{Binding HeightConstraint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" |
|||
Text="{Binding Height}" |
|||
ToolTip.Tip="{Binding HeightConstraint}" /> |
|||
</Border> |
|||
|
|||
<Border x:Name="HorizontalSize" Grid.Row="1" Grid.Column="0"> |
|||
<TextBlock HorizontalAlignment="Center" FontWeight="Bold" |
|||
Classes.with-constraint="{Binding WidthConstraint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" |
|||
Text="{Binding Width}" |
|||
ToolTip.Tip="{Binding WidthConstraint}" /> |
|||
</Border> |
|||
|
|||
<controls:ThicknessEditor Grid.Row="0" Grid.Column="0" Header="margin" VerticalAlignment="Top" |
|||
HorizontalAlignment="Center" |
|||
Foreground="{StaticResource ThicknessEditorForeground}" |
|||
Background="{StaticResource MarginBackgroundBrush}" |
|||
Highlight="{StaticResource MarginHighlightBrush}" |
|||
Thickness="{Binding MarginThickness}"> |
|||
<controls:ThicknessEditor x:Name="BorderArea" Header="border" |
|||
Background="{StaticResource BorderBackgroundBrush}" |
|||
Highlight="{StaticResource BorderHighlightBrush}" |
|||
Thickness="{Binding BorderThickness}" |
|||
IsPresent="{Binding HasBorder}"> |
|||
<controls:ThicknessEditor x:Name="PaddingArea" Header="padding" |
|||
Background="{StaticResource PaddingBackgroundBrush}" |
|||
Highlight="{StaticResource PaddingHighlightBrush}" |
|||
Thickness="{Binding PaddingThickness}" |
|||
IsPresent="{Binding HasPadding}"> |
|||
<Border x:Name="ContentArea" BorderThickness="1" MinWidth="100" MinHeight="16" |
|||
VerticalAlignment="Stretch" HorizontalAlignment="Stretch"> |
|||
<Border.Styles> |
|||
<Style Selector="Border"> |
|||
<Setter Property="Background" Value="{StaticResource SizeBackgroundBrush}" /> |
|||
<Setter Property="BorderBrush" Value="{DynamicResource ThicknessBorderBrush}" /> |
|||
</Style> |
|||
<Style Selector="Border:pointerover"> |
|||
<Setter Property="Background" Value="{StaticResource SizeHighlightBrush}" /> |
|||
<Setter Property="BorderBrush" Value="{DynamicResource HighlightBorderBrush}" /> |
|||
</Style> |
|||
</Border.Styles> |
|||
<TextBlock Margin="2" HorizontalAlignment="Center" VerticalAlignment="Center" |
|||
Text="content" /> |
|||
</Border> |
|||
</controls:ThicknessEditor> |
|||
</controls:ThicknessEditor> |
|||
</controls:ThicknessEditor> |
|||
|
|||
<Canvas Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2"> |
|||
<Canvas.Styles> |
|||
<Style Selector="Rectangle"> |
|||
<Setter Property="StrokeDashArray" Value="1,3" /> |
|||
<Setter Property="Stroke" Value="{StaticResource SizeGuidelineBrush}" /> |
|||
<Setter Property="StrokeThickness" Value="1" /> |
|||
<Setter Property="IsHitTestVisible" Value="False" /> |
|||
</Style> |
|||
</Canvas.Styles> |
|||
<Rectangle x:Name="HorizontalSizeBegin" /> |
|||
<Rectangle x:Name="HorizontalSizeEnd" /> |
|||
<Rectangle x:Name="VerticalSizeBegin" /> |
|||
<Rectangle x:Name="VerticalSizeEnd" /> |
|||
</Canvas> |
|||
</Grid> |
|||
</Viewbox> |
|||
|
|||
<StackPanel Orientation="Vertical" Grid.Column="0" Grid.Row="1" Spacing="4" |
|||
Margin="4" HorizontalAlignment="Stretch"> |
|||
|
|||
<StackPanel.Resources> |
|||
<converters:EnumToCheckedConverter x:Key="AlignmentConverter" /> |
|||
</StackPanel.Resources> |
|||
|
|||
<StackPanel.Styles> |
|||
<Style Selector="RadioButton"> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Border |
|||
Background="{TemplateBinding Background}" |
|||
BorderThickness="{TemplateBinding BorderThickness}" |
|||
BorderBrush="{TemplateBinding BorderBrush}" |
|||
Padding="{TemplateBinding Padding}"> |
|||
<Path x:Name="PART_Path" Fill="Black" Width="16" Height="16" Stretch="Uniform" /> |
|||
</Border> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<Style Selector="RadioButton"> |
|||
<Setter Property="Background" Value="WhiteSmoke" /> |
|||
<Setter Property="Padding" Value="2" /> |
|||
</Style> |
|||
|
|||
<Style Selector="RadioButton:pointerover"> |
|||
<Setter Property="Background" Value="{DynamicResource ThemeControlHighlightMidBrush}" /> |
|||
</Style> |
|||
|
|||
<Style Selector="RadioButton:checked"> |
|||
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}" /> |
|||
</Style> |
|||
|
|||
</StackPanel.Styles> |
|||
|
|||
<DockPanel HorizontalAlignment="Stretch" LastChildFill="True"> |
|||
|
|||
<DockPanel.Styles> |
|||
<Style Selector="RadioButton.h-left /template/ Path#PART_Path"> |
|||
<Setter Property="Data" Value="M22 13V19H6V13H22M6 5V11H16V5H6M2 2V22H4V2H2" /> |
|||
</Style> |
|||
<Style Selector="RadioButton.h-center /template/ Path#PART_Path"> |
|||
<Setter Property="Data" |
|||
Value="M20 19H13V22H11V19H4V13H11V11H7V5H11V2H13V5H17V11H13V13H20V19Z" /> |
|||
</Style> |
|||
<Style Selector="RadioButton.h-right /template/ Path#PART_Path"> |
|||
<Setter Property="Data" Value="M18 13V19H2V13H18M8 5V11H18V5H8M20 2V22H22V2H20Z" /> |
|||
</Style> |
|||
<Style Selector="RadioButton.h-stretch /template/ Path#PART_Path"> |
|||
<Setter Property="Data" Value="M4,22H2V2h2V22z M22,2h-2v20h2V2z M13.5,7h-3v10h3V7z" /> |
|||
</Style> |
|||
</DockPanel.Styles> |
|||
|
|||
<Label DockPanel.Dock="Left" Content="Horizontal Alignment" /> |
|||
|
|||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right"> |
|||
<RadioButton GroupName="HAlignment" Classes="h-left" |
|||
ToolTip.Tip="Left" |
|||
IsChecked="{Binding HorizontalAlignment, Converter={StaticResource AlignmentConverter}, ConverterParameter={x:Static HorizontalAlignment.Left}}" /> |
|||
<RadioButton GroupName="HAlignment" Classes="h-center" BorderThickness="0,1,1,1" |
|||
ToolTip.Tip="Center" |
|||
IsChecked="{Binding HorizontalAlignment, Converter={StaticResource AlignmentConverter}, ConverterParameter={x:Static HorizontalAlignment.Center}}" /> |
|||
<RadioButton GroupName="HAlignment" Classes="h-right" BorderThickness="0,1,1,1" |
|||
ToolTip.Tip="Right" |
|||
IsChecked="{Binding HorizontalAlignment, Converter={StaticResource AlignmentConverter}, ConverterParameter={x:Static HorizontalAlignment.Right}}" /> |
|||
<RadioButton GroupName="HAlignment" Classes="h-stretch" |
|||
ToolTip.Tip="Stretch" |
|||
IsChecked="{Binding HorizontalAlignment, Converter={StaticResource AlignmentConverter}, ConverterParameter={x:Static HorizontalAlignment.Stretch}}" /> |
|||
</StackPanel> |
|||
|
|||
</DockPanel> |
|||
|
|||
<DockPanel HorizontalAlignment="Stretch" LastChildFill="True"> |
|||
<DockPanel.Styles> |
|||
<Style Selector="RadioButton.v-top /template/ Path#PART_Path"> |
|||
<Setter Property="Data" Value="M11 22H5V6H11V22M19 6H13V16H19V6M22 2H2V4H22V2Z" /> |
|||
</Style> |
|||
<Style Selector="RadioButton.v-center /template/ Path#PART_Path"> |
|||
<Setter Property="Data" |
|||
Value="M5 20V13H2V11H5V4H11V11H13V7H19V11H22V13H19V17H13V13H11V20H5Z" /> |
|||
</Style> |
|||
<Style Selector="RadioButton.v-bottom /template/ Path#PART_Path"> |
|||
<Setter Property="Data" Value="M11 18H5V2H11V18M19 8H13V18H19V8M22 20H2V22H22V20Z" /> |
|||
</Style> |
|||
<Style Selector="RadioButton.v-stretch /template/ Path#PART_Path"> |
|||
<Setter Property="Data" Value="M22,2v2H2V2H22z M7,10.5v3h10v-3H7z M2,20v2h20v-2H2z" /> |
|||
</Style> |
|||
</DockPanel.Styles> |
|||
|
|||
<Label DockPanel.Dock="Left" Content="Vertical Alignment" /> |
|||
|
|||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right"> |
|||
<RadioButton GroupName="VAlignment" Classes="v-top" |
|||
ToolTip.Tip="Top" |
|||
IsChecked="{Binding VerticalAlignment, Converter={StaticResource AlignmentConverter}, ConverterParameter={x:Static VerticalAlignment.Top}}" /> |
|||
<RadioButton GroupName="VAlignment" Classes="v-center" BorderThickness="0,1,1,1" |
|||
ToolTip.Tip="Center" |
|||
IsChecked="{Binding VerticalAlignment, Converter={StaticResource AlignmentConverter}, ConverterParameter={x:Static VerticalAlignment.Center}}" /> |
|||
<RadioButton GroupName="VAlignment" Classes="v-bottom" BorderThickness="0,1,1,1" |
|||
ToolTip.Tip="Bottom" |
|||
IsChecked="{Binding VerticalAlignment, Converter={StaticResource AlignmentConverter}, ConverterParameter={x:Static VerticalAlignment.Bottom}}" /> |
|||
<RadioButton GroupName="VAlignment" Classes="v-stretch" |
|||
ToolTip.Tip="Stretch" |
|||
IsChecked="{Binding VerticalAlignment, Converter={StaticResource AlignmentConverter}, ConverterParameter={x:Static VerticalAlignment.Stretch}}" /> |
|||
</StackPanel> |
|||
|
|||
</DockPanel> |
|||
</StackPanel> |
|||
</Grid> |
|||
|
|||
</UserControl> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue