Explorar el Código

Merge pull request #5866 from MarchingCube/devtools-events-ux

Improve UX of events view in dev tools
Dariusz Komosiński hace 4 años
padre
commit
47c097879c

+ 0 - 22
src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs

@@ -1,22 +0,0 @@
-using System;
-using System.Globalization;
-using Avalonia.Data.Converters;
-using Avalonia.Media;
-
-namespace Avalonia.Diagnostics.Converters
-{
-    internal class BoolToBrushConverter : IValueConverter
-    {
-        public IBrush Brush { get; set; }
-
-        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
-        {
-            return (bool)value ? Brush : Brushes.Transparent;
-        }
-
-        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
-        {
-            throw new NotImplementedException();
-        }
-    }
-}

+ 2 - 0
src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs

@@ -16,6 +16,8 @@ namespace Avalonia.Diagnostics.Models
 
         public object Handler { get; }
 
+        public bool BeginsNewRoute { get; set; }
+
         public string HandlerName
         {
             get

+ 1 - 9
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs

@@ -2,25 +2,17 @@
 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 =
-        {
-            Button.ClickEvent, InputElement.KeyDownEvent, InputElement.KeyUpEvent, InputElement.TextInputEvent,
-            InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent
-        };
-
         public EventOwnerTreeNode(Type type, IEnumerable<RoutedEvent> events, EventsPageViewModel vm)
             : base(null, type.Name)
         {
             Children = new AvaloniaList<EventTreeNodeBase>(events.OrderBy(e => e.Name)
-                .Select(e => new EventTreeNode(this, e, vm) { IsEnabled = s_defaultEvents.Contains(e) }));
+                .Select(e => new EventTreeNode(this, e, vm)));
             IsExpanded = true;
         }
 

+ 6 - 3
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs

@@ -9,7 +9,6 @@ namespace Avalonia.Diagnostics.ViewModels
 {
     internal class EventTreeNode : EventTreeNodeBase
     {
-        private readonly RoutedEvent _event;
         private readonly EventsPageViewModel _parentViewModel;
         private bool _isRegistered;
         private FiredEvent _currentEvent;
@@ -20,10 +19,12 @@ namespace Avalonia.Diagnostics.ViewModels
             Contract.Requires<ArgumentNullException>(@event != null);
             Contract.Requires<ArgumentNullException>(vm != null);
 
-            _event = @event;
+            Event = @event;
             _parentViewModel = vm;
         }
 
+        public RoutedEvent Event { get; }
+
         public override bool? IsEnabled
         {
             get => base.IsEnabled;
@@ -53,8 +54,10 @@ namespace Avalonia.Diagnostics.ViewModels
         {
             if (IsEnabled.GetValueOrDefault() && !_isRegistered)
             {
+                var allRoutes = RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble;
+
                 // FIXME: This leaks event handlers.
-                _event.AddClassHandler(typeof(object), HandleEvent, (RoutingStrategies)7, handledEventsToo: true);
+                Event.AddClassHandler(typeof(object), HandleEvent, allRoutes, handledEventsToo: true);
                 _isRegistered = true;
             }
         }

+ 8 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs

@@ -8,11 +8,13 @@ namespace Avalonia.Diagnostics.ViewModels
         internal bool _updateParent = true;
         private bool _isExpanded;
         private bool? _isEnabled = false;
+        private bool _isVisible;
 
         protected EventTreeNodeBase(EventTreeNodeBase parent, string text)
         {
             Parent = parent;
             Text = text;
+            IsVisible = true;
         }
 
         public IAvaloniaReadOnlyList<EventTreeNodeBase> Children
@@ -33,6 +35,12 @@ namespace Avalonia.Diagnostics.ViewModels
             set => RaiseAndSetIfChanged(ref _isEnabled, value);
         }
 
+        public bool IsVisible
+        {
+            get => _isVisible;
+            set => RaiseAndSetIfChanged(ref _isVisible, value);
+        }
+
         public EventTreeNodeBase Parent
         {
             get;

+ 153 - 7
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs

@@ -1,28 +1,43 @@
 using System;
+using System.Collections.Generic;
 using System.Collections.ObjectModel;
-using System.Globalization;
+using System.ComponentModel;
 using System.Linq;
 using Avalonia.Controls;
-using Avalonia.Data.Converters;
+using Avalonia.Diagnostics.Models;
+using Avalonia.Input;
 using Avalonia.Interactivity;
-using Avalonia.Media;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
     internal class EventsPageViewModel : ViewModelBase
     {
-        private readonly IControl _root;
+        private static readonly HashSet<RoutedEvent> s_defaultEvents = new HashSet<RoutedEvent>()
+        {
+            Button.ClickEvent,
+            InputElement.KeyDownEvent,
+            InputElement.KeyUpEvent,
+            InputElement.TextInputEvent,
+            InputElement.PointerReleasedEvent,
+            InputElement.PointerPressedEvent
+        };
+
+        private readonly MainViewModel _mainViewModel;
+        private string _eventTypeFilter;
         private FiredEvent _selectedEvent;
+        private EventTreeNodeBase _selectedNode;
 
-        public EventsPageViewModel(IControl root)
+        public EventsPageViewModel(MainViewModel mainViewModel)
         {
-            _root = root;
+            _mainViewModel = mainViewModel;
 
             Nodes = RoutedEventRegistry.Instance.GetAllRegistered()
                 .GroupBy(e => e.OwnerType)
                 .OrderBy(e => e.Key.Name)
                 .Select(g => new EventOwnerTreeNode(g.Key, g, this))
                 .ToArray();
+
+            EnableDefault();
         }
 
         public string Name => "Events";
@@ -37,9 +52,140 @@ namespace Avalonia.Diagnostics.ViewModels
             set => RaiseAndSetIfChanged(ref _selectedEvent, value);
         }
 
-        private void Clear()
+        public EventTreeNodeBase SelectedNode
+        {
+            get => _selectedNode;
+            set => RaiseAndSetIfChanged(ref _selectedNode, value);
+        }
+
+        public string EventTypeFilter
+        {
+            get => _eventTypeFilter;
+            set => RaiseAndSetIfChanged(ref _eventTypeFilter, value);
+        }
+
+        public void Clear()
         {
             RecordedEvents.Clear();
         }
+
+        public void DisableAll()
+        {
+            EvaluateNodeEnabled(_ => false);
+        }
+
+        public void EnableDefault()
+        {
+            EvaluateNodeEnabled(node => s_defaultEvents.Contains(node.Event));
+        }
+
+        public void RequestTreeNavigateTo(EventChainLink navTarget)
+        {
+            if (navTarget.Handler is IControl control)
+            {
+                _mainViewModel.RequestTreeNavigateTo(control, true);
+            }
+        }
+
+        public void SelectEventByType(RoutedEvent evt)
+        {
+            foreach (var node in Nodes)
+            {
+                var result = FindNode(node, evt);
+
+                if (result != null && result.IsVisible)
+                {
+                    SelectedNode = result;
+
+                    break;
+                }
+            }
+
+            static EventTreeNodeBase FindNode(EventTreeNodeBase node, RoutedEvent eventType)
+            {
+                if (node is EventTreeNode eventNode && eventNode.Event == eventType)
+                {
+                    return node;
+                }
+
+                if (node.Children != null)
+                {
+                    foreach (var child in node.Children)
+                    {
+                        var result = FindNode(child, eventType);
+
+                        if (result != null)
+                        {
+                            return result;
+                        }
+                    }
+                }
+
+                return null;
+            }
+        }
+
+        protected override void OnPropertyChanged(PropertyChangedEventArgs e)
+        {
+            base.OnPropertyChanged(e);
+
+            if (e.PropertyName == nameof(EventTypeFilter))
+            {
+                UpdateEventFilters();
+            }
+        }
+
+        private void EvaluateNodeEnabled(Func<EventTreeNode, bool> eval)
+        {
+            void ProcessNode(EventTreeNodeBase node)
+            {
+                if (node is EventTreeNode eventNode)
+                {
+                    node.IsEnabled = eval(eventNode);
+                }
+
+                if (node.Children != null)
+                {
+                    foreach (var childNode in node.Children)
+                    {
+                        ProcessNode(childNode);
+                    }
+                }
+            }
+
+            foreach (var node in Nodes)
+            {
+                ProcessNode(node);
+            }
+        }
+
+        private void UpdateEventFilters()
+        {
+            var filter = EventTypeFilter;
+            bool hasFilter = !string.IsNullOrEmpty(filter);
+
+            foreach (var node in Nodes)
+            {
+                FilterNode(node, false);
+            }
+
+            bool FilterNode(EventTreeNodeBase node, bool isParentVisible)
+            {
+                bool matchesFilter = !hasFilter || node.Text.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
+                bool hasVisibleChild = false;
+
+                if (node.Children != null)
+                {
+                    foreach (var childNode in node.Children)
+                    {
+                        hasVisibleChild |= FilterNode(childNode, matchesFilter);
+                    }
+                }
+
+                node.IsVisible = hasVisibleChild || matchesFilter || isParentVisible;
+
+                return node.IsVisible;
+            }
+        }
     }
 }

