diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index a2ff0ecf02..b807571a38 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -2,6 +2,9 @@ netstandard2.0 + + + @@ -17,4 +20,9 @@ + + + MSBuild:Compile + + \ No newline at end of file diff --git a/src/Avalonia.Diagnostics/DevTools.xaml b/src/Avalonia.Diagnostics/DevTools.xaml index 844670e794..a3b12f0ec5 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml +++ b/src/Avalonia.Diagnostics/DevTools.xaml @@ -3,6 +3,7 @@ + diff --git a/src/Avalonia.Diagnostics/DevTools.xaml.cs b/src/Avalonia.Diagnostics/DevTools.xaml.cs index be0863954a..d084ec3014 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml.cs +++ b/src/Avalonia.Diagnostics/DevTools.xaml.cs @@ -10,6 +10,7 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; +using Avalonia.Rendering; using Avalonia.VisualTree; namespace Avalonia @@ -28,6 +29,7 @@ namespace Avalonia.Diagnostics public class DevTools : UserControl { private static Dictionary s_open = new Dictionary(); + private static HashSet s_visualTreeRoots = new HashSet(); private IDisposable _keySubscription; public DevTools(IControl root) @@ -79,6 +81,7 @@ namespace Avalonia.Diagnostics devToolsWindow.Closed += devTools.DevToolsClosed; s_open.Add(control, devToolsWindow); + MarkAsDevTool(devToolsWindow); devToolsWindow.Show(); } } @@ -89,6 +92,7 @@ namespace Avalonia.Diagnostics var devToolsWindow = (Window)sender; var devTools = (DevTools)devToolsWindow.Content; s_open.Remove((TopLevel)devTools.Root); + RemoveDevTool(devToolsWindow); _keySubscription.Dispose(); devToolsWindow.Closed -= DevToolsClosed; } @@ -116,5 +120,24 @@ namespace Avalonia.Diagnostics } } } + + /// + /// Marks a visual as part of the DevTools, so it can be excluded from event tracking. + /// + /// The visual whose root is to be marked. + public static void MarkAsDevTool(IVisual visual) + { + s_visualTreeRoots.Add(visual.GetVisualRoot()); + } + + public static void RemoveDevTool(IVisual visual) + { + s_visualTreeRoots.Remove(visual.GetVisualRoot()); + } + + public static bool BelongsToDevTool(IVisual visual) + { + return s_visualTreeRoots.Contains(visual.GetVisualRoot()); + } } } diff --git a/src/Avalonia.Diagnostics/Models/EventChainLink.cs b/src/Avalonia.Diagnostics/Models/EventChainLink.cs new file mode 100644 index 0000000000..aab50a13dd --- /dev/null +++ b/src/Avalonia.Diagnostics/Models/EventChainLink.cs @@ -0,0 +1,38 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Interactivity; + +namespace Avalonia.Diagnostics.Models +{ + internal class EventChainLink + { + public EventChainLink(object handler, bool handled, RoutingStrategies route) + { + Contract.Requires(handler != null); + + this.Handler = handler; + this.Handled = handled; + this.Route = route; + } + + public object Handler { get; } + + public string HandlerName + { + get + { + if (Handler is INamed named && !string.IsNullOrEmpty(named.Name)) + { + return named.Name + " (" + Handler.GetType().Name + ")"; + } + return Handler.GetType().Name; + } + } + + public bool Handled { get; } + + public RoutingStrategies Route { get; } + } +} diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs index ce8ad36c17..c6d3f02e8b 100644 --- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs @@ -14,6 +14,7 @@ namespace Avalonia.Diagnostics.ViewModels private int _selectedTab; private TreePageViewModel _logicalTree; private TreePageViewModel _visualTree; + private EventsViewModel _eventsView; private string _focusedControl; private string _pointerOverElement; @@ -21,6 +22,7 @@ namespace Avalonia.Diagnostics.ViewModels { _logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root)); _visualTree = new TreePageViewModel(VisualTreeNode.Create(root)); + _eventsView = new EventsViewModel(root); UpdateFocusedControl(); KeyboardDevice.Instance.PropertyChanged += (s, e) => @@ -57,6 +59,9 @@ namespace Avalonia.Diagnostics.ViewModels case 1: Content = _visualTree; break; + case 2: + Content = _eventsView; + break; } RaisePropertyChanged(); diff --git a/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs new file mode 100644 index 0000000000..0674918400 --- /dev/null +++ b/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs @@ -0,0 +1,61 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class EventOwnerTreeNode : EventTreeNodeBase + { + private static readonly RoutedEvent[] s_defaultEvents = new RoutedEvent[] + { + Button.ClickEvent, + InputElement.KeyDownEvent, + InputElement.KeyUpEvent, + InputElement.TextInputEvent, + InputElement.PointerReleasedEvent, + InputElement.PointerPressedEvent, + }; + + public EventOwnerTreeNode(Type type, IEnumerable events, EventsViewModel vm) + : base(null, type.Name) + { + this.Children = new AvaloniaList(events.OrderBy(e => e.Name) + .Select(e => new EventTreeNode(this, e, vm) { IsEnabled = s_defaultEvents.Contains(e) })); + this.IsExpanded = true; + } + + public override bool? IsEnabled + { + get => base.IsEnabled; + set + { + if (base.IsEnabled != value) + { + base.IsEnabled = value; + if (_updateChildren && value != null) + { + foreach (var child in Children) + { + try + { + child._updateParent = false; + child.IsEnabled = value; + } + finally + { + child._updateParent = true; + } + } + } + } + } + } + } +} diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs new file mode 100644 index 0000000000..59c0f82414 --- /dev/null +++ b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs @@ -0,0 +1,98 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +using Avalonia.Diagnostics.Models; +using Avalonia.Interactivity; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class EventTreeNode : EventTreeNodeBase + { + private RoutedEvent _event; + private EventsViewModel _parentViewModel; + private bool _isRegistered; + private FiredEvent _currentEvent; + + public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsViewModel vm) + : base(parent, @event.Name) + { + Contract.Requires(@event != null); + Contract.Requires(vm != null); + + this._event = @event; + this._parentViewModel = vm; + } + + public override bool? IsEnabled + { + get => base.IsEnabled; + set + { + if (base.IsEnabled != value) + { + base.IsEnabled = value; + UpdateTracker(); + if (Parent != null && _updateParent) + { + try + { + Parent._updateChildren = false; + Parent.UpdateChecked(); + } + finally + { + Parent._updateChildren = true; + } + } + } + } + } + + private void UpdateTracker() + { + if (IsEnabled.GetValueOrDefault() && !_isRegistered) + { + _event.AddClassHandler(typeof(object), HandleEvent, (RoutingStrategies)7, handledEventsToo: true); + _isRegistered = true; + } + } + + private void HandleEvent(object sender, RoutedEventArgs e) + { + if (!_isRegistered || IsEnabled == false) + return; + if (sender is IVisual v && DevTools.BelongsToDevTool(v)) + return; + + var s = sender; + var handled = e.Handled; + var route = e.Route; + + Action handler = delegate + { + if (_currentEvent == null || !_currentEvent.IsPartOfSameEventChain(e)) + { + _currentEvent = new FiredEvent(e, new EventChainLink(s, handled, route)); + + _parentViewModel.RecordedEvents.Add(_currentEvent); + + while (_parentViewModel.RecordedEvents.Count > 100) + _parentViewModel.RecordedEvents.RemoveAt(0); + } + else + { + _currentEvent.AddToChain(new EventChainLink(s, handled, route)); + } + }; + + if (!Dispatcher.UIThread.CheckAccess()) + Dispatcher.UIThread.Post(handler); + else + handler(); + } + } +} diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs b/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs new file mode 100644 index 0000000000..146a8cea8e --- /dev/null +++ b/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs @@ -0,0 +1,78 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Collections; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal abstract class EventTreeNodeBase : ViewModelBase + { + internal bool _updateChildren = true; + internal bool _updateParent = true; + private bool _isExpanded; + private bool? _isEnabled = false; + + public EventTreeNodeBase(EventTreeNodeBase parent, string text) + { + this.Parent = parent; + this.Text = text; + } + + public IAvaloniaReadOnlyList Children + { + get; + protected set; + } + + public bool IsExpanded + { + get { return _isExpanded; } + set { RaiseAndSetIfChanged(ref _isExpanded, value); } + } + + public virtual bool? IsEnabled + { + get { return _isEnabled; } + set { RaiseAndSetIfChanged(ref _isEnabled, value); } + } + + public EventTreeNodeBase Parent + { + get; + } + + public string Text + { + get; + private set; + } + + internal void UpdateChecked() + { + IsEnabled = GetValue(); + + bool? GetValue() + { + if (Children == null) + return false; + bool? value = false; + for (int i = 0; i < Children.Count; i++) + { + if (i == 0) + { + value = Children[i].IsEnabled; + continue; + } + + if (value != Children[i].IsEnabled) + { + value = null; + break; + } + } + + return value; + } + } + } +} diff --git a/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs new file mode 100644 index 0000000000..a23677afc8 --- /dev/null +++ b/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs @@ -0,0 +1,60 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using System.Windows.Input; + +using Avalonia.Controls; +using Avalonia.Data.Converters; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class EventsViewModel : ViewModelBase + { + private readonly IControl _root; + private FiredEvent _selectedEvent; + + public EventsViewModel(IControl root) + { + this._root = root; + this.Nodes = RoutedEventRegistry.Instance.GetAllRegistered() + .GroupBy(e => e.OwnerType) + .OrderBy(e => e.Key.Name) + .Select(g => new EventOwnerTreeNode(g.Key, g, this)) + .ToArray(); + } + + public EventTreeNodeBase[] Nodes { get; } + + public ObservableCollection RecordedEvents { get; } = new ObservableCollection(); + + public FiredEvent SelectedEvent + { + get => _selectedEvent; + set => RaiseAndSetIfChanged(ref _selectedEvent, value); + } + + private void Clear() + { + RecordedEvents.Clear(); + } + } + + internal class BoolToBrushConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return (bool)value ? Brushes.LightGreen : Brushes.Transparent; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs new file mode 100644 index 0000000000..049280c390 --- /dev/null +++ b/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs @@ -0,0 +1,80 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.ObjectModel; + +using Avalonia.Diagnostics.Models; +using Avalonia.Interactivity; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class FiredEvent : ViewModelBase + { + private RoutedEventArgs _eventArgs; + private EventChainLink _handledBy; + + public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator) + { + Contract.Requires(eventArgs != null); + Contract.Requires(originator != null); + + this._eventArgs = eventArgs; + this.Originator = originator; + AddToChain(originator); + } + + public bool IsPartOfSameEventChain(RoutedEventArgs e) + { + return e == _eventArgs; + } + + public RoutedEvent Event => _eventArgs.RoutedEvent; + + public bool IsHandled => HandledBy?.Handled == true; + + public ObservableCollection EventChain { get; } = new ObservableCollection(); + + public string DisplayText + { + get + { + if (IsHandled) + { + return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine + + $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}"; + } + return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}"; + } + } + + public EventChainLink Originator { get; } + + public EventChainLink HandledBy + { + get { return _handledBy; } + set + { + if (_handledBy != value) + { + _handledBy = value; + RaisePropertyChanged(); + RaisePropertyChanged(nameof(IsHandled)); + RaisePropertyChanged(nameof(DisplayText)); + } + } + } + + public void AddToChain(object handler, bool handled, RoutingStrategies route) + { + AddToChain(new EventChainLink(handler, handled, route)); + } + + public void AddToChain(EventChainLink link) + { + EventChain.Add(link); + if (HandledBy == null && link.Handled) + HandledBy = link; + } + } +} diff --git a/src/Avalonia.Diagnostics/Views/EventsView.xaml b/src/Avalonia.Diagnostics/Views/EventsView.xaml new file mode 100644 index 0000000000..a5be86b613 --- /dev/null +++ b/src/Avalonia.Diagnostics/Views/EventsView.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +