Browse Source

feat(DevTools): Allow to attach DevTools at Application (#6771)

* feat(DevTools): Allow to attach DevTools at Application

* fixes(DevTools): PointerOverElement when attach to Application

* feat: Add AvaloniaVersion

* feat: IsDevelopmentBuild

* feat: Add some useful properties to Application Decorator

* fixes: Hide Layout Viewer when select appliction node

* fix: removed MachineId

* fixes: DesignerSupportTests fails

* fix(DevTools): Update XML Comment and nits

* fix(DevTools): Avoid interaction of Layout Visualizer with keyboard when it is hidden.

* Added some comment

* fixes: Code formatting

* fixes(DevTools): Strip unnecessary property from Application Decorator

* fixes(ControlCatalog): remove AttachDevTools from DecoratedWindow

* fixes(DevTools): Application doesn't close when the last window closed when DevTools is open

* fixes: Missing Application properties decoration

* fixes(DevTools): Nullable annotations

* fixes(DevTools): Unified the behavior of  AttachDevTools

* fixes(DevTools): typo

* fixes(DevTools): Null Annotation

Co-authored-by: Max Katz <maxkatz6@outlook.com>
pull/7270/head
workgroupengineering 5 years ago
committed by GitHub
parent
commit
c39cc1b083
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      samples/ControlCatalog/App.xaml.cs
  2. 1
      samples/ControlCatalog/DecoratedWindow.xaml.cs
  3. 5
      samples/ControlCatalog/MainWindow.xaml.cs
  4. 44
      src/Avalonia.Diagnostics/DevToolsExtensions.cs
  5. 47
      src/Avalonia.Diagnostics/Diagnostics/Behaviors/ColumnDefinition.cs
  6. 109
      src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs
  7. 95
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  8. 1
      src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
  9. 28
      src/Avalonia.Diagnostics/Diagnostics/KeyGestureExtesions.cs
  10. 27
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  11. 90
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
  12. 66
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  13. 9
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  14. 14
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs
  15. 80
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
  16. 22
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  17. 2
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
  18. 21
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  19. 7
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs

5
samples/ControlCatalog/App.xaml.cs

@ -94,6 +94,11 @@ namespace ControlCatalog
singleViewLifetime.MainView = new MainView();
base.OnFrameworkInitializationCompleted();
this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()
{
StartupScreenIndex = 1,
});
}
}
}

1
samples/ControlCatalog/DecoratedWindow.xaml.cs

@ -11,7 +11,6 @@ namespace ControlCatalog
public DecoratedWindow()
{
this.InitializeComponent();
this.AttachDevTools();
}
void SetupSide(string name, StandardCursorType cursor, WindowEdge edge)

5
samples/ControlCatalog/MainWindow.xaml.cs

@ -17,10 +17,7 @@ namespace ControlCatalog
public MainWindow()
{
this.InitializeComponent();
this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()
{
StartupScreenIndex = 1,
});
//Renderer.DrawFps = true;
//Renderer.DrawDirtyRects = Renderer.DrawFps = true;

44
src/Avalonia.Diagnostics/DevToolsExtensions.cs

@ -37,5 +37,49 @@ namespace Avalonia
{
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);
}
}
}

47
src/Avalonia.Diagnostics/Diagnostics/Behaviors/ColumnDefinition.cs

@ -0,0 +1,47 @@
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);
}
}
}

109
src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs

@ -0,0 +1,109 @@
using System;
using Avalonia.Controls;
using Lifetimes = Avalonia.Controls.ApplicationLifetimes;
using App = Avalonia.Application;
namespace Avalonia.Diagnostics.Controls
{
class Application : AvaloniaObject
, Input.ICloseable
{
private readonly App _application;
private static readonly Version s_version = typeof(IAvaloniaObject).Assembly?.GetName()?.Version
?? Version.Parse("0.0.00");
public event EventHandler? Closed;
public Application(App application)
{
_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;
}
}
internal App 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 focus manager.
/// </summary>
/// <value>
/// The application's focus manager.
/// </value>
public Input.IFocusManager? FocusManager =>
_application.FocusManager;
/// <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 clipboard.
/// </summary>
public Input.Platform.IClipboard? Clipboard =>
_application.Clipboard;
/// <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;
}
}

