From 0ae36f98ba4deefce188e1bd8026f523d2570e20 Mon Sep 17 00:00:00 2001 From: Stephen Monaco <1782158+stevemonaco@users.noreply.github.com> Date: Tue, 14 May 2024 06:01:00 -0400 Subject: [PATCH] Add HotKeys Page to DevTools (#15700) * Add HotKeys Page to DevTools * Centralize hotkeys and hoist into DevToolsOptions --- .../Diagnostics/DevToolsOptions.cs | 5 + .../Diagnostics/HotKeyConfiguration.cs | 32 +++++ .../ViewModels/HotKeyPageViewModel.cs | 40 ++++++ .../Diagnostics/ViewModels/MainViewModel.cs | 12 ++ .../Diagnostics/Views/HotKeyPageView.axaml | 36 +++++ .../Diagnostics/Views/HotKeyPageView.axaml.cs | 18 +++ .../Diagnostics/Views/MainView.xaml | 2 + .../Diagnostics/Views/MainWindow.xaml | 3 - .../Diagnostics/Views/MainWindow.xaml.cs | 135 +++++++++++------- 9 files changed, 231 insertions(+), 52 deletions(-) create mode 100644 src/Avalonia.Diagnostics/Diagnostics/HotKeyConfiguration.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/ViewModels/HotKeyPageViewModel.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Views/HotKeyPageView.axaml create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Views/HotKeyPageView.axaml.cs diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs index b2d1ab98c9..6993935aca 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs @@ -57,5 +57,10 @@ namespace Avalonia.Diagnostics /// Set the kind of diagnostic view that show at launch of DevTools /// public DevToolsViewKind LaunchView { get; init; } + + /// + /// Gets or inits the used to activate DevTools features + /// + internal HotKeyConfiguration HotKeys { get; init; } = new(); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/HotKeyConfiguration.cs b/src/Avalonia.Diagnostics/Diagnostics/HotKeyConfiguration.cs new file mode 100644 index 0000000000..85d88a5a59 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/HotKeyConfiguration.cs @@ -0,0 +1,32 @@ +using Avalonia.Input; + +namespace Avalonia.Diagnostics +{ + internal class HotKeyConfiguration + { + /// + /// Freezes refreshing the Value Frames inspector for the selected Control + /// + public KeyGesture ValueFramesFreeze { get; init; } = new(Key.S, KeyModifiers.Alt); + + /// + /// Resumes refreshing the Value Frames inspector for the selected Control + /// + public KeyGesture ValueFramesUnfreeze { get; init; } = new(Key.D, KeyModifiers.Alt); + + /// + /// Inspects the hovered Control in the Logical or Visual Tree Page + /// + public KeyGesture InspectHoveredControl { get; init; } = new(Key.None, KeyModifiers.Shift | KeyModifiers.Control); + + /// + /// Toggles the freezing of Popups which prevents visible Popups from closing so they can be inspected + /// + public KeyGesture TogglePopupFreeze { get; init; } = new(Key.F, KeyModifiers.Alt | KeyModifiers.Control); + + /// + /// Saves a Screenshot of the Selected Control in the Logical or Visual Tree Page + /// + public KeyGesture ScreenshotSelectedControl { get; init; } = new(Key.F8); + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/HotKeyPageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/HotKeyPageViewModel.cs new file mode 100644 index 0000000000..5fdcc689ca --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/HotKeyPageViewModel.cs @@ -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? _hotKeyDescriptions; + public ObservableCollection? 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(); + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index eaa5802aa5..36e0cf5ebc 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -18,6 +18,7 @@ namespace Avalonia.Diagnostics.ViewModels 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; @@ -40,6 +41,7 @@ namespace Avalonia.Diagnostics.ViewModels _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root), _pinnedProperties); _visualTree = new TreePageViewModel(this, VisualTreeNode.Create(root), _pinnedProperties); _events = new EventsPageViewModel(this); + _hotKeys = new HotKeyPageViewModel(); UpdateFocusedControl(); @@ -194,6 +196,9 @@ namespace Avalonia.Diagnostics.ViewModels case 2: Content = _events; break; + case 3: + Content = _hotKeys; + break; default: Content = _logicalTree; break; @@ -231,6 +236,11 @@ namespace Avalonia.Diagnostics.ViewModels private set => RaiseAndSetIfChanged(ref _pointerOverElementName, value); } + public void ShowHotKeys() + { + SelectedTab = 3; + } + public void SelectControl(Control control) { var tree = Content as TreePageViewModel; @@ -333,6 +343,8 @@ namespace Avalonia.Diagnostics.ViewModels ShowImplementedInterfaces = options.ShowImplementedInterfaces; FocusHighlighter = options.FocusHighlighterBrush; SelectedTab = (int)options.LaunchView; + + _hotKeys.SetOptions(options); } public bool ShowImplementedInterfaces diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/HotKeyPageView.axaml b/src/Avalonia.Diagnostics/Diagnostics/Views/HotKeyPageView.axaml new file mode 100644 index 0000000000..381cd7fba6 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/HotKeyPageView.axaml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/HotKeyPageView.axaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/HotKeyPageView.axaml.cs new file mode 100644 index 0000000000..df50bfadf0 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/HotKeyPageView.axaml.cs @@ -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); + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index bcb1e56d20..c36ccde8c0 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -72,6 +72,7 @@ + @@ -255,6 +256,7 @@ + - - - diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 63feb41656..fb0e452daf 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -23,6 +23,7 @@ namespace Avalonia.Diagnostics.Views private readonly HashSet _frozenPopupStates; private AvaloniaObject? _root; private PixelPoint _lastPointerPosition; + private HotKeyConfiguration? _hotKeys; public MainWindow() { @@ -169,83 +170,117 @@ namespace Avalonia.Diagnostics.Views 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) @@ -261,6 +296,8 @@ namespace Avalonia.Diagnostics.Views public void SetOptions(DevToolsOptions options) { + _hotKeys = options.HotKeys; + (DataContext as MainViewModel)?.SetOptions(options); if (options.ThemeVariant is { } themeVariant) {