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
workgroupengineering 2 years ago
parent
commit
cbf86c4b89

+ 47 - 0
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 - 0
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;
+    }
+}

+ 7 - 2
src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs

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

+ 43 - 14
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@@ -9,6 +9,8 @@ using Avalonia.Threading;
 using Avalonia.Reactive;
 using Avalonia.Rendering;
 using System.Collections.Generic;
+using Avalonia.Media;
+using Avalonia.Controls.Primitives;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
@@ -31,7 +33,9 @@ namespace Avalonia.Diagnostics.ViewModels
         private bool _showPropertyType;
         private bool _showImplementedInterfaces;
         private readonly HashSet<string> _pinnedProperties = new();
-        
+        private IBrush? _FocusHighlighter;
+        private IDisposable? _currentFocusHighlightAdorner = default;
+
         public MainViewModel(AvaloniaObject root)
         {
             _root = root;
@@ -210,10 +214,10 @@ namespace Avalonia.Diagnostics.ViewModels
             private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
         }
 
-        public IInputRoot? PointerOverRoot 
-        { 
+        public IInputRoot? PointerOverRoot
+        {
             get => _pointerOverRoot;
-            private  set => RaiseAndSetIfChanged( ref _pointerOverRoot , value); 
+            private set => RaiseAndSetIfChanged(ref _pointerOverRoot, value);
         }
 
         public IInputElement? PointerOverElement
@@ -264,7 +268,7 @@ namespace Avalonia.Diagnostics.ViewModels
             _pointerOverSubscription.Dispose();
             _logicalTree.Dispose();
             _visualTree.Dispose();
-
+            _currentFocusHighlightAdorner?.Dispose();
             if (TryGetRenderer() is { } renderer)
             {
                 renderer.Diagnostics.DebugOverlays = RendererDebugOverlays.None;
@@ -273,7 +277,20 @@ namespace Avalonia.Diagnostics.ViewModels
 
         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)
@@ -299,7 +316,7 @@ namespace Avalonia.Diagnostics.ViewModels
         }
 
         public int? StartupScreenIndex { get; private set; } = default;
-        
+
         [DependsOn(nameof(TreePageViewModel.SelectedNode))]
         [DependsOn(nameof(Content))]
         public bool CanShot(object? parameter)
@@ -333,12 +350,13 @@ namespace Avalonia.Diagnostics.ViewModels
             _screenshotHandler = options.ScreenshotHandler;
             StartupScreenIndex = options.StartupScreenIndex;
             ShowImplementedInterfaces = options.ShowImplementedInterfaces;
+            FocusHighlighter = options.FocusHighlighterBrush;
         }
 
-        public bool ShowImplementedInterfaces 
-        { 
-            get => _showImplementedInterfaces; 
-            private set => RaiseAndSetIfChanged(ref _showImplementedInterfaces , value); 
+        public bool ShowImplementedInterfaces
+        {
+            get => _showImplementedInterfaces;
+            private set => RaiseAndSetIfChanged(ref _showImplementedInterfaces, value);
         }
 
         public void ToggleShowImplementedInterfaces(object parameter)
@@ -351,14 +369,25 @@ namespace Avalonia.Diagnostics.ViewModels
         }
 
         public bool ShowDetailsPropertyType
-        { 
-            get => _showPropertyType; 
-            private set => RaiseAndSetIfChanged(ref  _showPropertyType , value); 
+        {
+            get => _showPropertyType;
+            private set => RaiseAndSetIfChanged(ref _showPropertyType, value);
         }
 
         public void ToggleShowDetailsPropertyType(object parameter)
         {
             ShowDetailsPropertyType = !ShowDetailsPropertyType;
         }
+
+        public IBrush? FocusHighlighter
+        {
+            get => _FocusHighlighter;
+            private set => RaiseAndSetIfChanged(ref _FocusHighlighter, value);
+        }
+
+        public void SelectFocusHighlighter(object parameter)
+        {
+            FocusHighlighter = parameter as IBrush;
+        }
     }
 }

+ 156 - 2
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

@@ -2,8 +2,24 @@
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:views="using:Avalonia.Diagnostics.Views"
              xmlns:viewModels="using:Avalonia.Diagnostics.ViewModels"
+             xmlns:convertes="using:Avalonia.Diagnostics.Converters"
              x:Class="Avalonia.Diagnostics.Views.MainView"
              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">
     <Menu>
       <MenuItem Header="_File">
@@ -61,8 +77,7 @@
                         IsChecked="{Binding ShowDetailsPropertyType}"
                         IsEnabled="False"/>
             </MenuItem.Icon>
-
-          </MenuItem>          
+          </MenuItem>
         </MenuItem>
       </MenuItem>
       <MenuItem Header="_Overlays">
@@ -101,6 +116,145 @@
                       IsEnabled="False" />
           </MenuItem.Icon>
         </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>
     </Menu>