95
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@ -1,17 +1,21 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Diagnostics.Views;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
namespace Avalonia.Diagnostics
{
public static class DevTools
{
private static readonly Dictionary<TopLevel, MainWindow> s_open =
new Dictionary<TopLevel, MainWindow>();
private static readonly Dictionary<AvaloniaObject, MainWindow> s_open =
new Dictionary<AvaloniaObject, MainWindow>();
private static bool s_attachedToApplication;
public static IDisposable Attach(TopLevel root, KeyGesture gesture)
{
@ -23,6 +27,11 @@ namespace Avalonia.Diagnostics
public static IDisposable Attach(TopLevel root, DevToolsOptions options)
{
if (s_attachedToApplication == true)
{
throw new ArgumentException("DevTools already attached to application", nameof(root));
}
void PreviewKeyDown(object? sender, KeyEventArgs e)
{
if (options.Gesture.Matches(e))
@ -37,45 +46,95 @@ namespace Avalonia.Diagnostics
RoutingStrategies.Tunnel);
}
public static IDisposable Open(TopLevel root) => Open(root, new DevToolsOptions());
public static IDisposable Open(TopLevel root) =>
Open(Application.Current,new DevToolsOptions(),root as Window);
public static IDisposable Open(TopLevel root, DevToolsOptions options) =>
Open(Application.Current, options, root as Window);
public static IDisposable Open(TopLevel root, DevToolsOptions options)
private static void DevToolsClosed(object? sender, EventArgs e)
{
if (s_open.TryGetValue(root, out var window))
var window = (MainWindow)sender!;
window.Closed -= DevToolsClosed;
if (window.Root is Controls.Application host)
{
s_open.Remove(host.Instance);
}
else
{
s_open.Remove(window.Root!);
}
}
internal static IDisposable Attach(Application? application, DevToolsOptions options, Window? owner = null)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
var result = Disposable.Empty;
// Skip if call on Design Mode
if (!Avalonia.Controls.Design.IsDesignMode
&& !s_attachedToApplication)
{
var lifeTime = application.ApplicationLifetime
as Avalonia.Controls.ApplicationLifetimes.IControlledApplicationLifetime;
if (lifeTime is null)
{
throw new ArgumentNullException(nameof(Application.ApplicationLifetime));
}
if (application.InputManager is { })
{
s_attachedToApplication = true;
application.InputManager.PreProcess.OfType<RawKeyEventArgs>().Subscribe(e =>
{
if (options.Gesture.Matches(e))
{
result = Open(application, options, owner);
}
});
}
}
return result;
}
private static IDisposable Open(Application? application, DevToolsOptions options, Window? owner = default)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (s_open.TryGetValue(application, out var window))
{
window.Activate();
}
else
{
window = new MainWindow
{
Root = root,
Root = new Controls.Application(application),
Width = options.Size.Width,
Height = options.Size.Height,
};
window.SetOptions(options);
window.Closed += DevToolsClosed;
s_open.Add(root, window);
if (options.ShowAsChildWindow && root is Window inspectedWindow)
s_open.Add(application, window);
if (options.ShowAsChildWindow && owner is { })
{
window.Show(inspectedWindow);
window.Show(owner);
}
else
{
window.Show();
}
}
return Disposable.Create(() => window?.Close());
}
private static void DevToolsClosed(object? sender, EventArgs e)
{
var window = (MainWindow)sender!;
s_open.Remove(window.Root!);
window.Closed -= DevToolsClosed;
}
}
}

1
src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs

@ -16,6 +16,7 @@ namespace Avalonia.Diagnostics
/// 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>