+ 10 - 5
src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs

@@ -62,13 +62,18 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
-        public void AddToChain(object handler, bool handled, RoutingStrategies route)
-        {
-            AddToChain(new EventChainLink(handler, handled, route));
-        }
-
         public void AddToChain(EventChainLink link)
         {
+            if (EventChain.Count > 0)
+            {
+                var prevLink = EventChain[EventChain.Count-1];
+
+                if (prevLink.Route != link.Route)
+                {
+                    link.BeginsNewRoute = true;
+                }
+            }
+
             EventChain.Add(link);
             if (HandledBy == null && link.Handled)
                 HandledBy = link;

+ 16 - 1
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@@ -4,6 +4,7 @@ using Avalonia.Controls;
 using Avalonia.Diagnostics.Models;
 using Avalonia.Input;
 using Avalonia.Threading;
+using Avalonia.VisualTree;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
@@ -27,7 +28,7 @@ namespace Avalonia.Diagnostics.ViewModels
             _root = root;
             _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
             _visualTree = new TreePageViewModel(this, VisualTreeNode.Create(root));
-            _events = new EventsPageViewModel(root);
+            _events = new EventsPageViewModel(this);
 
             UpdateFocusedControl();
             KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged;
@@ -193,5 +194,19 @@ namespace Avalonia.Diagnostics.ViewModels
                 UpdateFocusedControl();
             }
         }
+
+        public void RequestTreeNavigateTo(IControl control, bool isVisualTree)
+        {
+            var tree = isVisualTree ? _visualTree : _logicalTree;
+
+            var node = tree.FindNode(control);
+
+            if (node != null)
+            {
+                SelectedTab = isVisualTree ? 1 : 0;
+
+                tree.SelectControl(control);
+            }
+        }
     }
 }

+ 103 - 32
src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml

@@ -2,58 +2,129 @@
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
              xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
