Browse Source

Merge pull request #6191 from pr8x/feature-devtools-inspect-popup

DevTools: Support for inspecting Popup visual tree
release/0.10.7
Dariusz Komosiński 5 years ago
committed by Dan Walmsley
parent
commit
c69ab4535a
  1. 17
      src/Avalonia.Controls/ContextMenu.cs
  2. 23
      src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs
  3. 15
      src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs
  4. 18
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  5. 16
      src/Avalonia.Controls/Primitives/Popup.cs
  6. 40
      src/Avalonia.Controls/ToolTip.cs
  7. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  8. 33
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  9. 123
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
  10. 56
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
  11. 165
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  12. 2
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
  13. 3
      src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs

17
src/Avalonia.Controls/ContextMenu.cs

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
@ -21,7 +21,7 @@ namespace Avalonia.Controls
/// <summary>
/// A control context menu.
/// </summary>
public class ContextMenu : MenuBase, ISetterValue
public class ContextMenu : MenuBase, ISetterValue, IPopupHostProvider
{
/// <summary>
/// Defines the <see cref="HorizontalOffset"/> property.
@ -82,6 +82,7 @@ namespace Avalonia.Controls
private Popup? _popup;
private List<Control>? _attachedControls;
private IInputElement? _previousFocus;
private Action<IPopupHost?>? _popupHostChangedHandler;
/// <summary>
/// Initializes a new instance of the <see cref="ContextMenu"/> class.
@ -304,6 +305,14 @@ namespace Avalonia.Controls
}
}
IPopupHost? IPopupHostProvider.PopupHost => _popup?.Host;
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value;
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
@ -364,6 +373,8 @@ namespace Avalonia.Controls
{
_previousFocus = FocusManager.Instance?.Current;
Focus();
_popupHostChangedHandler?.Invoke(_popup!.Host);
}
private void PopupClosing(object sender, CancelEventArgs e)
@ -397,6 +408,8 @@ namespace Avalonia.Controls
RoutedEvent = MenuClosedEvent,
Source = this,
});
_popupHostChangedHandler?.Invoke(null);
}
private void PopupKeyUp(object sender, KeyEventArgs e)

23
src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs

@ -0,0 +1,23 @@
using System;
using Avalonia.Controls.Primitives;
#nullable enable
namespace Avalonia.Controls.Diagnostics
{
/// <summary>
/// Diagnostics interface to retrieve an associated <see cref="IPopupHost"/>.
/// </summary>
public interface IPopupHostProvider
{
/// <summary>
/// The popup host.
/// </summary>
IPopupHost? PopupHost { get; }
/// <summary>
/// Raised when the popup host changes.
/// </summary>
event Action<IPopupHost?>? PopupHostChanged;
}
}

15
src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs

@ -0,0 +1,15 @@
#nullable enable
namespace Avalonia.Controls.Diagnostics
{
/// <summary>
/// Helper class to provide diagnostics information for <see cref="ToolTip"/>.
/// </summary>
public static class ToolTipDiagnostics
{
/// <summary>
/// Provides access to the internal <see cref="ToolTip.ToolTipProperty"/> for use in DevTools.
/// </summary>
public static AvaloniaProperty<ToolTip?> ToolTipProperty = ToolTip.ToolTipProperty;
}
}

18
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

