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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Diagnostics/Views/EventsView.xaml.cs b/src/Avalonia.Diagnostics/Views/EventsView.xaml.cs
new file mode 100644
index 0000000000..a51cb4b7d9
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Views/EventsView.xaml.cs
@@ -0,0 +1,32 @@
+// 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.Linq;
+
+using Avalonia.Controls;
+using Avalonia.Diagnostics.ViewModels;
+using Avalonia.Markup.Xaml;
+
+namespace Avalonia.Diagnostics.Views
+{
+ public class EventsView : UserControl
+ {
+ private ListBox _events;
+
+ public EventsView()
+ {
+ this.InitializeComponent();
+ _events = this.FindControl("events");
+ }
+
+ private void RecordedEvents_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+ {
+ _events.ScrollIntoView(_events.Items.OfType().LastOrDefault());
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}