Explorar o código

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 <[email protected]>
workgroupengineering %!s(int64=3) %!d(string=hai) anos
pai
achega
c39cc1b083

+ 5 - 0
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,
+            });
         }
     }
 }

+ 0 - 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)

+ 1 - 4
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 - 0
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 - 0
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 - 0
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;
+    }
+}

+ 77 - 18
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 - 0
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 - 0
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;
+            }
+        }
+    }
+}

+ 14 - 13
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)

+ 85 - 5
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();
+            }
+        }
     }
 }

+ 52 - 14
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()

+ 5 - 4
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 - 0
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;

+ 75 - 5
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();
+            }
+        }
     }
 }

+ 18 - 4
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" />

+ 1 - 1
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"

+ 11 - 10
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;

+ 6 - 1
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);