Skip to content

Commit

Permalink
Add HotKeys Page to DevTools (#15700)
Browse files Browse the repository at this point in the history
* Add HotKeys Page to DevTools

* Centralize hotkeys and hoist into DevToolsOptions
  • Loading branch information
stevemonaco authored May 14, 2024
1 parent 8fe6e08 commit 0ae36f9
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 52 deletions.
5 changes: 5 additions & 0 deletions src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,10 @@ public class DevToolsOptions
/// Set the <see cref="DevToolsViewKind">kind</see> of diagnostic view that show at launch of DevTools
/// </summary>
public DevToolsViewKind LaunchView { get; init; }

/// <summary>
/// Gets or inits the <see cref="HotKeyConfiguration" /> used to activate DevTools features
/// </summary>
internal HotKeyConfiguration HotKeys { get; init; } = new();
}
}
32 changes: 32 additions & 0 deletions src/Avalonia.Diagnostics/Diagnostics/HotKeyConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Avalonia.Input;

namespace Avalonia.Diagnostics
{
internal class HotKeyConfiguration
{
/// <summary>
/// Freezes refreshing the Value Frames inspector for the selected Control
/// </summary>
public KeyGesture ValueFramesFreeze { get; init; } = new(Key.S, KeyModifiers.Alt);

/// <summary>
/// Resumes refreshing the Value Frames inspector for the selected Control
/// </summary>
public KeyGesture ValueFramesUnfreeze { get; init; } = new(Key.D, KeyModifiers.Alt);

/// <summary>
/// Inspects the hovered Control in the Logical or Visual Tree Page
/// </summary>
public KeyGesture InspectHoveredControl { get; init; } = new(Key.None, KeyModifiers.Shift | KeyModifiers.Control);

/// <summary>
/// Toggles the freezing of Popups which prevents visible Popups from closing so they can be inspected
/// </summary>
public KeyGesture TogglePopupFreeze { get; init; } = new(Key.F, KeyModifiers.Alt | KeyModifiers.Control);

/// <summary>
/// Saves a Screenshot of the Selected Control in the Logical or Visual Tree Page
/// </summary>
public KeyGesture ScreenshotSelectedControl { get; init; } = new(Key.F8);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Collections.ObjectModel;
using Avalonia.Input;

namespace Avalonia.Diagnostics.ViewModels
{
internal record HotKeyDescription(string Gesture, string BriefDescription, string? DetailedDescription = null);

internal class HotKeyPageViewModel : ViewModelBase
{
private ObservableCollection<HotKeyDescription>? _hotKeyDescriptions;
public ObservableCollection<HotKeyDescription>? HotKeyDescriptions
{
get => _hotKeyDescriptions;
private set => RaiseAndSetIfChanged(ref _hotKeyDescriptions, value);
}

public void SetOptions(DevToolsOptions options)
{
var hotKeys = options.HotKeys;

HotKeyDescriptions = new()
{
new(CreateDescription(options.Gesture), "Launch DevTools", "Launches DevTools to inspect the TopLevel that received the hotkey input"),
new(CreateDescription(hotKeys.ValueFramesFreeze), "Freeze Value Frames", "Pauses refreshing the Value Frames inspector for the selected Control"),
new(CreateDescription(hotKeys.ValueFramesUnfreeze), "Unfreeze Value Frames", "Resumes refreshing the Value Frames inspector for the selected Control"),
new(CreateDescription(hotKeys.InspectHoveredControl), "Inspect Control Under Pointer", "Inspects the hovered Control in the Logical or Visual Tree Page"),
new(CreateDescription(hotKeys.TogglePopupFreeze), "Toggle Popup Freeze", "Prevents visible Popups from closing so they can be inspected"),
new(CreateDescription(hotKeys.ScreenshotSelectedControl), "Screenshot Selected Control", "Saves a Screenshot of the Selected Control in the Logical or Visual Tree Page")
};
}

private string CreateDescription(KeyGesture gesture)
{
if (gesture.Key == Key.None && gesture.KeyModifiers != KeyModifiers.None)
return gesture.ToString().Replace("+None", "");
else
return gesture.ToString();
}
}
}
12 changes: 12 additions & 0 deletions src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal class MainViewModel : ViewModelBase, IDisposable
private readonly TreePageViewModel _logicalTree;
private readonly TreePageViewModel _visualTree;
private readonly EventsPageViewModel _events;
private readonly HotKeyPageViewModel _hotKeys;
private readonly IDisposable _pointerOverSubscription;
private ViewModelBase? _content;
private int _selectedTab;
Expand All @@ -40,6 +41,7 @@ public MainViewModel(AvaloniaObject root)
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root), _pinnedProperties);
_visualTree = new TreePageViewModel(this, VisualTreeNode.Create(root), _pinnedProperties);
_events = new EventsPageViewModel(this);
_hotKeys = new HotKeyPageViewModel();