-             x:Class="Avalonia.Diagnostics.Views.EventsPageView">
-  <UserControl.Resources>
-    <conv:BoolToBrushConverter x:Key="boolToBrush" Brush="#d9ffdc"/>
-  </UserControl.Resources>
-  <Grid ColumnDefinitions="*,4,3*">
-    <TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}"
-              Grid.RowSpan="2">
-      <TreeView.DataTemplates>
-        <TreeDataTemplate DataType="vm:EventTreeNodeBase"
-                          ItemsSource="{Binding Children}">
-          <CheckBox Content="{Binding Text}" IsChecked="{Binding IsEnabled, Mode=TwoWay}" />
-        </TreeDataTemplate>
-      </TreeView.DataTemplates>
-      <TreeView.Styles>
-        <Style Selector="TreeViewItem">
-          <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
-        </Style>
-      </TreeView.Styles>
-    </TreeView>
+             x:Class="Avalonia.Diagnostics.Views.EventsPageView"
+             Margin="2">
+  <UserControl.Styles>
+    <Style Selector="TextBlock.nav" >
+      <Setter Property="TextDecorations">
+        <TextDecorationCollection>
+          <TextDecoration Location="Underline" Stroke="Black" StrokeThickness="1" StrokeDashArray="2,2"/>
+        </TextDecorationCollection>
+      </Setter>
+    </Style>
+
+    <Style Selector="TextBlock.nav:pointerover" >
+      <Setter Property="Foreground" Value="{DynamicResource ThemeAccentBrush}" />
+      <Setter Property="Cursor" Value="Help" />
+    </Style>
+
+    <Style Selector="ListBoxItem" >
+      <Setter Property="BorderThickness" Value="1" />
+    </Style>
+
+    <Style Selector="ListBoxItem:selected /template/ ContentPresenter" >
+      <Setter Property="BorderBrush" Value="Black" />
+    </Style>
+
+    <Style Selector="ListBoxItem.handled" >
+      <Setter Property="Background" Value="#d9ffdc" />
+    </Style>
+  </UserControl.Styles>
+
+  <Grid ColumnDefinitions="1.1*,4,3*">
+
+    <Grid Grid.Column="0" RowDefinitions="Auto,*,Auto">
+
+      <TextBox Classes="clearButton" Grid.Row="0" Margin="0,0,0,2" Text="{Binding EventTypeFilter}" Watermark="Search event types" />
+
+      <TreeView Grid.Row="1" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}" >
+        <TreeView.DataTemplates>
+          <TreeDataTemplate DataType="vm:EventTreeNodeBase"
+                            ItemsSource="{Binding Children}">
+            <CheckBox Content="{Binding Text}" IsChecked="{Binding IsEnabled, Mode=TwoWay}" />
+          </TreeDataTemplate>
+        </TreeView.DataTemplates>
+        <TreeView.Styles>
+          <Style Selector="TreeViewItem">
+            <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
+            <Setter Property="IsVisible" Value="{Binding IsVisible}" />
+          </Style>
+        </TreeView.Styles>
+      </TreeView>
+
+      <StackPanel Grid.Row="2" Margin="0,2" Orientation="Horizontal" Spacing="2">
+        <Button Content="Disable all" Command="{Binding DisableAll}" />
+        <Button Content="Enable default" Command="{Binding EnableDefault}" />
+      </StackPanel>
+
+    </Grid>
 
     <GridSplitter Width="4" Grid.Column="1" />
+
     <Grid RowDefinitions="*,4,2*,Auto" Grid.Column="2">
-      <ListBox Name="eventsList" Items="{Binding RecordedEvents}"
+
+      <ListBox Name="EventsList" Items="{Binding RecordedEvents}"
                SelectedItem="{Binding SelectedEvent, Mode=TwoWay}">
+
         <ListBox.ItemTemplate>
           <DataTemplate>
-            <TextBlock Background="{Binding IsHandled, Converter={StaticResource boolToBrush}}"
-                       Text="{Binding DisplayText}" />
+            <ListBoxItem Classes.handled="{Binding IsHandled}">
+              <Grid ColumnDefinitions="Auto,Auto,*,Auto">
+
+                <StackPanel Grid.Column="0" Spacing="2" Orientation="Horizontal" >
+                  <TextBlock Tag="{Binding Event}" DoubleTapped="NavigateTo" Text="{Binding Event.Name}" FontWeight="Bold" Classes="nav" />
+                  <TextBlock Text="on" />
+                  <TextBlock Tag="{Binding Originator}" DoubleTapped="NavigateTo" Text="{Binding Originator.HandlerName}" Classes="nav" />
+                </StackPanel>
+
+                <StackPanel Margin="2,0,0,0" Grid.Column="1" Spacing="2" Orientation="Horizontal" IsVisible="{Binding IsHandled}" >
+                  <TextBlock Text="::" />
+                  <TextBlock Text="Handled by" />
+                  <TextBlock Tag="{Binding HandledBy}" DoubleTapped="NavigateTo" Text="{Binding HandledBy.HandlerName}" Classes="nav" />
+                </StackPanel>
+
+                <StackPanel Grid.Column="3" Orientation="Horizontal" HorizontalAlignment="Right">
+                  <TextBlock Text="Routing (" />
+                  <TextBlock Text="{Binding Event.RoutingStrategies}"/>
+                  <TextBlock Text=")"/>
+                </StackPanel>
+
+              </Grid>
+            </ListBoxItem>
           </DataTemplate>
         </ListBox.ItemTemplate>
       </ListBox>