@ -1,19 +1,18 @@
using System;
using System.ComponentModel;
using Avalonia.Controls.Diagnostics;
using System.Linq;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.Rendering;
#nullable enable
namespace Avalonia.Controls.Primitives
{
public abstract class FlyoutBase : AvaloniaObject
public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider
{
static FlyoutBase()
{
@ -59,6 +58,7 @@ namespace Avalonia.Controls.Primitives
private Rect? _enlargedPopupRect;
private PixelRect? _enlargePopupRectScreenPixelRect;
private IDisposable? _transientDisposable;
private Action<IPopupHost?>? _popupHostChangedHandler;
public FlyoutBase()
{
@ -103,6 +103,14 @@ namespace Avalonia.Controls.Primitives
private set => SetAndRaise(TargetProperty, ref _target, value);
}
IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host;
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value;
}
public event EventHandler? Closed;
public event EventHandler<CancelEventArgs>? Closing;
public event EventHandler? Opened;
@ -363,6 +371,8 @@ namespace Avalonia.Controls.Primitives
private void OnPopupOpened(object sender, EventArgs e)
{
IsOpen = true;
_popupHostChangedHandler?.Invoke(Popup!.Host);
}
private void OnPopupClosing(object sender, CancelEventArgs e)
@ -376,6 +386,8 @@ namespace Avalonia.Controls.Primitives
private void OnPopupClosed(object sender, EventArgs e)
{
HideCore(false);
_popupHostChangedHandler?.Invoke(null);
}
// This method is handling both popup logical tree and target logical tree.

16
src/Avalonia.Controls/Primitives/Popup.cs

@ -2,6 +2,7 @@ using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
@ -18,7 +19,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Displays a popup window.
/// </summary>
public class Popup : Control, IVisualTreeHost
public class Popup : Control, IVisualTreeHost, IPopupHostProvider
{
public static readonly StyledProperty<bool> WindowManagerAddShadowHintProperty =
AvaloniaProperty.Register<PopupRoot, bool>(nameof(WindowManagerAddShadowHint), true);
@ -134,6 +135,7 @@ namespace Avalonia.Controls.Primitives
private bool _ignoreIsOpenChanged;
private PopupOpenState? _openState;
private IInputElement _overlayInputPassThroughElement;
private Action<IPopupHost?>? _popupHostChangedHandler;
/// <summary>
/// Initializes static members of the <see cref="Popup"/> class.
@ -351,6 +353,14 @@ namespace Avalonia.Controls.Primitives
/// </summary>
IVisual? IVisualTreeHost.Root => _openState?.PopupHost.HostedVisualTreeRoot;
IPopupHost? IPopupHostProvider.PopupHost => Host;
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value;
}
/// <summary>
/// Opens the popup.
/// </summary>
@ -482,6 +492,8 @@ namespace Avalonia.Controls.Primitives
}
Opened?.Invoke(this, EventArgs.Empty);
_popupHostChangedHandler?.Invoke(Host);
}
/// <summary>
@ -591,6 +603,8 @@ namespace Avalonia.Controls.Primitives
_openState.Dispose();
_openState = null;
_popupHostChangedHandler?.Invoke(null);
using (BeginIgnoringIsOpen())
{
IsOpen = false;

40
src/Avalonia.Controls/ToolTip.cs

@ -1,9 +1,8 @@
#nullable enable
using System;
using System.Reactive.Linq;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@ -17,7 +16,7 @@ namespace Avalonia.Controls
/// assigning the content that you want displayed.
/// </remarks>
[PseudoClasses(":open")]
public class ToolTip : ContentControl
public class ToolTip : ContentControl, IPopupHostProvider
{
/// <summary>
/// Defines the ToolTip.Tip attached property.
@ -61,7 +60,8 @@ namespace Avalonia.Controls
internal static readonly AttachedProperty<ToolTip?> ToolTipProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip?>("ToolTip");
private IPopupHost? _popup;
private IPopupHost? _popupHost;
private Action<IPopupHost?>? _popupHostChangedHandler;
/// <summary>
/// Initializes static members of the <see cref="ToolTip"/> class.
@ -251,35 +251,45 @@ namespace Avalonia.Controls
tooltip.RecalculatePosition(control);
}
IPopupHost? IPopupHostProvider.PopupHost => _popupHost;
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value;
}
internal void RecalculatePosition(Control control)
{
_popup?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
_popupHost?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
}
private void Open(Control control)
{
Close();
_popup = OverlayPopupHost.CreatePopupHost(control, null);
_popup.SetChild(this);
((ISetLogicalParent)_popup).SetParent(control);
_popupHost = OverlayPopupHost.CreatePopupHost(control, null);
_popupHost.SetChild(this);
((ISetLogicalParent)_popupHost).SetParent(control);
_popup.ConfigurePosition(control, GetPlacement(control),
_popupHost.ConfigurePosition(control, GetPlacement(control),
new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
WindowManagerAddShadowHintChanged(_popup, false);
WindowManagerAddShadowHintChanged(_popupHost, false);
_popup.Show();
_popupHost.Show();
_popupHostChangedHandler?.Invoke(_popupHost);
}
private void Close()
{
if (_popup != null)
if (_popupHost != null)
{
_popup.SetChild(null);
_popup.Dispose();
_popup = null;
_popupHost.SetChild(null);
_popupHost.Dispose();
_popupHost = null;
_popupHostChangedHandler?.Invoke(null);
}
}

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

@ -1,6 +1,5 @@
using System;
using System.ComponentModel;
using Avalonia.Controls;
using Avalonia.Diagnostics.Models;
using Avalonia.Input;
@ -22,6 +21,7 @@ namespace Avalonia.Diagnostics.ViewModels
private bool _shouldVisualizeMarginPadding = true;
private bool _shouldVisualizeDirtyRects;
private bool _showFpsOverlay;
private bool _freezePopups;
#nullable disable
// Remove "nullable disable" after MemberNotNull will work on our CI.
@ -41,6 +41,12 @@ namespace Avalonia.Diagnostics.ViewModels
Console = new ConsoleViewModel(UpdateConsoleContext);
}
public bool FreezePopups
{
get => _freezePopups;
set => RaiseAndSetIfChanged(ref _freezePopups, value);
}
public bool ShouldVisualizeMarginPadding
{
get => _shouldVisualizeMarginPadding;

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

@ -1,26 +1,28 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Reactive;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
internal abstract class TreeNode : ViewModelBase, IDisposable
{
private IDisposable? _classesSubscription;
private readonly IDisposable? _classesSubscription;
private string _classes;
private bool _isExpanded;
public TreeNode(IVisual visual, TreeNode? parent)
protected TreeNode(IVisual visual, TreeNode? parent, string? customName = null)
{
_classes = string.Empty;
Parent = parent;
Type = visual.GetType().Name;
Type = customName ?? visual.GetType().Name;
Visual = visual;
_classes = string.Empty;
FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal;
if (visual is IControl control)
{
@ -52,6 +54,12 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
private bool IsRoot => Visual is TopLevel ||
Visual is ContextMenu ||
Visual is IPopupHost;
public FontWeight FontWeight { get; }
public abstract TreeNodeCollection Children
{
get;
@ -95,20 +103,5 @@ namespace Avalonia.Diagnostics.ViewModels
_classesSubscription?.Dispose();
Children.Dispose();
}
private static int IndexOf(IReadOnlyList<TreeNode> collection, TreeNode item)
{
var count = collection.Count;
for (var i = 0; i < count; ++i)
{
if (collection[i] == item)
{
return i;
}
}
throw new AvaloniaInternalException("TreeNode was not present in parent Children collection.");
}
}
}

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

@ -1,5 +1,10 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Primitives;
using Avalonia.Styling;
using Avalonia.VisualTree;
@ -7,31 +12,30 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class VisualTreeNode : TreeNode
{
public VisualTreeNode(IVisual visual, TreeNode? parent)
: base(visual, parent)
public VisualTreeNode(IVisual visual, TreeNode? parent, string? customName = null)
: base(visual, parent, customName)
{
Children = new VisualTreeNodeCollection(this, visual);
if ((Visual is IStyleable styleable))
{
if (Visual is IStyleable styleable)
IsInTemplate = styleable.TemplatedParent != null;
}
}
public bool IsInTemplate { get; private set; }
public bool IsInTemplate { get; }
public override TreeNodeCollection Children { get; }
public static VisualTreeNode[] Create(object control)
{
var visual = control as IVisual;
return visual != null ? new[] { new VisualTreeNode(visual, null) } : Array.Empty<VisualTreeNode>();
return control is IVisual visual ?
new[] { new VisualTreeNode(visual, null) } :
Array.Empty<VisualTreeNode>();
}
internal class VisualTreeNodeCollection : TreeNodeCollection
{
private readonly IVisual _control;
private IDisposable? _subscription;
private readonly CompositeDisposable _subscriptions = new CompositeDisposable(2);
public VisualTreeNodeCollection(TreeNode owner, IVisual control)
: base(owner)
@ -41,15 +45,106 @@ namespace Avalonia.Diagnostics.ViewModels
public override void Dispose()
{
_subscription?.Dispose();
_subscriptions.Dispose();
}
private static IObservable<PopupRoot?>? GetHostedPopupRootObservable(IVisual visual)
{
static IObservable<PopupRoot?> GetPopupHostObservable(
IPopupHostProvider popupHostProvider,
string? providerName = null)
{
return Observable.FromEvent<IPopupHost?>(
x => popupHostProvider.PopupHostChanged += x,
x => popupHostProvider.PopupHostChanged -= x)
.StartWith(popupHostProvider.PopupHost)
.Select(popupHost =>
{
if (popupHost is IControl 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(
c.GetObservable(Control.ContextFlyoutProperty),
c.GetObservable(Control.ContextMenuProperty),
c.GetObservable(FlyoutBase.AttachedFlyoutProperty),
c.GetObservable(ToolTipDiagnostics.ToolTipProperty),
(ContextFlyout, ContextMenu, AttachedFlyout, ToolTip) =>
{
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");
return Observable.Return<PopupRoot?>(null);
})
.Switch(),
_ => null
};
}
protected override void Initialize(AvaloniaList<TreeNode> nodes)
{
_subscription = _control.VisualChildren.ForEachItem(
(i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)),
(i, item) => nodes.RemoveAt(i),
() => nodes.Clear());
_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(item, Owner)),
(i, item) => nodes.RemoveAt(i),
() => nodes.Clear()));
}
private struct PopupRoot
{
public PopupRoot(IControl root, string? customName = null)
{
Root = root;
CustomName = customName;
}
public IControl Root { get; }
public string? CustomName { get; }
}
}
}

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

@ -5,14 +5,14 @@
<Grid Name="rootGrid" RowDefinitions="Auto,Auto,*,Auto,0,Auto">
<Menu>
<MenuItem Header="_File">
<MenuItem Header="E_xit" Command="{Binding $parent[Window].Close}"/>
<MenuItem Header="E_xit" Command="{Binding $parent[Window].Close}" />
</MenuItem>
<MenuItem Header="_View">
<MenuItem Header="_Console" Command="{Binding $parent[UserControl].ToggleConsole}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding Console.IsVisible}"
IsEnabled="False"/>
IsEnabled="False" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
@ -21,58 +21,68 @@
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShouldVisualizeMarginPadding}"
IsEnabled="False"/>
IsEnabled="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Visualize dirty rects" Command="{Binding ToggleVisualizeDirtyRects}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShouldVisualizeDirtyRects}"
IsEnabled="False"/>
IsEnabled="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Show fps overlay" Command="{Binding ToggleFpsOverlay}">
<MenuItem Header="Show fps overlay" Command="{Binding ToggleFpsOverlay}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShowFpsOverlay}"
IsEnabled="False"/>
IsEnabled="False" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</Menu>
<TabStrip Grid.Row="1" SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">
<TabStripItem Content="Logical Tree"/>
<TabStripItem Content="Visual Tree"/>
<TabStripItem Content="Events"/>
<TabStripItem Content="Logical Tree" />
<TabStripItem Content="Visual Tree" />
<TabStripItem Content="Events" />
</TabStrip>
<ContentControl Grid.Row="2"
BorderBrush="{DynamicResource ThemeControlMidBrush}"
BorderThickness="0,1,0,0"
Content="{Binding Content}"/>
Content="{Binding Content}" />
<GridSplitter Name="consoleSplitter" Grid.Row="3" Height="1"
Background="{DynamicResource ThemeControlMidBrush}"
IsVisible="False"/>
IsVisible="False" />
<views:ConsoleView Name="console"
Grid.Row="4"
DataContext="{Binding Console}"
IsVisible="{Binding IsVisible}"/>
IsVisible="{Binding IsVisible}" />
<Border Grid.Row="5"
BorderBrush="{DynamicResource ThemeControlMidBrush}"
BorderThickness="0,1,0,0">
<StackPanel Spacing="4" Orientation="Horizontal">
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<Separator Width="8"/>
<TextBlock>Focused:</TextBlock>
<TextBlock Text="{Binding FocusedControl}"/>
<Separator Width="8"/>
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}"/>
</StackPanel>
<Grid ColumnDefinitions="*, Auto">
<StackPanel Grid.Column="0" Spacing="4" Orientation="Horizontal">
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<Separator Width="8" />
<TextBlock>Focused:</TextBlock>
<TextBlock Text="{Binding FocusedControl}" />
<Separator Width="8" />
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}" />
</StackPanel>
<TextBlock Grid.Column="1"
Foreground="Gray"
Margin="2 0"
Text="Popups frozen"
IsVisible="{Binding FreezePopups}" />
</Grid>
</Border>
</Grid>
</UserControl>

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