UpdateFocusedControl();

Expand Down Expand Up @@ -194,6 +196,9 @@ public int SelectedTab
case 2:
Content = _events;
break;
case 3:
Content = _hotKeys;
break;
default:
Content = _logicalTree;
break;
Expand Down Expand Up @@ -231,6 +236,11 @@ public string? PointerOverElementName
private set => RaiseAndSetIfChanged(ref _pointerOverElementName, value);
}

public void ShowHotKeys()
{
SelectedTab = 3;
}

public void SelectControl(Control control)
{
var tree = Content as TreePageViewModel;
Expand Down Expand Up @@ -333,6 +343,8 @@ public void SetOptions(DevToolsOptions options)
ShowImplementedInterfaces = options.ShowImplementedInterfaces;
FocusHighlighter = options.FocusHighlighterBrush;
SelectedTab = (int)options.LaunchView;

_hotKeys.SetOptions(options);
}

public bool ShowImplementedInterfaces
Expand Down
36 changes: 36 additions & 0 deletions src/Avalonia.Diagnostics/Diagnostics/Views/HotKeyPageView.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Avalonia.Diagnostics.ViewModels"
Padding="4"
x:Class="Avalonia.Diagnostics.Views.HotKeyPageView"
x:DataType="vm:HotKeyPageViewModel">
<Grid RowDefinitions="auto,*" Grid.IsSharedSizeScope="True">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="A" Width="auto" />
<ColumnDefinition Width="8" />
<ColumnDefinition SharedSizeGroup="B" Width="*" />
</Grid.ColumnDefinitions>

<TextBlock FontWeight="Bold" Text="Action" />
<TextBlock Grid.Column="2" FontWeight="Bold" Text="Gesture" />
</Grid>

<ItemsControl Grid.Row="1" Grid.ColumnSpan="3" ItemsSource="{Binding HotKeyDescriptions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="A" Width="auto" />
<ColumnDefinition Width="8" />
<ColumnDefinition SharedSizeGroup="B" Width="*" />
</Grid.ColumnDefinitions>

<TextBlock Text="{Binding BriefDescription}" ToolTip.Tip="{Binding DetailedDescription}" />
<TextBlock Grid.Column="2" Text="{Binding Gesture}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</UserControl>
18 changes: 18 additions & 0 deletions src/Avalonia.Diagnostics/Diagnostics/Views/HotKeyPageView.axaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;

