Skip to content

Commit d043d35

Browse files
committed
Fix tab navigation for editor and focus editor when the focused element is deleted
1 parent 75397db commit d043d35

File tree

13 files changed

+112
-26
lines changed

13 files changed

+112
-26
lines changed

Examples/Nodify.Shared/Controls/EditableTextBlock.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ public override void OnApplyTemplate()
9999
{
100100
base.OnApplyTemplate();
101101

102+
if (TextBox != null)
103+
{
104+
TextBox.LostFocus -= OnLostFocus;
105+
TextBox.LostKeyboardFocus -= OnLostFocus;
106+
TextBox.IsVisibleChanged -= OnTextBoxVisiblityChanged;
107+
}
108+
102109
TextBox = GetTemplateChild(ElementTextBox) as TextBox;
103110

104111
if (TextBox != null)
@@ -163,7 +170,7 @@ protected override void OnKeyDown(KeyEventArgs e)
163170
IsEditing = false;
164171
}
165172

166-
if(e.Key == Key.Enter && IsFocused && !IsEditing)
173+
if (e.Key == Key.Enter && IsFocused && !IsEditing)
167174
{
168175
IsEditing = true;
169176
}

Nodify/Connections/ConnectionContainer.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Windows;
44
using System.Windows.Controls;
55
using System.Windows.Input;
6+
using System.Windows.Media;
67

78
namespace Nodify
89
{
@@ -88,11 +89,25 @@ static ConnectionContainer()
8889
FocusVisualStyleProperty.OverrideMetadata(typeof(ConnectionContainer), new FrameworkPropertyMetadata(new Style()));
8990
}
9091

91-
internal ConnectionContainer(ConnectionsMultiSelector selector)
92+
public ConnectionContainer(ConnectionsMultiSelector selector)
9293
{
9394
Selector = selector;
9495
}
9596

97+
protected override void OnVisualParentChanged(DependencyObject oldParent)
98+
{
99+
if (VisualTreeHelper.GetParent(this) == null && IsKeyboardFocusWithin)
100+
{
101+
base.OnVisualParentChanged(oldParent);
102+
103+
Selector.Editor?.Focus();
104+
}
105+
else
106+
{
107+
base.OnVisualParentChanged(oldParent);
108+
}
109+
}
110+
96111
protected override void OnIsKeyboardFocusedChanged(DependencyPropertyChangedEventArgs e)
97112
{
98113
if (Connection is BaseConnection baseConnection)

Nodify/Connections/ConnectionsMultiSelector.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ static ConnectionsMultiSelector()
8484

8585
KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(KeyboardNavigationMode.Once));
8686
KeyboardNavigation.DirectionalNavigationProperty.OverrideMetadata(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(KeyboardNavigationMode.None));
87-
FocusManager.IsFocusScopeProperty.OverrideMetadata(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(BoxValue.True));
87+
//FocusManager.IsFocusScopeProperty.OverrideMetadata(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(BoxValue.True));
8888
}
8989

9090
public ConnectionsMultiSelector()
@@ -95,6 +95,7 @@ public ConnectionsMultiSelector()
9595
#region Keyboard Navigation
9696

9797
KeyboardNavigationLayerId IKeyboardNavigationLayer.Id { get; } = KeyboardNavigationLayerId.Connections;
98+
object? IKeyboardNavigationLayer.LastFocusedElement => _focusNavigator.LastFocusedElement;
9899

99100
private readonly StatefulFocusNavigator<ConnectionContainer> _focusNavigator;
100101

@@ -103,6 +104,11 @@ bool IKeyboardNavigationLayer.TryMoveFocus(TraversalRequest request)
103104
return _focusNavigator.TryMoveFocus(request, TryFindContainerToFocus);
104105
}
105106