28
src/Avalonia.Diagnostics/Diagnostics/KeyGestureExtesions.cs

@ -0,0 +1,28 @@
using Avalonia.Input;
using Avalonia.Input.Raw;
namespace Avalonia.Diagnostics
{
static class KeyGestureExtesions
{
public static bool Matches(this KeyGesture gesture, RawKeyEventArgs keyEvent) =>
keyEvent != null &&
(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;
}
}
}
}

27
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@ -16,7 +16,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class ControlDetailsViewModel : ViewModelBase, IDisposable
{
private readonly IVisual _control;
private readonly IAvaloniaObject _avaloniaObject;
private IDictionary<object, List<PropertyViewModel>>? _propertyIndex;
private PropertyViewModel? _selectedProperty;
private DataGridCollectionView? _propertiesView;
@ -28,20 +28,21 @@ namespace Avalonia.Diagnostics.ViewModels
private string? _selectedEntityName;
private string? _selectedEntityType;
public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
public ControlDetailsViewModel(TreePageViewModel treePage, IAvaloniaObject avaloniaObject)
{
_control = control;
_avaloniaObject = avaloniaObject;
TreePage = treePage;
Layout = new ControlLayoutViewModel(control);
TreePage = treePage;
Layout = avaloniaObject is IVisual
? new ControlLayoutViewModel((IVisual)avaloniaObject)
: default;
NavigateToProperty(control, (control as IControl)?.Name ?? control.ToString());
NavigateToProperty(_avaloniaObject, (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString());
AppliedStyles = new ObservableCollection<StyleViewModel>();
PseudoClasses = new ObservableCollection<PseudoClassViewModel>();
if (control is StyledElement styledElement)
if (avaloniaObject is StyledElement styledElement)
{
styledElement.Classes.CollectionChanged += OnClassesChanged;
@ -181,7 +182,7 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _styleStatus, value);
}
public ControlLayoutViewModel Layout { get; }
public ControlLayoutViewModel? Layout { get; }
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
@ -215,17 +216,17 @@ namespace Avalonia.Diagnostics.ViewModels
public void Dispose()
{
if (_control is INotifyPropertyChanged inpc)
if (_avaloniaObject is INotifyPropertyChanged inpc)
{
inpc.PropertyChanged -= ControlPropertyChanged;
}
if (_control is AvaloniaObject ao)
if (_avaloniaObject is AvaloniaObject ao)
{
ao.PropertyChanged -= ControlPropertyChanged;
}
if (_control is StyledElement se)
if (_avaloniaObject is StyledElement se)
{
se.Classes.CollectionChanged -= OnClassesChanged;
}
@ -278,7 +279,7 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
Layout.ControlPropertyChanged(sender, e);
Layout?.ControlPropertyChanged(sender, e);
}
private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e)

90
src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs

@ -1,23 +1,31 @@
using System;
using System.Reactive.Disposables;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Lifetimes = Avalonia.Controls.ApplicationLifetimes;
using System.Linq;
namespace Avalonia.Diagnostics.ViewModels
{
internal class LogicalTreeNode : TreeNode
{
public LogicalTreeNode(ILogical logical, TreeNode? parent)
: base((Control)logical, parent)
public LogicalTreeNode(IAvaloniaObject avaloniaObject, TreeNode? parent)
: base(avaloniaObject, parent)
{
Children = new LogicalTreeNodeCollection(this, logical);
Children = avaloniaObject switch
{
ILogical logical => new LogicalTreeNodeCollection(this, logical),
Controls.Application host => new ApplicationHostLogical(this, host),
_ => TreeNodeCollection.Empty
};
}
public override TreeNodeCollection Children { get; }
public static LogicalTreeNode[] Create(object control)
{
var logical = control as ILogical;
var logical = control as IAvaloniaObject;
return logical != null ? new[] { new LogicalTreeNode(logical, null) } : Array.Empty<LogicalTreeNode>();
}
@ -41,10 +49,82 @@ namespace Avalonia.Diagnostics.ViewModels
protected override void Initialize(AvaloniaList<TreeNode> nodes)
{
_subscription = _control.LogicalChildren.ForEachItem(
(i, item) => nodes.Insert(i, new LogicalTreeNode(item, Owner)),
(i, item) => nodes.Insert(i, new LogicalTreeNode((IAvaloniaObject)item, Owner)),
(i, item) => nodes.RemoveAt(i),
() => nodes.Clear());
}
}
internal class ApplicationHostLogical : TreeNodeCollection
{
readonly Controls.Application _application;
CompositeDisposable _subscriptions = new CompositeDisposable(2);
public ApplicationHostLogical(TreeNode owner, Controls.Application host) :
base(owner)
{
_application = host;
}
protected override void Initialize(AvaloniaList<TreeNode> nodes)
{
if (_application.ApplicationLifetime is Lifetimes.ISingleViewApplicationLifetime single)
{
nodes.Add(new LogicalTreeNode(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 LogicalTreeNode(window, Owner));
}
_subscriptions = new System.Reactive.Disposables.CompositeDisposable()
{
Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (s,e)=>
{
if (s is Views.MainWindow)
{
return;
}
nodes.Add(new LogicalTreeNode((IAvaloniaObject)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);
}
if(nodes.Count == 0)
{
if (Avalonia.Application.Current?.ApplicationLifetime is Lifetimes.IControlledApplicationLifetime controller)
{
controller.Shutdown();
}
else
{
Environment.Exit(0);
}
}
}),
};
}
}
public override void Dispose()
{
_subscriptions?.Dispose();
base.Dispose();
}
}
}
}

