Add project files.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
internal readonly struct DirectionalFocusNavigator<TElement>
|
||||
where TElement : UIElement, IKeyboardFocusTarget<TElement>
|
||||
{
|
||||
private readonly IEnumerable<IKeyboardFocusTarget<TElement>> _availableTargets;
|
||||
|
||||
public DirectionalFocusNavigator(IEnumerable<IKeyboardFocusTarget<TElement>> availableTargets)
|
||||
{
|
||||
_availableTargets = availableTargets;
|
||||
}
|
||||
|
||||
public readonly IKeyboardFocusTarget<TElement>? FindNextFocusTarget(IKeyboardFocusTarget<TElement> currentContainer, TraversalRequest request)
|
||||
{
|
||||
var currentContainerBounds = currentContainer.Bounds;
|
||||
|
||||
IEnumerable<IKeyboardFocusTarget<TElement>> candidates = request.FocusNavigationDirection switch
|
||||
{
|
||||
FocusNavigationDirection.Left => _availableTargets.Where(c => c.Bounds.Left < currentContainerBounds.Left),
|
||||
FocusNavigationDirection.Right => _availableTargets.Where(c => c.Bounds.Left > currentContainerBounds.Left),
|
||||
FocusNavigationDirection.Up => _availableTargets.Where(c => c.Bounds.Top < currentContainerBounds.Top),
|
||||
FocusNavigationDirection.Down => _availableTargets.Where(c => c.Bounds.Top > currentContainerBounds.Top),
|
||||
FocusNavigationDirection.Previous => FindCandidatesLinearly(currentContainer, request),
|
||||
FocusNavigationDirection.Next => FindCandidatesLinearly(currentContainer, request),
|
||||
FocusNavigationDirection.First => FindCandidatesLinearly(currentContainer, request),
|
||||
FocusNavigationDirection.Last => FindCandidatesLinearly(currentContainer, request),
|
||||
_ => Array.Empty<IKeyboardFocusTarget<TElement>>()
|
||||
};
|
||||
|
||||
// Wrap focus if no candidates found in the current direction
|
||||
if (!candidates.Any())
|
||||
{
|
||||
candidates = request.FocusNavigationDirection switch
|
||||
{
|
||||
FocusNavigationDirection.Left => _availableTargets.OrderByDescending(c => c.Bounds.Left).Take(1),
|
||||
FocusNavigationDirection.Right => _availableTargets.OrderBy(c => c.Bounds.Left).Take(1),
|
||||
FocusNavigationDirection.Up => _availableTargets.OrderByDescending(c => c.Bounds.Top).Take(1),
|
||||
FocusNavigationDirection.Down => _availableTargets.OrderBy(c => c.Bounds.Top).Take(1),
|
||||
_ => Array.Empty<IKeyboardFocusTarget<TElement>>()
|
||||
};
|
||||
|
||||
request.Wrapped = true;
|
||||
}
|
||||
|
||||
IKeyboardFocusTarget<TElement>? best = null;
|
||||
double minDistanceSquared = double.MaxValue;
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
double distanceSquared = (candidate.Bounds.TopLeft - currentContainerBounds.TopLeft).LengthSquared;
|
||||
if (distanceSquared < minDistanceSquared)
|
||||
{
|
||||
minDistanceSquared = distanceSquared;
|
||||
best = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private IKeyboardFocusTarget<TElement>[] FindCandidatesLinearly(IKeyboardFocusTarget<TElement> currentContainer, TraversalRequest request)
|
||||
{
|
||||
var nextTarget = new LinearFocusNavigator<TElement>(_availableTargets).FindNextFocusTarget(currentContainer, request);
|
||||
return nextTarget is null ? Array.Empty<IKeyboardFocusTarget<TElement>>() : new[] { nextTarget };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a unique identifier for a keyboard navigation layer.
|
||||
/// </summary>
|
||||
public class KeyboardNavigationLayerId
|
||||
{
|
||||
public static readonly KeyboardNavigationLayerId Nodes = new KeyboardNavigationLayerId();
|
||||
public static readonly KeyboardNavigationLayerId Connections = new KeyboardNavigationLayerId();
|
||||
public static readonly KeyboardNavigationLayerId Decorators = new KeyboardNavigationLayerId();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a group of keyboard navigation layers that can be activated and navigated through.
|
||||
/// </summary>
|
||||
public interface IKeyboardNavigationLayerGroup : IReadOnlyCollection<IKeyboardNavigationLayer>
|
||||
{
|
||||
/// <summary>
|
||||
/// The current active keyboard navigation layer in the group, if any.
|
||||
/// </summary>
|
||||
IKeyboardNavigationLayer? ActiveNavigationLayer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Event that is raised when the active keyboard navigation layer changes.
|
||||
/// </summary>
|
||||
event Action<KeyboardNavigationLayerId>? ActiveNavigationLayerChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Activates the next keyboard navigation layer in the group, allowing focus to be restored to the last focused element in that layer.
|
||||
/// </summary>
|
||||
/// <returns>Returns true if the navigation layer was activated, false otherwise.</returns>
|
||||
bool ActivateNextNavigationLayer();
|
||||
|
||||
/// <summary>
|
||||
/// Activates the previous keyboard navigation layer in the group, allowing focus to be restored to the last focused element in that layer.
|
||||
/// </summary>
|
||||
/// <returns>Returns true if the navigation layer was activated, false otherwise.</returns>
|
||||
bool ActivatePreviousNavigationLayer();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new keyboard navigation layer to the group, allowing it to handle focus movement and restoration.
|
||||
/// </summary>
|
||||
/// <param name="layer">The navigation layer.</param>
|
||||
/// <returns></returns>
|
||||
bool RegisterNavigationLayer(IKeyboardNavigationLayer layer);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified keyboard navigation layer from the group.
|
||||
/// </summary>
|
||||
/// <param name="layerId">The navigation layer id.</param>
|
||||
/// <returns>Returns true if the layer was removed, false otherwise.</returns>
|
||||
bool RemoveNavigationLayer(KeyboardNavigationLayerId layerId);
|
||||
|
||||
/// <summary>
|
||||
/// Activates the specified keyboard navigation layer, making it the active layer for focus management.
|
||||
/// </summary>
|
||||
/// <param name="layerId">The navigation layer id to activate.</param>
|
||||
/// <returns>Returns true if the navigation layer was activated, false otherwise.</returns>
|
||||
bool ActivateNavigationLayer(KeyboardNavigationLayerId layerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a layer of keyboard navigation that can handle focus movement and restoration.
|
||||
/// </summary>
|
||||
public interface IKeyboardNavigationLayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique identifier for this keyboard navigation layer.
|
||||
/// </summary>
|
||||
KeyboardNavigationLayerId Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last focused element within this layer, if any.
|
||||
/// </summary>
|
||||
IKeyboardFocusTarget<UIElement>? LastFocusedElement { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to move focus within this layer based on the provided traversal request.
|
||||
/// </summary>
|
||||
/// <param name="request">The traversal request.</param>
|
||||
/// <returns>Returns true if the focus was moved, false otherwise.</returns>
|
||||
bool TryMoveFocus(TraversalRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to restore focus to the last focused element within this layer.
|
||||
/// </summary>
|
||||
/// <returns>Returns true if the focus was restored, false otherwise.</returns>
|
||||
bool TryRestoreFocus();
|
||||
|
||||
/// <summary>
|
||||
/// Called when the layer is activated, allowing for any necessary setup or focus management.
|
||||
/// </summary>
|
||||
void OnActivated();
|
||||
|
||||
/// <summary>
|
||||
/// Called when the layer is deactivated, allowing for any necessary cleanup or focus management.
|
||||
/// </summary>
|
||||
void OnDeactivated();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a target for keyboard focus within a specific layer, providing bounds and the associated UI element.
|
||||
/// </summary>
|
||||
/// <typeparam name="TElement">The associated UI element.</typeparam>
|
||||
public interface IKeyboardFocusTarget<out TElement>
|
||||
where TElement : UIElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the bounds of the focus target within the layer.
|
||||
/// </summary>
|
||||
Rect Bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the associated UI element for this focus target.
|
||||
/// </summary>
|
||||
TElement Element { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
internal readonly struct LinearFocusNavigator<TElement>
|
||||
where TElement : UIElement, IKeyboardFocusTarget<TElement>
|
||||
{
|
||||
private enum LinearNavigationDirection
|
||||
{
|
||||
First,
|
||||
Last,
|
||||
Forward,
|
||||
Backward
|
||||
}
|
||||
|
||||
private readonly IEnumerable<IKeyboardFocusTarget<TElement>> _availableTargets;
|
||||
|
||||
public LinearFocusNavigator(IEnumerable<IKeyboardFocusTarget<TElement>> availableTargets)
|
||||
{
|
||||
_availableTargets = availableTargets;
|
||||
}
|
||||
|
||||
public readonly IKeyboardFocusTarget<TElement>? FindNextFocusTarget(IKeyboardFocusTarget<TElement> currentContainer, TraversalRequest request)
|
||||
{
|
||||
var direction = IsBackward(request.FocusNavigationDirection) ? LinearNavigationDirection.Backward
|
||||
: IsForward(request.FocusNavigationDirection) ? LinearNavigationDirection.Forward
|
||||
: request.FocusNavigationDirection == FocusNavigationDirection.First ? LinearNavigationDirection.First : LinearNavigationDirection.Last;
|
||||
|
||||
var availableTargets = _availableTargets as List<IKeyboardFocusTarget<TElement>> ?? _availableTargets.ToList();
|
||||
int currentIndex = availableTargets.IndexOf(currentContainer);
|
||||
|
||||
IKeyboardFocusTarget<TElement>? candidate = direction switch
|
||||
{
|
||||
LinearNavigationDirection.Forward when currentIndex >= 0 && currentIndex + 1 < availableTargets.Count => availableTargets[currentIndex + 1],
|
||||
LinearNavigationDirection.Backward when currentIndex > 0 => availableTargets[currentIndex - 1],
|
||||
LinearNavigationDirection.First when availableTargets.Count > 0 => availableTargets[0],
|
||||
LinearNavigationDirection.Last when availableTargets.Count > 0 => availableTargets[availableTargets.Count - 1],
|
||||
_ => null
|
||||
};
|
||||
|
||||
// Wrap focus if no candidates found in the current direction
|
||||
if (candidate is null)
|
||||
{
|
||||
candidate = direction switch
|
||||
{
|
||||
LinearNavigationDirection.Forward when availableTargets.Count > 0 => availableTargets[0],
|
||||
LinearNavigationDirection.Backward when availableTargets.Count > 0 => availableTargets[availableTargets.Count - 1],
|
||||
_ => null
|
||||
};
|
||||
|
||||
request.Wrapped = candidate != null;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private static bool IsForward(FocusNavigationDirection dir)
|
||||
{
|
||||
return dir == FocusNavigationDirection.Right || dir == FocusNavigationDirection.Up || dir == FocusNavigationDirection.Next;
|
||||
}
|
||||
|
||||
private static bool IsBackward(FocusNavigationDirection dir)
|
||||
{
|
||||
return dir == FocusNavigationDirection.Left || dir == FocusNavigationDirection.Down || dir == FocusNavigationDirection.Previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
internal class StatefulFocusNavigator<TElement>
|
||||
where TElement : UIElement, IKeyboardFocusTarget<TElement>
|
||||
{
|
||||
public delegate bool FindNextFocusTargetDelegate(TElement? currentElement, TraversalRequest request, out TElement? elementToFocus);
|
||||
|
||||
private readonly WeakReference<TElement?> _previousFocusedElement = new WeakReference<TElement?>(null);
|
||||
private readonly WeakReference<TElement?> _lastFocusedElement = new WeakReference<TElement?>(null);
|
||||
private FocusNavigationDirection? _previousFocusNavigationDirection;
|
||||
|
||||
private readonly Action<IKeyboardFocusTarget<TElement>> _onFocus;
|
||||
|
||||
public TElement? LastFocusedElement => _lastFocusedElement.TryGetTarget(out var target) ? target : null;
|
||||
|
||||
public StatefulFocusNavigator(Action<IKeyboardFocusTarget<TElement>> onFocus)
|
||||
{
|
||||
_onFocus = onFocus;
|
||||
}
|
||||
|
||||
public bool TryMoveFocus(TraversalRequest request, FindNextFocusTargetDelegate findNext)
|
||||
{
|
||||
var currentTarget = Keyboard.FocusedElement as TElement;
|
||||
|
||||
// If the request is in the opposite direction of the last focus navigation, try to restore the previous focused container
|
||||
if (_previousFocusedElement.TryGetTarget(out var prevTarget)
|
||||
&& _previousFocusNavigationDirection.HasValue
|
||||
&& request.FocusNavigationDirection.IsOppositeOf(_previousFocusNavigationDirection.Value)
|
||||
&& prevTarget!.Focus())
|
||||
{
|
||||
_previousFocusNavigationDirection = request.FocusNavigationDirection;
|
||||
_previousFocusedElement.SetTarget(currentTarget);
|
||||
_lastFocusedElement.SetTarget(prevTarget);
|
||||
|
||||
_onFocus(prevTarget);
|
||||
return true;
|
||||
}
|
||||
else if (findNext(currentTarget, request, out var nextTarget) && nextTarget!.Element.Focus())
|
||||
{
|
||||
_previousFocusNavigationDirection = request.FocusNavigationDirection;
|
||||
_previousFocusedElement.SetTarget(currentTarget);
|
||||
_lastFocusedElement.SetTarget(nextTarget);
|
||||
|
||||
_onFocus(nextTarget);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryRestoreFocus()
|
||||
{
|
||||
if (_lastFocusedElement.TryGetTarget(out var lastTarget))
|
||||
{
|
||||
if (lastTarget!.IsKeyboardFocused)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (lastTarget.Focus())
|
||||
{
|
||||
_onFocus.Invoke(lastTarget);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user