+
       <GridSplitter Height="4" Grid.Row="1" />
+
       <DockPanel Grid.Row="2" LastChildFill="True">
         <TextBlock DockPanel.Dock="Top" FontSize="16" Text="Event chain:" />
+
         <ListBox Items="{Binding SelectedEvent.EventChain}">
           <ListBox.ItemTemplate>
             <DataTemplate>
-              <StackPanel Orientation="Horizontal"
-                          Background="{Binding Handled, Converter={StaticResource boolToBrush}}">
-                <TextBlock Text="{Binding Route}" />
-                <TextBlock Text=": " />
-                <TextBlock Text="{Binding HandlerName}" />
-                <TextBlock Text=" handled: " />
-                <TextBlock Text="{Binding Handled}" />
-              </StackPanel>
+              <ListBoxItem Classes.handled="{Binding Handled}">
+                <StackPanel Orientation="Vertical">
+
+                  <Rectangle IsVisible="{Binding BeginsNewRoute}" StrokeDashArray="2,2" StrokeThickness="1" Stroke="Gray" />
+
+                  <StackPanel Orientation="Horizontal" Spacing="2">
+                    <TextBlock Text="{Binding Route}" FontWeight="Bold" />
+                    <TextBlock Tag="{Binding}" DoubleTapped="NavigateTo" Text="{Binding HandlerName}" Classes="nav" />
+                  </StackPanel>
+
+                </StackPanel>
+              </ListBoxItem>
             </DataTemplate>
           </ListBox.ItemTemplate>
         </ListBox>
+
       </DockPanel>
-      <StackPanel Orientation="Horizontal" Grid.Row="3">
-        <Button Content="Clear" Margin="3" Command="{Binding Clear}" />
+
+      <StackPanel Orientation="Horizontal" Grid.Row="3" Spacing="2" Margin="0,2">
+        <Button Content="Clear" Command="{Binding Clear}" />
       </StackPanel>
+
     </Grid>
   </Grid>
 </UserControl>

+ 52 - 5
src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs

@@ -1,7 +1,14 @@
-using System.Linq;
+using System;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Linq;
 using Avalonia.Controls;
+using Avalonia.Diagnostics.Models;
 using Avalonia.Diagnostics.ViewModels;
+using Avalonia.Input;
+using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
 
 namespace Avalonia.Diagnostics.Views
 {
@@ -12,13 +19,53 @@ namespace Avalonia.Diagnostics.Views
         public EventsPageView()
         {
             InitializeComponent();
-            _events = this.FindControl<ListBox>("events");
+            _events = this.FindControl<ListBox>("EventsList");
         }
 
-        private void RecordedEvents_CollectionChanged(object sender,
-            System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+        public void NavigateTo(object sender, TappedEventArgs e)
         {
-            _events.ScrollIntoView(_events.Items.OfType<FiredEvent>().LastOrDefault());
+            if (DataContext is EventsPageViewModel vm && sender is Control control)
+            {
+                switch (control.Tag)
+                {
+                    case EventChainLink chainLink:
+                    {
+                        vm.RequestTreeNavigateTo(chainLink);
+                        break;
+                    }
+                    case RoutedEvent evt:
+                    {
+                        vm.SelectEventByType(evt);
+
+                        break;
+                    }
+                }
+            }
+        }
+
+        protected override void OnDataContextChanged(EventArgs e)
+        {
+            base.OnDataContextChanged(e);
+
+            if (DataContext is EventsPageViewModel vm)
+            {
+                vm.RecordedEvents.CollectionChanged += OnRecordedEventsChanged;
+            }
+        }
+
+        private void OnRecordedEventsChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (sender is ObservableCollection<FiredEvent> events)
+            {
+                var evt = events.LastOrDefault();
+
+                if (evt is null)
+                {
+                    return;
+                }
+
+                Dispatcher.UIThread.Post(() => _events.ScrollIntoView(evt));
+            }
         }
 
         private void InitializeComponent()