Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HotKeys Page to DevTools #15700

Merged
merged 2 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit cludgy here, but changing SelectedTab is necessary so other tabs do not remain selected.

}

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
Loading