From f10faf004e89558c45ff0668cf529fe30cd014e7 Mon Sep 17 00:00:00 2001 From: Andrey Nekrasov Date: Wed, 1 Sep 2021 21:23:10 +0300 Subject: [PATCH] [FancyZonesEditor]: Grid Editor keyboard control (#12969) - Ctrl+Tab to switch between zones and layout overlay window - Tab to focus between grid zones and resizers - While resizer is focused: arrows to move it; Del to remove it - While zone is focused: (Shift)+S to split it horizontally/vertically --- .../editor/FancyZonesEditor/App.xaml.cs | 6 + .../FancyZonesEditor/GridEditor.xaml.cs | 149 +++++++++++++++++- .../FancyZonesEditor/GridEditorWindow.xaml | 8 + .../editor/FancyZonesEditor/GridResizer.xaml | 5 + .../editor/FancyZonesEditor/GridZone.xaml | 2 + .../editor/FancyZonesEditor/GridZone.xaml.cs | 30 +++- .../editor/FancyZonesEditor/Overlay.cs | 11 +- .../Properties/Resources.Designer.cs | 23 +++ .../Properties/Resources.resx | 11 ++ 9 files changed, 242 insertions(+), 3 deletions(-) diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/App.xaml.cs b/src/modules/fancyzones/editor/FancyZonesEditor/App.xaml.cs index 2882af0ab4..0cd4e262b6 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/App.xaml.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/App.xaml.cs @@ -12,6 +12,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; +using System.Windows.Input; using FancyZonesEditor.Utils; using ManagedCommon; using Microsoft.PowerToys.Common.UI; @@ -178,6 +179,11 @@ namespace FancyZonesEditor { MainWindowSettings.IsShiftKeyPressed = true; } + else if (e.Key == Key.Tab && (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))) + { + e.Handled = true; + App.Overlay.FocusEditor(); + } } public static void ShowExceptionMessageBox(string message, Exception exception = null) diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/GridEditor.xaml.cs b/src/modules/fancyzones/editor/FancyZonesEditor/GridEditor.xaml.cs index 694da44bbf..c539d92abf 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/GridEditor.xaml.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/GridEditor.xaml.cs @@ -4,9 +4,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Windows; using System.Windows.Controls; +using System.Windows.Controls.Primitives; using System.Windows.Input; using FancyZonesEditor.Models; @@ -38,9 +40,20 @@ namespace FancyZonesEditor InitializeComponent(); Loaded += GridEditor_Loaded; Unloaded += GridEditor_Unloaded; + KeyDown += GridEditor_KeyDown; + KeyUp += GridEditor_KeyUp; gridEditorUniqueId = ++gridEditorUniqueIdCounter; } + public void FocusZone() + { + if (Preview.Children.Count > 0) + { + var zone = Preview.Children[0] as GridZone; + zone.Focus(); + } + } + private void GridEditor_Loaded(object sender, RoutedEventArgs e) { ((App)Application.Current).MainWindowSettings.PropertyChanged += ZoneSettings_PropertyChanged; @@ -58,6 +71,134 @@ namespace FancyZonesEditor SetupUI(); } + private void HandleResizerKeyDown(GridResizer resizer, KeyEventArgs e) + { + DragDeltaEventArgs args = null; + if (resizer.Orientation == Orientation.Horizontal) + { + if (e.Key == Key.Up) + { + args = new DragDeltaEventArgs(0, -1); + } + else if (e.Key == Key.Down) + { + args = new DragDeltaEventArgs(0, 1); + } + } + else + { + if (e.Key == Key.Left) + { + args = new DragDeltaEventArgs(-1, 0); + } + else if (e.Key == Key.Right) + { + args = new DragDeltaEventArgs(1, 0); + } + } + + if (args != null) + { + e.Handled = true; + Resizer_DragDelta(resizer, args); + } + + if (e.Key == Key.Delete) + { + int resizerIndex = AdornerLayer.Children.IndexOf(resizer); + var resizerData = _data.Resizers[resizerIndex]; + + var indices = new List(resizerData.PositiveSideIndices); + indices.AddRange(resizerData.NegativeSideIndices); + _data.DoMerge(indices); + SetupUI(); + e.Handled = true; + } + } + + private void HandleResizerKeyUp(GridResizer resizer, KeyEventArgs e) + { + if (resizer.Orientation == Orientation.Horizontal) + { + e.Handled = e.Key == Key.Up || e.Key == Key.Down; + } + else + { + e.Handled = e.Key == Key.Left || e.Key == Key.Right; + } + + if (e.Handled) + { + int resizerIndex = AdornerLayer.Children.IndexOf(resizer); + Resizer_DragCompleted(resizer, null); + Debug.Assert(AdornerLayer.Children.Count > resizerIndex, "Resizer index out of range"); + Keyboard.Focus(AdornerLayer.Children[resizerIndex]); + _dragY = _dragX = 0; + } + } + + private void HandleGridZoneKeyUp(GridZone gridZone, KeyEventArgs e) + { + if (e.Key != Key.S) + { + return; + } + + Orientation orient = Orientation.Horizontal; + int offset = 0; + + int zoneIndex = Preview.Children.IndexOf(gridZone); + var zone = _data.Zones[zoneIndex]; + Debug.Assert(Preview.Children.Count > zoneIndex, "Zone index out of range"); + + if (((App)Application.Current).MainWindowSettings.IsShiftKeyPressed) + { + orient = Orientation.Vertical; + offset = gridZone.SnapAtHalfX(); + } + else + { + offset = gridZone.SnapAtHalfY(); + } + + gridZone.DoSplit(orient, offset); + } + + private void GridEditor_KeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Tab && (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))) + { + e.Handled = true; + App.Overlay.FocusEditorWindow(); + } + else + { + var resizer = Keyboard.FocusedElement as GridResizer; + if (resizer != null) + { + HandleResizerKeyDown(resizer, e); + return; + } + } + } + + private void GridEditor_KeyUp(object sender, KeyEventArgs e) + { + var resizer = Keyboard.FocusedElement as GridResizer; + if (resizer != null) + { + HandleResizerKeyUp(resizer, e); + return; + } + + var gridZone = Keyboard.FocusedElement as GridZone; + if (gridZone != null) + { + HandleGridZoneKeyUp(gridZone, e); + return; + } + } + private void GridEditor_Unloaded(object sender, RoutedEventArgs e) { ((App)Application.Current).MainWindowSettings.PropertyChanged -= ZoneSettings_PropertyChanged; @@ -267,7 +408,7 @@ namespace FancyZonesEditor delta = Convert.ToInt32(_dragY / actualSize.Height * GridData.Multiplier); } - if (_data.CanDrag(resizerIndex, delta)) + if (resizerIndex != -1 && _data.CanDrag(resizerIndex, delta)) { // Just update the UI, don't tell _data if (resizer.Orientation == Orientation.Vertical) @@ -328,6 +469,12 @@ namespace FancyZonesEditor { GridResizer resizer = (GridResizer)sender; int resizerIndex = AdornerLayer.Children.IndexOf(resizer); + if (resizerIndex == -1) + { + // Resizer was removed during drag + return; + } + Size actualSize = WorkAreaSize(); double pixelDelta = resizer.Orientation == Orientation.Vertical ? diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/GridEditorWindow.xaml b/src/modules/fancyzones/editor/FancyZonesEditor/GridEditorWindow.xaml index 0395ae46da..dbb348beda 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/GridEditorWindow.xaml +++ b/src/modules/fancyzones/editor/FancyZonesEditor/GridEditorWindow.xaml @@ -53,6 +53,14 @@ Text="{x:Static props:Resources.MergeName}" /> + + + + diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/GridResizer.xaml b/src/modules/fancyzones/editor/FancyZonesEditor/GridResizer.xaml index de499119fc..0717b8f756 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/GridResizer.xaml +++ b/src/modules/fancyzones/editor/FancyZonesEditor/GridResizer.xaml @@ -5,6 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:FancyZonesEditor" mc:Ignorable="d" + Focusable="True" d:DesignHeight="300" d:DesignWidth="300"> @@ -35,6 +36,10 @@ TargetName="Body" Value="{DynamicResource SystemAccentColorLight1Brush}"/> + + + diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/GridZone.xaml b/src/modules/fancyzones/editor/FancyZonesEditor/GridZone.xaml index 74f040b750..e3b55d8200 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/GridZone.xaml +++ b/src/modules/fancyzones/editor/FancyZonesEditor/GridZone.xaml @@ -13,6 +13,8 @@ BorderBrush="{DynamicResource SystemControlBackgroundAccentBrush}" BorderThickness="1" Opacity="1" + Focusable="True" + IsTabStop="True" ui:ControlHelper.CornerRadius="4" mc:Ignorable="d"> diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/GridZone.xaml.cs b/src/modules/fancyzones/editor/FancyZonesEditor/GridZone.xaml.cs index 7cf84e1ddf..015ae1fb75 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/GridZone.xaml.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/GridZone.xaml.cs @@ -23,6 +23,7 @@ namespace FancyZonesEditor private const string GridZoneBackgroundBrushID = "GridZoneBackgroundBrush"; private const string SecondaryForegroundBrushID = "SecondaryForegroundBrush"; private const string AccentColorBrushID = "SystemControlBackgroundAccentBrush"; + private const string CanvasCanvasZoneBorderBrushID = "CanvasCanvasZoneBorderBrush"; public static readonly DependencyProperty IsSelectedProperty = DependencyProperty.Register(ObjectDependencyID, typeof(bool), typeof(GridZone), new PropertyMetadata(false, OnSelectionChanged)); @@ -75,12 +76,39 @@ namespace FancyZonesEditor SizeChanged += GridZone_SizeChanged; + GotKeyboardFocus += GridZone_GotKeyboardFocus; + LostKeyboardFocus += GridZone_LostKeyboardFocus; + _snapX = snapX; _snapY = snapY; _canSplit = canSplit; _zone = zone; } + public int SnapAtHalfX() + { + var half = (_zone.Right - _zone.Left) / 2; + var pixelX = _snapX.DataToPixelWithoutSnapping(_zone.Left + half); + return _snapX.PixelToDataWithSnapping(pixelX, _zone.Left, _zone.Right); + } + + public int SnapAtHalfY() + { + var half = (_zone.Bottom - _zone.Top) / 2; + var pixelY = _snapY.DataToPixelWithoutSnapping(_zone.Top + half); + return _snapY.PixelToDataWithSnapping(pixelY, _zone.Top, _zone.Bottom); + } + + private void GridZone_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) + { + Opacity = 1; + } + + private void GridZone_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) + { + Opacity = 0.5; + } + private void GridZone_SizeChanged(object sender, SizeChangedEventArgs e) { // using current culture as this is end user facing @@ -241,7 +269,7 @@ namespace FancyZonesEditor MergeComplete?.Invoke(this, e); } - private void DoSplit(Orientation orientation, int offset) + public void DoSplit(Orientation orientation, int offset) { Split?.Invoke(this, new SplitEventArgs(orientation, offset)); } diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/Overlay.cs b/src/modules/fancyzones/editor/FancyZonesEditor/Overlay.cs index 30401323e7..443030c0c2 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/Overlay.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/Overlay.cs @@ -253,10 +253,19 @@ namespace FancyZonesEditor public void FocusEditor() { - if (_editorLayout != null && _editorLayout is CanvasEditor canvasEditor) + if (_editorLayout == null) + { + return; + } + + if (_editorLayout is CanvasEditor canvasEditor) { canvasEditor.FocusZone(); } + else if (_editorLayout is GridEditor gridEditor) + { + gridEditor.FocusZone(); + } } public void FocusEditorWindow() diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/Properties/Resources.Designer.cs b/src/modules/fancyzones/editor/FancyZonesEditor/Properties/Resources.Designer.cs index 1c9a39e3d6..d9e9ce3715 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/Properties/Resources.Designer.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/Properties/Resources.Designer.cs @@ -447,6 +447,29 @@ namespace FancyZonesEditor.Properties { } } + /// + /// Looks up a localized string similar to + /// - [Shift]+S to split currently focused zone. + /// - Ctrl+Tab to focus zones/resizers. + /// - Tab to cycle zones and resizers. + /// - Delete to remove the focused resizer. + /// - Arrows to move the focused resizer.. + /// + public static string KeyboardControlsDescription { + get { + return ResourceManager.GetString("KeyboardControlsDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Keyboard Navigation:. + /// + public static string KeyboardControlsName { + get { + return ResourceManager.GetString("KeyboardControlsName", resourceCulture); + } + } + /// /// Looks up a localized string similar to Create layouts that have overlapping zones. /// diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/Properties/Resources.resx b/src/modules/fancyzones/editor/FancyZonesEditor/Properties/Resources.resx index c8d3ffd41b..23d0018ce6 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/Properties/Resources.resx +++ b/src/modules/fancyzones/editor/FancyZonesEditor/Properties/Resources.resx @@ -336,6 +336,17 @@ Merge/Delete: Title for concept behind Merging two zones together or removing an zone + + Keyboard Navigation: + + + + - [Shift]+S to split currently focused zone. + - Ctrl+Tab to focus zones/resizers. + - Tab to cycle zones and resizers. + - Delete to remove the focused resizer. + - Arrows to move the focused resizer. + Hold Shift key for vertical split. A segmenter visual for splitting one item into two. This would be the vertical line. Shift key is referring to key on keyboard