Browse Source

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

Improve UX of events view in dev tools
release/0.10.4
Dariusz Komosiński 5 years ago
committed by Dan Walmsley
parent
commit
f2b9114eb9
  1. 22
      src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs
  2. 2
      src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
  3. 10
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
  4. 9
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
  5. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
  6. 160
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
  7. 15
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
  8. 17
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  9. 135
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
  10. 57
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs

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
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

10
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;
}

9
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
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;

160
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;
}
}
}
}

15
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;

17
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);
}
}
}
}

135
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>

57
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()

Loading…
Cancel
Save