@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Input.Raw;
@ -15,6 +18,7 @@ namespace Avalonia.Diagnostics.Views
internal class MainWindow : Window, IStyleHost
{
private readonly IDisposable _keySubscription;
private readonly Dictionary<Popup, IDisposable> _frozenPopupStates;
private TopLevel? _root;
public MainWindow()
@ -23,23 +27,26 @@ namespace Avalonia.Diagnostics.Views
_keySubscription = InputManager.Instance.Process
.OfType<RawKeyEventArgs>()
.Where(x => x.Type == RawKeyEventType.KeyDown)
.Subscribe(RawKeyDown);
_frozenPopupStates = new Dictionary<Popup, IDisposable>();
EventHandler? lh = default;
lh = (s, e) =>
{
this.Opened -= lh;
if ((DataContext as MainViewModel)?.StartupScreenIndex is int index)
{
var screens = this.Screens;
if (index > -1 && index < screens.ScreenCount)
{
var screen = screens.All[index];
this.Position = screen.Bounds.TopLeft;
this.WindowState = WindowState.Maximized;
}
}
};
{
this.Opened -= lh;
if ((DataContext as MainViewModel)?.StartupScreenIndex is { } index)
{
var screens = this.Screens;
if (index > -1 && index < screens.ScreenCount)
{
var screen = screens.All[index];
this.Position = screen.Bounds.TopLeft;
this.WindowState = WindowState.Maximized;
}
}
};
this.Opened += lh;
}
@ -77,6 +84,13 @@ namespace Avalonia.Diagnostics.Views
base.OnClosed(e);
_keySubscription.Dispose();
foreach (var state in _frozenPopupStates)
{
state.Value.Dispose();
}
_frozenPopupStates.Clear();
if (_root != null)
{
_root.Closed -= RootClosed;
@ -91,6 +105,53 @@ namespace Avalonia.Diagnostics.Views
AvaloniaXamlLoader.Load(this);
}
private IControl? GetHoveredControl(TopLevel topLevel)
{
#pragma warning disable CS0618 // Type or member is obsolete
var point = (topLevel as IInputRoot)?.MouseDevice?.GetPosition(topLevel) ?? default;
#pragma warning restore CS0618 // Type or member is obsolete
return (IControl?)topLevel.GetVisualsAt(point, x =>
{
if (x is AdornerLayer || !x.IsVisible)
{
return false;
}
return !(x is IInputElement ie) || ie.IsHitTestVisible;
})
.FirstOrDefault();
}
private static List<PopupRoot> GetPopupRoots(IVisual root)
{
var popupRoots = new List<PopupRoot>();
void ProcessProperty<T>(IControl control, AvaloniaProperty<T> property)
{
if (control.GetValue(property) is IPopupHostProvider popupProvider
&& popupProvider.PopupHost is PopupRoot popupRoot)
{
popupRoots.Add(popupRoot);
}
}
foreach (var control in root.GetVisualDescendants().OfType<IControl>())
{
if (control is Popup p && p.Host is PopupRoot popupRoot)
{
popupRoots.Add(popupRoot);
}
ProcessProperty(control, ContextFlyoutProperty);
ProcessProperty(control, ContextMenuProperty);
ProcessProperty(control, FlyoutBase.AttachedFlyoutProperty);
ProcessProperty(control, ToolTipDiagnostics.ToolTipProperty);
}
return popupRoots;
}
private void RawKeyDown(RawKeyEventArgs e)
{
var vm = (MainViewModel?)DataContext;
@ -99,34 +160,72 @@ namespace Avalonia.Diagnostics.Views
return;
}
const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift;
if (e.Modifiers == modifiers)
switch (e.Modifiers)
{
#pragma warning disable CS0618 // Type or member is obsolete
var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default;
#pragma warning restore CS0618 // Type or member is obsolete
case RawInputModifiers.Control | RawInputModifiers.Shift:
{
IControl? control = null;
foreach (var popupRoot in GetPopupRoots(Root))
{
control = GetHoveredControl(popupRoot);
if (control != null)
{
break;
}
}
control ??= GetHoveredControl(Root);
var control = Root.GetVisualsAt(point, x =>
if (control != null)
{
if (x is AdornerLayer || !x.IsVisible) return false;
if (!(x is IInputElement ie)) return true;
return ie.IsHitTestVisible;
})
.FirstOrDefault();
vm.SelectControl(control);
}
break;
}
if (control != null)
case RawInputModifiers.Control | RawInputModifiers.Alt when e.Key == Key.F:
{
vm.SelectControl((IControl)control);
vm.FreezePopups = !vm.FreezePopups;
foreach (var popupRoot in GetPopupRoots(Root))
{
if (popupRoot.Parent is Popup popup)
{
if (vm.FreezePopups)
{
var lightDismissEnabledState = popup.SetValue(
Popup.IsLightDismissEnabledProperty,
!vm.FreezePopups,
BindingPriority.Animation);
if (lightDismissEnabledState != null)
{
_frozenPopupStates[popup] = lightDismissEnabledState;
}
}
else
{
//TODO Use Dictionary.Remove(Key, out Value) in netstandard 2.1
if (_frozenPopupStates.ContainsKey(popup))
{
_frozenPopupStates[popup].Dispose();
_frozenPopupStates.Remove(popup);
}
}
}
}
break;
}
}
else if (e.Modifiers == RawInputModifiers.Alt)
{
if (e.Key == Key.S || e.Key == Key.D)
case RawInputModifiers.Alt when e.Key == Key.S || e.Key == Key.D:
{
var enable = e.Key == Key.S;
vm.EnableSnapshotStyles(e.Key == Key.S);
vm.EnableSnapshotStyles(enable);
break;
}
}
}

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

@ -11,7 +11,7 @@
<TreeDataTemplate DataType="vm:TreeNode"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{Binding Type}"/>
<TextBlock Text="{Binding Type}" FontWeight="{Binding FontWeight}"/>
<TextBlock Text="{Binding Classes}"/>
<TextBlock Foreground="Gray" Text="{Binding ElementName}"/>
</StackPanel>

3
src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs

@ -1,8 +1,11 @@
using System;
namespace Avalonia.VisualTree
{
/// <summary>
/// Interface for controls that host their own separate visual tree, such as popups.
/// </summary>
[Obsolete]
public interface IVisualTreeHost
{
/// <summary>

Loading…
Cancel
Save