66
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -4,12 +4,14 @@ using Avalonia.Controls;
using Avalonia.Diagnostics.Models;
using Avalonia.Input;
using Avalonia.Threading;
using System.Reactive.Linq;
using System.Linq;
namespace Avalonia.Diagnostics.ViewModels
{
internal class MainViewModel : ViewModelBase, IDisposable
{
private readonly TopLevel _root;
private readonly AvaloniaObject _root;
private readonly TreePageViewModel _logicalTree;
private readonly TreePageViewModel _visualTree;
private readonly EventsPageViewModel _events;
@ -17,13 +19,14 @@ namespace Avalonia.Diagnostics.ViewModels
private ViewModelBase? _content;
private int _selectedTab;
private string? _focusedControl;
private string? _pointerOverElement;
private IInputElement? _pointerOverElement;
private bool _shouldVisualizeMarginPadding = true;
private bool _shouldVisualizeDirtyRects;
private bool _showFpsOverlay;
private bool _freezePopups;
public MainViewModel(TopLevel root)
private string? _pointerOverElementName;
private IInputRoot? _pointerOverRoot;
public MainViewModel(AvaloniaObject root)
{
_root = root;
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
@ -35,8 +38,24 @@ namespace Avalonia.Diagnostics.ViewModels
if (KeyboardDevice.Instance is not null)
KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged;
SelectedTab = 0;
_pointerOverSubscription = root.GetObservable(TopLevel.PointerOverElementProperty)
.Subscribe(x => PointerOverElement = x?.GetType().Name);
if (root is TopLevel topLevel)
{
_pointerOverSubscription = topLevel.GetObservable(TopLevel.PointerOverElementProperty)
.Subscribe(x => PointerOverElement = x);
}
else
{
#nullable disable
_pointerOverSubscription = InputManager.Instance.PreProcess
.OfType<Input.Raw.RawPointerEventArgs>()
.Subscribe(e =>
{
PointerOverRoot = e.Root;
PointerOverElement = e.Root.GetInputElementsAt(e.Position).FirstOrDefault();
});
#nullable restore
}
Console = new ConsoleViewModel(UpdateConsoleContext);
}
@ -51,13 +70,13 @@ namespace Avalonia.Diagnostics.ViewModels
get => _shouldVisualizeMarginPadding;
set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value);
}
public bool ShouldVisualizeDirtyRects
{
get => _shouldVisualizeDirtyRects;
set
{
_root.Renderer.DrawDirtyRects = value;
((TopLevel)_root).Renderer.DrawDirtyRects = value;
RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value);
}
}
@ -77,7 +96,7 @@ namespace Avalonia.Diagnostics.ViewModels
get => _showFpsOverlay;
set
{
_root.Renderer.DrawFps = value;
((TopLevel)_root).Renderer.DrawFps = value;
RaiseAndSetIfChanged(ref _showFpsOverlay, value);
}
}
@ -150,12 +169,28 @@ namespace Avalonia.Diagnostics.ViewModels
private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
}
public string? PointerOverElement
public IInputRoot? PointerOverRoot
{
get => _pointerOverRoot;
private set => RaiseAndSetIfChanged( ref _pointerOverRoot , value);
}
public IInputElement? PointerOverElement
{
get { return _pointerOverElement; }
private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); }
private set
{
RaiseAndSetIfChanged(ref _pointerOverElement, value);
PointerOverElementName = value?.GetType()?.Name;
}
}
public string? PointerOverElementName
{
get => _pointerOverElementName;
private set => RaiseAndSetIfChanged(ref _pointerOverElementName, value);
}
private void UpdateConsoleContext(ConsoleContext context)
{
context.root = _root;
@ -188,8 +223,11 @@ namespace Avalonia.Diagnostics.ViewModels
_pointerOverSubscription.Dispose();
_logicalTree.Dispose();
_visualTree.Dispose();
_root.Renderer.DrawDirtyRects = false;
_root.Renderer.DrawFps = false;
if (_root is TopLevel top)
{
top.Renderer.DrawDirtyRects = false;
top.Renderer.DrawFps = false;
}
}
private void UpdateFocusedControl()

