Browse Source

feat(DevTools): Focus follower (#12813)

* feat(DevTools): Focus follower

* fix: remove commented code

* fix: Address Review

* fix: unused field

* fix: missing dispose

* fix: do not Avalonia.Diagnostics Popup

* feat: using FocusAdornerProperty

* code clean up

* fix: null annotation

* Revert using FocusAdorner
UnsafeAsSpan-logical-children
workgroupengineering 3 years ago
committed by GitHub
parent
commit
cbf86c4b89
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 47
      src/Avalonia.Diagnostics/Diagnostics/Controls/ControlHighlightAdorner.cs
  2. 40
      src/Avalonia.Diagnostics/Diagnostics/Converters/BrushSelectorConveter.cs
  3. 9
      src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
  4. 57
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  5. 158
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

47
src/Avalonia.Diagnostics/Diagnostics/Controls/ControlHighlightAdorner.cs

@ -0,0 +1,47 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Reactive;
namespace Avalonia.Diagnostics.Controls;
internal class ControlHighlightAdorner : Control
{
readonly IPen _pen;
private ControlHighlightAdorner(IPen pen)
{
_pen = pen;
this.Clip = null;
}
public static IDisposable? Add(InputElement owner, IBrush highlightBrush)
{
if (AdornerLayer.GetAdornerLayer(owner) is { } layer)
{
var pen = new Pen(highlightBrush, 2).ToImmutable();
var adorner = new ControlHighlightAdorner(pen)
{
[AdornerLayer.AdornedElementProperty] = owner
};
layer.Children.Add(adorner);
return Disposable.Create((layer, adorner), state =>
{
state.layer.Children.Remove(state.adorner);
});
}
return default;
}
public override void Render(DrawingContext context)
{
base.Render(context);
context.DrawRectangle(_pen, Bounds.Deflate(2));
}
}

40
src/Avalonia.Diagnostics/Diagnostics/Converters/BrushSelectorConveter.cs

@ -0,0 +1,40 @@
using System;
using System.Globalization;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace Avalonia.Diagnostics.Converters;
internal class BrushSelectorConveter : AvaloniaObject, IValueConverter
{
public static readonly DirectProperty<BrushSelectorConveter, IBrush?> BrushProperty =
AvaloniaProperty.RegisterDirect<BrushSelectorConveter, IBrush?>(nameof(Brush)
, o => o.Brush
, (o, v) => o.Brush = v);
public IBrush? Brush { get; set; }
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (ReferenceEquals(value, parameter))
{
return Brush;
}
else if (value is ISolidColorBrush a
&& parameter is ISolidColorBrush b
&& a.Color == b.Color
&& a.Transform == b.Transform
&& b.Opacity == a.Opacity
)
{
return Brush;
}
return null;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return BindingOperations.DoNothing;
}
}

9
src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs

@ -1,5 +1,5 @@
using System; using Avalonia.Input;
using Avalonia.Input; using Avalonia.Media;
using Avalonia.Styling; using Avalonia.Styling;
namespace Avalonia.Diagnostics namespace Avalonia.Diagnostics
@ -47,5 +47,10 @@ namespace Avalonia.Diagnostics
/// Gets or sets whether DevTools theme. /// Gets or sets whether DevTools theme.
/// </summary> /// </summary>
public ThemeVariant? ThemeVariant { get; set; } public ThemeVariant? ThemeVariant { get; set; }
/// <summary>
/// Get or set Focus Highlighter <see cref="Brush"/>
/// </summary>
public IBrush? FocusHighlighterBrush { get; set; }
} }
} }

