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/ChainLink.cs b/src/Avalonia.Diagnostics/Models/ChainLink.cs
new file mode 100644
index 0000000000..9ea99ff1ae
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Models/ChainLink.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Interactivity;
+
+namespace Avalonia.Diagnostics.Models
+{
+ internal class ChainLink
+ {
+ public object Handler { get; private set; }
+ 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; private set; }
+ public RoutingStrategies Route { get; private set; }
+
+ public ChainLink(object handler, bool handled, RoutingStrategies route)
+ {
+ Contract.Requires(handler != null);
+
+ this.Handler = handler;
+ this.Handled = handled;
+ this.Route = route;
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/ViewModels/ControlTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/ControlTreeNode.cs
new file mode 100644
index 0000000000..d8a6a3bdc3
--- /dev/null
+++ b/src/Avalonia.Diagnostics/ViewModels/ControlTreeNode.cs
@@ -0,0 +1,65 @@
+// 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 ControlTreeNode : EventTreeNode
+ {
+ public ControlTreeNode(Type type, IEnumerable events, EventsViewModel vm)
+ : base(null, type.Name)
+ {
+ this.Children = new AvaloniaList(events.OrderBy(e => e.Name).Select(e => new EventEntryTreeNode(this, e, vm) { IsEnabled = IsDefault(e) }));
+ this.IsExpanded = true;
+ }
+
+ RoutedEvent[] defaultEvents = new RoutedEvent[]
+ {
+ Button.ClickEvent,
+ InputElement.KeyDownEvent,
+ InputElement.KeyUpEvent,
+ InputElement.TextInputEvent,
+ InputElement.PointerReleasedEvent,
+ InputElement.PointerPressedEvent,
+ };
+
+ private bool IsDefault(RoutedEvent e)
+ {
+ return defaultEvents.Contains(e);
+ }
+
+ 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/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/EventEntryTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventEntryTreeNode.cs
new file mode 100644
index 0000000000..8eead869d5
--- /dev/null
+++ b/src/Avalonia.Diagnostics/ViewModels/EventEntryTreeNode.cs
@@ -0,0 +1,97 @@
+// 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 EventEntryTreeNode : EventTreeNode
+ {
+ RoutedEvent _event;
+ EventsViewModel _parentViewModel;
+ bool _isRegistered;
+ FiredEvent _currentEvent;
+
+ public EventEntryTreeNode(ControlTreeNode 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 ChainLink(s, handled, route));
+
+ _parentViewModel.RecordedEvents.Add(_currentEvent);
+
+ while (_parentViewModel.RecordedEvents.Count > 100)
+ _parentViewModel.RecordedEvents.RemoveAt(0);
+ }
+ else
+ {
+ _currentEvent.AddToChain(new ChainLink(s, handled, route));
+ }
+ };
+
+ if (!Dispatcher.UIThread.CheckAccess())
+ Dispatcher.UIThread.Post(handler);
+ else
+ handler();
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs
new file mode 100644
index 0000000000..50ea0c9c31
--- /dev/null
+++ b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.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.Diagnostics;
+using Avalonia.Collections;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+ internal abstract class EventTreeNode : ViewModelBase
+ {
+ internal bool _updateChildren = true;
+ internal bool _updateParent = true;
+ private bool _isExpanded;
+ private bool? _isEnabled = false;
+
+ public EventTreeNode(EventTreeNode 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 EventTreeNode 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..a9a6334689
--- /dev/null
+++ b/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs
@@ -0,0 +1,75 @@
+// 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.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Windows.Input;
+using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Data.Converters;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Threading;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+ internal class EventsViewModel : ViewModelBase
+ {
+ private IControl _root;
+ private FiredEvent _selectedEvent;
+ private ICommand ClearCommand { get; }
+
+ public EventsViewModel(IControl root)
+ {
+ this._root = root;
+ this.Nodes = RoutedEventRegistry.Instance.GetAllRegistered()
+ .GroupBy(e => e.OwnerType)
+ .OrderBy(e => e.Key.Name)
+ .Select(g => new ControlTreeNode(g.Key, g, this))
+ .ToArray();
+ }
+
+ private void ClearExecute()
+ {
+ Action action = delegate
+ {
+ RecordedEvents.Clear();
+ };
+ if (!Dispatcher.UIThread.CheckAccess())
+ {
+ Dispatcher.UIThread.Post(action);
+ }
+ else
+ {
+ action();
+ }
+ }
+
+ public EventTreeNode[] Nodes { get; }
+
+ public ObservableCollection RecordedEvents { get; } = new ObservableCollection();
+
+ public FiredEvent SelectedEvent
+ {
+ get => _selectedEvent;
+ set => RaiseAndSetIfChanged(ref _selectedEvent, value);
+ }
+ }
+
+ 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..523525b634
--- /dev/null
+++ b/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs
@@ -0,0 +1,93 @@
+// 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 ChainLink _handledBy;
+ private ChainLink _originator;
+
+ public FiredEvent(RoutedEventArgs eventArgs, ChainLink 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 ChainLink Originator
+ {
+ get { return _originator; }
+ set
+ {
+ if (_originator != value)
+ {
+ _originator = value;
+ RaisePropertyChanged();
+ RaisePropertyChanged(nameof(DisplayText));
+ }
+ }
+ }
+
+ public ChainLink 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 ChainLink(handler, handled, route));
+ }
+
+ public void AddToChain(ChainLink 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..79f78d2bba
--- /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..bc5f5cf65f
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Views/EventsView.xaml.cs
@@ -0,0 +1,28 @@
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Diagnostics.ViewModels;
+using Avalonia.Markup.Xaml;
+
+namespace Avalonia.Diagnostics.Views
+{
+ public class EventsView : UserControl
+ {
+ 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);
+ }
+ }
+}