107+
bool IKeyboardNavigationLayer.TryRestoreFocus()
108+
{
109+
return _focusNavigator.TryRestoreFocus();
110+
}
111+
106112
private bool TryFindContainerToFocus(TraversalRequest request, out ConnectionContainer? containerToFocus)
107113
{
108114
containerToFocus = null;

Nodify/Containers/ItemContainer.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ static ItemContainer()
276276
DefaultStyleKeyProperty.OverrideMetadata(typeof(ItemContainer), new FrameworkPropertyMetadata(typeof(ItemContainer)));
277277
FocusableProperty.OverrideMetadata(typeof(ItemContainer), new FrameworkPropertyMetadata(BoxValue.True));
278278

279+
KeyboardNavigation.IsTabStopProperty.OverrideMetadata(typeof(ItemContainer), new FrameworkPropertyMetadata(BoxValue.True));
280+
//KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(ItemContainer), new FrameworkPropertyMetadata(KeyboardNavigationMode.Contained));
279281
//KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(ItemContainer), new FrameworkPropertyMetadata(KeyboardNavigationMode.Local));
280282
//KeyboardNavigation.DirectionalNavigationProperty.OverrideMetadata(typeof(ItemContainer), new FrameworkPropertyMetadata(KeyboardNavigationMode.Contained));
281283
//FocusManager.IsFocusScopeProperty.OverrideMetadata(typeof(ItemContainer), new FrameworkPropertyMetadata(BoxValue.True));
@@ -293,6 +295,20 @@ public ItemContainer(NodifyEditor editor)
293295
InputProcessor.AddSharedHandlers(this);
294296
}
295297

298+
protected override void OnVisualParentChanged(DependencyObject oldParent)
299+
{
300+
if (VisualTreeHelper.GetParent(this) == null && IsKeyboardFocusWithin)
301+
{
302+
base.OnVisualParentChanged(oldParent);
303+
304+
Editor.Focus();
305+
}
306+
else
307+
{
308+
base.OnVisualParentChanged(oldParent);
309+
}
310+
}
311+
296312
/// <inheritdoc />
297313
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
298314
{

Nodify/Editor/NodifyEditor.KeyboardNavigation.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ namespace Nodify
1111
{
1212
public partial class NodifyEditor : IKeyboardNavigationLayer, IKeyboardNavigationLayerGroup
1313
{
14+
/// <summary>
15+
/// Gets or sets the default viewport edge offset applied when bringing an item into view as a result of keyboard focus.
16+
/// </summary>
17+
public static double BringIntoViewEdgeOffset { get; set; } = 32d;
18+
19+
/// <summary>
20+
/// Automatically focus first container on navigation layer change or editor focus.
21+
/// </summary>
22+
public static bool AutoFocusFirstElement { get; set; } = true;
23+
1424
private readonly List<IKeyboardNavigationLayer> _navigationLayers = new List<IKeyboardNavigationLayer>();
1525
private IKeyboardNavigationLayer? _activeKeyboardNavigationLayer;
1626
private IKeyboardNavigationLayer KeyboardNavigationLayer => this;
@@ -19,6 +29,7 @@ public partial class NodifyEditor : IKeyboardNavigationLayer, IKeyboardNavigatio
1929
IKeyboardNavigationLayer? IKeyboardNavigationLayerGroup.ActiveLayer => _activeKeyboardNavigationLayer;
2030

2131
KeyboardNavigationLayerId IKeyboardNavigationLayer.Id => KeyboardNavigationLayerId.Nodes;
32+
object? IKeyboardNavigationLayer.LastFocusedElement => _focusNavigator.LastFocusedElement;
2233

2334
int IReadOnlyCollection<IKeyboardNavigationLayer>.Count => _navigationLayers.Count;
2435

@@ -33,6 +44,11 @@ bool IKeyboardNavigationLayer.TryMoveFocus(TraversalRequest request)
3344
return _focusNavigator.TryMoveFocus(request, TryFindContainerToFocus);
3445
}
3546

47+
bool IKeyboardNavigationLayer.TryRestoreFocus()
48+
{
49+
return _focusNavigator.TryRestoreFocus();
50+
}
51+
3652
private bool TryFindContainerToFocus(TraversalRequest request, out ItemContainer? containerToFocus)
3753
{
3854
containerToFocus = null;
@@ -79,10 +95,38 @@ void IKeyboardNavigationLayer.OnDeactivate()
7995
{
8096
}
8197

98+
protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
99+
{
100+
bool isKeyboardInitiated = InputManager.Current.MostRecentInputDevice is KeyboardDevice;
101+
var activeKbdLayer = KeyboardNavigationLayerGroup.ActiveLayer;
102+
103+
if (isKeyboardInitiated && activeKbdLayer != null)
104+
{
105+
bool isFocusComingFromOutside = e.OldFocus is null || e.OldFocus is DependencyObject dpo && !IsAncestorOf(dpo);
106+
107+
if (isFocusComingFromOutside && activeKbdLayer.TryRestoreFocus())
108+
{
109+
e.Handled = true;
110+
}
111+
else if (activeKbdLayer.LastFocusedElement is null && e.NewFocus == this && AutoFocusFirstElement)
112+
{
113+
e.Handled = activeKbdLayer.TryMoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
114+
}
115+
}
116+
}
117+
82118
#endregion
83119

84120
#region Layer Management
85121

122+
protected virtual void OnKeyboardNavigationLayerActivated(KeyboardNavigationLayerId layerId)
123+
{
124+
if (AutoFocusFirstElement && !KeyboardNavigationLayerGroup.ActiveLayer!.TryRestoreFocus())
125+
{
126+
KeyboardNavigationLayerGroup.ActiveLayer.TryMoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
127+
}
128+
}
129+
86130
bool IKeyboardNavigationLayerGroup.RegisterLayer(IKeyboardNavigationLayer layer)
87131
{
88132
if (_navigationLayers.Any(l => l.Id == layer.Id))
@@ -120,6 +164,7 @@ bool IKeyboardNavigationLayerGroup.ActivateLayer(KeyboardNavigationLayerId layer
120164
_activeKeyboardNavigationLayer?.OnDeactivate();
121165
_activeKeyboardNavigationLayer = layer;
122166
_activeKeyboardNavigationLayer.OnActivate();
167+
OnKeyboardNavigationLayerActivated(layer.Id);
123168
Debug.WriteLine($"Activated {_activeKeyboardNavigationLayer.GetType().Name} as a keyboard navigation layer in {GetType().Name}");
124169
ActiveLayerChanged?.Invoke(layerId);
125170
return true;

Nodify/Editor/NodifyEditor.cs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -502,11 +502,6 @@ public ICommand? RemoveConnectionCommand
502502
/// </summary>
503503
public static double FitToScreenExtentMargin { get; set; } = 30;
504504

505-
/// <summary>
506-
/// Gets or sets the default viewport edge offset applied when bringing an item into view as a result of keyboard focus.
507-
/// </summary>
508-
public static double BringIntoViewEdgeOffset { get; set; } = 32d;
509-
510505
/// <summary>
511506
/// Gets or sets the maximum distance, in pixels, that the mouse can move before suppressing certain mouse actions.
512507
/// This is useful for suppressing actions like showing a <see cref="ContextMenu"/> if the mouse has moved significantly.
@@ -557,9 +552,10 @@ static NodifyEditor()
557552
DefaultStyleKeyProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(typeof(NodifyEditor)));
558553
FocusableProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.True));
559554

560-
KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(KeyboardNavigationMode.Once));
555+
KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(KeyboardNavigationMode.None));
556+
KeyboardNavigation.ControlTabNavigationProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(KeyboardNavigationMode.None));
561557
KeyboardNavigation.DirectionalNavigationProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(KeyboardNavigationMode.None));
562-
FocusManager.IsFocusScopeProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.True));
558+
//FocusManager.IsFocusScopeProperty.OverrideMetadata(typeof(NodifyEditor), new FrameworkPropertyMetadata(BoxValue.True));
563559

564560
EditorCommands.RegisterCommandBindings<NodifyEditor>();
565561
}
@@ -583,8 +579,8 @@ public NodifyEditor()
583579

584580
InputProcessor.AddSharedHandlers(this);
585581