57
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -9,6 +9,8 @@ using Avalonia.Threading;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Rendering; using Avalonia.Rendering;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Controls.Primitives;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
@ -31,7 +33,9 @@ namespace Avalonia.Diagnostics.ViewModels
private bool _showPropertyType; private bool _showPropertyType;
private bool _showImplementedInterfaces; private bool _showImplementedInterfaces;
private readonly HashSet<string> _pinnedProperties = new(); private readonly HashSet<string> _pinnedProperties = new();
private IBrush? _FocusHighlighter;
private IDisposable? _currentFocusHighlightAdorner = default;
public MainViewModel(AvaloniaObject root) public MainViewModel(AvaloniaObject root)
{ {
_root = root; _root = root;
@ -210,10 +214,10 @@ namespace Avalonia.Diagnostics.ViewModels
private set { RaiseAndSetIfChanged(ref _focusedControl, value); } private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
} }
public IInputRoot? PointerOverRoot public IInputRoot? PointerOverRoot
{ {
get => _pointerOverRoot; get => _pointerOverRoot;
private set => RaiseAndSetIfChanged( ref _pointerOverRoot , value); private set => RaiseAndSetIfChanged(ref _pointerOverRoot, value);
} }
public IInputElement? PointerOverElement public IInputElement? PointerOverElement
@ -264,7 +268,7 @@ namespace Avalonia.Diagnostics.ViewModels
_pointerOverSubscription.Dispose(); _pointerOverSubscription.Dispose();
_logicalTree.Dispose(); _logicalTree.Dispose();
_visualTree.Dispose(); _visualTree.Dispose();
_currentFocusHighlightAdorner?.Dispose();
if (TryGetRenderer() is { } renderer) if (TryGetRenderer() is { } renderer)
{ {
renderer.Diagnostics.DebugOverlays = RendererDebugOverlays.None; renderer.Diagnostics.DebugOverlays = RendererDebugOverlays.None;
@ -273,7 +277,20 @@ namespace Avalonia.Diagnostics.ViewModels
private void UpdateFocusedControl() private void UpdateFocusedControl()
{ {
FocusedControl = KeyboardDevice.Instance?.FocusedElement?.GetType().Name; var element = KeyboardDevice.Instance?.FocusedElement;
FocusedControl = element?.GetType().Name;
_currentFocusHighlightAdorner?.Dispose();
if (FocusHighlighter is IBrush brush
&& element is InputElement input
&& TopLevel.GetTopLevel(input) is { } topLevel
&& (topLevel is not Views.MainWindow))
{
if (topLevel is PopupRoot pr && pr.ParentTopLevel is Views.MainWindow)
{
return;
}
_currentFocusHighlightAdorner = Controls.ControlHighlightAdorner.Add(input, brush);
}
} }
private void KeyboardPropertyChanged(object? sender, PropertyChangedEventArgs e) private void KeyboardPropertyChanged(object? sender, PropertyChangedEventArgs e)
@ -299,7 +316,7 @@ namespace Avalonia.Diagnostics.ViewModels
} }
public int? StartupScreenIndex { get; private set; } = default; public int? StartupScreenIndex { get; private set; } = default;
[DependsOn(nameof(TreePageViewModel.SelectedNode))] [DependsOn(nameof(TreePageViewModel.SelectedNode))]
[DependsOn(nameof(Content))] [DependsOn(nameof(Content))]
public bool CanShot(object? parameter) public bool CanShot(object? parameter)
@ -333,12 +350,13 @@ namespace Avalonia.Diagnostics.ViewModels
_screenshotHandler = options.ScreenshotHandler; _screenshotHandler = options.ScreenshotHandler;
StartupScreenIndex = options.StartupScreenIndex; StartupScreenIndex = options.StartupScreenIndex;
ShowImplementedInterfaces = options.ShowImplementedInterfaces; ShowImplementedInterfaces = options.ShowImplementedInterfaces;
FocusHighlighter = options.FocusHighlighterBrush;
} }
public bool ShowImplementedInterfaces public bool ShowImplementedInterfaces
{ {
get => _showImplementedInterfaces; get => _showImplementedInterfaces;
private set => RaiseAndSetIfChanged(ref _showImplementedInterfaces , value); private set => RaiseAndSetIfChanged(ref _showImplementedInterfaces, value);
} }
public void ToggleShowImplementedInterfaces(object parameter) public void ToggleShowImplementedInterfaces(object parameter)
@ -351,14 +369,25 @@ namespace Avalonia.Diagnostics.ViewModels
} }
public bool ShowDetailsPropertyType public bool ShowDetailsPropertyType
{ {
get => _showPropertyType; get => _showPropertyType;
private set => RaiseAndSetIfChanged(ref _showPropertyType , value); private set => RaiseAndSetIfChanged(ref _showPropertyType, value);
} }
public void ToggleShowDetailsPropertyType(object parameter) public void ToggleShowDetailsPropertyType(object parameter)
{ {
ShowDetailsPropertyType = !ShowDetailsPropertyType; ShowDetailsPropertyType = !ShowDetailsPropertyType;
} }
public IBrush? FocusHighlighter
{
get => _FocusHighlighter;
private set => RaiseAndSetIfChanged(ref _FocusHighlighter, value);
}
public void SelectFocusHighlighter(object parameter)
{
FocusHighlighter = parameter as IBrush;
}
} }
} }

158
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