9
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs

@ -16,12 +16,13 @@ namespace Avalonia.Diagnostics.ViewModels
private string _classes;
private bool _isExpanded;
protected TreeNode(IVisual visual, TreeNode? parent, string? customName = null)
protected TreeNode(IAvaloniaObject avaloniaObject, TreeNode? parent, string? customName = null)
{
_classes = string.Empty;
Parent = parent;
Type = customName ?? visual.GetType().Name;
Visual = visual;
var visual = avaloniaObject ;
Type = customName ?? avaloniaObject.GetType().Name;
Visual = visual!;
FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal;
if (visual is IControl control)
@ -76,7 +77,7 @@ namespace Avalonia.Diagnostics.ViewModels
get;
}
public IVisual Visual
public IAvaloniaObject Visual
{
get;
}

14
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs

@ -10,6 +10,20 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList<TreeNode>, 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;

80
src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs

@ -7,15 +7,22 @@ using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Primitives;
using Avalonia.Styling;
using Avalonia.VisualTree;
using Lifetimes = Avalonia.Controls.ApplicationLifetimes;
using System.Linq;
namespace Avalonia.Diagnostics.ViewModels
{
internal class VisualTreeNode : TreeNode
{
public VisualTreeNode(IVisual visual, TreeNode? parent, string? customName = null)
: base(visual, parent, customName)
public VisualTreeNode(IAvaloniaObject avaloniaObject, TreeNode? parent, string? customName = null)
: base(avaloniaObject, parent, customName)
{
Children = new VisualTreeNodeCollection(this, visual);
Children = avaloniaObject switch
{
IVisual visual => new VisualTreeNodeCollection(this, visual),
Controls.Application host => new ApplicationHostVisuals(this, host),
_ => TreeNodeCollection.Empty
};
if (Visual is IStyleable styleable)
IsInTemplate = styleable.TemplatedParent != null;
@ -27,7 +34,7 @@ namespace Avalonia.Diagnostics.ViewModels
public static VisualTreeNode[] Create(object control)
{
return control is IVisual visual ?
return control is IAvaloniaObject visual ?
new[] { new VisualTreeNode(visual, null) } :
Array.Empty<VisualTreeNode>();
}
@ -130,7 +137,7 @@ namespace Avalonia.Diagnostics.ViewModels
_subscriptions.Add(
_control.VisualChildren.ForEachItem(
(i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)),
(i, item) => nodes.Insert(i, new VisualTreeNode((IAvaloniaObject)item, Owner)),
(i, item) => nodes.RemoveAt(i),
() => nodes.Clear()));
}
@ -147,5 +154,68 @@ namespace Avalonia.Diagnostics.ViewModels
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)
{
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 System.Reactive.Disposables.CompositeDisposable()
{
Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (s,e)=>
{
if (s is Views.MainWindow)
{
return;
}
nodes.Add(new VisualTreeNode((IAvaloniaObject)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();
}
}
}
}

22
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@ -4,6 +4,7 @@
xmlns:local="clr-namespace:Avalonia.Diagnostics.Views"
xmlns:controls="clr-namespace:Avalonia.Diagnostics.Controls"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
xmlns:lb="using:Avalonia.Diagnostics.Behaviors"
x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"
x:Name="Main">
@ -11,8 +12,20 @@
<conv:BoolToOpacityConverter x:Key="BoolToOpacity" Opacity="0.6"/>
</UserControl.Resources>
<Grid ColumnDefinitions="*,Auto,320">
<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 Witdth 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">
@ -53,9 +66,10 @@
</Grid>
<GridSplitter Grid.Column="1" />
<GridSplitter Grid.Column="1"/>
<Grid Grid.Column="2" RowDefinitions="*,Auto,*" >
<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" />

2
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

@ -73,7 +73,7 @@
<TextBlock Text="{Binding FocusedControl}" />
<Separator Width="8" />
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}" />
<TextBlock Text="{Binding PointerOverElementName}" />
</StackPanel>
<TextBlock Grid.Column="1"

