diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs index b2d1ab98c90..6993935acae 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs @@ -57,5 +57,10 @@ public class DevToolsOptions /// 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 00000000000..85d88a5a59f --- /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 00000000000..5fdcc689ca0 --- /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 eaa5802aa5f..36e0cf5ebcc 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -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; @@ -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(); @@ -194,6 +196,9 @@ public int SelectedTab case 2: Content = _events; break; + case 3: + Content = _hotKeys; + break; default: Content = _logicalTree; break; @@ -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; @@ -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 diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/HotKeyPageView.axaml b/src/Avalonia.Diagnostics/Diagnostics/Views/HotKeyPageView.axaml new file mode 100644 index 00000000000..381cd7fba68 --- /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 00000000000..df50bfadf07 --- /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 bcb1e56d209..c36ccde8c02 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 63feb416564..fb0e452dafe 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -23,6 +23,7 @@ internal class MainWindow : Window, IStyleHost private readonly HashSet _frozenPopupStates; private AvaloniaObject? _root; private PixelPoint _lastPointerPosition; + private HotKeyConfiguration? _hotKeys; public MainWindow() { @@ -169,83 +170,117 @@ void ProcessProperty(Control control, AvaloniaProperty 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) @@ -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) {