586-
Unloaded += OnEditorUnloaded;
587-
582+
Unloaded += OnEditorUnloaded;
583+
588584
_focusNavigator = new StatefulFocusNavigator<ItemContainer>(target => BringIntoView(target.Element.Bounds, BringIntoViewEdgeOffset));
589585
}
590586

Nodify/Editor/States/KeyboardNavigation.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ public KeyboardNavigation(NodifyEditor element) : base(element)
1313
{
1414
}
1515

16-
// TODO: If focus is within, do not allow escaping focus trap unless the escape gesture is performed. (some keys like Space or System Keys could try to escape)
1716
protected override void OnKeyDown(KeyEventArgs e)
1817
{
1918
double cellSize = Element.GridCellSize;
@@ -71,6 +70,7 @@ protected override void OnKeyUp(KeyEventArgs e)
7170
e.Handled = true;
7271
}
7372
// TODO: How to get the selected connections count without a hard reference to the connections multi selector?
73+
// This currently assumes we have a binding to the SelectedConnectionsProperty dependency property
7474
else if (Element.SelectedConnections?.Count > 0 && ActiveKeyboardNavigationLayer?.Id == KeyboardNavigationLayerId.Connections)
7575
{
7676
Element.UnselectAllConnections();
@@ -94,7 +94,6 @@ private bool CanDragSelection()
9494
return ActiveKeyboardNavigationLayer?.Id == KeyboardNavigationLayerId.Nodes && Element.SelectedContainersCount > 0;
9595
}
9696

97-
// TODO: Allow for extensibility because connections can be custom
9897
private static bool IsEditorControl(object originalSource)
9998
{
10099
return originalSource is NodifyEditor || originalSource is ItemContainer || originalSource is Connector || originalSource is ConnectionContainer;

Nodify/Interactivity/KeyboardNavigation/DirectionalFocusNavigator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ public DirectionalFocusNavigator(IEnumerable<IKeyboardFocusTarget<TElement>> ava
6464
return best;
6565
}
6666

67-
private static IKeyboardFocusTarget<TElement>[] FindCandidatesLinearly(IKeyboardFocusTarget<TElement> currentContainer, TraversalRequest request)
67+
private IKeyboardFocusTarget<TElement>[] FindCandidatesLinearly(IKeyboardFocusTarget<TElement> currentContainer, TraversalRequest request)
6868
{
69-
var nextTarget = new LinearFocusNavigator<TElement>().FindNextFocusTarget(currentContainer, request);
69+
var nextTarget = new LinearFocusNavigator<TElement>(_availableTargets).FindNextFocusTarget(currentContainer, request);
7070
return nextTarget is null ? Array.Empty<IKeyboardFocusTarget<TElement>>() : new[] { nextTarget };
7171
}
7272
}

Nodify/Interactivity/KeyboardNavigation/IKeyboardNavigationLayer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ public interface IKeyboardNavigationLayerGroup : IReadOnlyCollection<IKeyboardNa
3232
public interface IKeyboardNavigationLayer
3333
{
3434
KeyboardNavigationLayerId Id { get; }
35+
object? LastFocusedElement { get; }
3536

3637
bool TryMoveFocus(TraversalRequest request);
38+
bool TryRestoreFocus();
3739

3840
void OnActivate();
3941
void OnDeactivate();

Nodify/Interactivity/KeyboardNavigation/StatefulFocusNavigator.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ internal class StatefulFocusNavigator<TElement>
1616

1717
private readonly Action<IKeyboardFocusTarget<TElement>> _onFocus;
1818

19+
public TElement? LastFocusedElement => _lastFocusedContainer.TryGetTarget(out var target) ? target : null;
20+
1921
public StatefulFocusNavigator(Action<IKeyboardFocusTarget<TElement>> onFocus)
2022
{
2123
_onFocus = onFocus;
@@ -56,6 +58,7 @@ public bool TryRestoreFocus()
5658
if (_lastFocusedContainer.TryGetTarget(out var lastTarget) && lastTarget!.Focus())
5759
{
5860
_onFocus.Invoke(lastTarget);
61+
return true;
5962
}
6063

6164
return false;

0 commit comments

Comments
 (0)