[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
This commit is contained in:
Andrey Nekrasov 2021-09-01 21:23:10 +03:00 committed by GitHub
parent f0750997de
commit f10faf004e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 242 additions and 3 deletions

View File

@ -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)

View File

@ -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<int>(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 ?

View File

@ -53,6 +53,14 @@
Text="{x:Static props:Resources.MergeName}" />
<Run Text="{x:Static props:Resources.MergeDescription}" />
</TextBlock>
<TextBlock
Margin="0,8,0,0"
TextWrapping="Wrap">
<Run
FontWeight="Bold"
Text="{x:Static props:Resources.KeyboardControlsName}" />
<Run Text="{x:Static props:Resources.KeyboardControlsDescription}" />
</TextBlock>
</StackPanel>
<Grid Margin="0,24,0,-4">
<Grid.ColumnDefinitions>

View File

@ -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">
<Thumb.Template>
<ControlTemplate>
@ -35,6 +36,10 @@
TargetName="Body"
Value="{DynamicResource SystemAccentColorLight1Brush}"/>
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter Property="Background" TargetName="Body"
Value="{DynamicResource SystemAccentColorLight3Brush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Thumb.Template>

View File

@ -13,6 +13,8 @@
BorderBrush="{DynamicResource SystemControlBackgroundAccentBrush}"
BorderThickness="1"
Opacity="1"
Focusable="True"
IsTabStop="True"
ui:ControlHelper.CornerRadius="4"
mc:Ignorable="d">

View File

@ -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));
}

View File

@ -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()

View File

@ -447,6 +447,29 @@ namespace FancyZonesEditor.Properties {
}
}
/// <summary>
/// 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..
/// </summary>
public static string KeyboardControlsDescription {
get {
return ResourceManager.GetString("KeyboardControlsDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Keyboard Navigation:.
/// </summary>
public static string KeyboardControlsName {
get {
return ResourceManager.GetString("KeyboardControlsName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Create layouts that have overlapping zones.
/// </summary>

View File

@ -336,6 +336,17 @@
<value>Merge/Delete:</value>
<comment>Title for concept behind Merging two zones together or removing an zone</comment>
</data>
<data name="KeyboardControlsName" xml:space="preserve">
<value>Keyboard Navigation:</value>
</data>
<data name="KeyboardControlsDescription" xml:space="preserve">
<value>
- [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.</value>
</data>
<data name="SplitterDescription" xml:space="preserve">
<value>Hold Shift key for vertical split.</value>
<comment>A segmenter visual for splitting one item into two. This would be the vertical line. Shift key is referring to key on keyboard</comment>