21
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs

@ -19,7 +19,7 @@ namespace Avalonia.Diagnostics.Views
{
private readonly IDisposable? _keySubscription;
private readonly Dictionary<Popup, IDisposable> _frozenPopupStates;
private TopLevel? _root;
private AvaloniaObject? _root;
public MainWindow()
{
@ -50,23 +50,23 @@ namespace Avalonia.Diagnostics.Views
this.Opened += lh;
}
public TopLevel? Root
public AvaloniaObject? Root
{
get => _root;
set
{
if (_root != value)
{
if (_root != null)
if (_root is ICloseable oldClosable)
{
_root.Closed -= RootClosed;
oldClosable.Closed -= RootClosed;
}
_root = value;
if (_root != null)
if (_root is ICloseable newClosable)
{
_root.Closed += RootClosed;
newClosable.Closed += RootClosed;
DataContext = new MainViewModel(_root);
}
else
@ -91,9 +91,9 @@ namespace Avalonia.Diagnostics.Views
_frozenPopupStates.Clear();
if (_root != null)
if (_root is ICloseable cloneable)
{
_root.Closed -= RootClosed;
cloneable.Closed -= RootClosed;
_root = null;
}
@ -123,7 +123,7 @@ namespace Avalonia.Diagnostics.Views
.FirstOrDefault();
}
private static List<PopupRoot> GetPopupRoots(IVisual root)
private static List<PopupRoot> GetPopupRoots(TopLevel root)
{
var popupRoots = new List<PopupRoot>();
@ -160,7 +160,8 @@ namespace Avalonia.Diagnostics.Views
return;
}
var root = Root;
var root = Root as TopLevel
?? vm.PointerOverRoot as TopLevel;
if (root is null)
{
return;

7
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs

@ -47,7 +47,12 @@ namespace Avalonia.Diagnostics.Views
return;
}
var visual = (Visual)node.Visual;
var visual = node.Visual as Visual;
if (visual is null)
{
return;
}
_currentLayer = AdornerLayer.GetAdornerLayer(visual);

Loading…
Cancel
Save