namespace Avalonia.Diagnostics.Views
{
internal class HotKeyPageView : UserControl
{
public HotKeyPageView()
{
InitializeComponent();
}

private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
2 changes: 2 additions & 0 deletions src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="_HotKeys" Command="{Binding ShowHotKeys}" />
</MenuItem>
<MenuItem Header="_Overlays">
<MenuItem Header="Margin/padding" Command="{Binding ToggleVisualizeMarginPadding}">
Expand Down Expand Up @@ -255,6 +256,7 @@
<TabStripItem Content="Logical Tree" />
<TabStripItem Content="Visual Tree" />
<TabStripItem Content="Events" />
<TabStripItem Content="HotKeys" IsVisible="False" />
</TabStrip>

<ContentControl Grid.Row="2"
Expand Down
3 changes: 0 additions & 3 deletions src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,5 @@
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.axaml" />
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml" />
</Window.Styles>
<Window.KeyBindings>
<KeyBinding Gesture="F8" Command="{Binding Shot}"/>
</Window.KeyBindings>
<views:MainView/>
</Window>
135 changes: 86 additions & 49 deletions src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal class MainWindow : Window, IStyleHost
private readonly HashSet<Popup> _frozenPopupStates;
private AvaloniaObject? _root;
private PixelPoint _lastPointerPosition;
private HotKeyConfiguration? _hotKeys;

public MainWindow()
{
Expand Down Expand Up @@ -169,83 +170,117 @@ void ProcessProperty<T>(Control control, AvaloniaProperty<T> property)

private void RawKeyDown(RawKeyEventArgs e)
{
var vm = (MainViewModel?)DataContext;
if (vm is null)
if (_hotKeys is null ||
DataContext is not MainViewModel vm ||
vm.PointerOverRoot is not TopLevel root)
{
return;
}

var root = vm.PointerOverRoot as TopLevel;
if (root is PopupRoot pr && pr.ParentTopLevel != null)
{
root = pr.ParentTopLevel;
}

var modifiers = MergeModifiers(e.Key, e.Modifiers.ToKeyModifiers());

if (root is null)
if (IsMatched(_hotKeys.ValueFramesFreeze, e.Key, modifiers))
{
return;
FreezeValueFrames(vm);
}
else if (IsMatched(_hotKeys.ValueFramesUnfreeze, e.Key, modifiers))
{
UnfreezeValueFrames(vm);
}
else if (IsMatched(_hotKeys.TogglePopupFreeze, e.Key, modifiers))
{
ToggleFreezePopups(root, vm);
}
else if (IsMatched(_hotKeys.ScreenshotSelectedControl, e.Key, modifiers))
{
ScreenshotSelectedControl(vm);
}
else if (IsMatched(_hotKeys.InspectHoveredControl, e.Key, modifiers))
{
InspectHoveredControl(root, vm);
}

if (root is PopupRoot pr && pr.ParentTopLevel != null)
static bool IsMatched(KeyGesture gesture, Key key, KeyModifiers modifiers)
{
root = pr.ParentTopLevel;
return (gesture.Key == key || gesture.Key == Key.None) && modifiers.HasAllFlags(gesture.KeyModifiers);
}

switch (e.Modifiers)
// When Control, Shift, or Alt are initially pressed, they are the Key and not part of Modifiers
// This merges so modifier keys alone can more easily trigger actions
static KeyModifiers MergeModifiers(Key key, KeyModifiers modifiers)
{
case RawInputModifiers.Control when (e.Key == Key.LeftShift || e.Key == Key.RightShift):
case RawInputModifiers.Shift when (e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl):
case RawInputModifiers.Shift | RawInputModifiers.Control:
return key switch
{
Control? control = null;
Key.LeftCtrl or Key.RightCtrl => modifiers | KeyModifiers.Control,
Key.LeftShift or Key.RightShift => modifiers | KeyModifiers.Shift,
Key.LeftAlt or Key.RightAlt => modifiers | KeyModifiers.Alt,
_ => modifiers
};
}
}

foreach (var popupRoot in GetPopupRoots(root))
{
control = GetHoveredControl(popupRoot);
private void FreezeValueFrames(MainViewModel vm)
{
vm.EnableSnapshotStyles(true);
}

if (control != null)
{
break;
}
}
private void UnfreezeValueFrames(MainViewModel vm)
{
vm.EnableSnapshotStyles(false);
}

control ??= GetHoveredControl(root);
private void ToggleFreezePopups(TopLevel root, MainViewModel vm)
{
vm.FreezePopups = !vm.FreezePopups;

if (control != null)
foreach (var popupRoot in GetPopupRoots(root))
{
if (popupRoot.Parent is Popup popup)
{
if (vm.FreezePopups)
{
vm.SelectControl(control);
popup.Closing += PopupOnClosing;
_frozenPopupStates.Add(popup);
}
else
{
popup.Closing -= PopupOnClosing;
_frozenPopupStates.Remove(popup);
}

break;
}
}
}

case RawInputModifiers.Control | RawInputModifiers.Alt when e.Key == Key.F:
{
vm.FreezePopups = !vm.FreezePopups;
private void ScreenshotSelectedControl(MainViewModel vm)
{
vm.Shot(null);
}

foreach (var popupRoot in GetPopupRoots(root))
{
if (popupRoot.Parent is Popup popup)
{
if (vm.FreezePopups)
{
popup.Closing += PopupOnClosing;
_frozenPopupStates.Add(popup);
}
else
{
popup.Closing -= PopupOnClosing;
_frozenPopupStates.Remove(popup);
}
}
}
private void InspectHoveredControl(TopLevel root, MainViewModel vm)
{
Control? control = null;

break;
}
foreach (var popupRoot in GetPopupRoots(root))
{
control = GetHoveredControl(popupRoot);

case RawInputModifiers.Alt when e.Key == Key.S || e.Key == Key.D:
if (control != null)
{
vm.EnableSnapshotStyles(e.Key == Key.S);

break;
}
}

control ??= GetHoveredControl(root);

if (control != null)
{
vm.SelectControl(control);
}
}

private void PopupOnClosing(object? sender, CancelEventArgs e)
Expand All @@ -261,6 +296,8 @@ private void PopupOnClosing(object? sender, CancelEventArgs e)

public void SetOptions(DevToolsOptions options)
{
_hotKeys = options.HotKeys;

(DataContext as MainViewModel)?.SetOptions(options);
if (options.ThemeVariant is { } themeVariant)
{
Expand Down

0 comments on commit 0ae36f9

Please sign in to comment.