@ -2,8 +2,24 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="using:Avalonia.Diagnostics.Views" xmlns:views="using:Avalonia.Diagnostics.Views"
xmlns:viewModels="using:Avalonia.Diagnostics.ViewModels" xmlns:viewModels="using:Avalonia.Diagnostics.ViewModels"
xmlns:convertes="using:Avalonia.Diagnostics.Converters"
x:Class="Avalonia.Diagnostics.Views.MainView" x:Class="Avalonia.Diagnostics.Views.MainView"
x:DataType="viewModels:MainViewModel"> x:DataType="viewModels:MainViewModel">
<UserControl.Resources>
<x:Double x:Key="SampleSize">16</x:Double>
<convertes:BrushSelectorConveter x:Key="bsc"
Brush="{DynamicResource HighlightBrush}"/>
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="Border.Sample">
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderHighBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Grid.Column" Value="1"/>
<Setter Property="Width" Value="{StaticResource SampleSize}"/>
<Setter Property="Height" Value="{StaticResource SampleSize}"/>
<Setter Property="Margin" Value="10 0 -20 0"/>
</Style>
</UserControl.Styles>
<Grid Name="rootGrid" RowDefinitions="Auto,Auto,*,Auto,0,Auto"> <Grid Name="rootGrid" RowDefinitions="Auto,Auto,*,Auto,0,Auto">
<Menu> <Menu>
<MenuItem Header="_File"> <MenuItem Header="_File">
@ -61,8 +77,7 @@
IsChecked="{Binding ShowDetailsPropertyType}" IsChecked="{Binding ShowDetailsPropertyType}"
IsEnabled="False"/> IsEnabled="False"/>
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem>
</MenuItem>
</MenuItem> </MenuItem>
</MenuItem> </MenuItem>
<MenuItem Header="_Overlays"> <MenuItem Header="_Overlays">
@ -101,6 +116,145 @@
IsEnabled="False" /> IsEnabled="False" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<MenuItem Header="Focus Highlighter" Grid.IsSharedSizeScope="True">
<MenuItem.Items>
<!-- None -->
<MenuItem Command="{Binding SelectFocusHighlighter}"
CommandParameter="{x:Null}">
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition SharedSizeGroup="Sample"/>
</Grid.ColumnDefinitions>
<TextBlock Text="(none)"
VerticalAlignment="Center"/>
<Border Classes="Sample"/>
</Grid>
</MenuItem.Header>
<MenuItem.Icon>
<Border CornerRadius="8"
Width="16"
Height="16"
BorderBrush="{DynamicResource ThemeBorderHighBrush}"
BorderThickness="1"
>
<Border Background="{Binding FocusHighlighter,Converter={StaticResource bsc},ConverterParameter={x:Null}}"
Margin="2"
CornerRadius="6"/>
</Border>
</MenuItem.Icon>
</MenuItem>
<!-- Red -->
<MenuItem Command="{Binding SelectFocusHighlighter}"
CommandParameter="{x:Static Brushes.Red}">
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition SharedSizeGroup="Sample"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Red"
VerticalAlignment="Center"/>
<Border Classes="Sample" Background="Red"/>
</Grid>
</MenuItem.Header>
<MenuItem.Icon>
<Border CornerRadius="8"
Width="16"
Height="16"
BorderBrush="{DynamicResource ThemeBorderHighBrush}"
BorderThickness="1"
>
<Border Background="{Binding FocusHighlighter,Converter={StaticResource bsc},ConverterParameter={x:Static Brushes.Red}}"
Margin="2"
CornerRadius="6"/>
</Border>
</MenuItem.Icon>
</MenuItem>
<!-- Blue -->
<MenuItem Command="{Binding SelectFocusHighlighter}"
CommandParameter="{x:Static Brushes.Blue}">
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition SharedSizeGroup="Sample"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Blue"
VerticalAlignment="Center"/>
<Border Classes="Sample" Background="Blue"/>
</Grid>
</MenuItem.Header>
<MenuItem.Icon>
<Border CornerRadius="8"
Width="16"
Height="16"
BorderBrush="{DynamicResource ThemeBorderHighBrush}"
BorderThickness="1"
>
<Border Background="{Binding FocusHighlighter,Converter={StaticResource bsc},ConverterParameter={x:Static Brushes.Blue}}"
Margin="2"
CornerRadius="6"/>
</Border>
</MenuItem.Icon>
</MenuItem>
<!-- Black -->
<MenuItem Command="{Binding SelectFocusHighlighter}"
CommandParameter="{x:Static Brushes.Black}">
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition SharedSizeGroup="Sample"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Black"
VerticalAlignment="Center"/>
<Border Classes="Sample" Background="Black"/>
</Grid>
</MenuItem.Header>
<MenuItem.Icon>
<Border CornerRadius="8"
Width="16"
Height="16"
BorderBrush="{DynamicResource ThemeBorderHighBrush}"
BorderThickness="1"
>
<Border Background="{Binding FocusHighlighter,Converter={StaticResource bsc},ConverterParameter={x:Static Brushes.Black}}"
Margin="2"
CornerRadius="6"/>
</Border>
</MenuItem.Icon>
</MenuItem>
<!-- White -->
<MenuItem Command="{Binding SelectFocusHighlighter}"
CommandParameter="{x:Static Brushes.White}">
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition SharedSizeGroup="Sample"/>
</Grid.ColumnDefinitions>
<TextBlock Text="White"
VerticalAlignment="Center"/>
<Border Classes="Sample" Background="White"/>
</Grid>
</MenuItem.Header>
<MenuItem.Icon>
<Border CornerRadius="8"
Width="16"
Height="16"
BorderBrush="{DynamicResource ThemeBorderHighBrush}"
BorderThickness="1"
>
<Border Background="{Binding FocusHighlighter,Converter={StaticResource bsc},ConverterParameter={x:Static Brushes.White}}"
Margin="2"
CornerRadius="6"/>
</Border>
</MenuItem.Icon>
</MenuItem>
</MenuItem.Items>
</MenuItem>
</MenuItem> </MenuItem>
</Menu> </Menu>

Loading…
Cancel
Save